using Opc.Ua;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Shouldly;
using Xunit;

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

/// <summary>
///     R2-07 Phase 2 (T8/T9/T10) — surgical IN-PLACE removal on the node manager: a single value variable
///     (<see cref="OtOpcUaNodeManager.RemoveVariableNode"/>), a single Part 9 alarm condition
///     (<see cref="OtOpcUaNodeManager.RemoveAlarmConditionNode"/>), and an equipment folder + its whole
///     descendant subtree with notifier demotion (<see cref="OtOpcUaNodeManager.RemoveEquipmentSubtree"/>).
///     Each removal detaches only the scoped node(s), cleans the matching maps, and raises a Part 3
///     NodeDeleted model-change; an unknown id returns false (the caller falls back to a full rebuild).
/// </summary>
public sealed class NodeManagerSurgicalRemoveTests : IDisposable
{
    private static CancellationToken Ct => TestContext.Current.CancellationToken;

    private readonly string _pkiRoot = Path.Combine(
        Path.GetTempPath(),
        $"otopcua-surgical-remove-{Guid.NewGuid():N}");

    // ---------- T8: RemoveVariableNode ----------

    /// <summary>Ensure a variable then remove it: it disappears from the maps (TryGetVariable null,
    /// VariableCount decremented), its historized-tagname registration is dropped, and the call returns
    /// true.</summary>
    [Trait("Category", "Unit")]
    [Fact]
    public async Task RemoveVariableNode_drops_variable_and_historian_registration()
    {
        var (host, server) = await BootAsync();
        var nm = server.NodeManager!;
        nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
        nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A", realm: AddressSpaceRealm.Uns);
        nm.EnsureVariable("eq-1/B", "eq-1", "B", "Float", writable: false, realm: AddressSpaceRealm.Uns);
        var countBefore = nm.VariableCount;
        nm.TryGetHistorizedTagname("eq-1/A", out _).ShouldBeTrue();

        nm.RemoveVariableNode("eq-1/A", realm: AddressSpaceRealm.Uns).ShouldBeTrue();

        nm.TryGetVariable("eq-1/A").ShouldBeNull();
        nm.VariableCount.ShouldBe(countBefore - 1);
        nm.TryGetHistorizedTagname("eq-1/A", out _).ShouldBeFalse();
        // The sibling variable is untouched.
        nm.TryGetVariable("eq-1/B").ShouldNotBeNull();

        await host.DisposeAsync();
    }

    /// <summary>Removing an unknown variable id returns false (map drift ⇒ caller rebuilds).</summary>
    [Trait("Category", "Unit")]
    [Fact]
    public async Task RemoveVariableNode_unknown_id_returns_false()
    {
        var (host, server) = await BootAsync();
        var nm = server.NodeManager!;

        nm.RemoveVariableNode("eq-1/nope", realm: AddressSpaceRealm.Uns).ShouldBeFalse();

        await host.DisposeAsync();
    }

    /// <summary>The built removed-node event announces the deleted node with verb NodeDeleted.</summary>
    [Trait("Category", "Unit")]
    [Fact]
    public async Task Built_removed_event_announces_the_deleted_node_with_NodeDeleted_verb()
    {
        var (host, server) = await BootAsync();
        var nm = server.NodeManager!;
        nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
        nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, realm: AddressSpaceRealm.Uns);

        var e = nm.BuildNodesRemovedModelChange("eq-1/A");

        e.ShouldNotBeNull();
        e.Changes.ShouldNotBeNull();
        var changes = e.Changes.Value;
        changes.Length.ShouldBe(1);
        changes[0].Verb.ShouldBe((byte)ModelChangeStructureVerbMask.NodeDeleted);

        await host.DisposeAsync();
    }

    // ---------- T9: RemoveAlarmConditionNode ----------

    /// <summary>Materialise a native alarm condition then remove it: it disappears from the condition map and
    /// the native flag is cleared; unknown id returns false.</summary>
    [Trait("Category", "Unit")]
    [Fact]
    public async Task RemoveAlarmConditionNode_drops_condition_and_native_flag()
    {
        var (host, server) = await BootAsync();
        var nm = server.NodeManager!;
        nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
        nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
        nm.TryGetAlarmCondition("eq-1/OverTemp").ShouldNotBeNull();
        nm.IsNativeAlarmNode("eq-1/OverTemp").ShouldBeTrue();

        nm.RemoveAlarmConditionNode("eq-1/OverTemp", realm: AddressSpaceRealm.Uns).ShouldBeTrue();

        nm.TryGetAlarmCondition("eq-1/OverTemp").ShouldBeNull();
        nm.IsNativeAlarmNode("eq-1/OverTemp").ShouldBeFalse();

        nm.RemoveAlarmConditionNode("eq-1/OverTemp", realm: AddressSpaceRealm.Uns).ShouldBeFalse(); // already gone ⇒ false

        await host.DisposeAsync();
    }

    /// <summary>A scripted condition removes cleanly too (no native flag was set).</summary>
    [Trait("Category", "Unit")]
    [Fact]
    public async Task RemoveAlarmConditionNode_removes_scripted_condition()
    {
        var (host, server) = await BootAsync();
        var nm = server.NodeManager!;
        nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
        nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 500, isNative: false, realm: AddressSpaceRealm.Uns);

        nm.RemoveAlarmConditionNode("alm-1", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
        nm.TryGetAlarmCondition("alm-1").ShouldBeNull();

        await host.DisposeAsync();
    }

    // ---------- T10: RemoveEquipmentSubtree ----------

    /// <summary>Remove an equipment folder carrying a sub-folder, variables (one historized), and a condition:
    /// every descendant disappears from every map, the notifier registration is demoted, and a SIBLING
    /// equipment is fully intact.</summary>
    [Trait("Category", "Unit")]
    [Fact]
    public async Task RemoveEquipmentSubtree_removes_all_descendants_and_leaves_siblings_intact()
    {
        var (host, server) = await BootAsync();
        var nm = server.NodeManager!;

        // Target equipment eq-1 with a sub-folder, two variables (one historized), and a native condition.
        nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
        nm.EnsureFolder("eq-1/Diag", parentNodeId: "eq-1", displayName: "Diag", realm: AddressSpaceRealm.Uns);
        nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A", realm: AddressSpaceRealm.Uns);
        nm.EnsureVariable("eq-1/Diag/T", "eq-1/Diag", "T", "Float", writable: false, realm: AddressSpaceRealm.Uns);
        nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);

        // Sibling equipment eq-2 that must survive untouched.
        nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2", realm: AddressSpaceRealm.Uns);
        nm.EnsureVariable("eq-2/S", "eq-2", "S", "Float", writable: false, realm: AddressSpaceRealm.Uns);

        nm.RemoveEquipmentSubtree("eq-1", realm: AddressSpaceRealm.Uns).ShouldBeTrue();

        // Every eq-1 descendant is gone from every map.
        nm.TryGetFolder("eq-1").ShouldBeNull();
        nm.TryGetFolder("eq-1/Diag").ShouldBeNull();
        nm.TryGetVariable("eq-1/A").ShouldBeNull();
        nm.TryGetVariable("eq-1/Diag/T").ShouldBeNull();
        nm.TryGetAlarmCondition("eq-1/OverTemp").ShouldBeNull();
        nm.TryGetHistorizedTagname("eq-1/A", out _).ShouldBeFalse();
        nm.IsNativeAlarmNode("eq-1/OverTemp").ShouldBeFalse();

        // Sibling eq-2 is fully intact.
        nm.TryGetFolder("eq-2").ShouldNotBeNull();
        nm.TryGetVariable("eq-2/S").ShouldNotBeNull();

        // Re-materialising an alarm under eq-2 still works (the notifier machinery was not corrupted by the
        // eq-1 demotion) — proves no orphaned root-notifier ref broke the event path.
        Should.NotThrow(() => nm.MaterialiseAlarmCondition("eq-2/Alm", "eq-2", "Alm", "OffNormalAlarm", 300, isNative: false, realm: AddressSpaceRealm.Uns));

        await host.DisposeAsync();
    }

    /// <summary>Removing an unknown equipment id returns false (map drift ⇒ caller rebuilds).</summary>
    [Trait("Category", "Unit")]
    [Fact]
    public async Task RemoveEquipmentSubtree_unknown_id_returns_false()
    {
        var (host, server) = await BootAsync();
        var nm = server.NodeManager!;

        nm.RemoveEquipmentSubtree("eq-nope", realm: AddressSpaceRealm.Uns).ShouldBeFalse();

        await host.DisposeAsync();
    }

    private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
    {
        var host = new OpcUaApplicationHost(
            new OpcUaApplicationHostOptions
            {
                ApplicationName = "OtOpcUa.SurgicalRemoveTest",
                ApplicationUri = $"urn:OtOpcUa.SurgicalRemoveTest:{Guid.NewGuid():N}",
                OpcUaPort = AllocateFreePort(),
                PublicHostname = "localhost",
                PkiStoreRoot = _pkiRoot,
            },
            Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance);

        var server = new OtOpcUaSdkServer();
        await host.StartAsync(server, Ct);
        return (host, server);
    }

    private static int AllocateFreePort()
    {
        using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
        listener.Start();
        var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    }

    /// <summary>Cleans up the PKI root directory.</summary>
    public void Dispose()
    {
        if (Directory.Exists(_pkiRoot))
        {
            try { Directory.Delete(_pkiRoot, recursive: true); }
            catch { /* best-effort cleanup */ }
        }
    }
}
