using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;

/// <summary>
/// Verifies the bulk <see cref="UnsTreeService.ImportEquipmentAsync"/> path: valid rows insert,
/// rows whose MachineCode already exists (in the DB or earlier in the same batch) are skipped,
/// rows referencing an unknown UNS line or unknown driver are reported as errors, and the
/// decision-#122 driver-cluster guard rejects a driver in a different cluster than the line.
/// </summary>
[Trait("Category", "Unit")]
public sealed class UnsTreeServiceImportTests
{
    private static (UnsTreeService Service, string DbName) Fresh()
    {
        var dbName = $"uns-import-{Guid.NewGuid():N}";
        return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
    }

    /// <summary>
    /// Seeds a line under an area in <paramref name="lineCluster"/>, plus an optional driver in
    /// <paramref name="driverCluster"/>. The line id is always <c>LINE-1</c>; the driver (when
    /// requested) is always <c>DRV-1</c>.
    /// </summary>
    private static void SeedLineAndDriver(string dbName, string lineCluster, string? driverCluster)
    {
        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = lineCluster, Name = "a" });
        db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" });
        if (driverCluster is not null)
        {
            db.DriverInstances.Add(new DriverInstance
            {
                DriverInstanceId = "DRV-1",
                ClusterId = driverCluster,
                NamespaceId = "NS-1",
                Name = "drv",
                DriverType = "ModbusTcp",
                DriverConfig = "{}",
            });
        }
        db.SaveChanges();
    }

    private static EquipmentInput Input(string name, string machineCode, string unsLineId, string? driverInstanceId) =>
        new(name, machineCode, unsLineId, driverInstanceId,
            ZTag: null, SAPID: null, Manufacturer: null, Model: null, SerialNumber: null,
            HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: null,
            AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: true);

    /// <summary>Valid rows insert with system-generated EQ- ids and are counted in <c>Inserted</c>.</summary>
    [Fact]
    public async Task Import_inserts_valid_rows()
    {
        var (service, dbName) = Fresh();
        SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);

        var result = await service.ImportEquipmentAsync(
        [
            Input("machine-1", "mc_1", "LINE-1", null),
            Input("machine-2", "mc_2", "LINE-1", null),
        ]);

        result.Inserted.ShouldBe(2);
        result.Skipped.ShouldBe(0);
        result.Errors.ShouldBeEmpty();

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Equipment.Count().ShouldBe(2);
        var eq = db.Equipment.Single(e => e.MachineCode == "mc_1");
        eq.EquipmentId.ShouldStartWith("EQ-");
        eq.EquipmentId.Length.ShouldBe(15); // "EQ-" + 12 hex chars
        eq.Name.ShouldBe("machine-1");
        eq.UnsLineId.ShouldBe("LINE-1");
        eq.Enabled.ShouldBeTrue();
    }

    /// <summary>
    /// A row whose MachineCode already exists in the DB is skipped (not an error), and a second row
    /// later in the batch that reuses an earlier row's MachineCode is also skipped.
    /// </summary>
    [Fact]
    public async Task Import_skips_duplicate_machinecode()
    {
        var (service, dbName) = Fresh();
        SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
        // Pre-existing equipment with MachineCode "mc_existing".
        await service.CreateEquipmentAsync(Input("machine-x", "mc_existing", "LINE-1", null));

        var result = await service.ImportEquipmentAsync(
        [
            Input("machine-1", "mc_existing", "LINE-1", null), // dup of DB row → skip
            Input("machine-2", "mc_new", "LINE-1", null),      // inserts
            Input("machine-3", "mc_new", "LINE-1", null),      // dup of in-batch row → skip
        ]);

        result.Inserted.ShouldBe(1);
        result.Skipped.ShouldBe(2);
        result.Errors.ShouldBeEmpty();

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Equipment.Count(e => e.MachineCode == "mc_new").ShouldBe(1);
        db.Equipment.Count(e => e.MachineCode == "mc_existing").ShouldBe(1);
    }

    /// <summary>A row whose UnsLineId does not exist is reported as an error and not inserted.</summary>
    [Fact]
    public async Task Import_reports_unknown_line()
    {
        var (service, dbName) = Fresh();
        SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);

        var result = await service.ImportEquipmentAsync(
        [
            Input("machine-1", "mc_1", "LINE-BOGUS", null),
            Input("machine-2", "mc_2", "LINE-1", null),
        ]);

        result.Inserted.ShouldBe(1);
        result.Skipped.ShouldBe(0);
        result.Errors.Count.ShouldBe(1);
        result.Errors[0].ShouldContain("mc_1");
        result.Errors[0].ShouldContain("LINE-BOGUS");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse();
        db.Equipment.Any(e => e.MachineCode == "mc_2").ShouldBeTrue();
    }

    /// <summary>A driver-bound row whose DriverInstanceId does not resolve is reported as an error.</summary>
    [Fact]
    public async Task Import_reports_unknown_driver()
    {
        var (service, dbName) = Fresh();
        SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null); // no driver seeded

        var result = await service.ImportEquipmentAsync(
        [
            Input("machine-1", "mc_1", "LINE-1", "DRV-GHOST"),
        ]);

        result.Inserted.ShouldBe(0);
        result.Skipped.ShouldBe(0);
        result.Errors.Count.ShouldBe(1);
        result.Errors[0].ShouldContain("mc_1");
        result.Errors[0].ShouldContain("DRV-GHOST");
        result.Errors[0].ShouldContain("not found");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse();
    }

    /// <summary>
    /// The decision-#122 guard rejects a row that binds a driver living in a different cluster than
    /// the row's UNS line.
    /// </summary>
    [Fact]
    public async Task Import_enforces_122_cluster()
    {
        var (service, dbName) = Fresh();
        SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "SITE-A");

        var result = await service.ImportEquipmentAsync(
        [
            Input("machine-1", "mc_1", "LINE-1", "DRV-1"),
        ]);

        result.Inserted.ShouldBe(0);
        result.Skipped.ShouldBe(0);
        result.Errors.Count.ShouldBe(1);
        result.Errors[0].ShouldContain("mc_1");
        result.Errors[0].ShouldContain("decision #122");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse();
    }

    /// <summary>A driver in the same cluster as the line imports cleanly.</summary>
    [Fact]
    public async Task Import_allows_driver_in_same_cluster()
    {
        var (service, dbName) = Fresh();
        SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "MAIN");

        var result = await service.ImportEquipmentAsync(
        [
            Input("machine-1", "mc_1", "LINE-1", "DRV-1"),
        ]);

        result.Inserted.ShouldBe(1);
        result.Skipped.ShouldBe(0);
        result.Errors.ShouldBeEmpty();

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Equipment.Single(e => e.MachineCode == "mc_1").DriverInstanceId.ShouldBe("DRV-1");
    }
}
