using Opc.Ua;
using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;

/// <summary>
///     Regression coverage for driver-specs.md §8 namespace handling — the
///     <c>TargetNamespaceKind</c> startup enforcement and the server-stable NodeId encoding
///     that <see cref="NamespaceMap"/> produces (findings Driver.OpcUaClient-004).
/// </summary>
[Trait("Category", "Unit")]
public sealed class OpcUaClientNamespaceTests
{
    // ---- Driver.OpcUaClient-004: TargetNamespaceKind startup enforcement ----

    /// <summary>Verifies that Equipment namespace kind without mapping table is rejected.</summary>
    [Fact]
    public void ValidateNamespaceKind_Equipment_without_mapping_table_is_rejected()
    {
        // §8: an Equipment-kind instance gateways raw equipment data and MUST carry a
        // config-driven UNS mapping table — remote nodes don't conform to UNS by default.
        var opts = new OpcUaClientDriverOptions { TargetNamespaceKind = OpcUaTargetNamespaceKind.Equipment };
        Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateNamespaceKind(opts))
            .Message.ShouldContain("UnsMappingTable");
    }

    /// <summary>Verifies that Equipment namespace kind with mapping table passes.</summary>
    [Fact]
    public void ValidateNamespaceKind_Equipment_with_mapping_table_passes()
    {
        var opts = new OpcUaClientDriverOptions
        {
            TargetNamespaceKind = OpcUaTargetNamespaceKind.Equipment,
            UnsMappingTable = new Dictionary<string, string> { ["Line1"] = "Site/AreaA/Line1" },
        };
        Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts));
    }

    /// <summary>Verifies that SystemPlatform namespace kind with mapping table is rejected.</summary>
    [Fact]
    public void ValidateNamespaceKind_SystemPlatform_with_mapping_table_is_rejected()
    {
        // §8: processed data preserves its own hierarchy — a UNS mapping table is ambiguous.
        var opts = new OpcUaClientDriverOptions
        {
            TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
            UnsMappingTable = new Dictionary<string, string> { ["x"] = "y" },
        };
        Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateNamespaceKind(opts))
            .Message.ShouldContain("SystemPlatform");
    }

    /// <summary>Verifies that SystemPlatform namespace kind without mapping table passes.</summary>
    [Fact]
    public void ValidateNamespaceKind_SystemPlatform_without_mapping_table_passes()
    {
        var opts = new OpcUaClientDriverOptions { TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform };
        Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts));
    }

    /// <summary>Verifies that the default target namespace kind is Equipment.</summary>
    [Fact]
    public void Default_TargetNamespaceKind_is_Equipment()
    {
        // §8 comparison table: OPC UA Client default lane is Equipment.
        new OpcUaClientDriverOptions().TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.Equipment);
    }

    // ---- Driver.OpcUaClient-004: server-stable NodeId encoding ----

    /// <summary>Verifies that NamespaceMap.FromSession rejects null session.</summary>
    [Fact]
    public void NamespaceMap_FromSession_rejects_null_session() =>
        Should.Throw<ArgumentNullException>(() => NamespaceMap.FromSession(null!));

    /// <summary>Verifies that namespace 0 NodeId keeps compact form.</summary>
    [Fact]
    public void NamespaceMap_namespace0_NodeId_keeps_compact_form()
    {
        // Namespace 0 is fixed by the OPC UA spec — it never moves, so the compact i=… form
        // is already server-stable and must not be rewritten to nsu=…
        var map = TestMap("http://opcfoundation.org/UA/", "urn:remote:server");
        var coreNode = new NodeId(2253u); // i=2253, Server object, namespace 0
        map.ToStableReference(coreNode).ShouldBe(coreNode.ToString());
        map.ToStableReference(coreNode).ShouldNotContain("nsu=");
    }

    /// <summary>Verifies that nonzero namespace NodeId is encoded with URI, not index.</summary>
    [Fact]
    public void NamespaceMap_nonzero_namespace_NodeId_is_encoded_with_uri_not_index()
    {
        // A ns=1 reference is session-relative; if the remote reorders its table the index
        // points at the wrong namespace. ToStableReference must embed the URI instead.
        var map = TestMap("http://opcfoundation.org/UA/", "urn:remote:server");
        var node = new NodeId("Temperature", 1);
        var stable = map.ToStableReference(node);
        stable.ShouldContain("nsu=urn:remote:server");
        stable.ShouldContain("s=Temperature");
        stable.ShouldNotStartWith("ns=1");
    }

    /// <summary>Verifies that unknown namespace index falls back to raw form.</summary>
    [Fact]
    public void NamespaceMap_unknown_namespace_index_falls_back_to_raw_form()
    {
        // An index past the captured table can't be URI-encoded; fall back rather than throw —
        // the read/write path surfaces BadNodeIdInvalid if it truly can't resolve.
        var map = TestMap("http://opcfoundation.org/UA/");
        var node = new NodeId("Orphan", 5);
        Should.NotThrow(() => map.ToStableReference(node));
    }

    /// <summary>Verifies that namespace index and URI lookups are bidirectional.</summary>
    [Fact]
    public void NamespaceMap_index_and_uri_lookups_are_bidirectional()
    {
        var map = TestMap("http://opcfoundation.org/UA/", "urn:remote:server", "urn:remote:vendor");
        map.Count.ShouldBe(3);
        map.UriForIndex(1).ShouldBe("urn:remote:server");
        map.IndexForUri("urn:remote:vendor").ShouldBe((ushort)2);
        map.IndexForUri("urn:not:present").ShouldBeNull();
        map.UriForIndex(99).ShouldBeNull();
    }

    /// <summary>Verifies that NamespaceMap.TryResolve rejects empty and null input.</summary>
    [Fact]
    public void NamespaceMap_TryResolve_rejects_empty_and_null_input()
    {
        NamespaceMap.TryResolve(null!, "ns=1;s=X", out _).ShouldBeFalse();
    }

    /// <summary>
    ///     Build a <see cref="NamespaceMap"/> directly from a URI table, mirroring what
    ///     <see cref="NamespaceMap.FromSession"/> snapshots — lets the encoding be tested
    ///     without standing up a live <c>ISession</c>.
    /// </summary>
    private static NamespaceMap TestMap(params string[] uris)
    {
        var table = new NamespaceTable();
        // Index 0 (the OPC UA core namespace) is seeded by NamespaceTable's ctor; append the
        // rest. Skip uris[0] when it is the core namespace to avoid a duplicate.
        for (var i = 0; i < uris.Length; i++)
        {
            if (i == 0 && uris[i] == "http://opcfoundation.org/UA/") continue;
            table.Append(uris[i]);
        }
        return NamespaceMap.FromTable(table);
    }
}
