using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;

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

[Trait("Category", "Unit")]
public sealed class DraftValidatorTests
{
    /// <summary>Verifies that UnsSegment validation rejects uppercase and special characters.</summary>
    /// <param name="name">The segment name to validate.</param>
    /// <param name="shouldPass">Whether the validation should pass for this name.</param>
    [Theory]
    [InlineData("valid-name", true)]
    [InlineData("line-01", true)]
    [InlineData("_default", true)]
    [InlineData("UPPER", false)]
    [InlineData("with space", false)]
    [InlineData("", false)]
    public void UnsSegment_rule_accepts_lowercase_or_default_only(string name, bool shouldPass)
    {
        var uuid = Guid.NewGuid();
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            Equipment =
            [
                new Equipment
                {
                    EquipmentUuid = uuid,
                    EquipmentId = DraftValidator.DeriveEquipmentId(uuid),
                    Name = name,
                    DriverInstanceId = "d",
                    UnsLineId = "line-a",
                    MachineCode = "m",
                },
            ],
        };

        var errors = DraftValidator.Validate(draft);
        var hasUnsError = errors.Any(e => e.Code == "UnsSegmentInvalid");
        hasUnsError.ShouldBe(!shouldPass);
    }

    /// <summary>Verifies that namespace cannot be bound to a different cluster.</summary>
    [Fact]
    public void Cross_cluster_namespace_binding_is_rejected()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c-A",
            Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-B", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
            DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "ModbusTcp", DriverConfig = "{}" }],
        };

        var errors = DraftValidator.Validate(draft);
        errors.ShouldContain(e => e.Code == "BadCrossClusterNamespaceBinding");
    }

    /// <summary>Verifies that namespace in the same cluster is accepted.</summary>
    [Fact]
    public void Same_cluster_namespace_binding_is_accepted()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c-A",
            Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-A", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
            DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "ModbusTcp", DriverConfig = "{}" }],
        };

        DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "BadCrossClusterNamespaceBinding");
    }

    /// <summary>Verifies that equipment UUID must remain immutable across generations.</summary>
    [Fact]
    public void EquipmentUuid_change_across_generations_is_rejected()
    {
        var oldUuid = Guid.Parse("11111111-1111-1111-1111-111111111111");
        var newUuid = Guid.Parse("22222222-2222-2222-2222-222222222222");
        var eid = DraftValidator.DeriveEquipmentId(oldUuid);

        var draft = new DraftSnapshot
        {
            GenerationId = 2, ClusterId = "c",
            Equipment = [new Equipment { EquipmentUuid = newUuid, EquipmentId = eid, Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }],
            PriorEquipment = [new Equipment { EquipmentUuid = oldUuid, EquipmentId = eid, Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentUuidImmutable");
    }

    /// <summary>Verifies that a ZTag cannot be reserved by a different equipment UUID.</summary>
    [Fact]
    public void ZTag_reserved_by_different_uuid_is_rejected()
    {
        var uuid = Guid.NewGuid();
        var otherUuid = Guid.NewGuid();

        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = DraftValidator.DeriveEquipmentId(uuid), Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m", ZTag = "ZT-001" }],
            ActiveReservations = [new ExternalIdReservation { Kind = ReservationKind.ZTag, Value = "ZT-001", EquipmentUuid = otherUuid, ClusterId = "c", FirstPublishedBy = "t" }],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "BadDuplicateExternalIdentifier");
    }

    /// <summary>Verifies that equipment ID must be derived from its UUID.</summary>
    [Fact]
    public void EquipmentId_that_does_not_match_derivation_is_rejected()
    {
        var uuid = Guid.NewGuid();
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-operator-typed", Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentIdNotDerived");
    }

    /// <summary>Verifies that the canonical Galaxy driver type (GalaxyMxGateway, per PR 7.2 —
    /// it was "Galaxy" pre-PR-7.2) is allowed in a SystemPlatform namespace, i.e. produces no
    /// kind-mismatch error.</summary>
    [Fact]
    public void GalaxyMxGateway_driver_in_SystemPlatform_namespace_is_allowed()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c", NamespaceUri = "urn:x", Kind = NamespaceKind.SystemPlatform }],
            DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c", NamespaceId = "ns-1", Name = "drv", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
        };

        DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "DriverNamespaceKindMismatch");
    }

    /// <summary>Verifies that the canonical Galaxy driver type cannot be placed in an Equipment namespace.</summary>
    [Fact]
    public void GalaxyMxGateway_driver_in_Equipment_namespace_is_rejected()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
            DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c", NamespaceId = "ns-1", Name = "drv", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "DriverNamespaceKindMismatch");
    }

    /// <summary>Verifies that a non-Galaxy driver cannot be placed in a SystemPlatform namespace.</summary>
    [Fact]
    public void NonGalaxy_driver_in_SystemPlatform_namespace_is_rejected()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c", NamespaceUri = "urn:x", Kind = NamespaceKind.SystemPlatform }],
            DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c", NamespaceId = "ns-1", Name = "drv", DriverType = "ModbusTcp", DriverConfig = "{}" }],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "DriverNamespaceKindMismatch");
    }

    /// <summary>Verifies that all validation errors are reported simultaneously.</summary>
    [Fact]
    public void Draft_with_three_violations_surfaces_all_three()
    {
        var uuid = Guid.NewGuid();
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c-A",
            Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-B", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
            DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
            Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-wrong", Name = "BAD NAME", DriverInstanceId = "d-1", UnsLineId = "line-a", MachineCode = "m" }],
        };

        var errors = DraftValidator.Validate(draft);
        errors.ShouldContain(e => e.Code == "BadCrossClusterNamespaceBinding");
        errors.ShouldContain(e => e.Code == "DriverNamespaceKindMismatch");
        errors.ShouldContain(e => e.Code == "EquipmentIdNotDerived");
        errors.ShouldContain(e => e.Code == "UnsSegmentInvalid");
    }

    // ------------------------------------------------------------------------------------
    // Phase 6.3 task #148 part 2 — ValidateClusterTopology
    // ------------------------------------------------------------------------------------

    /// <summary>Verifies that cluster topology validation checks node count against declared redundancy mode.</summary>
    /// <param name="nodeCount">The declared cluster node count.</param>
    /// <param name="mode">The declared redundancy mode.</param>
    /// <param name="enabledNodes">The number of enabled nodes to include in the topology.</param>
    /// <param name="expectedDeclaredErrors">The number of ClusterRedundancyModeInvalid errors expected.</param>
    [Theory]
    [InlineData(1, RedundancyMode.None, 1, 0)]  // single-node standalone — ok
    [InlineData(2, RedundancyMode.Warm, 2, 0)]  // 2-node warm — ok
    [InlineData(2, RedundancyMode.Hot,  2, 0)]  // 2-node hot — ok
    [InlineData(1, RedundancyMode.Warm, 1, 1)]  // declared mismatch — should flag
    [InlineData(2, RedundancyMode.None, 2, 1)]  // None with 2 nodes — should flag
    public void ValidateClusterTopology_checks_declared_pair(
        byte nodeCount, RedundancyMode mode, int enabledNodes, int expectedDeclaredErrors)
    {
        var cluster = BuildCluster(nodeCount: nodeCount, mode: mode);
        var nodes = Enumerable.Range(0, enabledNodes)
            .Select(i => BuildNode($"n-{i}", enabled: true))
            .ToList();

        var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
        errors.Count(e => e.Code == "ClusterRedundancyModeInvalid").ShouldBe(expectedDeclaredErrors);
    }

    /// <summary>Verifies that disabled nodes cause topology validation to fail.</summary>
    [Fact]
    public void ValidateClusterTopology_flags_disabled_node_mismatch()
    {
        // Declared 2 + Hot, but one node disabled — runtime would boot InvalidTopology.
        var cluster = BuildCluster(nodeCount: 2, mode: RedundancyMode.Hot);
        var nodes = new[]
        {
            BuildNode("primary", enabled: true),
            BuildNode("backup",  enabled: false),
        };

        var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
        errors.ShouldContain(e => e.Code == "ClusterEnabledNodeCountMismatch");
    }

    // v2: the "exactly one Primary per cluster" check is gone — Akka cluster's
    // role-leader-of-"driver" elects the primary at runtime. The corresponding
    // ValidateClusterTopology_flags_multiple_Primary test (and the
    // ClusterMultiplePrimary error code it asserted) were removed alongside Task 14d.

    /// <summary>Verifies that a valid standalone cluster passes validation.</summary>
    [Fact]
    public void ValidateClusterTopology_returns_no_errors_on_valid_standalone()
    {
        var cluster = BuildCluster(nodeCount: 1, mode: RedundancyMode.None);
        var nodes = new[] { BuildNode("only", enabled: true) };

        var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
        errors.ShouldBeEmpty();
    }

    private static ServerCluster BuildCluster(byte nodeCount, RedundancyMode mode) => new()
    {
        ClusterId = "c-test",
        Name = "Test",
        Enterprise = "zb",
        Site = "dev",
        NodeCount = nodeCount,
        RedundancyMode = mode,
        Enabled = true,
        CreatedBy = "t",
    };

    private static ClusterNode BuildNode(string id, bool enabled) => new()
    {
        NodeId = id,
        ClusterId = "c-test",
        Host = "localhost",
        OpcUaPort = 4840,
        DashboardPort = 5001,
        ApplicationUri = $"urn:{id}",
        ServiceLevelBase = 200,
        Enabled = enabled,
        CreatedBy = "t",
    };

    // ------------------------------------------------------------------------------------
    // ValidatePathLength — Enterprise/Site length precision (Configuration-003)
    // ------------------------------------------------------------------------------------

    /// <summary>Verifies that path length validation uses actual Enterprise and Site lengths.</summary>
    [Fact]
    public void PathLength_uses_actual_Enterprise_Site_when_provided()
    {
        // Craft a snapshot where the 32+32 approximation would flag PathTooLong but the
        // actual Enterprise/Site lengths would not. Equipment name intentionally exceeds the
        // UNS-segment 32-char limit (it would also trigger UnsSegmentInvalid — that is fine;
        // both checks are independent and the test is filtering only on PathTooLong).
        //
        // approx: 32+32 + 32+32+90 +4 = 222  > 200 → would flag PathTooLong
        // actual:  2 +1  + 32+32+90 +4 = 161 ≤ 200 → no PathTooLong
        var areaId = "area-a";
        var lineId = "line-b";
        var uuid = Guid.NewGuid();
        var eqName = new string('x', 90); // 90 chars — exceeds UNS regex but that's a separate error

        var snapshot = new DraftSnapshot
        {
            GenerationId = 1,
            ClusterId = "c",
            Enterprise = "zb",  // 2 chars (actual)
            Site = "s",         // 1 char (actual)
            UnsAreas = [new UnsArea { UnsAreaId = areaId, ClusterId = "c", Name = new string('a', 32) }],
            UnsLines = [new UnsLine { UnsLineId = lineId, UnsAreaId = areaId, Name = new string('b', 32) }],
            Equipment =
            [
                new Equipment
                {
                    EquipmentUuid = uuid,
                    EquipmentId   = DraftValidator.DeriveEquipmentId(uuid),
                    Name          = eqName,
                    DriverInstanceId = "d",
                    UnsLineId     = lineId,
                    MachineCode   = "m",
                },
            ],
        };

        var errors = DraftValidator.Validate(snapshot);

        errors.ShouldNotContain(e => e.Code == "PathTooLong",
            "actual Enterprise='zb' + Site='s' keeps total path at 161 chars — under the 200-char limit");
    }

    /// <summary>Verifies that path length validation uses conservative fallback when Enterprise and Site are absent.</summary>
    [Fact]
    public void PathLength_conservative_fallback_when_Enterprise_Site_absent()
    {
        // Without Enterprise/Site on the snapshot the validator assumes 32+32.
        // A path whose segments sum to 93 chars (area=32 + line=32 + eq=29) fits
        // under 200 even with the 32+32 approximation (32+32+93+4 = 161 ≤ 200) and
        // must NOT be flagged — the fallback must not over-penalise valid paths that
        // would also be valid under real Enterprise/Site values.
        var areaId = "area-x";
        var lineId = "line-y";
        var uuid = Guid.NewGuid();

        var snapshot = new DraftSnapshot
        {
            GenerationId = 1,
            ClusterId = "c",
            // Enterprise and Site deliberately omitted — conservative fallback path
            UnsAreas = [new UnsArea { UnsAreaId = areaId, ClusterId = "c", Name = new string('a', 32) }],
            UnsLines = [new UnsLine { UnsLineId = lineId, UnsAreaId = areaId, Name = new string('b', 32) }],
            Equipment =
            [
                new Equipment
                {
                    EquipmentUuid = uuid,
                    EquipmentId   = DraftValidator.DeriveEquipmentId(uuid),
                    Name          = new string('c', 29),
                    DriverInstanceId = "d",
                    UnsLineId     = lineId,
                    MachineCode   = "m",
                },
            ],
        };

        var errors = DraftValidator.Validate(snapshot);

        errors.ShouldNotContain(e => e.Code == "PathTooLong",
            "conservative 32+32+32+32+29+4 = 161 chars is still under the 200-char limit");
    }
}
