using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Security;

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

[Trait("Category", "Integration")]
public sealed class OpcUaServerIntegrationTests : IAsyncLifetime
{
    // Use a non-default port + per-test-run PKI root to avoid colliding with anything else
    // running on the box (a live v1 Host or a developer's previous run).
    private static readonly int Port = 48400 + Random.Shared.Next(0, 99);
    private readonly string _endpoint = $"opc.tcp://localhost:{Port}/OtOpcUaTest";
    private readonly string _pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-test-{Guid.NewGuid():N}");

    private DriverHost _driverHost = null!;
    private OpcUaApplicationHost _server = null!;
    private FakeDriver _driver = null!;

    public async ValueTask InitializeAsync()
    {
        _driverHost = new DriverHost();
        _driver = new FakeDriver();
        await _driverHost.RegisterAsync(_driver, "{}", CancellationToken.None);

        var options = new OpcUaServerOptions
        {
            EndpointUrl = _endpoint,
            ApplicationName = "OtOpcUaTest",
            ApplicationUri = "urn:OtOpcUa:Server:Test",
            PkiStoreRoot = _pkiRoot,
            AutoAcceptUntrustedClientCertificates = true, HealthEndpointsEnabled = false,
        };

        _server = new OpcUaApplicationHost(options, _driverHost, new DenyAllUserAuthenticator(),
            NullLoggerFactory.Instance, NullLogger<OpcUaApplicationHost>.Instance);
        await _server.StartAsync(CancellationToken.None);
    }

    public async ValueTask DisposeAsync()
    {
        await _server.DisposeAsync();
        await _driverHost.DisposeAsync();
        try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort */ }
    }

    [Fact]
    public async Task Client_can_connect_and_browse_driver_subtree()
    {
        using var session = await OpenSessionAsync();

        // Browse the driver subtree registered under ObjectsFolder. FakeDriver registers one
        // folder ("TestFolder") with one variable ("Var1"), so we expect to see our driver's
        // root folder plus standard UA children.
        var rootRef = new NodeId("fake", (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:fake"));
        session.Browse(null, null, rootRef, 0, BrowseDirection.Forward, ReferenceTypeIds.HierarchicalReferences,
            true, (uint)NodeClass.Object | (uint)NodeClass.Variable, out _, out var references);

        references.Count.ShouldBeGreaterThan(0);
        references.ShouldContain(r => r.BrowseName.Name == "TestFolder");
    }

    [Fact]
    public async Task Client_can_read_a_driver_variable_through_the_node_manager()
    {
        using var session = await OpenSessionAsync();

        var nsIndex = (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:fake");
        var varNodeId = new NodeId("TestFolder.Var1", nsIndex);

        var dv = session.ReadValue(varNodeId);
        dv.ShouldNotBeNull();
        // FakeDriver.ReadAsync returns 42 as the value.
        dv.Value.ShouldBe(42);
    }

    private async Task<ISession> OpenSessionAsync()
    {
        var cfg = new ApplicationConfiguration
        {
            ApplicationName = "OtOpcUaTestClient",
            ApplicationUri = "urn:OtOpcUa:TestClient",
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = new SecurityConfiguration
            {
                ApplicationCertificate = new CertificateIdentifier
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = Path.Combine(_pkiRoot, "client-own"),
                    SubjectName = "CN=OtOpcUaTestClient",
                },
                TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-issuers") },
                TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-trusted") },
                RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-rejected") },
                AutoAcceptUntrustedCertificates = true,
                AddAppCertToTrustedStore = true,
            },
            TransportConfigurations = new TransportConfigurationCollection(),
            TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
            ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
        };
        await cfg.Validate(ApplicationType.Client);
        cfg.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;

        var instance = new ApplicationInstance { ApplicationConfiguration = cfg, ApplicationType = ApplicationType.Client };
        await instance.CheckApplicationInstanceCertificate(true, CertificateFactory.DefaultKeySize);

        // Let the client fetch the live endpoint description from the running server so the
        // UserTokenPolicy it signs with matches what the server actually advertised (including
        // the PolicyId = "Anonymous" the server sets).
        var selected = CoreClientUtils.SelectEndpoint(cfg, _endpoint, useSecurity: false);
        var endpointConfig = EndpointConfiguration.Create(cfg);
        var configuredEndpoint = new ConfiguredEndpoint(null, selected, endpointConfig);

        return await Session.Create(cfg, configuredEndpoint, false, "OtOpcUaTestClientSession", 60000,
            new UserIdentity(new AnonymousIdentityToken()), null);
    }

    /// <summary>
    ///     Minimum driver that implements enough of IDriver + ITagDiscovery + IReadable to drive
    ///     the integration test. Returns a single folder with one variable that reads as 42.
    /// </summary>
    private sealed class FakeDriver : IDriver, ITagDiscovery, IReadable
    {
        public string DriverInstanceId => "fake";
        public string DriverType => "Fake";

        public Task InitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
        public Task ReinitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
        public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
        public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
        public long GetMemoryFootprint() => 0;
        public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;

        public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
        {
            var folder = builder.Folder("TestFolder", "TestFolder");
            folder.Variable("Var1", "Var1", new DriverAttributeInfo(
                "TestFolder.Var1", DriverDataType.Int32, false, null, SecurityClassification.FreeAccess, false, IsAlarm: false));
            return Task.CompletedTask;
        }

        public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
            IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
        {
            var now = DateTime.UtcNow;
            IReadOnlyList<DataValueSnapshot> result =
                fullReferences.Select(_ => new DataValueSnapshot(42, 0u, now, now)).ToArray();
            return Task.FromResult(result);
        }
    }
}
