using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Scheduling;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;

namespace Jiaowu.Api.Tests;

public sealed class AutomaticScheduleGeneratorTests
{
    [Fact]
    public async Task Development_migration_chain_accepts_current_sqlite_schema()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();
        await db.Database.ExecuteSqlRawAsync(
            "DROP TABLE \"BackgroundJobOutboxMessages\"");
        await db.Database.ExecuteSqlRawAsync(
            "DROP TABLE \"ExamRoomInvigilators\"");
        await db.Database.ExecuteSqlRawAsync(
            "DROP TABLE \"ExamSeats\"");
        await db.Database.ExecuteSqlRawAsync(
            "DROP TABLE \"ExamRoomSessions\"");
        await db.Database.ExecuteSqlRawAsync(
            "DROP TABLE \"ExamRooms\"");

        var migrator = new DevelopmentSqliteMigrator(
            db,
            NullLogger<DevelopmentSqliteMigrator>.Instance);
        await migrator.MigrateAsync();

        Assert.True(await db.ScheduleTimeSlots.CountAsync() == 0);
        Assert.True(await db.AutomaticScheduleJobs.CountAsync() == 0);
        Assert.True(await db.BackgroundJobOutboxMessages.CountAsync() == 0);
        Assert.True(await db.ExamRooms.CountAsync() == 0);
        Assert.True(await db.ExamRoomSessions.CountAsync() == 0);
        Assert.True(await db.ExamSeats.CountAsync() == 0);
        Assert.True(await db.ExamRoomInvigilators.CountAsync() == 0);
        Assert.True(await db.Database
            .SqlQueryRaw<int>(
                """
                SELECT COUNT(*) AS "Value"
                FROM pragma_index_list('BackgroundJobOutboxMessages')
                WHERE name = 'IX_BackgroundJobOutboxMessages_State_CompletedAt'
                """)
            .AnyAsync(value => value > 0));
        Assert.True(await db.Database
            .SqlQueryRaw<int>(
                """
                SELECT COUNT(*) AS "Value"
                FROM pragma_table_info('ScheduleEntries')
                WHERE name = 'ClassroomId' AND "notnull" = 0
                """)
            .AnyAsync(value => value > 0));
        Assert.True(await db.Database
            .SqlQueryRaw<int>(
                """
                SELECT COUNT(*) AS "Value"
                FROM pragma_table_info('TeachingTasks')
                WHERE name = 'SchedulingMode'
                """)
            .AnyAsync(value => value > 0));
    }

    [Fact]
    public async Task Generator_skips_flexible_courses()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var term = new AcademicTerm
        {
            Code = "2026-F",
            Name = "2026 秋季",
            AcademicYear = "2026-2027",
            Season = TermSeason.Autumn,
            StartDate = new DateOnly(2026, 9, 1),
            EndDate = new DateOnly(2027, 1, 15)
        };
        var college = new College { Code = "PUBLIC", Name = "公共教学部" };
        var course = new Course
        {
            Code = "FLEX-01",
            Name = "社会实践",
            College = college,
            Credits = 1,
            TotalHours = 16,
            PracticeHours = 16
        };
        var task = new TeachingTask
        {
            TaskNumber = "TASK-FLEX",
            Name = "社会实践教学班",
            AcademicTerm = term,
            Course = course,
            Capacity = 100,
            StartWeek = 1,
            EndWeek = 16,
            WeeklyHours = 1,
            SchedulingMode = TeachingTaskSchedulingMode.Flexible,
            Status = TeachingTaskStatus.Published
        };
        var plan = new SchedulePlan
        {
            AcademicTerm = term,
            Name = "非排时课程测试",
            Version = "V1"
        };
        db.AddRange(term, college, course, task, plan);
        db.ScheduleTimeSlots.Add(new ScheduleTimeSlot
        {
            AcademicTerm = term,
            PeriodNumber = 1,
            Name = "第 1 节",
            StartsAt = new TimeOnly(8, 0),
            EndsAt = new TimeOnly(8, 45)
        });
        await db.SaveChangesAsync();

        var result = await new AutomaticScheduleGenerator(db)
            .GenerateAsync(plan, CancellationToken.None);

        Assert.Equal(0, result.TotalTasks);
        Assert.Equal(0, result.CreatedEntries);
        Assert.Empty(await db.ScheduleEntries.ToListAsync());
    }

    [Fact]
    public async Task Generator_excludes_practice_hours_from_regular_schedule()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var term = new AcademicTerm
        {
            Code = "2026-LAB",
            Name = "2026 实验学时测试",
            AcademicYear = "2026-2027",
            Season = TermSeason.Autumn,
            StartDate = new DateOnly(2026, 9, 1),
            EndDate = new DateOnly(2027, 1, 15)
        };
        var college = new College { Code = "LAB", Name = "实验学院" };
        var campus = new Campus { Code = "LAB-CAMPUS", Name = "实验校区" };
        var building = new Building
        {
            Code = "LAB-BUILDING",
            Name = "实验楼",
            Campus = campus
        };
        var laboratory = new Classroom
        {
            Code = "LAB-101",
            Name = "实验室 101",
            Building = building,
            Capacity = 40,
            RoomType = "实验室",
            TeachingVenueNature = TeachingVenueNature.Laboratory
        };
        var course = new Course
        {
            Code = "LAB-01",
            Name = "实验混合课程",
            College = college,
            Credits = 2,
            TotalHours = 32,
            LectureHours = 16,
            PracticeHours = 16
        };
        var task = new TeachingTask
        {
            TaskNumber = "TASK-LAB",
            Name = "实验混合教学班",
            AcademicTerm = term,
            Course = course,
            Capacity = 30,
            StartWeek = 1,
            EndWeek = 16,
            WeeklyHours = 2,
            Status = TeachingTaskStatus.Published
        };
        var plan = new SchedulePlan
        {
            AcademicTerm = term,
            Name = "实验学时拆分测试",
            Version = "V1"
        };
        db.AddRange(term, college, campus, building, laboratory, course, task, plan);
        db.ScheduleTimeSlots.AddRange(
            new ScheduleTimeSlot
            {
                AcademicTerm = term,
                PeriodNumber = 1,
                Name = "第 1 节",
                StartsAt = new TimeOnly(8, 0),
                EndsAt = new TimeOnly(8, 45)
            },
            new ScheduleTimeSlot
            {
                AcademicTerm = term,
                PeriodNumber = 2,
                Name = "第 2 节",
                StartsAt = new TimeOnly(8, 55),
                EndsAt = new TimeOnly(9, 40)
            });
        await db.SaveChangesAsync();

        var result = await new AutomaticScheduleGenerator(db)
            .GenerateAsync(plan, CancellationToken.None);

        Assert.Equal(2, result.CreatedEntries);
        Assert.Equal(1, result.CompletedTasks);
        var entries = await db.ScheduleEntries.OrderBy(x => x.Kind).ToListAsync();
        Assert.Equal(2, entries.Count);
        Assert.Contains(entries, x =>
            x.Kind == ScheduleEntryKind.Lecture &&
            TeachingTaskHours.ScheduledHours(x) == 16);
        Assert.Contains(entries, x =>
            x.Kind == ScheduleEntryKind.Experiment &&
            x.ClassroomId == laboratory.Id &&
            TeachingTaskHours.ScheduledHours(x) == 16);
    }

    [Fact]
    public async Task Generator_schedules_roomless_course_within_allowed_time()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var term = new AcademicTerm
        {
            Code = "2026-A",
            Name = "2026 秋季",
            AcademicYear = "2026-2027",
            Season = TermSeason.Autumn,
            StartDate = new DateOnly(2026, 9, 1),
            EndDate = new DateOnly(2027, 1, 15)
        };
        var college = new College { Code = "CS", Name = "计算机学院" };
        var course = new Course
        {
            Code = "MOOC-01",
            Name = "在线专题",
            College = college,
            Credits = 1,
            TotalHours = 32,
            LectureHours = 32
        };
        var task = new TeachingTask
        {
            TaskNumber = "TASK-MOOC",
            Name = "在线专题教学班",
            AcademicTerm = term,
            Course = course,
            Capacity = 200,
            StartWeek = 1,
            EndWeek = 16,
            WeeklyHours = 2,
            Status = TeachingTaskStatus.Published
        };
        var plan = new SchedulePlan
        {
            AcademicTerm = term,
            Name = "自动排课测试",
            Version = "V1"
        };
        db.AddRange(term, college, course, task, plan);
        db.ScheduleTimeSlots.AddRange(
            new ScheduleTimeSlot
            {
                AcademicTerm = term,
                PeriodNumber = 1,
                Name = "第 1 节",
                StartsAt = new TimeOnly(8, 0),
                EndsAt = new TimeOnly(8, 45)
            },
            new ScheduleTimeSlot
            {
                AcademicTerm = term,
                PeriodNumber = 2,
                Name = "第 2 节",
                StartsAt = new TimeOnly(8, 55),
                EndsAt = new TimeOnly(9, 40)
            });
        db.TeachingTaskScheduleConstraints.Add(new TeachingTaskScheduleConstraint
        {
            TeachingTask = task,
            RequiresClassroom = false,
            AllowedDayOfWeeks = "3",
            EarliestPeriod = 1,
            LatestPeriod = 2
        });
        await db.SaveChangesAsync();

        var progressUpdates = new List<AutomaticScheduleProgress>();
        var result = await new AutomaticScheduleGenerator(db)
            .GenerateAsync(
                plan,
                (progress, _) =>
                {
                    progressUpdates.Add(progress);
                    return Task.CompletedTask;
                },
                true,
                CancellationToken.None);
        var entry = await db.ScheduleEntries.SingleAsync();

        Assert.Equal(1, result.CreatedEntries);
        Assert.Empty(result.Messages);
        Assert.Collection(
            progressUpdates,
            progress =>
            {
                Assert.Equal(1, progress.TotalTasks);
                Assert.Equal(0, progress.ProcessedTasks);
            },
            progress =>
            {
                Assert.Equal(1, progress.TotalTasks);
                Assert.Equal(1, progress.ProcessedTasks);
                Assert.Equal(1, progress.CreatedEntries);
            });
        Assert.Equal(3, entry.DayOfWeek);
        Assert.Equal(1, entry.StartPeriod);
        Assert.Equal(2, entry.PeriodCount);
        Assert.Null(entry.ClassroomId);
    }

    [Fact]
    public async Task Generator_uses_scoped_rooms_without_attaching_duplicate_buildings()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var term = new AcademicTerm
        {
            Code = "2026-B",
            Name = "2026 秋季",
            AcademicYear = "2026-2027",
            Season = TermSeason.Autumn,
            StartDate = new DateOnly(2026, 9, 1),
            EndDate = new DateOnly(2027, 1, 15)
        };
        var campus = new Campus { Code = "MAIN", Name = "主校区" };
        var building = new Building
        {
            Code = "A",
            Name = "教学楼 A",
            Campus = campus
        };
        var smallRoom = new Classroom
        {
            Code = "A101",
            Name = "A101",
            Building = building,
            Capacity = 20
        };
        var largeRoom = new Classroom
        {
            Code = "A102",
            Name = "A102",
            Building = building,
            Capacity = 30
        };
        var college = new College { Code = "CS", Name = "计算机学院" };
        var course = new Course
        {
            Code = "CS-01",
            Name = "程序设计",
            College = college,
            Credits = 2,
            TotalHours = 32,
            LectureHours = 32
        };
        var smallTask = new TeachingTask
        {
            TaskNumber = "TASK-SMALL",
            Name = "小班教学任务",
            AcademicTerm = term,
            Course = course,
            Capacity = 20,
            StartWeek = 1,
            EndWeek = 16,
            WeeklyHours = 2,
            Status = TeachingTaskStatus.Published
        };
        var largeTask = new TeachingTask
        {
            TaskNumber = "TASK-LARGE",
            Name = "大班教学任务",
            AcademicTerm = term,
            Course = course,
            Capacity = 30,
            StartWeek = 1,
            EndWeek = 16,
            WeeklyHours = 2,
            Status = TeachingTaskStatus.Published
        };
        var plan = new SchedulePlan
        {
            AcademicTerm = term,
            Name = "同楼多教室排课",
            Version = "V1"
        };
        db.AddRange(
            term,
            campus,
            building,
            smallRoom,
            largeRoom,
            college,
            course,
            smallTask,
            largeTask,
            plan);
        db.ScheduleTimeSlots.AddRange(
            new ScheduleTimeSlot
            {
                AcademicTerm = term,
                PeriodNumber = 1,
                Name = "第 1 节",
                StartsAt = new TimeOnly(8, 0),
                EndsAt = new TimeOnly(8, 45)
            },
            new ScheduleTimeSlot
            {
                AcademicTerm = term,
                PeriodNumber = 2,
                Name = "第 2 节",
                StartsAt = new TimeOnly(8, 55),
                EndsAt = new TimeOnly(9, 40)
            });
        await db.SaveChangesAsync();
        db.TeachingTaskScheduleConstraints.AddRange(
            new TeachingTaskScheduleConstraint
            {
                TeachingTaskId = smallTask.Id,
                RequiredCampusId = campus.Id,
                RequiredBuildingId = building.Id,
                AllowedClassrooms =
                [
                    new TeachingTaskAllowedClassroom
                    {
                        ClassroomId = smallRoom.Id
                    }
                ]
            },
            new TeachingTaskScheduleConstraint
            {
                TeachingTaskId = largeTask.Id,
                RequiredCampusId = campus.Id,
                RequiredBuildingId = building.Id,
                AllowedClassrooms =
                [
                    new TeachingTaskAllowedClassroom
                    {
                        ClassroomId = largeRoom.Id
                    }
                ]
            });
        await db.SaveChangesAsync();

        db.ChangeTracker.Clear();
        plan = await db.SchedulePlans.SingleAsync();

        var result = await new AutomaticScheduleGenerator(db)
            .GenerateAsync(plan, CancellationToken.None);
        var entries = await db.ScheduleEntries
            .OrderBy(x => x.TeachingTaskId)
            .ToListAsync();

        Assert.Equal(2, result.CreatedEntries);
        Assert.Empty(result.Messages);
        Assert.Equal(2, entries.Count);
        Assert.True(
            new HashSet<Guid> { smallRoom.Id, largeRoom.Id }
                .SetEquals(entries.Select(x => x.ClassroomId!.Value)));
        Assert.Equal(1, await db.Buildings.CountAsync());
        Assert.Equal(2, await db.Classrooms.CountAsync());
    }
}
