using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Admin.Services;
using ZB.MOM.WW.OtOpcUa.Configuration;

namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;

[Trait("Category", "Unit")]
public sealed class EquipmentImportBatchServiceTests : IDisposable
{
    private readonly OtOpcUaConfigDbContext _db;
    private readonly EquipmentImportBatchService _svc;

    public EquipmentImportBatchServiceTests()
    {
        var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
            .UseInMemoryDatabase($"import-batch-{Guid.NewGuid():N}")
            .Options;
        _db = new OtOpcUaConfigDbContext(options);
        _svc = new EquipmentImportBatchService(_db);
    }

    public void Dispose() => _db.Dispose();

    // Unique SAPID per row — FinaliseBatch reserves ZTag + SAPID via filtered-unique index, so
    // two rows sharing a SAPID under different EquipmentUuids collide as intended.
    private static EquipmentCsvRow Row(string zTag, string name = "eq-1") => new()
    {
        ZTag = zTag,
        MachineCode = "mc",
        SAPID = $"sap-{zTag}",
        EquipmentId = "eq-id",
        EquipmentUuid = Guid.NewGuid().ToString(),
        Name = name,
        UnsAreaName = "area",
        UnsLineName = "line",
    };

    [Fact]
    public async Task CreateBatch_PopulatesId_AndTimestamp()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);

        batch.Id.ShouldNotBe(Guid.Empty);
        batch.CreatedAtUtc.ShouldBeGreaterThan(DateTime.UtcNow.AddMinutes(-1));
        batch.RowsStaged.ShouldBe(0);
    }

    [Fact]
    public async Task StageRows_AcceptedAndRejected_AllPersist()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);

        await _svc.StageRowsAsync(batch.Id,
            acceptedRows: [Row("z-1"), Row("z-2")],
            rejectedRows: [new EquipmentCsvRowError(LineNumber: 5, Reason: "duplicate ZTag")],
            CancellationToken.None);

        var reloaded = await _db.EquipmentImportBatches.Include(b => b.Rows).FirstAsync(b => b.Id == batch.Id);
        reloaded.RowsStaged.ShouldBe(3);
        reloaded.RowsAccepted.ShouldBe(2);
        reloaded.RowsRejected.ShouldBe(1);
        reloaded.Rows.Count.ShouldBe(3);
        reloaded.Rows.Count(r => r.IsAccepted).ShouldBe(2);
        reloaded.Rows.Single(r => !r.IsAccepted).RejectReason.ShouldBe("duplicate ZTag");
    }

    [Fact]
    public async Task DropBatch_RemovesBatch_AndCascades_Rows()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        await _svc.StageRowsAsync(batch.Id, [Row("z-1")], [], CancellationToken.None);

        await _svc.DropBatchAsync(batch.Id, CancellationToken.None);

        (await _db.EquipmentImportBatches.AnyAsync(b => b.Id == batch.Id)).ShouldBeFalse();
        (await _db.EquipmentImportRows.AnyAsync(r => r.BatchId == batch.Id)).ShouldBeFalse("cascaded delete clears rows");
    }

    [Fact]
    public async Task DropBatch_AfterFinalise_Throws()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        await _svc.StageRowsAsync(batch.Id, [Row("z-1")], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(batch.Id, generationId: 1, driverInstanceIdForRows: "drv-1", unsLineIdForRows: "line-1", CancellationToken.None);

        await Should.ThrowAsync<ImportBatchAlreadyFinalisedException>(
            () => _svc.DropBatchAsync(batch.Id, CancellationToken.None));
    }

    [Fact]
    public async Task Finalise_AcceptedRows_BecomeEquipment()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        await _svc.StageRowsAsync(batch.Id,
            [Row("z-1", name: "alpha"), Row("z-2", name: "beta")],
            rejectedRows: [new EquipmentCsvRowError(1, "rejected")],
            CancellationToken.None);

        await _svc.FinaliseBatchAsync(batch.Id, 5, "drv-modbus", "line-warsaw", CancellationToken.None);

        var equipment = await _db.Equipment.Where(e => e.GenerationId == 5).ToListAsync();
        equipment.Count.ShouldBe(2);
        equipment.Select(e => e.Name).ShouldBe(["alpha", "beta"], ignoreOrder: true);
        equipment.All(e => e.DriverInstanceId == "drv-modbus").ShouldBeTrue();
        equipment.All(e => e.UnsLineId == "line-warsaw").ShouldBeTrue();

        var reloaded = await _db.EquipmentImportBatches.FirstAsync(b => b.Id == batch.Id);
        reloaded.FinalisedAtUtc.ShouldNotBeNull();
    }

    [Fact]
    public async Task Finalise_Twice_Throws()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        await _svc.StageRowsAsync(batch.Id, [Row("z-1")], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(batch.Id, 1, "drv", "line", CancellationToken.None);

        await Should.ThrowAsync<ImportBatchAlreadyFinalisedException>(
            () => _svc.FinaliseBatchAsync(batch.Id, 2, "drv", "line", CancellationToken.None));
    }

    [Fact]
    public async Task Finalise_MissingBatch_Throws()
    {
        await Should.ThrowAsync<ImportBatchNotFoundException>(
            () => _svc.FinaliseBatchAsync(Guid.NewGuid(), 1, "drv", "line", CancellationToken.None));
    }

    [Fact]
    public async Task Stage_After_Finalise_Throws()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        await _svc.StageRowsAsync(batch.Id, [Row("z-1")], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(batch.Id, 1, "drv", "line", CancellationToken.None);

        await Should.ThrowAsync<ImportBatchAlreadyFinalisedException>(
            () => _svc.StageRowsAsync(batch.Id, [Row("z-2")], [], CancellationToken.None));
    }

    [Fact]
    public async Task ListByUser_FiltersByCreator_AndFinalised()
    {
        var a = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        var b = await _svc.CreateBatchAsync("c1", "bob", CancellationToken.None);
        await _svc.StageRowsAsync(a.Id, [Row("z-a")], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(a.Id, 1, "d", "l", CancellationToken.None);
        _ = b;

        var aliceOpen = await _svc.ListByUserAsync("alice", includeFinalised: false, CancellationToken.None);
        aliceOpen.ShouldBeEmpty("alice's only batch is finalised");

        var aliceAll = await _svc.ListByUserAsync("alice", includeFinalised: true, CancellationToken.None);
        aliceAll.Count.ShouldBe(1);

        var bobOpen = await _svc.ListByUserAsync("bob", includeFinalised: false, CancellationToken.None);
        bobOpen.Count.ShouldBe(1);
    }

    [Fact]
    public async Task DropBatch_Unknown_IsNoOp()
    {
        await _svc.DropBatchAsync(Guid.NewGuid(), CancellationToken.None);
        // no throw
    }

    [Fact]
    public async Task FinaliseBatch_Creates_ExternalIdReservations_ForZTagAndSAPID()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        await _svc.StageRowsAsync(batch.Id, [Row("z-new-1")], [], CancellationToken.None);

        await _svc.FinaliseBatchAsync(batch.Id, 1, "drv", "line", CancellationToken.None);

        var active = await _db.ExternalIdReservations.AsNoTracking()
            .Where(r => r.ReleasedAt == null)
            .ToListAsync();
        active.Count.ShouldBe(2);
        active.ShouldContain(r => r.Kind == ZB.MOM.WW.OtOpcUa.Configuration.Enums.ReservationKind.ZTag && r.Value == "z-new-1");
        active.ShouldContain(r => r.Kind == ZB.MOM.WW.OtOpcUa.Configuration.Enums.ReservationKind.SAPID && r.Value == "sap-z-new-1");
    }

    [Fact]
    public async Task FinaliseBatch_SameEquipmentUuid_ReusesExistingReservation()
    {
        var batch1 = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        var sharedUuid = Guid.NewGuid();
        var row = new EquipmentCsvRow
        {
            ZTag = "z-shared", MachineCode = "mc", SAPID = "sap-shared",
            EquipmentId = "eq-1", EquipmentUuid = sharedUuid.ToString(),
            Name = "eq-1", UnsAreaName = "a", UnsLineName = "l",
        };
        await _svc.StageRowsAsync(batch1.Id, [row], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(batch1.Id, 1, "drv", "line", CancellationToken.None);

        var countAfterFirst = _db.ExternalIdReservations.Count(r => r.ReleasedAt == null);

        // Second finalise with same EquipmentUuid + same ZTag — should NOT create a duplicate.
        var batch2 = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        await _svc.StageRowsAsync(batch2.Id, [row], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(batch2.Id, 2, "drv", "line", CancellationToken.None);

        _db.ExternalIdReservations.Count(r => r.ReleasedAt == null).ShouldBe(countAfterFirst);
    }

    [Fact]
    public async Task FinaliseBatch_DifferentEquipmentUuid_SameZTag_Throws_Conflict()
    {
        var batchA = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        var rowA = new EquipmentCsvRow
        {
            ZTag = "z-collide", MachineCode = "mc-a", SAPID = "sap-a",
            EquipmentId = "eq-a", EquipmentUuid = Guid.NewGuid().ToString(),
            Name = "a", UnsAreaName = "ar", UnsLineName = "ln",
        };
        await _svc.StageRowsAsync(batchA.Id, [rowA], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(batchA.Id, 1, "drv", "line", CancellationToken.None);

        var batchB = await _svc.CreateBatchAsync("c1", "bob", CancellationToken.None);
        var rowB = new EquipmentCsvRow
        {
            ZTag = "z-collide", MachineCode = "mc-b", SAPID = "sap-b", // same ZTag, different EquipmentUuid
            EquipmentId = "eq-b", EquipmentUuid = Guid.NewGuid().ToString(),
            Name = "b", UnsAreaName = "ar", UnsLineName = "ln",
        };
        await _svc.StageRowsAsync(batchB.Id, [rowB], [], CancellationToken.None);

        var ex = await Should.ThrowAsync<ExternalIdReservationConflictException>(() =>
            _svc.FinaliseBatchAsync(batchB.Id, 2, "drv", "line", CancellationToken.None));
        ex.Message.ShouldContain("z-collide");

        // Second finalise must have rolled back — no partial Equipment row for batch B.
        var equipmentB = await _db.Equipment.AsNoTracking()
            .Where(e => e.EquipmentId == "eq-b")
            .ToListAsync();
        equipmentB.ShouldBeEmpty();
    }

    [Fact]
    public async Task FinaliseBatch_EmptyZTagAndSAPID_SkipsReservation()
    {
        var batch = await _svc.CreateBatchAsync("c1", "alice", CancellationToken.None);
        var row = new EquipmentCsvRow
        {
            ZTag = "", MachineCode = "mc", SAPID = "",
            EquipmentId = "eq-nil", EquipmentUuid = Guid.NewGuid().ToString(),
            Name = "nil", UnsAreaName = "ar", UnsLineName = "ln",
        };
        await _svc.StageRowsAsync(batch.Id, [row], [], CancellationToken.None);
        await _svc.FinaliseBatchAsync(batch.Id, 1, "drv", "line", CancellationToken.None);

        _db.ExternalIdReservations.Count().ShouldBe(0);
    }
}
