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

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

/// <summary>
/// Verifies the equipment-bound Tag CRUD mutations on <see cref="UnsTreeService"/>, including
/// TagConfig JSON validity, the namespace-kind guard (tree tags must bind to an Equipment-kind
/// namespace), the decision-#122 driver-cluster guard, duplicate-id / duplicate-name guards, and
/// the driver-candidate loader scoped to the equipment's cluster.
/// </summary>
/// <remarks>
/// The EF InMemory provider does not enforce <c>RowVersion</c> concurrency, so the
/// <c>DbUpdateConcurrencyException</c> branches are not exercised here by design.
/// </remarks>
[Trait("Category", "Unit")]
public sealed class UnsTreeServiceTagTests
{
    private static (UnsTreeService Service, string DbName) Fresh()
    {
        var dbName = $"uns-tag-{Guid.NewGuid():N}";
        return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
    }

    /// <summary>
    /// Seeds an area→line→equipment path in <paramref name="equipmentCluster"/>. The equipment id is
    /// always <c>EQ-1</c>. Optionally seeds an Equipment-kind driver (<c>DRV-EQ</c>) in the equipment's
    /// cluster, a SystemPlatform-kind driver (<c>DRV-SP</c>) in the equipment's cluster, and an
    /// Equipment-kind driver (<c>DRV-OTHER</c>) in <paramref name="otherCluster"/>.
    /// </summary>
    private static void SeedHierarchyAndDrivers(
        string dbName,
        string equipmentCluster,
        bool seedEquipmentDriver = false,
        bool seedSystemPlatformDriver = false,
        string? otherCluster = null)
    {
        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = equipmentCluster, Name = "a" });
        db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" });
        db.Equipment.Add(new Equipment
        {
            EquipmentId = "EQ-1",
            EquipmentUuid = Guid.NewGuid(),
            UnsLineId = "LINE-1",
            Name = "machine-1",
            MachineCode = "machine_001",
        });

        // Equipment-kind namespace in the equipment's cluster.
        db.Namespaces.Add(new Namespace
        {
            NamespaceId = "NS-EQ",
            ClusterId = equipmentCluster,
            Kind = NamespaceKind.Equipment,
            NamespaceUri = "urn:zb:eq",
        });
        // SystemPlatform-kind namespace in the equipment's cluster.
        db.Namespaces.Add(new Namespace
        {
            NamespaceId = "NS-SP",
            ClusterId = equipmentCluster,
            Kind = NamespaceKind.SystemPlatform,
            NamespaceUri = "urn:zb:sp",
        });

        if (seedEquipmentDriver)
        {
            db.DriverInstances.Add(new DriverInstance
            {
                DriverInstanceId = "DRV-EQ",
                ClusterId = equipmentCluster,
                NamespaceId = "NS-EQ",
                Name = "equipment driver",
                DriverType = "ModbusTcp",
                DriverConfig = "{}",
            });
        }

        if (seedSystemPlatformDriver)
        {
            db.DriverInstances.Add(new DriverInstance
            {
                DriverInstanceId = "DRV-SP",
                ClusterId = equipmentCluster,
                NamespaceId = "NS-SP",
                Name = "galaxy driver",
                DriverType = "Galaxy",
                DriverConfig = "{}",
            });
        }

        if (otherCluster is not null)
        {
            // Equipment-kind namespace + driver in a different cluster.
            db.Namespaces.Add(new Namespace
            {
                NamespaceId = "NS-OTHER",
                ClusterId = otherCluster,
                Kind = NamespaceKind.Equipment,
                NamespaceUri = "urn:zb:other",
            });
            db.DriverInstances.Add(new DriverInstance
            {
                DriverInstanceId = "DRV-OTHER",
                ClusterId = otherCluster,
                NamespaceId = "NS-OTHER",
                Name = "other-cluster driver",
                DriverType = "ModbusTcp",
                DriverConfig = "{}",
            });
        }

        db.SaveChanges();
    }

    private static TagInput Input(
        string tagId,
        string name,
        string driverInstanceId,
        string tagConfig = "{}") =>
        new(tagId, name, driverInstanceId, DataType: "Float",
            AccessLevel: TagAccessLevel.Read, WriteIdempotent: false,
            PollGroupId: null, TagConfig: tagConfig);

    // ----- CreateTag -----

    /// <summary>A valid equipment-bound tag persists with EquipmentId set and FolderPath null.</summary>
    [Fact]
    public async Task CreateTag_equipment_bound_persists()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);

        var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        var tag = db.Tags.Single(t => t.TagId == "TAG-1");
        tag.EquipmentId.ShouldBe("EQ-1");
        tag.FolderPath.ShouldBeNull();
        tag.DriverInstanceId.ShouldBe("DRV-EQ");
        tag.Name.ShouldBe("speed");
        tag.DataType.ShouldBe("Float");
    }

    /// <summary>A tag with invalid TagConfig JSON is blocked.</summary>
    [Fact]
    public async Task CreateTag_invalid_json_blocked()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);

        var result = await service.CreateTagAsync(
            "EQ-1", Input("TAG-1", "speed", "DRV-EQ", tagConfig: "{ not json"));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("TagConfig is not valid JSON.");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
    }

    /// <summary>Binding a tag to a driver in a different cluster than the equipment is blocked (#122).</summary>
    [Fact]
    public async Task CreateTag_driver_in_other_cluster_blocked()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", otherCluster: "SITE-A");

        var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-OTHER"));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("decision #122");
        result.Error.ShouldContain("DRV-OTHER");
        result.Error.ShouldContain("SITE-A");
        result.Error.ShouldContain("MAIN");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
    }

    /// <summary>Binding a tree tag to a driver in a SystemPlatform-kind namespace is blocked.</summary>
    [Fact]
    public async Task CreateTag_driver_systemplatform_namespace_blocked()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedSystemPlatformDriver: true);

        var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-SP"));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("DRV-SP");
        result.Error.ShouldContain("Equipment-kind namespace");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
    }

    /// <summary>Creating a tag with a TagId that already exists is blocked.</summary>
    [Fact]
    public async Task CreateTag_duplicate_tagid_blocked()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
        await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));

        var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "another", "DRV-EQ"));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("Tag 'TAG-1' already exists.");
    }

    /// <summary>Creating a tag for an equipment id that does not exist returns a not-found error.</summary>
    [Fact]
    public async Task CreateTag_unresolvable_equipment_returns_error()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);

        var result = await service.CreateTagAsync("EQ-NOPE", Input("TAG-1", "speed", "DRV-EQ"));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("not found");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
    }

    /// <summary>Creating a tag whose Name already exists on the same equipment is blocked.</summary>
    [Fact]
    public async Task CreateTag_duplicate_name_on_equipment_blocked()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
        await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));

        var result = await service.CreateTagAsync("EQ-1", Input("TAG-2", "speed", "DRV-EQ"));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("A tag named 'speed' already exists on this equipment.");
    }

    // ----- UpdateTag -----

    /// <summary>Updating a tag changes its mutable fields and keeps EquipmentId / FolderPath.</summary>
    [Fact]
    public async Task UpdateTag_changes_fields()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
        await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.Tags.Single(t => t.TagId == "TAG-1").RowVersion;
        }

        var updated = new TagInput("TAG-1", "renamed", "DRV-EQ", DataType: "Int32",
            AccessLevel: TagAccessLevel.ReadWrite, WriteIdempotent: true,
            PollGroupId: "  ", TagConfig: """{ "register": 40001 }""");

        var result = await service.UpdateTagAsync("TAG-1", updated, rv);

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();

        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        var after = verify.Tags.Single(t => t.TagId == "TAG-1");
        after.Name.ShouldBe("renamed");
        after.DataType.ShouldBe("Int32");
        after.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
        after.WriteIdempotent.ShouldBeTrue();
        after.PollGroupId.ShouldBeNull(); // whitespace collapses to null
        after.EquipmentId.ShouldBe("EQ-1");
        after.FolderPath.ShouldBeNull();
    }

    /// <summary>Updating a tag that no longer exists returns the row-gone error.</summary>
    [Fact]
    public async Task UpdateTag_missing_row_returns_error()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);

        var result = await service.UpdateTagAsync("TAG-nope", Input("TAG-nope", "x", "DRV-EQ"), []);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("Row no longer exists.");
    }

    // ----- DeleteTag -----

    /// <summary>Deleting a tag removes the row.</summary>
    [Fact]
    public async Task DeleteTag_removes_row()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
        await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.Tags.Single(t => t.TagId == "TAG-1").RowVersion;
        }

        var result = await service.DeleteTagAsync("TAG-1", rv);

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();

        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        verify.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
    }

    /// <summary>Deleting a tag that is already gone is a no-op success.</summary>
    [Fact]
    public async Task DeleteTag_already_gone_returns_ok()
    {
        var (service, _) = Fresh();

        var result = await service.DeleteTagAsync("TAG-ghost", []);

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();
    }

    // ----- LoadTagDriversForEquipmentAsync -----

    /// <summary>
    /// The driver loader returns only Equipment-kind drivers in the equipment's cluster — excluding
    /// SystemPlatform-kind drivers in the same cluster and Equipment-kind drivers in other clusters.
    /// </summary>
    [Fact]
    public async Task LoadTagDriversForEquipment_returns_only_equipment_kind_drivers_in_cluster()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(
            dbName,
            equipmentCluster: "MAIN",
            seedEquipmentDriver: true,
            seedSystemPlatformDriver: true,
            otherCluster: "SITE-A");

        var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-1");

        drivers.Count.ShouldBe(1);
        drivers[0].DriverInstanceId.ShouldBe("DRV-EQ");
        drivers[0].Display.ShouldContain("DRV-EQ");
        drivers[0].Display.ShouldContain("equipment driver");
    }

    /// <summary>An unresolvable equipment yields an empty driver list.</summary>
    [Fact]
    public async Task LoadTagDriversForEquipment_unresolvable_equipment_returns_empty()
    {
        var (service, dbName) = Fresh();
        SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);

        var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-nope");

        drivers.ShouldBeEmpty();
    }
}
