using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;

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

/// <summary>
///     OpcUaServer-004 — a fresh <see cref="OtOpcUaNodeManager"/> whose <c>CreateAddressSpace</c> has NOT
///     yet run (i.e. the server has not started) has a null <c>_root</c>. Every public address-space mutator
///     (<see cref="OtOpcUaNodeManager.WriteValue"/>, <see cref="OtOpcUaNodeManager.WriteAlarmCondition"/>,
///     <see cref="OtOpcUaNodeManager.EnsureFolder"/>, <see cref="OtOpcUaNodeManager.EnsureVariable"/>,
///     <see cref="OtOpcUaNodeManager.MaterialiseAlarmCondition"/>) must now fail with a legible
///     <see cref="InvalidOperationException"/> instead of a bare NRE out of <c>ResolveParentFolder</c> /
///     <c>CreateVariable</c>.
///     <para>
///         The node manager's ctor needs a real <see cref="Opc.Ua.Server.IServerInternal"/> +
///         <see cref="ApplicationConfiguration"/>, which only the SDK boot produces — so we boot a real
///         <see cref="OpcUaApplicationHost"/>, borrow those two from the LIVE (already-started) node manager
///         (its public <c>Server</c> + <c>Server.Configuration</c>), then construct a SECOND, fresh node
///         manager from them. That second manager never had <c>CreateAddressSpace</c> driven, so it
///         reproduces the pre-start ordering hazard exactly.
///     </para>
/// </summary>
public sealed class NodeManagerPreStartGuardTests : IDisposable
{
    private static CancellationToken Ct => TestContext.Current.CancellationToken;

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

    [Fact]
    public async Task EnsureFolder_before_CreateAddressSpace_throws_InvalidOperationException()
    {
        var (host, nm) = await BuildPreStartNodeManagerAsync();
        try
        {
            var ex = Should.Throw<InvalidOperationException>(() =>
                nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns));
            ex.Message.ShouldContain("address space has not been created");
        }
        finally
        {
            await host.DisposeAsync();
        }
    }

    [Fact]
    public async Task EnsureVariable_before_CreateAddressSpace_throws_InvalidOperationException()
    {
        var (host, nm) = await BuildPreStartNodeManagerAsync();
        try
        {
            Should.Throw<InvalidOperationException>(() =>
                nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp",
                    dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns));
        }
        finally
        {
            await host.DisposeAsync();
        }
    }

    [Fact]
    public async Task WriteValue_before_CreateAddressSpace_throws_InvalidOperationException()
    {
        var (host, nm) = await BuildPreStartNodeManagerAsync();
        try
        {
            Should.Throw<InvalidOperationException>(() =>
                nm.WriteValue("eq-1/temp", 1.0, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns));
        }
        finally
        {
            await host.DisposeAsync();
        }
    }

    [Fact]
    public async Task MaterialiseAlarmCondition_before_CreateAddressSpace_throws_InvalidOperationException()
    {
        var (host, nm) = await BuildPreStartNodeManagerAsync();
        try
        {
            Should.Throw<InvalidOperationException>(() =>
                nm.MaterialiseAlarmCondition("alarm-1", "eq-1", "Cond", "OffNormalAlarm", severity: 500, realm: AddressSpaceRealm.Uns));
        }
        finally
        {
            await host.DisposeAsync();
        }
    }

    /// <summary>Boot a real host, borrow the live node manager's real
    /// <see cref="Opc.Ua.Server.IServerInternal"/>, then construct a SECOND node manager from it (with a
    /// fresh <see cref="ApplicationConfiguration"/> — the ctor only records it + sets namespaces) that has
    /// NEVER had <c>CreateAddressSpace</c> driven (so <c>_root</c> is null). The host is returned so the
    /// caller disposes it after exercising the guard.</summary>
    private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager NodeManager)> BuildPreStartNodeManagerAsync()
    {
        var host = new OpcUaApplicationHost(
            new OpcUaApplicationHostOptions
            {
                ApplicationName = "OtOpcUa.PreStartGuardTest",
                ApplicationUri = $"urn:OtOpcUa.PreStartGuardTest:{Guid.NewGuid():N}",
                OpcUaPort = AllocateFreePort(),
                PublicHostname = "localhost",
                PkiStoreRoot = _pkiRoot,
            },
            NullLogger<OpcUaApplicationHost>.Instance);

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

        var live = server.NodeManager!;
        // Borrow the SDK's real IServerInternal from the live manager and build a brand-new node manager —
        // CreateAddressSpace has not been driven on THIS instance, so _root is null and every mutator must
        // hit the EnsureAddressSpaceCreated guard.
        var fresh = new OtOpcUaNodeManager(live.Server, new ApplicationConfiguration());
        return (host, fresh);
    }

    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;
    }

    public void Dispose()
    {
        if (Directory.Exists(_pkiRoot))
        {
            try { Directory.Delete(_pkiRoot, recursive: true); }
            catch { /* best-effort cleanup */ }
        }
    }
}
