using System.Reflection;
using System.Text.RegularExpressions;
using ClosedXML.Excel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using SolutionErp.Application.Master.Suppliers.Dtos;
using SolutionErp.Domain.Master;
using SolutionErp.Infrastructure.Services;
using SolutionErp.Infrastructure.Tests.Common;

namespace SolutionErp.Infrastructure.Tests.Services;

// ============================================================================
// Supplier Phase B — SupplierExcelImportService (import NCC từ file "Database NCC").
// TEST-BEFORE-MERGE: CRITICAL-ALGO (parser layout-locked + upsert) + BUG-FIX class
// (case-collation dedup) → tests bắt buộc trước commit (docs/rules.md §7).
//
// Test theo CODE trên đĩa (S34 single-source-of-truth). KHÔNG sửa production code —
// nếu test lộ bug prod thật → REPORT em main, không tự fix.
//
// 🔴 BUG-CLASS (Case 1): unique-index Code ở SQL Server = case-INSENSITIVE. Nếu service
//    so Ordinal, "truonggiang" (file) ≠ "TRUONGGIANG" (DB) → miss → quyết INSERT →
//    ConfirmAsync nổ unique-violation → 500 cả batch. Service dùng OrdinalIgnoreCase ở
//    cả preview + confirm để chặn. Test chứng minh dedup CI hoạt động (chỉ 1 NCC, update
//    tại chỗ, không insert hàng thứ 2).
//    LƯU Ý collation: SQLite (fixture) mặc định BINARY (case-SENSITIVE) → DB-level unique
//    KHÔNG bắt "truonggiang" vs "TRUONGGIANG". Đó chính là điểm test: dedup phải nằm ở
//    SERVICE (in-memory OrdinalIgnoreCase), KHÔNG dựa DB — nếu dựa DB thì prod SQL Server
//    sẽ ném 500. Test này xanh chứng tỏ service không dựa DB.
//
// ⚠️ SPEC-DRIFT đã biết (không phải bug tôi phát hiện): service.ExpectedHeaderTokens hiện
//    là BEST-GUESS, CHƯA khớp 30 token row-4 thật của file (sẽ sửa ở close-review adjust).
//    → Các test cần "valid layout" (Case 4-7) DỰNG header từ chính ExpectedHeaderTokens
//    của service (đọc qua REFLECTION) nên LUÔN đồng bộ — không vỡ khi tokens được sửa.
//    → Case 9 (PreviewAsync_RealFileRow4Tokens...) nạp 30 token THẬT và khẳng định
//    PreviewAsync đồng ý với so-khớp-fingerprint trực tiếp: hiện tại lệch → LayoutValid=false;
//    khi tokens sửa = token thật → tự động khẳng định LayoutValid=true (không phải sửa test).
// ============================================================================
public class SupplierExcelImportServiceTests
{
    private const int HeaderRow = 4;

    private static SupplierExcelImportService NewService(TestApplicationDbContext db)
        => new(db, NullLogger<SupplierExcelImportService>.Instance);

    // ========================================================================
    // Case 1 (NON-NEGOTIABLE) — case-collation dedup qua ConfirmAsync.
    // ========================================================================
    [Fact]
    public async Task ConfirmAsync_LowercaseCode_MatchesExistingUppercase_UpdatesInPlace_NoDuplicateInsert()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        db.Suppliers.Add(new Supplier
        {
            Code = "TRUONGGIANG",
            Name = "Công ty TNHH Trường Giang",
            Type = SupplierType.NhaCungCap,
            Phone = null,      // để trống → fill-nulls có việc làm, chứng minh cùng-1-row bị update
            Address = null,
        });
        await db.SaveChangesAsync(CancellationToken.None);

        var rows = new List<SupplierImportRowDto>
        {
            new() { RowIndex = 5, Code = "truonggiang", Name = "Trường Giang", Phone = "0909", Address = "123 Hà Nội" },
        };

        var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None);

        result.Committed.Should().BeTrue();
        result.Inserted.Should().Be(0, "Code chữ thường khớp CI với NCC đã có → KHÔNG insert bản ghi mới");
        result.Updated.Should().Be(1, "khớp case-insensitive → fill-nulls update tại chỗ");

        (await db.Suppliers.CountAsync()).Should().Be(1,
            "chỉ đúng 1 NCC — dedup OrdinalIgnoreCase chặn hàng thứ 2 (nếu so Ordinal → prod SQL Server nổ unique-violation 500)");
        (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(1, "không có bản ghi phantom nào kể cả đã soft-delete");

        var only = await db.Suppliers.SingleAsync();
        only.Code.Should().Be("TRUONGGIANG", "Code gốc giữ nguyên hoa/thường — update entity cũ, không thay bằng entity mới");
        only.Phone.Should().Be("0909", "fill-null: Phone đang null → được điền (chứng minh chính row cũ bị update)");
        only.Address.Should().Be("123 Hà Nội", "fill-null: Address đang null → được điền");
    }

    // ========================================================================
    // Case 1b — PreviewAsync phân loại hàng chữ thường là Update (không phải New).
    // ========================================================================
    [Fact]
    public async Task PreviewAsync_LowercaseCode_OfExistingUppercase_ClassifiedAsUpdate_NotNew()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        db.Suppliers.Add(new Supplier
        {
            Code = "TRUONGGIANG",
            Name = "Công ty Trường Giang",
            Type = SupplierType.NhaCungCap,
        });
        await db.SaveChangesAsync(CancellationToken.None);

        using var stream = BuildWorkbook(ReflectExpectedHeaderTokens(), ws =>
        {
            ws.Cell(5, 3).Value = "NCC";
            ws.Cell(5, 4).Value = "truonggiang";
            ws.Cell(5, 5).Value = "Trường Giang";
        });

        var preview = await NewService(db).PreviewAsync(stream, CancellationToken.None);

        preview.LayoutValid.Should().BeTrue();
        var row = preview.Rows.Single();
        row.Status.Should().Be(RowImportStatus.Update, "Code chữ thường khớp CI với 'TRUONGGIANG' → Update, KHÔNG phải New");
        row.ExistingSupplierId.Should().NotBeNull("Update phải trỏ tới NCC đã tồn tại");
        preview.UpdateCount.Should().Be(1);
        preview.NewCount.Should().Be(0);
    }

    // ========================================================================
    // Case 2 (NON-NEGOTIABLE) — fill-nulls SAFE (decision 4): không đè non-null, chỉ điền null.
    // ========================================================================
    [Fact]
    public async Task ConfirmAsync_FillNulls_DoesNotOverwriteNonNull_FillsOnlyNull()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        db.Suppliers.Add(new Supplier
        {
            Code = "X",
            Name = "X Co",
            Type = SupplierType.NhaCungCap,
            Phone = "0900",    // non-null → PHẢI giữ nguyên
            Address = null,    // null → được điền
        });
        await db.SaveChangesAsync(CancellationToken.None);

        var rows = new List<SupplierImportRowDto>
        {
            new() { RowIndex = 5, Code = "X", Name = "X Co", Phone = "9999", Address = "Hà Nội" },
        };

        var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None);

        result.Updated.Should().Be(1);
        result.Committed.Should().BeTrue();

        var x = await db.Suppliers.SingleAsync();
        x.Phone.Should().Be("0900", "Phone đang non-null KHÔNG bị đè bởi '9999' (fill-nulls SAFE)");
        x.Address.Should().Be("Hà Nội", "Address đang null → được điền");
    }

    // ========================================================================
    // Case 3 (NON-NEGOTIABLE) — all-or-nothing: 1 hard-error → abort cả batch, DB không đổi.
    // ========================================================================
    [Fact]
    public async Task ConfirmAsync_AnyHardErrorRow_AbortsWholeBatch_NothingWritten()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var rows = new List<SupplierImportRowDto>
        {
            new() { RowIndex = 5, Code = "A", Name = "Alpha" },   // hợp lệ
            new() { RowIndex = 6, Code = "B", Name = "  " },      // hard-error: thiếu Name
        };

        var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None);

        result.Committed.Should().BeFalse("có hard-error → all-or-nothing abort, KHÔNG ghi gì");
        result.Errors.Should().NotBeEmpty("phải liệt kê dòng lỗi");
        result.Inserted.Should().Be(0);
        (await db.Suppliers.IgnoreQueryFilters().CountAsync())
            .Should().Be(0, "kể cả hàng hợp lệ 'A' cũng KHÔNG được ghi (all-or-nothing)");
    }

    // ========================================================================
    // Case 4 (decision 5) — PHÂN LOẠI lạ → Type mặc định NhaCungCap + cảnh báo; type biết → map đúng.
    // Đi qua PARSE path (MapType private, chỉ chạy khi parse xlsx).
    // ========================================================================
    [Fact]
    public async Task PreviewAsync_UnrecognizedType_DefaultsToNhaCungCap_WithWarning_KnownTypeStillMapped()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        using var stream = BuildWorkbook(ReflectExpectedHeaderTokens(), ws =>
        {
            // row 5 — PHÂN LOẠI không nhận dạng
            ws.Cell(5, 3).Value = "Loại không xác định";
            ws.Cell(5, 4).Value = "U1";
            ws.Cell(5, 5).Value = "NCC lạ";
            // row 6 — "Cả hai" (positive control: mapper KHÔNG phải luôn default)
            ws.Cell(6, 3).Value = "Cả hai";
            ws.Cell(6, 4).Value = "U2";
            ws.Cell(6, 5).Value = "NCC cả hai";
        });

        var preview = await NewService(db).PreviewAsync(stream, CancellationToken.None);

        preview.LayoutValid.Should().BeTrue();
        preview.Rows.Should().HaveCount(2);

        var r5 = preview.Rows.Single(x => x.RowIndex == 5);
        r5.Type.Should().Be(SupplierType.NhaCungCap, "PHÂN LOẠI lạ → mặc định NCC (decision 5, KHÔNG reject)");
        r5.Messages.Should().Contain(m => m.Contains("không nhận dạng"),
            "có cảnh báo mềm — chứng minh đã đi nhánh unknown, không chỉ là default của DTO");

        var r6 = preview.Rows.Single(x => x.RowIndex == 6);
        r6.Type.Should().Be(SupplierType.CaHai, "positive control: 'Cả hai' map đúng CaHai");
    }

    // ========================================================================
    // Case 5 — strip emoji đầu chuỗi tình trạng: "✅ Đang hoạt động" → DangHoatDong (parse → persist).
    // ========================================================================
    [Fact]
    public async Task PreviewThenConfirm_StatusWithEmojiPrefix_StripsAndPersistsDangHoatDong()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        using var stream = BuildWorkbook(ReflectExpectedHeaderTokens(), ws =>
        {
            ws.Cell(5, 4).Value = "EMO1";
            ws.Cell(5, 5).Value = "NCC Emoji";
            ws.Cell(5, 27).Value = "✅ Đang hoạt động";
        });

        var svc = NewService(db);
        var preview = await svc.PreviewAsync(stream, CancellationToken.None);

        preview.LayoutValid.Should().BeTrue();
        var row = preview.Rows.Single();
        row.SupplierStatus.Should().Be(SupplierStatus.DangHoatDong,
            "emoji + khoảng trắng đầu bị strip → còn 'Đang hoạt động'");

        var result = await svc.ConfirmAsync(preview.Rows, "tester", CancellationToken.None);
        result.Inserted.Should().Be(1);

        var persisted = await db.Suppliers.SingleAsync();
        persisted.Status.Should().Be(SupplierStatus.DangHoatDong, "tình trạng đã strip được persist đúng");
    }

    // ========================================================================
    // Case 6 — valid layout (header từ ExpectedHeaderTokens qua reflection) → parse field
    // theo CHỈ SỐ CỘT TUYỆT ĐỐI; LinkGuq giữ RAW backslash NAS.
    // ========================================================================
    [Fact]
    public async Task PreviewAsync_ValidLayout_ParsesFieldsByAbsoluteIndex_IncludingRawBackslashLink()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        const string nasLink = @"\\NAS-SOL\GUQ\truong-giang.pdf";
        using var stream = BuildWorkbook(ReflectExpectedHeaderTokens(), ws =>
        {
            ws.Cell(5, 2).Value = "Gói thầu A";
            ws.Cell(5, 3).Value = "NCC";
            ws.Cell(5, 4).Value = "TG-01";
            ws.Cell(5, 5).Value = "Công ty Trường Giang";
            ws.Cell(5, 16).Value = nasLink;
        });

        var preview = await NewService(db).PreviewAsync(stream, CancellationToken.None);

        preview.LayoutValid.Should().BeTrue(
            "header row-4 khớp ExpectedHeaderTokens (đọc qua reflection → luôn đồng bộ kể cả sau khi close-review sửa tokens)");
        preview.Rows.Should().ContainSingle();

        var row = preview.Rows.Single();
        row.PackageCategory.Should().Be("Gói thầu A", "col 2 → PackageCategory");
        row.Type.Should().Be(SupplierType.NhaCungCap, "col 3 'NCC' → NhaCungCap");
        row.Code.Should().Be("TG-01", "col 4 → Code");
        row.Name.Should().Be("Công ty Trường Giang", "col 5 → Name");
        row.LinkGuq.Should().Be(nasLink, "col 16 → LinkGuq giữ RAW backslash NAS — KHÔNG trim/normalize/escape");
        row.Status.Should().Be(RowImportStatus.New, "DB rỗng → hàng mới");
    }

    // ========================================================================
    // Case 6b — header sai → từ chối file (LayoutValid=false, Rows rỗng, có cảnh báo).
    // ========================================================================
    [Fact]
    public async Task PreviewAsync_WrongHeader_RejectsFile_LayoutInvalid_NoRows_WithWarning()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var wrongHeader = Enumerable.Range(1, 30).Select(i => $"CỘT SAI {i}").ToArray();
        using var stream = BuildWorkbook(wrongHeader, ws =>
        {
            ws.Cell(5, 4).Value = "X1";
            ws.Cell(5, 5).Value = "Sẽ bị bỏ vì layout sai";
        });

        var preview = await NewService(db).PreviewAsync(stream, CancellationToken.None);

        preview.LayoutValid.Should().BeFalse("fingerprint header không khớp → từ chối cả file");
        preview.Rows.Should().BeEmpty("KHÔNG map hàng nào để tránh lệch cột");
        preview.Warnings.Should().Contain(w => w.Contains("Sai layout"), "phải nêu lý do sai layout cho user");
    }

    // ========================================================================
    // Case 9 (spec-drift documentation) — 30 token row-4 THẬT của file "Database NCC".
    // Khẳng định PreviewAsync ĐỒNG Ý với so-khớp-fingerprint trực tiếp (real vs ExpectedHeaderTokens
    // hiện tại). Robust 2 chiều: hiện lệch → LayoutValid=false; sau khi tokens sửa = token thật →
    // tự động khẳng định LayoutValid=true (KHÔNG cần sửa test).
    // ========================================================================
    [Fact]
    public async Task PreviewAsync_RealFileRow4Tokens_FingerprintConsistentWithCurrentExpected()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        var real = RealFileHeaderTokens();
        var expected = ReflectExpectedHeaderTokens();
        real.Should().HaveCount(30, "sanity: đúng 30 cột row-4 file thật");

        // shouldMatch tính bằng CHÍNH cách service normalize (replicate NormalizeHeader) →
        // không thể mis-predict.
        bool shouldMatch = NormalizeJoin(real) == NormalizeJoin(expected);

        using var stream = BuildWorkbook(real, ws =>
        {
            ws.Cell(5, 4).Value = "RT-01";
            ws.Cell(5, 5).Value = "NCC token thật";
        });

        var preview = await NewService(db).PreviewAsync(stream, CancellationToken.None);

        preview.LayoutValid.Should().Be(shouldMatch,
            "PreviewAsync PHẢI đồng ý với so-khớp-fingerprint trực tiếp. Hiện tại ExpectedHeaderTokens là BEST-GUESS "
            + "≠ token thật → shouldMatch=false → LayoutValid=false. Khi close-review sửa ExpectedHeaderTokens = token thật "
            + "→ shouldMatch=true → test tự chuyển sang khẳng định LayoutValid=true (không cần sửa test).");

        if (!shouldMatch)
            preview.Rows.Should().BeEmpty("layout bị từ chối → không map hàng nào");
    }

    // ========================================================================
    // Case 7 — chỉ số cột tuyệt đối: ô rỗng giữa (FAX col 9) KHÔNG làm dồn cột sau; literal #REF! → null.
    // ========================================================================
    [Fact]
    public async Task PreviewAsync_EmptyMiddleCell_NoColumnDrift_AndErrorLiteralBecomesNull()
    {
        using var fix = new SqliteDbFixture();
        var db = fix.Db;

        using var stream = BuildWorkbook(ReflectExpectedHeaderTokens(), ws =>
        {
            ws.Cell(5, 4).Value = "DRIFT-1";
            ws.Cell(5, 5).Value = "NCC drift";
            ws.Cell(5, 8).Value = "PHONE-0908";   // col 8  → Phone
            // col 9 (FAX) BỎ TRỐNG cố ý
            ws.Cell(5, 10).Value = "BANK-ALPHA";  // col 10 → BankAccount
            ws.Cell(5, 11).Value = "BANK-BETA";   // col 11 → SecondaryBankAccount
            ws.Cell(5, 12).Value = "TAX-3133";    // col 12 → TaxCode
            ws.Cell(5, 22).Value = "#REF!";       // col 22 → Email (literal lỗi Excel)
        });

        var preview = await NewService(db).PreviewAsync(stream, CancellationToken.None);

        preview.LayoutValid.Should().BeTrue();
        var row = preview.Rows.Single();

        row.Phone.Should().Be("PHONE-0908", "col 8 → Phone");
        row.Fax.Should().BeNull("ô FAX (col 9) rỗng → null");
        row.BankAccount.Should().Be("BANK-ALPHA", "chỉ số cột tuyệt đối → col 10 KHÔNG dồn trái dù col 9 rỗng");
        row.SecondaryBankAccount.Should().Be("BANK-BETA", "col 11 vẫn đúng chỗ");
        row.TaxCode.Should().Be("TAX-3133",
            "col 12 vẫn rơi đúng TaxCode — không lệch cột (bug-class 'CellsUse() bỏ ô rỗng' đã tránh)");
        row.Email.Should().BeNull("literal '#REF!' → null (ErrorLiterals guard)");
    }

    // ========================================================================
    // Helpers
    // ========================================================================

    // Đọc ExpectedHeaderTokens (private static readonly string[]) qua reflection → dựng header
    // đúng-với-service để test valid-layout luôn đồng bộ (kể cả sau khi tokens được sửa).
    private static string[] ReflectExpectedHeaderTokens()
    {
        var f = typeof(SupplierExcelImportService)
            .GetField("ExpectedHeaderTokens", BindingFlags.NonPublic | BindingFlags.Static);
        f.Should().NotBeNull("service phải có static ExpectedHeaderTokens (đọc qua reflection để test đồng bộ header)");
        return (string[])f!.GetValue(null)!;
    }

    // Dựng .xlsx in-memory: 30 header token ở row 4, data từ row 5 (writeData set từng ô tuyệt đối).
    private static MemoryStream BuildWorkbook(string[] headerRow4, Action<IXLWorksheet> writeData)
    {
        var ms = new MemoryStream();
        using (var wb = new XLWorkbook())
        {
            var ws = wb.AddWorksheet("Database NCC");
            for (int c = 1; c <= headerRow4.Length; c++)
                ws.Cell(HeaderRow, c).Value = headerRow4[c - 1];
            writeData(ws);
            wb.SaveAs(ms);
        }
        ms.Position = 0;
        return ms;
    }

    // Replicate y hệt SupplierExcelImportService.NormalizeHeader + join fingerprint → so khớp
    // deterministic (Case 9), không mis-predict hành vi service.
    private static string NormalizeJoin(IEnumerable<string> tokens)
    {
        static string Norm(string raw)
        {
            if (string.IsNullOrEmpty(raw)) return string.Empty;
            var collapsed = Regex.Replace(raw.Replace("\n", " ").Replace("\r", " "), @"\s+", " ");
            return collapsed.Trim().ToUpperInvariant();
        }
        return string.Join(" | ", tokens.Select(Norm));
    }

    // 30 token row-4 THẬT của file "Database NCC" (theo brief close-review). EXACT strings.
    private static string[] RealFileHeaderTokens() =>
    [
        "STT",                                                          // 1
        "GÓI THẦU",                                                     // 2
        "PHÂN LOẠI (NTP/NCC/Cả hai)",                                   // 3
        "TÊN VIẾT TẮT (Dùng trong HĐ)",                                 // 4
        "TÊN CÔNG TY (Đầy đủ, đúng pháp lý)",                           // 5
        "ĐỊA CHỈ XUẤT HÓA ĐƠN (Địa chỉ đăng ký kinh doanh)",           // 6
        "ĐỊA CHỈ VĂN PHÒNG (nếu có)",                                   // 7
        "SỐ ĐIỆN THOẠI CÔNG TY",                                        // 8
        "FAX",                                                          // 9
        "SỐ TÀI KHOẢN+ TÊN+CN. NGÂN HÀNG (Đầy đủ, đúng pháp lý)",       // 10
        "SỐ TK PHỤ (nếu có)",                                           // 11
        "MÃ SỐ THUẾ",                                                   // 12
        "NGƯỜI ĐẠI DIỆN PHÁP LUẬT",                                     // 13
        "CHỨC VỤ ĐẠI DIỆN",                                             // 14
        "GIẤY ỦY QUYỀN (số, ngày, người ủy quyền)",                     // 15
        "Link GUQ",                                                     // 16
        "Link GPKD",                                                    // 17
        "Link HSNL",                                                    // 18
        "NGƯỜI LIÊN HỆ CHÍNH",                                          // 19
        "CHỨC VỤ NGƯỜI LH",                                             // 20
        "SĐT CHÍNH",                                                    // 21
        "EMAIL",                                                        // 22
        "ĐỊA CHỈ GỬI THƯ",                                              // 23
        "NGƯỜI NHẬN THƯ/ SDT",                                          // 24
        "NGUỒN GIỚI THIỆU",                                             // 25
        "NGƯỜI PHỤ TRÁCH (PMH)",                                        // 26
        "TÌNH TRẠNG HIỆN TẠI",                                          // 27
        "GHI CHÚ / LÝ DO BLACKLIST",                                    // 28
        "NGÀY CẬP NHẬT CUỐI",                                           // 29
        "NGƯỜI CẬP NHẬT",                                               // 30
    ];
}
