using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

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

/// <summary>
///     Smoke tests for the PR 4.0 driver skeleton. The IDriver shape, factory parsing,
///     and lifecycle methods land in PR 4.0; capability bodies (browse / read / write /
///     subscribe / health forwarder / probe watcher) are tested in PRs 4.1–4.7 each
///     against their own seam.
/// </summary>
public sealed class GalaxyDriverFactoryTests
{
    private const string MinimalConfig = """
        {
          "Gateway": { "Endpoint": "https://mxgw.test:5001", "ApiKeySecretRef": "galaxy:apiKey" },
          "MxAccess": { "ClientName": "OtOpcUa-A" }
        }
        """;

    /// <summary>Verifies that minimal config is parsed and defaults are applied.</summary>
    [Fact]
    public void CreateInstance_ParsesMinimalConfig_AndAppliesDefaults()
    {
        var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-instance-a", MinimalConfig);

        driver.DriverInstanceId.ShouldBe("galaxy-instance-a");
        driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName);
        driver.Options.Gateway.Endpoint.ShouldBe("https://mxgw.test:5001");
        driver.Options.Gateway.ApiKeySecretRef.ShouldBe("galaxy:apiKey");
        driver.Options.Gateway.UseTls.ShouldBeTrue();
        driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(10);
        driver.Options.MxAccess.ClientName.ShouldBe("OtOpcUa-A");
        driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(1000);
        driver.Options.Repository.DiscoverPageSize.ShouldBe(5000);
        driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeTrue();
    }

    /// <summary>Verifies that defaults are overridden from full config.</summary>
    [Fact]
    public void CreateInstance_OverridesDefaults_FromFullConfig()
    {
        const string fullConfig = """
            {
              "Gateway": {
                "Endpoint": "https://mxgw.prod:5001",
                "ApiKeySecretRef": "secret:abc",
                "UseTls": false,
                "CaCertificatePath": "C:/certs/ca.crt",
                "ConnectTimeoutSeconds": 5,
                "DefaultCallTimeoutSeconds": 3,
                "StreamTimeoutSeconds": 60
              },
              "MxAccess": {
                "ClientName": "OtOpcUa-Prod",
                "PublishingIntervalMs": 250,
                "WriteUserId": 17
              },
              "Repository": { "DiscoverPageSize": 1000, "WatchDeployEvents": false },
              "Reconnect": { "InitialBackoffMs": 100, "MaxBackoffMs": 5000, "ReplayOnSessionLost": false }
            }
            """;

        var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-prod", fullConfig);

        driver.Options.Gateway.UseTls.ShouldBeFalse();
        driver.Options.Gateway.CaCertificatePath.ShouldBe("C:/certs/ca.crt");
        driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(5);
        driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(250);
        driver.Options.MxAccess.WriteUserId.ShouldBe(17);
        driver.Options.Repository.DiscoverPageSize.ShouldBe(1000);
        driver.Options.Repository.WatchDeployEvents.ShouldBeFalse();
        driver.Options.Reconnect.InitialBackoffMs.ShouldBe(100);
        driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeFalse();
    }

    /// <summary>Verifies that the factory binds the v3 RawTags array from the driver config JSON.</summary>
    [Fact]
    public void CreateInstance_BindsRawTags_FromConfig()
    {
        const string configWithRawTags = """
            {
              "Gateway": { "Endpoint": "https://mxgw.test:5001", "ApiKeySecretRef": "galaxy:apiKey" },
              "MxAccess": { "ClientName": "OtOpcUa-A" },
              "RawTags": [
                { "RawPath": "area/line/tank1/level", "TagConfig": "{\"attributeRef\":\"Tank1.Level\"}", "WriteIdempotent": true },
                { "RawPath": "area/line/tank1/sp", "TagConfig": "{\"attributeRef\":\"Tank1.SP\"}", "WriteIdempotent": false }
              ]
            }
            """;

        var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-rawtags", configWithRawTags);

        driver.Options.RawTags.Count.ShouldBe(2);
        driver.Options.RawTags[0].RawPath.ShouldBe("area/line/tank1/level");
        driver.Options.RawTags[0].TagConfig.ShouldContain("Tank1.Level");
        driver.Options.RawTags[0].WriteIdempotent.ShouldBeTrue();
        driver.Options.RawTags[1].WriteIdempotent.ShouldBeFalse();
    }

    /// <summary>Verifies that config without a RawTags array binds to an empty list (identity path).</summary>
    [Fact]
    public void CreateInstance_NoRawTags_DefaultsToEmpty()
    {
        var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-none", MinimalConfig);
        driver.Options.RawTags.ShouldBeEmpty();
    }

    /// <summary>Verifies that missing endpoint throws an exception.</summary>
    [Fact]
    public void CreateInstance_MissingEndpoint_Throws()
    {
        const string bad = """{"Gateway":{"ApiKeySecretRef":"x"},"MxAccess":{"ClientName":"y"}}""";
        Should.Throw<InvalidOperationException>(
            () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("Gateway.Endpoint");
    }

    /// <summary>Verifies that missing API key throws an exception.</summary>
    [Fact]
    public void CreateInstance_MissingApiKey_Throws()
    {
        const string bad = """{"Gateway":{"Endpoint":"x"},"MxAccess":{"ClientName":"y"}}""";
        Should.Throw<InvalidOperationException>(
            () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("ApiKeySecretRef");
    }

    /// <summary>Verifies that missing client name throws an exception.</summary>
    [Fact]
    public void CreateInstance_MissingClientName_Throws()
    {
        const string bad = """{"Gateway":{"Endpoint":"x","ApiKeySecretRef":"y"}}""";
        Should.Throw<InvalidOperationException>(
            () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("MxAccess.ClientName");
    }

    /// <summary>Verifies that the factory is registered in the driver factory registry.</summary>
    [Fact]
    public void Register_AddsFactoryToRegistry()
    {
        var registry = new DriverFactoryRegistry();
        GalaxyDriverFactoryExtensions.Register(registry, NullSecretResolver.Instance);

        registry.RegisteredTypes.ShouldContain(GalaxyDriverFactoryExtensions.DriverTypeName);

        var factory = registry.TryGet(GalaxyDriverFactoryExtensions.DriverTypeName);
        factory.ShouldNotBeNull();

        var driver = factory!.Invoke("galaxy-x", MinimalConfig);
        driver.ShouldNotBeNull();
        driver.DriverInstanceId.ShouldBe("galaxy-x");
        driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName);
    }

    /// <summary>Verifies that driver lifecycle toggles health state on initialize and shutdown.</summary>
    [Fact]
    public async Task DriverLifecycle_InitializeShutdown_ToggleHealth()
    {
        // Inject a no-op subscriber seam so InitializeAsync skips BuildProductionRuntimeAsync
        // (no gw connect attempt). The factory tests only care about the lifecycle health
        // toggle; real-runtime wire-up is exercised in PR 4.W's BuildProductionRuntime tests.
        using var driver = new GalaxyDriver(
            "galaxy-x", BuildOptions(), hierarchySource: null, dataReader: null,
            dataWriter: null, subscriber: new NoopSubscriber());
        driver.GetHealth().State.ShouldBe(DriverState.Unknown);

        await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
        driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();

        await driver.ShutdownAsync(CancellationToken.None);
        driver.GetHealth().State.ShouldBe(DriverState.Unknown);

        driver.GetMemoryFootprint().ShouldBe(0);
        await driver.FlushOptionalCachesAsync(CancellationToken.None); // no-op shouldn't throw
    }

    /// <summary>Verifies that reinitializing with equivalent config refreshes health.</summary>
    [Fact]
    public async Task ReinitializeAsync_RefreshesHealth_WhenConfigIsEquivalent()
    {
        // Driver.Galaxy-013: ReinitializeAsync now compares the incoming JSON to the
        // live options. An equivalent config is accepted and refreshes health; a
        // non-equivalent reapply throws NotSupportedException (covered in
        // GalaxyDriverInfrastructureTests.ReinitializeAsync_RejectsNonEquivalentConfigChange).
        // Build a config JSON whose parsed shape equals BuildOptions() so the
        // equivalence check passes.
        const string equivalentConfig = """
            {
              "Gateway": { "Endpoint": "https://mxgw.test:5001", "ApiKeySecretRef": "key" },
              "MxAccess": { "ClientName": "OtOpcUa-A" }
            }
            """;
        using var driver = new GalaxyDriver(
            "galaxy-x", BuildOptions(), hierarchySource: null, dataReader: null,
            dataWriter: null, subscriber: new NoopSubscriber());
        await driver.InitializeAsync(equivalentConfig, CancellationToken.None);
        var firstStamp = driver.GetHealth().LastSuccessfulRead!.Value;

        // Force a measurable clock delta so the comparison is stable on fast machines.
        await Task.Delay(20);
        await driver.ReinitializeAsync(equivalentConfig, CancellationToken.None);

        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
        driver.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(firstStamp);
    }

    private static GalaxyDriverOptions BuildOptions() => new(
        new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
        new GalaxyMxAccessOptions("OtOpcUa-A"),
        new GalaxyRepositoryOptions(),
        new GalaxyReconnectOptions());

    /// <summary>
    ///     Minimum-surface <see cref="IGalaxySubscriber"/> seam — enough to satisfy
    ///     <see cref="GalaxyDriver.InitializeAsync"/>'s "skip production-runtime build" branch
    ///     without driving any actual subscribe/event-pump traffic.
    /// </summary>
    private sealed class NoopSubscriber : IGalaxySubscriber
    {
        /// <inheritdoc />
        public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
            IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
            => Task.FromResult<IReadOnlyList<SubscribeResult>>([]);

        /// <inheritdoc />
        public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
            => Task.CompletedTask;

        /// <inheritdoc />
        public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
            [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await Task.CompletedTask;
            yield break;
        }
    }

    /// <summary>Verifies that dispose is idempotent and shutdown after dispose is harmless.</summary>
    [Fact]
    public void Dispose_IsIdempotent_AndShutdownAfterDisposeIsHarmless()
    {
        var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
        driver.Dispose();
        Should.NotThrow(() => driver.Dispose());
    }

    /// <summary>Verifies that initializing after dispose throws an exception.</summary>
    [Fact]
    public async Task InitializeAfterDispose_Throws()
    {
        var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
        driver.Dispose();
        await Should.ThrowAsync<ObjectDisposedException>(() =>
            driver.InitializeAsync(MinimalConfig, CancellationToken.None));
    }

    /// <summary>Verifies that the driver implements all Phase 4 capabilities.</summary>
    [Fact]
    public void DriverImplementsAllPhase4Capabilities()
    {
        // PR 4.W contract pin — every capability the in-process Galaxy driver must
        // expose for parity with GalaxyProxyDriver. If any of these regress, the
        // Galaxy:Backend flag flip in PR 7.1 will silently lose surface.
        var driver = GalaxyDriverFactoryExtensions.CreateInstance("g", MinimalConfig);
        driver.ShouldBeAssignableTo<ITagDiscovery>();
        driver.ShouldBeAssignableTo<IReadable>();
        driver.ShouldBeAssignableTo<IWritable>();
        driver.ShouldBeAssignableTo<ISubscribable>();
        driver.ShouldBeAssignableTo<IRediscoverable>();
        driver.ShouldBeAssignableTo<IHostConnectivityProbe>();
    }

    /// <summary>Verifies that GetHostStatuses returns an empty snapshot after initialization with seam.</summary>
    [Fact]
    public async Task GetHostStatuses_AfterInitWithSeam_ReturnsEmptySnapshot()
    {
        // PR 4.W wire-up assertion: when InitializeAsync skips the production-runtime
        // build (seam injected), no transport-forwarder or probe-watcher pushes a
        // status, so the aggregator snapshot is empty. The forwarder + watcher have
        // their own unit tests in PR 4.7.
        using var driver = new GalaxyDriver(
            "g", BuildOptions(), hierarchySource: null, dataReader: null,
            dataWriter: null, subscriber: new NoopSubscriber());
        await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
        driver.GetHostStatuses().ShouldBeEmpty();
    }

    /// <summary>Verifies that the driver type is GalaxyMxGateway, not the legacy Galaxy type.</summary>
    [Fact]
    public void DriverType_IsGalaxyMxGateway_NotLegacyGalaxy()
    {
        // Must match GalaxyProxyDriver's DriverType ("Galaxy") side-by-side without
        // collision so DriverInstanceBootstrapper can resolve both factories.
        var driver = GalaxyDriverFactoryExtensions.CreateInstance("g", MinimalConfig);
        driver.DriverType.ShouldBe("GalaxyMxGateway");
    }
}
