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

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

[Trait("Category", "Unit")]
public sealed class ClusterTopologyLoaderTests
{
    private static ServerCluster Cluster(RedundancyMode mode = RedundancyMode.Warm) => new()
    {
        ClusterId = "c1",
        Name = "Warsaw-West",
        Enterprise = "zb",
        Site = "warsaw-west",
        RedundancyMode = mode,
        CreatedBy = "test",
    };

    private static ClusterNode Node(string id, RedundancyRole role, string host, int port = 4840, string? appUri = null) => new()
    {
        NodeId = id,
        ClusterId = "c1",
        RedundancyRole = role,
        Host = host,
        OpcUaPort = port,
        ApplicationUri = appUri ?? $"urn:{host}:OtOpcUa",
        CreatedBy = "test",
    };

    [Fact]
    public void SingleNode_Standalone_Loads()
    {
        var cluster = Cluster(RedundancyMode.None);
        var nodes = new[] { Node("A", RedundancyRole.Standalone, "hostA") };

        var topology = ClusterTopologyLoader.Load("A", cluster, nodes);

        topology.SelfNodeId.ShouldBe("A");
        topology.SelfRole.ShouldBe(RedundancyRole.Standalone);
        topology.Peers.ShouldBeEmpty();
        topology.SelfApplicationUri.ShouldBe("urn:hostA:OtOpcUa");
    }

    [Fact]
    public void TwoNode_Cluster_LoadsSelfAndPeer()
    {
        var cluster = Cluster();
        var nodes = new[]
        {
            Node("A", RedundancyRole.Primary,   "hostA"),
            Node("B", RedundancyRole.Secondary, "hostB"),
        };

        var topology = ClusterTopologyLoader.Load("A", cluster, nodes);

        topology.SelfNodeId.ShouldBe("A");
        topology.SelfRole.ShouldBe(RedundancyRole.Primary);
        topology.Peers.Count.ShouldBe(1);
        topology.Peers[0].NodeId.ShouldBe("B");
        topology.Peers[0].Role.ShouldBe(RedundancyRole.Secondary);
    }

    [Fact]
    public void ServerUriArray_Puts_Self_First_Peers_SortedLexicographically()
    {
        var cluster = Cluster();
        var nodes = new[]
        {
            Node("A", RedundancyRole.Primary,   "hostA", appUri: "urn:A"),
            Node("B", RedundancyRole.Secondary, "hostB", appUri: "urn:B"),
        };

        var topology = ClusterTopologyLoader.Load("A", cluster, nodes);

        topology.ServerUriArray().ShouldBe(["urn:A", "urn:B"]);
    }

    [Fact]
    public void EmptyNodes_Throws()
    {
        Should.Throw<InvalidTopologyException>(
            () => ClusterTopologyLoader.Load("A", Cluster(), []));
    }

    [Fact]
    public void SelfNotInCluster_Throws()
    {
        var nodes = new[] { Node("B", RedundancyRole.Primary, "hostB") };

        Should.Throw<InvalidTopologyException>(
            () => ClusterTopologyLoader.Load("A-missing", Cluster(), nodes));
    }

    [Fact]
    public void ThreeNodeCluster_Rejected_Per_Decision83()
    {
        var nodes = new[]
        {
            Node("A", RedundancyRole.Primary,   "hostA"),
            Node("B", RedundancyRole.Secondary, "hostB"),
            Node("C", RedundancyRole.Secondary, "hostC"),
        };

        var ex = Should.Throw<InvalidTopologyException>(
            () => ClusterTopologyLoader.Load("A", Cluster(), nodes));
        ex.Message.ShouldContain("decision #83");
    }

    [Fact]
    public void DuplicateApplicationUri_Rejected()
    {
        var nodes = new[]
        {
            Node("A", RedundancyRole.Primary,   "hostA", appUri: "urn:shared"),
            Node("B", RedundancyRole.Secondary, "hostB", appUri: "urn:shared"),
        };

        var ex = Should.Throw<InvalidTopologyException>(
            () => ClusterTopologyLoader.Load("A", Cluster(), nodes));
        ex.Message.ShouldContain("ApplicationUri");
    }

    [Fact]
    public void TwoPrimaries_InWarmMode_Rejected()
    {
        var nodes = new[]
        {
            Node("A", RedundancyRole.Primary, "hostA"),
            Node("B", RedundancyRole.Primary, "hostB"),
        };

        var ex = Should.Throw<InvalidTopologyException>(
            () => ClusterTopologyLoader.Load("A", Cluster(RedundancyMode.Warm), nodes));
        ex.Message.ShouldContain("2 Primary");
    }

    [Fact]
    public void CrossCluster_Node_Rejected()
    {
        var foreign = Node("B", RedundancyRole.Secondary, "hostB");
        foreign.ClusterId = "c-other";

        var nodes = new[] { Node("A", RedundancyRole.Primary, "hostA"), foreign };

        Should.Throw<InvalidTopologyException>(
            () => ClusterTopologyLoader.Load("A", Cluster(), nodes));
    }

    [Fact]
    public void None_Mode_Allows_Any_Role_Mix()
    {
        // Standalone clusters don't enforce Primary-count; operator can pick anything.
        var cluster = Cluster(RedundancyMode.None);
        var nodes = new[] { Node("A", RedundancyRole.Primary, "hostA") };

        var topology = ClusterTopologyLoader.Load("A", cluster, nodes);

        topology.Mode.ShouldBe(RedundancyMode.None);
    }
}
