using Jiaowu.Api.Infrastructure.Experiments;
using Jiaowu.Api.Infrastructure.Exams;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using MySql.Data.MySqlClient;

namespace Jiaowu.Api.Tests;

public sealed class MySqlMigrationTests
{
    private const string LatestMigration =
        "20260727133346_ExamArrangementJobs";

    [Fact]
    public void Production_migration_is_discoverable_and_generates_mysql_sql()
    {
        var options = CreateMySqlOptions();
        using var db = new AppDbContext(options);

        Assert.Contains(LatestMigration, db.Database.GetMigrations());

        var script = db.GetService<IMigrator>().GenerateScript(
            fromMigration: "20260725040310_SchedulePublishJobs",
            toMigration: LatestMigration);

        Assert.Contains("CREATE TABLE `GradeItems`", script);
        Assert.Contains("CREATE TABLE `EvaluationSetups`", script);
        Assert.Contains("CREATE TABLE `WarningRules`", script);
        Assert.Contains("ADD `CheckInMethod` int NOT NULL DEFAULT 1", script);
        Assert.Contains("CREATE UNIQUE INDEX `IX_AttendanceSheets_CheckInToken`", script);
        Assert.Contains("ADD COLUMN `WaitlistedAt` datetime(6) NULL", script);
        Assert.Contains(
            "CREATE INDEX `IX_CE_Offering_Status_WaitlistedAt`",
            script);
        Assert.Contains("@waitlisted_column_exists", script);
        Assert.Contains("@waitlist_index_exists", script);
        Assert.DoesNotContain(
            "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt",
            script);
        Assert.Contains(
            "ADD `MaxCourseCount` int NULL",
            script);
        Assert.Contains(
            "CREATE TABLE `CourseSelectionRoundGrades`",
            script);
        Assert.Contains(
            "CREATE UNIQUE INDEX `IX_CourseSelectionRoundGrades_CourseSelectionRoundId_Grade`",
            script);
        Assert.Contains(
            "ADD `CalendarSubscriptionStamp` varchar(64) NULL",
            script);
        Assert.Contains(
            "ADD `CalendarSubscriptionCreatedAt` datetime(6) NULL",
            script);
        Assert.Contains("CREATE TABLE `ExamRooms`", script);
        Assert.Contains("CREATE TABLE `ExamRoomSessions`", script);
        Assert.Contains("CREATE TABLE `ExamSeats`", script);
        Assert.Contains("CREATE TABLE `ExamRoomInvigilators`", script);
        Assert.Contains("CREATE TABLE `ExamArrangementJobs`", script);
        Assert.Contains(
            "CREATE UNIQUE INDEX `UX_ExamArrangementJobs_Kind_ActivePlan`",
            script);
        Assert.Contains("SET `ExamDate` = DATE(`StartsAt`)", script);
        Assert.Contains("DEFAULT 1", script);
        Assert.DoesNotContain("0001-01-01", script);
        Assert.DoesNotContain("0000-00-00", script);
    }

    [Fact]
    public void MySql_date_properties_use_datetime_provider_conversion()
    {
        using var db = new AppDbContext(CreateMySqlOptions());

        var dateProperties = db.Model.GetEntityTypes()
            .SelectMany(x => x.GetProperties())
            .Where(x => x.ClrType == typeof(DateOnly) ||
                        x.ClrType == typeof(DateOnly?))
            .ToList();

        Assert.NotEmpty(dateProperties);
        Assert.All(dateProperties, property =>
        {
            Assert.Equal("date", property.GetColumnType());
            Assert.Equal(
                typeof(DateTime),
                property.GetTypeMapping().Converter?.ProviderClrType);
        });
    }

    [Fact]
    public void MySql_guid_collections_use_provider_safe_predicates()
    {
        using var db = new AppDbContext(CreateMySqlOptions());
        var ids = new[]
        {
            Guid.Parse("11111111-1111-1111-1111-111111111111"),
            Guid.Parse("22222222-2222-2222-2222-222222222222")
        };

        var sql = db.Colleges
            .WhereIn(ids, x => x.Id)
            .ToQueryString();

        Assert.Contains(" IN (", sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(ids[0].ToString(), sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(ids[1].ToString(), sql, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void MySql_experiment_course_grade_lookup_uses_provider_safe_predicates()
    {
        using var db = new AppDbContext(CreateMySqlOptions());
        var teachingTaskId = Guid.Parse(
            "33333333-3333-3333-3333-333333333333");
        var studentIds = new[]
        {
            Guid.Parse("11111111-1111-1111-1111-111111111111"),
            Guid.Parse("22222222-2222-2222-2222-222222222222")
        };

        var sql = db.ExperimentCourseGrades
            .Where(x => x.TeachingTaskId == teachingTaskId)
            .WhereIn(studentIds, x => x.StudentId)
            .ToQueryString();

        Assert.Contains(" IN (", sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            teachingTaskId.ToString(),
            sql,
            StringComparison.OrdinalIgnoreCase);
        Assert.All(studentIds, studentId => Assert.Contains(
            studentId.ToString(),
            sql,
            StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void MySql_attendance_device_hash_query_uses_provider_safe_predicates()
    {
        using var db = new AppDbContext(CreateMySqlOptions());
        var deviceHashes = new[] { "device-hash-one", "device-hash-two" };

        var sql = db.AttendanceCheckInAttempts
            .Where(x =>
                x.IsSuccessful &&
                x.CreatedAt >= DateTime.UtcNow.AddHours(-24) &&
                x.DeviceIdentifierHash != null)
            .WhereIn(deviceHashes, x => x.DeviceIdentifierHash!)
            .GroupBy(x => x.DeviceIdentifierHash!)
            .Select(x => new
            {
                DeviceIdentifierHash = x.Key,
                StudentCount = x.Select(a => a.StudentId).Distinct().Count()
            })
            .ToQueryString();

        Assert.Contains(" IN (", sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(deviceHashes[0], sql, StringComparison.Ordinal);
        Assert.Contains(deviceHashes[1], sql, StringComparison.Ordinal);
    }

    [Fact]
    public void MySql_invigilator_candidate_query_is_translatable_without_uuid_ordering()
    {
        using var db = new AppDbContext(CreateMySqlOptions());
        var excludedTeacherId =
            Guid.Parse("11111111-1111-1111-1111-111111111111");

        var sql = InvigilatorCandidateQuery
            .Create(db, [excludedTeacherId])
            .ToQueryString();

        Assert.Contains("Teachers", sql, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("UUID(", sql, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void MySql_teaching_task_roster_query_is_translatable()
    {
        using var db = new AppDbContext(CreateMySqlOptions());
        var studentId =
            Guid.Parse("22222222-2222-2222-2222-222222222222");

        var sql = TeachingTaskRosterQuery
            .ForTask(
                db,
                Guid.Parse("11111111-1111-1111-1111-111111111111"))
            .ToQueryString();
        var taskIdsSql = TeachingTaskRosterQuery
            .TaskIdsForStudent(db, studentId)
            .ToQueryString();

        Assert.Contains("TeachingTaskClasses", sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("CourseEnrollments", sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            "TeachingTaskClasses",
            taskIdsSql,
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            "CourseEnrollments",
            taskIdsSql,
            StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void MySql_centralized_experiment_conflict_queries_are_translatable()
    {
        using var db = new AppDbContext(CreateMySqlOptions());
        var firstId =
            Guid.Parse("11111111-1111-1111-1111-111111111111");
        var secondId =
            Guid.Parse("22222222-2222-2222-2222-222222222222");
        Guid[] relatedIds = [firstId, secondId];

        var teacherTaskSql = CentralizedExperimentConflictQuery
            .TaskIdsForTeachers(db, relatedIds)
            .ToQueryString();
        var classTaskSql = CentralizedExperimentConflictQuery
            .TaskIdsForClasses(db, relatedIds)
            .ToQueryString();
        var scheduleSql = CentralizedExperimentConflictQuery
            .ScheduleEntries(
                db,
                relatedIds,
                firstId,
                1,
                2,
                1,
                2)
            .ToQueryString();
        var experimentSql = CentralizedExperimentConflictQuery
            .ExperimentSessions(
                db,
                relatedIds,
                new DateOnly(2026, 8, 1),
                1,
                2)
            .ToQueryString();

        Assert.Contains(
            "TeachingTaskTeachers",
            teacherTaskSql,
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            "TeachingTaskClasses",
            classTaskSql,
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            "ScheduleEntries",
            scheduleSql,
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            "ExperimentSessions",
            experimentSql,
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(" IN (", scheduleSql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            " IN (",
            experimentSql,
            StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void MySql_experiment_batch_project_query_is_translatable()
    {
        using var db = new AppDbContext(CreateMySqlOptions());
        var projectIds = new[]
        {
            Guid.Parse("11111111-1111-1111-1111-111111111111"),
            Guid.Parse("22222222-2222-2222-2222-222222222222")
        };
        var accessibleTaskIds = db.TeachingTasks.Select(x => x.Id);

        var sql = db.ExperimentProjects
            .Where(x => accessibleTaskIds.Contains(x.TeachingTaskId))
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.AcademicTerm)
            .WhereIn(projectIds, x => x.Id)
            .ToQueryString();

        Assert.Contains(
            "ExperimentProjects",
            sql,
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(" IN (", sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            projectIds[0].ToString(),
            sql,
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            projectIds[1].ToString(),
            sql,
            StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void MySql_index_names_fit_the_server_identifier_limit()
    {
        using var db = new AppDbContext(CreateMySqlOptions());

        var indexNames = db.Model.GetEntityTypes()
            .SelectMany(x => x.GetIndexes())
            .Select(x => x.GetDatabaseName())
            .Where(x => !string.IsNullOrEmpty(x))
            .ToList();

        Assert.NotEmpty(indexNames);
        Assert.All(indexNames, name => Assert.True(
            name!.Length <= 64,
            $"MySQL index name '{name}' has {name.Length} characters."));
    }

    [Fact]
    public void Application_mysql_connections_allow_migration_user_variables()
    {
        var normalized = MySqlConnectionStringPolicy.ForApplication(
            "Server=localhost;Database=jiaowu;User=test;Password=test;");

        var builder = new MySqlConnectionStringBuilder(normalized);

        Assert.True(builder.AllowUserVariables);
    }

    private static DbContextOptions<AppDbContext> CreateMySqlOptions() =>
        new DbContextOptionsBuilder<AppDbContext>()
            .UseMySQL(
                "Server=localhost;Database=jiaowu;User=__test__;Password=__not_used__;")
            .Options;
}
