using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.ContractSigningPlans;
using SolutionErp.Infrastructure.Services;

namespace SolutionErp.Infrastructure.Tests.Common;

// [W1 KHKK — S161 2026-07-29] Bẫy bảng cho module "Kế hoạch ký kết HĐ" (Mig 69,
// 7 CreateTable). Test-AFTER (feature mới, `docs/rules.md §7`) + phép đo A6 cho
// format mã phiếu mà owner chốt @S161.
//
// ⚠️ SPEC-DRIFT ĐÃ KHAI (test theo CODE, luật S34 — code là single source of truth):
//   Đề bài giao việc mô tả nhóm AuditableEntity là "header/Line/DossierItem/
//   LevelOpinion/Attachment ... có IsDeleted" ⇒ ngụ ý CẢ 5 đều phải có query filter.
//   ĐĨA nói khác: `ContractSigningPlanLevelOpinion` LÀ AuditableEntity NHƯNG
//   **CỐ Ý KHÔNG HasQueryFilter**, lý do viết sẵn 2 chỗ:
//     - `Domain/ContractSigningPlans/ContractSigningPlanLevelOpinion.cs:20-23`
//     - `Persistence/Configurations/ContractSigningPlanLevelOpinionConfiguration.cs:12-16`
//   "site dọn chữ ký MỒ CÔI (ApprovalWorkflowV2AdminFeatures ExecuteDeleteAsync) phải
//   THẤY được row đã soft-delete, nếu bị filter che thì row mồ côi sống sót và FK
//   Restrict lại nổ 500 lúc admin xoá Cấp."
//   ⇒ Test KHÔNG hạ chuẩn: assert LevelOpinion **KHÔNG có filter** như invariant CÓ
//   CHỦ Ý. Ai thêm filter về sau ⇒ test ĐỎ ⇒ buộc đọc lại lý do, không lỡ tay.
//
// Vì sao đọc metadata EF model chứ không đọc file config: query filter + cột audit là
// thứ RUNTIME dùng; đọc model = đo đúng cái chạy thật (config quên `AddConfiguration`
// hoặc quên DbSet vẫn "trông đúng" nếu chỉ đọc file).
public class ContractSigningPlanSchemaTests
{
    private const string ModuleNamespace = "SolutionErp.Domain.ContractSigningPlans";

    // Bộ kỳ vọng ĐO TỪ ĐĨA 2026-07-29 (entity + configuration), KHÔNG suy từ spec.
    private sealed record TableSpec(
        Type Clr, string Table, bool SoftDelete, bool QueryFilter, string Why);

    private static readonly TableSpec[] Expected =
    [
        new(typeof(ContractSigningPlan), "ContractSigningPlans", true, true,
            "header AuditableEntity — HasQueryFilter(!IsDeleted) `ContractSigningPlanConfiguration.cs:47`"),
        new(typeof(ContractSigningPlanLine), "ContractSigningPlanLines", true, true,
            "dòng giá per-NCC AuditableEntity — filter `ContractSigningPlanLineConfiguration.cs:36`"),
        new(typeof(ContractSigningPlanDossierItem), "ContractSigningPlanDossierItems", true, true,
            "căn cứ AuditableEntity — filter `ContractSigningPlanDossierItemConfiguration.cs:29`"),
        new(typeof(ContractSigningPlanAttachment), "ContractSigningPlanAttachments", true, true,
            "file đính kèm AuditableEntity — filter `ContractSigningPlanAttachmentConfiguration.cs:39`"),
        new(typeof(ContractSigningPlanLevelOpinion), "ContractSigningPlanLevelOpinions", true, false,
            "AuditableEntity NHƯNG CỐ Ý KHÔNG filter — site dọn chữ ký MỒ CÔI phải THẤY row "
            + "soft-deleted, che đi thì FK Restrict nổ 500 lúc admin xoá Cấp "
            + "(ContractSigningPlanLevelOpinion.cs:20-23). Thêm filter = ĐỎ CÓ CHỦ Ý."),
        new(typeof(ContractSigningPlanChangelog), "ContractSigningPlanChangelogs", false, false,
            "BaseEntity append-only audit — không có IsDeleted nên KHÔNG filter (parity PE/Contract)"),
        new(typeof(ContractSigningPlanApproval), "ContractSigningPlanApprovals", false, false,
            "BaseEntity append-only lịch sử transition — không có IsDeleted nên KHÔNG filter"),
    ];

    // ===================================================================
    // (1) Tên PIN theo spec §③-B: bẫy bảng — audit fields + query filter.
    // ===================================================================
    [Fact]
    public void BayBang_MoiBang_CoAuditFields_VaQueryFilter()
    {
        using var fix = new SqliteDbFixture();

        var mapped = fix.Db.Model.GetEntityTypes()
            .Where(t => t.ClrType.Namespace == ModuleNamespace)
            .ToDictionary(t => t.ClrType.Name);

        // (0) Chốt BỘ bảng: bảng thứ 8 thêm vào module mà không khai ở đây ⇒ ĐỎ.
        //     Cũng bắt luôn ca "viết Configuration nhưng quên DbSet / quên đăng ký"
        //     (entity không vào model ⇒ thiếu key ⇒ ĐỎ).
        mapped.Keys.Should().BeEquivalentTo(
            Expected.Select(e => e.Clr.Name),
            "module KHKK (Mig 69) = ĐÚNG 7 bảng; thêm/bớt bảng phải cập nhật invariant này");

        foreach (var spec in Expected)
        {
            var et = mapped[spec.Clr.Name];

            et.GetTableName().Should().Be(spec.Table,
                "tên bảng vật lý là hợp đồng với migration + SQL tay của ops ({0})", spec.Clr.Name);

            // (a) 4 cột audit BaseEntity — MỌI bảng, không ngoại lệ.
            foreach (var col in new[] { "CreatedAt", "UpdatedAt", "CreatedBy", "UpdatedBy" })
            {
                et.FindProperty(col).Should().NotBeNull(
                    "{0} phải map cột audit {1} (BaseEntity)", spec.Table, col);
            }

            // (b) bộ 3 soft-delete — CHỈ bảng AuditableEntity, và bảng BaseEntity
            //     phải KHÔNG có (assert 2 chiều: thiếu-thì-đỏ ∧ thừa-thì-cũng-đỏ).
            foreach (var col in new[] { "IsDeleted", "DeletedAt", "DeletedBy" })
            {
                if (spec.SoftDelete)
                {
                    et.FindProperty(col).Should().NotBeNull(
                        "{0} là AuditableEntity nên phải map {1}", spec.Table, col);
                }
                else
                {
                    et.FindProperty(col).Should().BeNull(
                        "{0} là BaseEntity (append-only) — có {1} là sai thiết kế", spec.Table, col);
                }
            }

            // (c) query filter — đo trên MODEL đang chạy, không đọc file config.
            var hasFilter = ((IReadOnlyEntityType)et).GetDeclaredQueryFilters().Any();
            hasFilter.Should().Be(spec.QueryFilter, spec.Why);
        }

        // (d) Neo chống vacuous: bảng CÓ filter thì filter phải THẬT SỰ chặn row
        //     soft-deleted (metadata "có filter" mà biểu thức sai vẫn đọc là xanh).
        var plan = new ContractSigningPlan
        {
            Id = Guid.NewGuid(),
            PurchaseEvaluationId = Guid.NewGuid(),
            ProjectId = Guid.NewGuid(),
            DrafterUserId = Guid.NewGuid(),
            IsDeleted = true,
        };
        fix.Db.ContractSigningPlans.Add(plan);
        fix.Db.SaveChanges();

        fix.Db.ContractSigningPlans.AsNoTracking().Count(x => x.Id == plan.Id)
            .Should().Be(0, "query filter phải GIẤU phiếu đã xoá mềm");
        fix.Db.ContractSigningPlans.IgnoreQueryFilters().AsNoTracking().Count(x => x.Id == plan.Id)
            .Should().Be(1, "row vẫn nằm trong DB — chỉ bị filter che, KHÔNG bị xoá cứng");
    }

    // ===================================================================
    // (2) Tên PIN theo spec §③-B (phép đo A6 — owner chốt format @S161):
    //     KHKK/{YYYY}/{Seq:D3}.
    // Chạy trên SQLite dù generator mở transaction IsolationLevel.Serializable —
    // tiền lệ `SqliteDbFixture.cs:36-42` + `PurchaseEvaluationCodeGeneratorTests.cs:17`
    // (provider map enum gracefully). KHÔNG đụng production code.
    // ===================================================================
    [Fact]
    public async Task SigningPlanCodeGenerator_Format_KhkkYearSeqD3()
    {
        using var fix = new SqliteDbFixture();
        var clock = new FixedDateTime(new DateTime(2026, 7, 29, 3, 0, 0, DateTimeKind.Utc));
        var gen = new ContractSigningPlanCodeGenerator(fix.Db, clock);

        var code1 = await gen.GenerateAsync();
        var code2 = await gen.GenerateAsync();

        // Hình dạng theo acceptance spec §③-B.
        code1.Should().MatchRegex(@"^KHKK/\d{4}/\d{3}$");
        code2.Should().MatchRegex(@"^KHKK/\d{4}/\d{3}$");

        // Giá trị TUYỆT ĐỐI — regex một mình vẫn xanh nếu seq đứng yên ở 001.
        code1.Should().Be("KHKK/2026/001");
        code2.Should().Be("KHKK/2026/002");

        // Seq lần 2 = lần 1 + 1, đo bằng SỐ (không chỉ so chuỗi).
        static int Seq(string code) => int.Parse(code.Split('/')[2]);
        Seq(code2).Should().Be(Seq(code1) + 1, "mỗi lần gen phải tiến đúng 1 nấc");

        // Sequence sống ở bảng DÙNG CHUNG WorkflowAppCodeSequences (spec §②-9),
        // KHÔNG bảng riêng — prefix per-năm là PK.
        var row = fix.Db.WorkflowAppCodeSequences.Single(s => s.Prefix == "KHKK/2026");
        row.LastSeq.Should().Be(2, "2 lần gen ⇒ LastSeq = 2 trên đúng prefix 'KHKK/2026'");

        // {YYYY} phải LIVE theo đồng hồ, không hardcode — `\d{4}` không phân biệt được
        // hai thứ đó, nên đẩy đồng hồ sang năm sau và đòi seq reset.
        clock.UtcNow = new DateTime(2027, 1, 4, 3, 0, 0, DateTimeKind.Utc);
        var code2027 = await gen.GenerateAsync();
        code2027.Should().Be("KHKK/2027/001", "sang năm ⇒ prefix mới ⇒ seq reset về 001");

        fix.Db.WorkflowAppCodeSequences.Single(s => s.Prefix == "KHKK/2026").LastSeq
            .Should().Be(2, "hàng 2026 KHÔNG bị đụng khi sang năm");
    }

    // ===================================================================
    // (3) gotcha #57 (7× tái phát: Mig 43/45/47/64) — UNIQUE (PlanId, SupplierId)
    //     PHẢI filtered `WHERE IsDeleted = 0`. Không filter ⇒ xoá mềm 1 NCC rồi
    //     thêm lại chính NCC đó sẽ nổ UNIQUE (SQLite error 19).
    //     Rủi ro này spec liệt ở §③-C.
    // ===================================================================
    [Fact]
    public async Task Line_UniqueFilteredTheoIsDeleted_ChoXoaMemRoiThemLaiCungNcc()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var plan = new ContractSigningPlan
        {
            Id = Guid.NewGuid(),
            PurchaseEvaluationId = Guid.NewGuid(),
            ProjectId = Guid.NewGuid(),
            DrafterUserId = Guid.NewGuid(),
        };
        db.ContractSigningPlans.Add(plan);

        var supplierId = Guid.NewGuid();
        // [K2 Mig 71] UNIQUE nay 3 cột (PlanId, SupplierId, CatalogEntryId). CẢ HAI vế của test
        // dùng CatalogEntryId NON-NULL GIỐNG NHAU để phép đo độc lập provider: SQLite coi
        // NULL ≠ NULL trong unique (cặp NULL không nổ) còn SQL Server coi bằng nhau — cặp NULL
        // do SUBMIT-GUARD (WorkflowService) chặn ở tầng nghiệp vụ, không đo ở đây.
        var catalogId = Guid.NewGuid();           // loose-Guid — index test không cần row danh mục thật
        db.ContractSigningPlanLines.Add(new ContractSigningPlanLine
        {
            Id = Guid.NewGuid(),
            ContractSigningPlanId = plan.Id,
            SupplierId = supplierId,
            CatalogEntryId = catalogId,
            PeReferenceAmount = 100_000_000m,
            ProposedAmount = 95_000_000m,
        });
        await db.SaveChangesAsync();

        // Xoá MỀM dòng NCC đó (app-layer set cờ — TestApplicationDbContext KHÔNG wire
        // AuditingInterceptor nên `Remove()` sẽ là xoá CỨNG, không đo được điều cần đo).
        var line1 = await db.ContractSigningPlanLines.SingleAsync(x => x.SupplierId == supplierId);
        line1.IsDeleted = true;
        line1.DeletedAt = DateTime.UtcNow;
        await db.SaveChangesAsync();

        // Thêm LẠI đúng NCC đó vào đúng phiếu đó — đây là ca thật (BCH bấm nhầm, xoá,
        // thêm lại). Index không filter ⇒ ném DbUpdateException tại đây.
        db.ContractSigningPlanLines.Add(new ContractSigningPlanLine
        {
            Id = Guid.NewGuid(),
            ContractSigningPlanId = plan.Id,
            SupplierId = supplierId,
            CatalogEntryId = catalogId,           // trùng cả 3 cột với dòng đã XOÁ MỀM
            PeReferenceAmount = 100_000_000m,
            ProposedAmount = 90_000_000m,
        });

        var act = async () => await db.SaveChangesAsync();
        await act.Should().NotThrowAsync(
            "UNIQUE (PlanId, SupplierId, CatalogEntryId) phải là filtered `WHERE [IsDeleted] = 0` — gotcha #57");

        db.ContractSigningPlanLines.AsNoTracking()
            .Count(x => x.ContractSigningPlanId == plan.Id)
            .Should().Be(1, "chỉ 1 dòng NCC còn sống sau khi xoá mềm + thêm lại");

        db.ContractSigningPlanLines.IgnoreQueryFilters().AsNoTracking()
            .Count(x => x.ContractSigningPlanId == plan.Id)
            .Should().Be(2, "row cũ vẫn nằm đó (xoá mềm), tổng 2 row vật lý");

        // 🔴 VẾ ĐỐI CHỨNG — bắt buộc, nếu không cả test trên là VACUOUS:
        // "không ném" cũng đúng trong thế giới mà UNIQUE index KHÔNG HỀ ĐƯỢC TẠO
        // (EnsureCreated nuốt filter lạ / index rơi mất). Vế này chứng index CÓ RĂNG:
        // 2 dòng CÙNG (PlanId, SupplierId) mà cả 2 còn SỐNG ⇒ PHẢI nổ.
        var plan2 = new ContractSigningPlan
        {
            Id = Guid.NewGuid(),
            PurchaseEvaluationId = Guid.NewGuid(),
            ProjectId = Guid.NewGuid(),
            DrafterUserId = Guid.NewGuid(),
        };
        db.ContractSigningPlans.Add(plan2);
        var dupSupplierId = Guid.NewGuid();
        var dupCatalogId = Guid.NewGuid();
        db.ContractSigningPlanLines.Add(new ContractSigningPlanLine
        {
            Id = Guid.NewGuid(),
            ContractSigningPlanId = plan2.Id,
            SupplierId = dupSupplierId,
            CatalogEntryId = dupCatalogId,
            PeReferenceAmount = 1m,
            ProposedAmount = 1m,
        });
        await db.SaveChangesAsync();              // dòng 1 sống — mốc đối chứng

        db.ContractSigningPlanLines.Add(new ContractSigningPlanLine
        {
            Id = Guid.NewGuid(),
            ContractSigningPlanId = plan2.Id,
            SupplierId = dupSupplierId,
            CatalogEntryId = dupCatalogId,        // TRÙNG cả 3 cột, cả 2 còn SỐNG
            PeReferenceAmount = 1m,
            ProposedAmount = 1m,
        });
        var actDup = async () => await db.SaveChangesAsync();
        await actDup.Should().ThrowAsync<DbUpdateException>(
            "2 dòng SỐNG cùng (PlanId, SupplierId, CatalogEntryId) phải bị UNIQUE chặn — vế này chứng "
            + "index thật sự tồn tại, nên vế 'xoá mềm rồi thêm lại KHÔNG nổ' ở trên "
            + "là do FILTER chứ không phải do thiếu index");
        db.ChangeTracker.Clear();

        // [K2 Mig 71] Vế MỞ — chính tính năng K2: CÙNG (Plan, NCC) nhưng KHÁC hạng mục = 2 dòng
        // hợp lệ (tách 1 NCC theo nhiều hạng mục). Unique 2 cột cũ CẤM ca này — nay phải QUA.
        db.ContractSigningPlanLines.Add(new ContractSigningPlanLine
        {
            Id = Guid.NewGuid(),
            ContractSigningPlanId = plan2.Id,
            SupplierId = dupSupplierId,
            CatalogEntryId = Guid.NewGuid(),      // hạng mục KHÁC ⇒ được phép
            PeReferenceAmount = 1m,
            ProposedAmount = 1m,
        });
        var actSplit = async () => await db.SaveChangesAsync();
        await actSplit.Should().NotThrowAsync(
            "unique 3 cột phải CHO PHÉP cùng (Plan, NCC) khác CatalogEntryId — mục đích của Mig 71");
    }

    // ===================================================================
    // (4) Slot enum PIN = 10. `ApplicableType` lưu bằng int (HasConversion<int>,
    //     `ApprovalWorkflowConfiguration.cs:17`) ⇒ đánh số lại = quy trình đã pin
    //     trong DB trỏ sang module KHÁC, hỏng im lặng. Đây là hằng số ĐÃ GHI XUỐNG DB.
    // ===================================================================
    [Fact]
    public void ApplicableType_ContractSigningPlan_PinSlot10_KhongTrungSlot()
    {
        ((int)ApprovalWorkflowApplicableType.ContractSigningPlan).Should().Be(10,
            "spec W1 §②-3 pin slot 10; số này ĐÃ nằm trong DB (HasConversion<int>) nên "
            + "đổi = phiếu/quy trình cũ trỏ sai module");

        Enum.GetValues<ApprovalWorkflowApplicableType>()
            .Count(v => (int)v == 10)
            .Should().Be(1, "không được có 2 module cùng chiếm slot 10 (append-only enum)");
    }
}
