using System.Threading.Channels;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
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>
///     Driver-level infrastructure tests for findings Driver.Galaxy-007 (dispose-CTS gate)
///     and Driver.Galaxy-011 (GetMemoryFootprint non-zero estimate).
/// </summary>
public sealed class GalaxyDriverInfrastructureTests
{
    private static GalaxyDriverOptions Opts() => new(
        new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
        new GalaxyMxAccessOptions("InfraTest"),
        new GalaxyRepositoryOptions(WatchDeployEvents: false),
        new GalaxyReconnectOptions());

    // ===== Driver.Galaxy-011 regression: GetMemoryFootprint reflects registry size =====

    /// <summary>Verifies that memory footprint is zero when there are no subscriptions.</summary>
    [Fact]
    public void GetMemoryFootprint_IsZeroWhenNoSubscriptions()
    {
        var sub = new NoOpSubscriber();
        using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);

        driver.GetMemoryFootprint().ShouldBe(0L);
    }

    /// <summary>Verifies that memory footprint is nonzero after subscriptions are active.</summary>
    [Fact]
    public async Task GetMemoryFootprint_IsNonZeroAfterSubscribe()
    {
        // When subscriptions are active the footprint estimate must be > 0 so the
        // server's memory-pressure mechanism sees the Galaxy driver as a participant.
        var sub = new NoOpSubscriber();
        using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);

        await driver.SubscribeAsync(["Tag.A", "Tag.B"], TimeSpan.Zero, CancellationToken.None);

        driver.GetMemoryFootprint().ShouldBeGreaterThan(0L);
    }

    /// <summary>Verifies that memory footprint decreases after unsubscribing.</summary>
    [Fact]
    public async Task GetMemoryFootprint_DecreasesAfterUnsubscribe()
    {
        var sub = new NoOpSubscriber();
        using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);

        var handle = await driver.SubscribeAsync(["Tag.A", "Tag.B"], TimeSpan.Zero, CancellationToken.None);
        var afterSubscribe = driver.GetMemoryFootprint();
        afterSubscribe.ShouldBeGreaterThan(0L);

        await driver.UnsubscribeAsync(handle, CancellationToken.None);
        var afterUnsubscribe = driver.GetMemoryFootprint();
        afterUnsubscribe.ShouldBeLessThan(afterSubscribe,
            "footprint must decrease when subscriptions are removed");
    }

    // ===== Driver.Galaxy-007 regression: Dispose cancels the dispose CTS =====

    /// <summary>Verifies that Dispose sets disposed flag and blocks further capability calls.</summary>
    [Fact]
    public async Task Dispose_SetsDisposedFlag_BlockingFurtherCapabilityCalls()
    {
        var sub = new NoOpSubscriber();
        var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);
        driver.Dispose();

        // Capability entry points all check ObjectDisposedException.ThrowIf — SubscribeAsync
        // is representative and is guarded on line 1 of its body.
        await Should.ThrowAsync<ObjectDisposedException>(() =>
            driver.SubscribeAsync(["Tag.A"], TimeSpan.Zero, CancellationToken.None));
    }

    /// <summary>Verifies that DisposeAsync can be awaited without deadlock.</summary>
    [Fact]
    public async Task DisposeAsync_CanBeAwaitedWithoutDeadlock()
    {
        var sub = new NoOpSubscriber();
        var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);
        // IAsyncDisposable.DisposeAsync must not block or throw.
        await Should.NotThrowAsync(async () => await driver.DisposeAsync());
    }

    // ===== Driver.Galaxy-013 regression: ReplayOnSessionLost gates the replay step =====

    /// <summary>Verifies that ReplayOnSessionLost=false skips resubscription on reconnect.</summary>
    [Fact]
    public async Task ReplayOnSessionLost_False_SkipsResubscribeBulk()
    {
        // ReplayOnSessionLost was a dangling option — defined + documented but never
        // read. After the fix, setting it to false makes the reconnect replay path
        // skip SubscribeBulk (operator opts out of replay; the gateway's session-level
        // ReplaySubscriptions handles state restoration).
        var sub = new ReplayCountingSubscriber();
        var opts = new GalaxyDriverOptions(
            new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
            new GalaxyMxAccessOptions("InfraTest"),
            new GalaxyRepositoryOptions(WatchDeployEvents: false),
            new GalaxyReconnectOptions(ReplayOnSessionLost: false));

        using var driver = new GalaxyDriver("drv-1", opts, null, null, null, sub);

        // Establish a subscription so the replay path has something to walk.
        await driver.SubscribeAsync(["Tag.A", "Tag.B"], TimeSpan.Zero, CancellationToken.None);
        sub.SubscribeCalls.ShouldBe(1);

        // Invoke the replay path directly via the internal test seam — the supervisor's
        // ReportTransportFailure spins it up async; for a deterministic assertion we
        // call the helper that ReplayAsync is wired against.
        await driver.InvokeReplayForTestAsync(CancellationToken.None);

        sub.SubscribeCalls.ShouldBe(1,
            "ReplayOnSessionLost=false must skip the re-SubscribeBulk fan-out on reconnect");
    }

    /// <summary>Verifies that ReplayOnSessionLost=true runs resubscription on reconnect.</summary>
    [Fact]
    public async Task ReplayOnSessionLost_True_RunsResubscribeBulk()
    {
        var sub = new ReplayCountingSubscriber();
        var opts = new GalaxyDriverOptions(
            new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
            new GalaxyMxAccessOptions("InfraTest"),
            new GalaxyRepositoryOptions(WatchDeployEvents: false),
            new GalaxyReconnectOptions(ReplayOnSessionLost: true));

        using var driver = new GalaxyDriver("drv-1", opts, null, null, null, sub);

        await driver.SubscribeAsync(["Tag.A"], TimeSpan.Zero, CancellationToken.None);
        sub.SubscribeCalls.ShouldBe(1);

        await driver.InvokeReplayForTestAsync(CancellationToken.None);

        sub.SubscribeCalls.ShouldBe(2,
            "default ReplayOnSessionLost=true must re-issue SubscribeBulk after a transport drop");
    }

    /// <summary>Tracks subscription calls for replay testing.</summary>
    private sealed class ReplayCountingSubscriber : IGalaxySubscriber
    {
        private readonly Channel<MxEvent> _stream = Channel.CreateUnbounded<MxEvent>();
        private int _nextHandle = 1;
        /// <summary>Gets the count of subscription calls.</summary>
        public int SubscribeCalls;

        /// <summary>Subscribes to multiple tags and counts the call.</summary>
        /// <param name="fullReferences">List of tag addresses to subscribe to.</param>
        /// <param name="bufferedUpdateIntervalMs">Buffered update interval in milliseconds.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
            IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
        {
            Interlocked.Increment(ref SubscribeCalls);
            var results = fullReferences.Select(r => new SubscribeResult
            {
                TagAddress = r,
                ItemHandle = Interlocked.Increment(ref _nextHandle),
                WasSuccessful = true,
            }).ToList();
            return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
        }

        /// <summary>Unsubscribes from multiple tags.</summary>
        /// <param name="itemHandles">List of subscription handles to unsubscribe.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
            => Task.CompletedTask;

        /// <summary>Streams subscription events.</summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
            => _stream.Reader.ReadAllAsync(cancellationToken);
    }

    // ===== Driver.Galaxy-013 regression: ReinitializeAsync rejects unsupported reapply =====

    /// <summary>Verifies that ReinitializeAsync rejects non-equivalent config changes.</summary>
    [Fact]
    public async Task ReinitializeAsync_RejectsNonEquivalentConfigChange()
    {
        // ReinitializeAsync was previously a silent no-op that ignored driverConfigJson.
        // After the fix it either applies an equivalent config (no-op) or throws
        // NotSupportedException so a config change isn't silently dropped.
        var sub = new NoOpSubscriber();
        using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);

        const string newConfig = "{\"Gateway\":{\"Endpoint\":\"https://other.test:5001\",\"ApiKeySecretRef\":\"dev:other\"}}";

        // The driver must NOT pretend the change was applied — either no-op equivalence
        // or an explicit rejection is acceptable. Silently dropping the new config
        // (the previous behaviour) is not.
        await Should.ThrowAsync<NotSupportedException>(async () =>
            await driver.ReinitializeAsync(newConfig, CancellationToken.None));
    }

    /// <summary>Verifies that ReinitializeAsync accepts equivalent config.</summary>
    [Fact]
    public async Task ReinitializeAsync_AcceptsEquivalentConfig()
    {
        var sub = new NoOpSubscriber();
        using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub);

        // An empty / null-equivalent config reapply (no field changes) must not throw —
        // it's a legitimate "refresh health" path. Pass a JSON object that round-trips
        // to the driver's current options.
        var json = "{\"Gateway\":{\"Endpoint\":\"https://mxgw.test:5001\",\"ApiKeySecretRef\":\"key\"}," +
                   "\"MxAccess\":{\"ClientName\":\"InfraTest\"}," +
                   "\"Repository\":{\"WatchDeployEvents\":false}," +
                   "\"Reconnect\":{}}";
        await Should.NotThrowAsync(async () =>
            await driver.ReinitializeAsync(json, CancellationToken.None));
    }

    // ===== Minimal IGalaxySubscriber fake that returns empty results for subscribe calls =====

    /// <summary>No-op subscriber that returns empty results for testing.</summary>
    private sealed class NoOpSubscriber : IGalaxySubscriber
    {
        private readonly Channel<MxEvent> _stream = Channel.CreateUnbounded<MxEvent>();

        /// <summary>Subscribes to multiple tags (no-op).</summary>
        /// <param name="fullReferences">List of tag addresses to subscribe to.</param>
        /// <param name="bufferedUpdateIntervalMs">Buffered update interval in milliseconds.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
            IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
        {
            var results = fullReferences.Select((r, i) => new SubscribeResult
            {
                TagAddress = r,
                ItemHandle = i + 1,
                WasSuccessful = true,
            }).ToList();
            return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
        }

        /// <summary>Unsubscribes from multiple tags (no-op).</summary>
        /// <param name="itemHandles">List of subscription handles to unsubscribe.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
            => Task.CompletedTask;

        /// <summary>Streams subscription events (no-op).</summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
            => _stream.Reader.ReadAllAsync(cancellationToken);
    }
}
