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.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Security;

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

/// <summary>
///     End-to-end proof that ADR-001 Option A wire-in (#212) flows: when
///     <see cref="OpcUaApplicationHost"/> is given an <c>equipmentContentLookup</c> that
///     returns a non-null <see cref="EquipmentNamespaceContent"/>, the walker runs BEFORE
///     the driver's DiscoverAsync + the UNS folder skeleton (Area → Line → Equipment) +
///     identifier properties are materialized into the driver's namespace + visible to an
///     OPC UA client via standard browse.
/// </summary>
[Trait("Category", "Integration")]
public sealed class OpcUaEquipmentWalkerIntegrationTests : IAsyncLifetime
{
    private static readonly int Port = 48500 + Random.Shared.Next(0, 99);
    private readonly string _endpoint = $"opc.tcp://localhost:{Port}/OtOpcUaWalkerTest";
    private readonly string _pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-walker-{Guid.NewGuid():N}");
    private const string DriverId = "galaxy-prod";

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

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

        var content = BuildFixture();

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

        _server = new OpcUaApplicationHost(
            options, _driverHost, new DenyAllUserAuthenticator(),
            NullLoggerFactory.Instance, NullLogger<OpcUaApplicationHost>.Instance,
            equipmentContentLookup: id => id == DriverId ? content : null);

        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 Walker_Materializes_Area_Line_Equipment_Folders_Visible_Via_Browse()
    {
        using var session = await OpenSessionAsync();
        var nsIndex = (ushort)session.NamespaceUris.GetIndex($"urn:OtOpcUa:{DriverId}");

        var areaFolder = new NodeId($"{DriverId}/warsaw", nsIndex);
        var lineFolder = new NodeId($"{DriverId}/warsaw/line-a", nsIndex);
        var equipmentFolder = new NodeId($"{DriverId}/warsaw/line-a/oven-3", nsIndex);

        BrowseChildren(session, areaFolder).ShouldContain(r => r.BrowseName.Name == "line-a");
        BrowseChildren(session, lineFolder).ShouldContain(r => r.BrowseName.Name == "oven-3");

        var equipmentChildren = BrowseChildren(session, equipmentFolder);
        equipmentChildren.ShouldContain(r => r.BrowseName.Name == "EquipmentId");
        equipmentChildren.ShouldContain(r => r.BrowseName.Name == "EquipmentUuid");
        equipmentChildren.ShouldContain(r => r.BrowseName.Name == "MachineCode");
    }

    [Fact]
    public async Task Walker_Emits_Tag_Variable_Under_Equipment_Readable_By_Client()
    {
        using var session = await OpenSessionAsync();
        var nsIndex = (ushort)session.NamespaceUris.GetIndex($"urn:OtOpcUa:{DriverId}");

        var tagNode = new NodeId("plcaddr-temperature", nsIndex);
        var equipmentFolder = new NodeId($"{DriverId}/warsaw/line-a/oven-3", nsIndex);

        BrowseChildren(session, equipmentFolder).ShouldContain(r => r.BrowseName.Name == "Temperature");

        var dv = session.ReadValue(tagNode);
        dv.ShouldNotBeNull();
    }

    private static ReferenceDescriptionCollection BrowseChildren(ISession session, NodeId node)
    {
        session.Browse(null, null, node, 0, BrowseDirection.Forward,
            ReferenceTypeIds.HierarchicalReferences, true,
            (uint)NodeClass.Object | (uint)NodeClass.Variable,
            out _, out var refs);
        return refs;
    }

    private static EquipmentNamespaceContent BuildFixture()
    {
        var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c-local", Name = "warsaw", GenerationId = 1 };
        var line = new UnsLine { UnsLineId = "line-a", UnsAreaId = "area-1", Name = "line-a", GenerationId = 1 };
        var oven = new Equipment
        {
            EquipmentRowId = Guid.NewGuid(), GenerationId = 1,
            EquipmentId = "eq-oven-3", EquipmentUuid = Guid.NewGuid(),
            DriverInstanceId = DriverId, UnsLineId = "line-a", Name = "oven-3",
            MachineCode = "MC-oven-3",
        };
        var tempTag = new Tag
        {
            TagRowId = Guid.NewGuid(), GenerationId = 1, TagId = "tag-1",
            DriverInstanceId = DriverId, EquipmentId = "eq-oven-3",
            Name = "Temperature", DataType = "Int32",
            AccessLevel = TagAccessLevel.ReadWrite, TagConfig = "plcaddr-temperature",
        };

        return new EquipmentNamespaceContent(
            Areas: new[] { area },
            Lines: new[] { line },
            Equipment: new[] { oven },
            Tags: new[] { tempTag });
    }

    private async Task<ISession> OpenSessionAsync()
    {
        var cfg = new ApplicationConfiguration
        {
            ApplicationName = "OtOpcUaWalkerTestClient",
            ApplicationUri = "urn:OtOpcUa:WalkerTestClient",
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = new SecurityConfiguration
            {
                ApplicationCertificate = new CertificateIdentifier
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = Path.Combine(_pkiRoot, "client-own"),
                    SubjectName = "CN=OtOpcUaWalkerTestClient",
                },
                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);

        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, "OtOpcUaWalkerTestClientSession", 60000,
            new UserIdentity(new AnonymousIdentityToken()), null);
    }

    /// <summary>
    ///     Driver that registers into the host + implements DiscoverAsync as a no-op. The
    ///     walker is the sole source of address-space content; if the UNS folders appear
    ///     under browse, they came from the wire-in (not from the driver's own discovery).
    /// </summary>
    private sealed class EmptyDriver : IDriver, ITagDiscovery, IReadable
    {
        public EmptyDriver(string id) { DriverInstanceId = id; }
        public string DriverInstanceId { get; }
        public string DriverType => "EmptyForWalkerTest";

        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) => Task.CompletedTask;

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