using System.Security.Cryptography;
using System.Text.Json;
using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.OfficialDocuments;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Jiaowu.Api.Tests;

public sealed class OfficialDocumentTests
{
    [Theory]
    [InlineData(typeof(IssueOfficialDocumentRequest), "Purpose")]
    [InlineData(typeof(SelfIssueOfficialDocumentRequest), "Purpose")]
    [InlineData(typeof(InvalidateOfficialDocumentRequest), "Reason")]
    [InlineData(typeof(ReissueOfficialDocumentRequest), "Reason")]
    [InlineData(typeof(ReissueOfficialDocumentRequest), "Purpose")]
    public void RequestRecords_DefineValidationOnPrimaryConstructorParameters(
        Type requestType,
        string parameterName)
    {
        var constructor = Assert.Single(requestType.GetConstructors());
        var parameter = Assert.Single(constructor.GetParameters()
            .Where(x => x.Name == parameterName));
        Assert.NotEmpty(parameter.GetCustomAttributes(typeof(ValidationAttribute), false));

        var property = requestType.GetProperty(parameterName);
        Assert.NotNull(property);
        Assert.Empty(property.GetCustomAttributes(typeof(ValidationAttribute), false));
    }

    [Theory]
    [InlineData(OfficialDocumentType.Transcript)]
    [InlineData(OfficialDocumentType.StudentStatusCertificate)]
    public void PdfGenerator_CreatesPdfWithStableHash(OfficialDocumentType type)
    {
        var options = CreateOptions();
        var generator = new OfficialDocumentPdfGenerator(options);
        var snapshot = CreateSnapshot(type);

        var result = generator.Generate(snapshot, "https://jw.example.edu/verify/test-code");

        Assert.True(result.Content.Length > 500, "生成的 PDF 不应为空。");
        Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(result.Content, 0, 4));
        Assert.Contains(System.Text.Encoding.ASCII.GetBytes("%%EOF"), result.Content);
        Assert.Equal(
            Convert.ToHexString(SHA256.HashData(result.Content)).ToLowerInvariant(),
            result.Sha256);
    }

    [Fact]
    public async Task Service_IssuesTranscriptFromPublishedGradesOnly()
    {
        await using var fixture = await Fixture.CreateAsync();

        var document = await fixture.Service.CreateAsync(
            fixture.Student.Id,
            OfficialDocumentType.Transcript,
            "升学申请",
            fixture.Manager.Id,
            fixture.Manager.DisplayName,
            "https://jw.example.edu",
            null,
            CancellationToken.None);

        var snapshot = OfficialDocumentService.DeserializeSnapshot(document.SnapshotJson);
        Assert.StartsWith("MXU-TR-", document.DocumentNumber);
        Assert.Equal(64, document.VerificationCodeHash.Length);
        Assert.Equal("升学申请", document.Purpose);
        Assert.Single(snapshot.Grades);
        Assert.Equal("程序设计基础", snapshot.Grades[0].CourseName);
        Assert.Equal(4m, snapshot.EarnedCredits);
        Assert.Equal(3.7m, snapshot.GradePointAverage);
        Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(document.PdfContent, 0, 4));
    }

    [Fact]
    public async Task Download_IsLogged_AndInvalidDocumentCannotBeDownloaded()
    {
        await using var fixture = await Fixture.CreateAsync();
        var document = await fixture.IssueAndSaveAsync();
        var controller = fixture.CreateController();

        var result = await controller.Download(document.Id, CancellationToken.None);

        Assert.IsType<FileContentResult>(result);
        var log = await fixture.Db.OfficialDocumentDownloads.SingleAsync();
        Assert.Equal(document.Id, log.OfficialDocumentId);
        Assert.Equal(fixture.Manager.Id, log.DownloadedByUserId);

        document.Status = OfficialDocumentStatus.Invalidated;
        await fixture.Db.SaveChangesAsync();
        var invalidResult = await controller.Download(document.Id, CancellationToken.None);
        Assert.IsType<ConflictObjectResult>(invalidResult);
        Assert.Single(await fixture.Db.OfficialDocumentDownloads.ToListAsync());
    }

    [Fact]
    public async Task Verify_ReturnsMaskedIdentityAndCurrentRevocationState()
    {
        await using var fixture = await Fixture.CreateAsync();
        const string verificationCode = "1234567890abcdef1234567890abcdef1234567890abcdef";
        var snapshot = CreateSnapshot(OfficialDocumentType.StudentStatusCertificate);
        var document = new OfficialDocument
        {
            DocumentNumber = snapshot.DocumentNumber,
            VerificationCodeHash = OfficialDocumentService.HashVerificationCode(verificationCode),
            Type = snapshot.Type,
            Status = OfficialDocumentStatus.Valid,
            StudentId = fixture.Student.Id,
            IssuedByUserId = fixture.Manager.Id,
            IssuedAt = snapshot.IssuedAt,
            SnapshotJson = JsonSerializer.Serialize(snapshot, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
            PdfContent = [1, 2, 3],
            PdfSha256 = new string('a', 64)
        };
        fixture.Db.OfficialDocuments.Add(document);
        await fixture.Db.SaveChangesAsync();
        var controller = fixture.CreateController();

        var action = await controller.Verify(verificationCode, CancellationToken.None);
        var ok = Assert.IsType<OkObjectResult>(action);
        using var json = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value));
        Assert.Equal("张*", json.RootElement.GetProperty("StudentName").GetString());
        Assert.Equal("20*****01", json.RootElement.GetProperty("StudentNumber").GetString());
        Assert.True(json.RootElement.GetProperty("Valid").GetBoolean());

        var numberAction = await controller.Verify(document.DocumentNumber, CancellationToken.None);
        var numberOk = Assert.IsType<OkObjectResult>(numberAction);
        using var numberJson = JsonDocument.Parse(JsonSerializer.Serialize(numberOk.Value));
        Assert.True(numberJson.RootElement.GetProperty("Valid").GetBoolean());

        document.Status = OfficialDocumentStatus.Invalidated;
        document.InvalidatedAt = DateTime.UtcNow;
        await fixture.Db.SaveChangesAsync();
        action = await controller.Verify(verificationCode, CancellationToken.None);
        ok = Assert.IsType<OkObjectResult>(action);
        using var invalidJson = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value));
        Assert.False(invalidJson.RootElement.GetProperty("Valid").GetBoolean());
    }

    [Fact]
    public async Task Reissue_SupersedesOriginalAndCreatesSingleLinkedReplacement()
    {
        await using var fixture = await Fixture.CreateAsync();
        var original = await fixture.IssueAndSaveAsync();
        var controller = fixture.CreateController();

        var result = await controller.Reissue(
            original.Id,
            new ReissueOfficialDocumentRequest("成绩更正后重新签发", null),
            CancellationToken.None);

        Assert.IsType<OkObjectResult>(result);
        fixture.Db.ChangeTracker.Clear();
        var savedOriginal = await fixture.Db.OfficialDocuments.SingleAsync(x => x.Id == original.Id);
        var replacement = await fixture.Db.OfficialDocuments.SingleAsync(
            x => x.ReissuedFromDocumentId == original.Id);
        Assert.Equal(OfficialDocumentStatus.Superseded, savedOriginal.Status);
        Assert.Equal(OfficialDocumentStatus.Valid, replacement.Status);
        Assert.NotEqual(savedOriginal.DocumentNumber, replacement.DocumentNumber);
        Assert.NotEqual(savedOriginal.VerificationCodeHash, replacement.VerificationCodeHash);

        var duplicate = await controller.Reissue(
            original.Id,
            new ReissueOfficialDocumentRequest("再次重签", null),
            CancellationToken.None);
        Assert.IsType<ConflictObjectResult>(duplicate);
        Assert.Equal(2, await fixture.Db.OfficialDocuments.CountAsync());
    }

    [Fact]
    public async Task StudentCanIssueOwnDocument_AndRecentDuplicateIsReused()
    {
        await using var fixture = await Fixture.CreateAsync();
        var controller = fixture.CreateStudentController();
        var request = new SelfIssueOfficialDocumentRequest(
            OfficialDocumentType.Transcript,
            "  实习申请  ");

        var first = await controller.IssueMine(request, CancellationToken.None);

        Assert.IsType<CreatedAtActionResult>(first);
        var document = await fixture.Db.OfficialDocuments.SingleAsync();
        Assert.Equal(fixture.Student.Id, document.StudentId);
        Assert.Equal(fixture.StudentUser.Id, document.IssuedByUserId);
        Assert.Equal("实习申请", document.Purpose);
        var snapshot = OfficialDocumentService.DeserializeSnapshot(document.SnapshotJson);
        Assert.Equal("教务处自助签发", snapshot.IssuedByName);
        Assert.Single(snapshot.Grades);

        var duplicate = await controller.IssueMine(request, CancellationToken.None);

        Assert.IsType<OkObjectResult>(duplicate);
        Assert.Single(await fixture.Db.OfficialDocuments.ToListAsync());
    }

    [Fact]
    public async Task StudentWithoutLinkedRecord_CannotIssueDocument()
    {
        await using var fixture = await Fixture.CreateAsync();
        var controller = fixture.CreateStudentController(Guid.NewGuid());

        var result = await controller.IssueMine(
            new SelfIssueOfficialDocumentRequest(
                OfficialDocumentType.StudentStatusCertificate,
                null),
            CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(result);
        Assert.Empty(await fixture.Db.OfficialDocuments.ToListAsync());
    }

    private static OfficialDocumentOptions CreateOptions() => new()
    {
        InstitutionName = "明序大学",
        IssuingOffice = "教务处",
        DocumentNumberPrefix = "MXU"
    };

    private static OfficialDocumentSnapshot CreateSnapshot(OfficialDocumentType type) => new(
        "明序大学",
        "教务处",
        type == OfficialDocumentType.Transcript
            ? "MXU-TR-20260726-ABCDEF123456"
            : "MXU-SC-20260726-ABCDEF123456",
        type,
        new DateTime(2026, 7, 26, 8, 30, 0, DateTimeKind.Utc),
        "教务管理员",
        "升学申请",
        new OfficialStudentSnapshot(
            "202600001",
            "张明",
            "男",
            "计算机学院",
            "计算机科学与技术",
            "计科 2026-1 班",
            2026,
            new DateOnly(2026, 9, 1),
            "在读"),
        type == OfficialDocumentType.Transcript
            ? [new OfficialTranscriptRow("2026-2027-1", "CS101", "程序设计基础", 4, 88, 3.7m, "正常")]
            : [],
        type == OfficialDocumentType.Transcript ? 4 : 0,
        type == OfficialDocumentType.Transcript ? 4 : 0,
        type == OfficialDocumentType.Transcript ? 3.7m : null);

    private sealed class Fixture : IAsyncDisposable
    {
        private readonly SqliteConnection connection;
        private readonly OfficialDocumentOptions options;

        private Fixture(
            SqliteConnection connection,
            AppDbContext db,
            ApplicationUser manager,
            ApplicationUser studentUser,
            Student student,
            OfficialDocumentOptions options)
        {
            this.connection = connection;
            this.options = options;
            Db = db;
            Manager = manager;
            StudentUser = studentUser;
            Student = student;
            Service = new OfficialDocumentService(
                db,
                options,
                new OfficialDocumentPdfGenerator(options));
        }

        public AppDbContext Db { get; }
        public ApplicationUser Manager { get; }
        public ApplicationUser StudentUser { get; }
        public Student Student { get; }
        public OfficialDocumentService Service { get; }

        public static async Task<Fixture> CreateAsync()
        {
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var db = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
                .UseSqlite(connection)
                .Options);
            await db.Database.EnsureCreatedAsync();

            var manager = new ApplicationUser
            {
                Id = Guid.NewGuid(),
                UserName = "academic-admin",
                NormalizedUserName = "ACADEMIC-ADMIN",
                DisplayName = "教务管理员"
            };
            var studentUser = new ApplicationUser
            {
                Id = Guid.NewGuid(),
                UserName = "202600001",
                NormalizedUserName = "202600001",
                DisplayName = "张明"
            };
            var college = new College { Code = "CS", Name = "计算机学院" };
            var major = new Major
            {
                Code = "080901",
                Name = "计算机科学与技术",
                DegreeType = "工学学士",
                CollegeId = college.Id
            };
            var administrativeClass = new AdministrativeClass
            {
                Code = "CS2026-1",
                Name = "计科 2026-1 班",
                MajorId = major.Id,
                Grade = 2026
            };
            var student = new Student
            {
                StudentNumber = "202600001",
                Name = "张明",
                Gender = Gender.Male,
                AdministrativeClassId = administrativeClass.Id,
                EnrollmentYear = 2026,
                EnrollmentDate = new DateOnly(2026, 9, 1),
                Status = StudentStatus.Active,
                UserId = studentUser.Id
            };
            var term = new AcademicTerm
            {
                Code = "2026-1",
                Name = "2026-2027 学年第一学期",
                AcademicYear = "2026-2027",
                Season = TermSeason.Autumn,
                StartDate = new DateOnly(2026, 9, 1),
                EndDate = new DateOnly(2027, 1, 20)
            };
            var course = new Course
            {
                Code = "CS101",
                Name = "程序设计基础",
                CollegeId = college.Id,
                Credits = 4,
                TotalHours = 64,
                LectureHours = 48,
                PracticeHours = 16
            };
            var task = new TeachingTask
            {
                TaskNumber = "2026-CS101-01",
                Name = "程序设计基础 01 班",
                AcademicTermId = term.Id,
                CourseId = course.Id,
                Capacity = 40,
                Status = TeachingTaskStatus.Closed
            };
            var published = new GradeSheet
            {
                TeachingTaskId = task.Id,
                Status = GradeSheetStatus.Published,
                Records =
                [
                    new GradeRecord
                    {
                        StudentId = student.Id,
                        TotalScore = 88,
                        GradePoint = 3.7m
                    }
                ]
            };
            var draftTask = new TeachingTask
            {
                TaskNumber = "2026-CS102-01",
                Name = "未发布课程",
                AcademicTermId = term.Id,
                CourseId = course.Id,
                Capacity = 40
            };
            var draft = new GradeSheet
            {
                TeachingTaskId = draftTask.Id,
                Status = GradeSheetStatus.Approved,
                Records =
                [
                    new GradeRecord
                    {
                        StudentId = student.Id,
                        TotalScore = 99,
                        GradePoint = 4m
                    }
                ]
            };
            db.AddRange(manager, studentUser, college, major, administrativeClass, student, term, course, task,
                published, draftTask, draft);
            await db.SaveChangesAsync();
            return new Fixture(connection, db, manager, studentUser, student, CreateOptions());
        }

        public async Task<OfficialDocument> IssueAndSaveAsync()
        {
            var document = await Service.CreateAsync(
                Student.Id,
                OfficialDocumentType.Transcript,
                null,
                Manager.Id,
                Manager.DisplayName,
                "https://jw.example.edu",
                null,
                CancellationToken.None);
            Db.OfficialDocuments.Add(document);
            await Db.SaveChangesAsync();
            return document;
        }

        public OfficialDocumentsController CreateController()
            => CreateController(new TestDataScope(Manager.Id));

        public OfficialDocumentsController CreateStudentController(Guid? userId = null)
            => CreateController(new TestStudentDataScope(userId ?? StudentUser.Id));

        private OfficialDocumentsController CreateController(ICurrentUserDataScope dataScope)
        {
            var controller = new OfficialDocumentsController(
                Db,
                dataScope,
                Service,
                Options.Create(options));
            controller.ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext()
            };
            controller.Request.Scheme = "https";
            controller.Request.Host = new HostString("jw.example.edu");
            controller.HttpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback;
            return controller;
        }

        public async ValueTask DisposeAsync()
        {
            await Db.DisposeAsync();
            await connection.DisposeAsync();
        }
    }

    private sealed class TestDataScope(Guid managerId) : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = new(
            managerId,
            "教务管理员",
            null,
            DataScope.All,
            new HashSet<string>([SystemRoles.AcademicAdmin]));
    }

    private sealed class TestStudentDataScope(Guid studentUserId) : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = new(
            studentUserId,
            "张明",
            null,
            DataScope.Self,
            new HashSet<string>([SystemRoles.Student]));
    }
}
