using System.Collections.Concurrent;
using System.Net;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
using MBN_STOCK_WEBVIEW.Playout.Interop;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class PgmConnectDiagnosticTests
{
    [Fact]
    public void RealContactDiagnosticApi_IsNotPublic()
    {
        Assert.False(typeof(PgmConnectDiagnosticRunner).IsVisible);
        Assert.False(typeof(PgmConnectDiagnosticRequest).IsVisible);
        Assert.False(typeof(PgmConnectDiagnosticReport).IsVisible);
        Assert.Null(typeof(PgmConnectDiagnosticRunner).GetMethod(
            nameof(PgmConnectDiagnosticRunner.ExecuteAsync),
            System.Reflection.BindingFlags.Instance |
            System.Reflection.BindingFlags.Public));
    }

    [Theory]
    [InlineData("localhost", 30001, "PGM")]
    [InlineData("192.0.2.1", 30001, "PGM")]
    [InlineData("127.0.0.1", 0, "PGM")]
    [InlineData("127.0.0.1", 30001, "TEST")]
    [InlineData("127.0.0.1", 30001, "CONPROGRAM")]
    public void RequestValidation_RejectsUnsafeEndpointOrWindow(
        string host,
        int port,
        string title)
    {
        var valid = PgmConnectDiagnosticRunner.TryValidateRequest(
            new PgmConnectDiagnosticRequest(host, port, title),
            out _);

        Assert.False(valid);
    }

    [Fact]
    public async Task Runner_SuccessUsesOnlyConnectThenDisconnectAndReportsUnconfirmedEvidence()
    {
        var snapshot = ReadySnapshot(10);
        var safety = new FakeSafetyProbe(snapshot, snapshot, snapshot, snapshot);
        var session = new FakeConnectOnlySession();
        var factory = new FakeConnectOnlySessionFactory(session);
        var runner = new PgmConnectDiagnosticRunner(
            safety,
            factory,
            new FakeRegistrationProbe(),
            new TrackingStaDispatcherFactory());
        var request = new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM");

        var report = await runner.ExecuteAsync(request);

        Assert.Equal(["Connect", "Disconnect"], session.Calls);
        Assert.True(report.Completed);
        Assert.False(report.OutcomeUnknown);
        Assert.True(report.ConnectRequestIssued);
        Assert.True(report.ComActivationAttempted);
        Assert.Equal(PlayoutKtapConnectState.AcceptedUnconfirmed, report.LastKtapConnectState);
        Assert.True(report.KtapConnectAttempted);
        Assert.True(report.KtapConnectAccepted);
        Assert.Null(report.KtapHelloObserved);
        Assert.True(report.NetworkMonitoringRecordExpected);
        Assert.True(report.NetworkMonitoringCheckRequired);
        Assert.Null(report.NetworkMonitoringVerified);
        Assert.Equal("success", report.DisconnectCode);
        Assert.False(report.RenderCommandSurfaceExposed);
        Assert.False(report.RenderCommandAttempted);
        Assert.False(report.AutomaticReconnectEnabled);

        var json = JsonSerializer.Serialize(report);
        Assert.DoesNotContain("127.0.0.1", json, StringComparison.Ordinal);
        Assert.DoesNotContain("30001", json, StringComparison.Ordinal);
        Assert.DoesNotContain("PGM", json, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Runner_TargetRejectedStopsBeforeRegistrationAndCom()
    {
        var rejected = new PgmDiagnosticTargetSnapshot(false, 0, 0, false, null);
        var factory = new FakeConnectOnlySessionFactory(new FakeConnectOnlySession());
        var registration = new FakeRegistrationProbe();
        var runner = new PgmConnectDiagnosticRunner(
            new FakeSafetyProbe(rejected),
            factory,
            registration,
            new TrackingStaDispatcherFactory());

        var report = await runner.ExecuteAsync(
            new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));

        Assert.False(report.Completed);
        Assert.False(report.ConnectRequestIssued);
        Assert.False(report.ComActivationAttempted);
        Assert.Equal(0, factory.CreateCount);
        Assert.Equal(0, registration.ProbeCount);
        Assert.Equal("pgm-target-rejected", report.ErrorCode);
    }

    [Fact]
    public async Task Runner_UnapprovedInstalledInteropIsDistinctFromComActivationFailure()
    {
        var snapshot = ReadySnapshot(10);
        var runner = new PgmConnectDiagnosticRunner(
            new FakeSafetyProbe(snapshot),
            new UnavailableInteropSessionFactory(),
            new FakeRegistrationProbe(),
            new TrackingStaDispatcherFactory());

        var report = await runner.ExecuteAsync(
            new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));

        Assert.False(report.Completed);
        Assert.False(report.ComActivationAttempted);
        Assert.False(report.KtapConnectAttempted);
        Assert.False(report.OutcomeUnknown);
        Assert.Equal("not-attempted", report.DisconnectCode);
        Assert.Equal("installed-interop-metadata-unavailable", report.ErrorCode);
    }

    [Fact]
    public async Task Runner_TargetChangeAfterAcceptanceAbandonsWithoutDisconnect()
    {
        var baseline = ReadySnapshot(10);
        var changed = ReadySnapshot(11);
        var session = new FakeConnectOnlySession();
        var runner = new PgmConnectDiagnosticRunner(
            new FakeSafetyProbe(baseline, baseline, baseline, changed),
            new FakeConnectOnlySessionFactory(session),
            new FakeRegistrationProbe(),
            new TrackingStaDispatcherFactory());

        var report = await runner.ExecuteAsync(
            new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));

        Assert.Equal(["Connect", "Abandon"], session.Calls);
        Assert.False(report.Completed);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.KtapConnectAttempted);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.Equal("pgm-target-changed-after-connect", report.ErrorCode);
    }

    [Fact]
    public async Task DynamicSession_InvokesOnlyKtapConnectAndDisconnect()
    {
        var log = new ConcurrentQueue<string>();
        var session = new DynamicKtapConnectOnlySession(new RecordingComBinding(log));
        await using var dispatcher = new Runtime.StaDispatcher(1);

        await dispatcher.InvokeAsync(
            () =>
            {
                session.Connect("127.0.0.1", 30001, static () => true);
                session.Disconnect();
                return true;
            },
            TimeSpan.FromSeconds(2),
            CancellationToken.None);

        Assert.Equal(
            [
                "CreateEventHandler",
                "CreateEngine",
                "KTAPConnect",
                "Disconnect",
                "Release",
                "Release"
            ],
            log);
        Assert.Equal(PlayoutKtapConnectState.AcceptedUnconfirmed, session.LastKtapConnectState);
    }

    [Fact]
    public async Task DynamicSession_FinalSafetyCheckRunsAfterActivationAndBeforeKtap()
    {
        var log = new ConcurrentQueue<string>();
        var session = new DynamicKtapConnectOnlySession(new RecordingComBinding(log));
        await using var dispatcher = new Runtime.StaDispatcher(1);

        await Assert.ThrowsAsync<PgmDiagnosticSafetyException>(() =>
            dispatcher.InvokeAsync(
                () =>
                {
                    session.Connect(
                        "127.0.0.1",
                        30001,
                        () =>
                        {
                            log.Enqueue("FinalSafetyCheck");
                            return false;
                        });
                    return true;
                },
                TimeSpan.FromSeconds(2),
                CancellationToken.None));

        Assert.Equal(
            [
                "CreateEventHandler",
                "CreateEngine",
                "FinalSafetyCheck",
                "Release",
                "Release"
            ],
            log);
        Assert.DoesNotContain("KTAPConnect", log);
        Assert.Equal(PlayoutKtapConnectState.NotAttempted, session.LastKtapConnectState);
    }

    [Fact]
    public async Task Runner_ConnectTimeoutPreventsLateKtapDispatch()
    {
        var snapshot = ReadySnapshot(10);
        using var safety = new BlockingSecondCaptureSafetyProbe(snapshot);
        var session = new FakeConnectOnlySession();
        var dispatcherFactory = new TrackingStaDispatcherFactory();
        var runner = new PgmConnectDiagnosticRunner(
            safety,
            new FakeConnectOnlySessionFactory(session),
            new FakeRegistrationProbe(),
            dispatcherFactory,
            TimeSpan.FromMilliseconds(50),
            TimeSpan.FromSeconds(1));

        var execution = runner.ExecuteAsync(
            new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));

        try
        {
            Assert.True(safety.SecondCaptureEntered.Wait(TimeSpan.FromSeconds(2)));
            Assert.True(session.PreventionAttempted.Wait(TimeSpan.FromSeconds(2)));
        }
        finally
        {
            safety.ReleaseSecondCapture.Set();
        }

        var report = await execution.WaitAsync(TimeSpan.FromSeconds(3));
        Assert.True(session.ConnectCompleted.Wait(TimeSpan.FromSeconds(2)));

        Assert.Equal("connect-timeout", report.ErrorCode);
        Assert.False(report.Completed);
        Assert.False(report.OutcomeUnknown);
        Assert.False(report.KtapConnectAttempted);
        Assert.Equal(PlayoutKtapConnectState.NotAttempted, report.LastKtapConnectState);
        Assert.False(session.ComActivationAttempted);
        Assert.Equal(0, session.DispatchCount);
        Assert.Contains("Connect", session.Calls);
    }

    [Fact]
    public void ConnectOnlySessionSurfaceHasNoRenderCommand()
    {
        var methodNames = typeof(IKtapConnectOnlySession)
            .GetMethods()
            .Select(method => method.Name)
            .ToArray();
        string[] forbidden =
        [
            "GetScenePlayer",
            "LoadScene",
            "Prepare",
            "Play",
            "PlayDirect",
            "TakeIn",
            "Next",
            "TakeOut",
            "CutOut",
            "Stop",
            "StopAll"
        ];

        Assert.DoesNotContain(methodNames, name =>
            forbidden.Contains(name, StringComparer.OrdinalIgnoreCase));
        Assert.Contains("Connect", methodNames);
        Assert.Contains("Disconnect", methodNames);
    }

    [Fact]
    public void ListenerPortDecoderUsesNetworkByteOrder()
    {
        const ushort port = 30001;
        var encoded = unchecked((uint)(ushort)IPAddress.HostToNetworkOrder((short)port));

        Assert.Equal(port, WindowsTcpListenerOwnershipProbe.DecodePort(encoded));
    }

    private static PgmDiagnosticTargetSnapshot ReadySnapshot(int processId) => new(
        true,
        1,
        1,
        true,
        new PgmDiagnosticTargetIdentity(processId, 100));

    private sealed class FakeSafetyProbe : IPgmDiagnosticSafetyProbe
    {
        private readonly Queue<PgmDiagnosticTargetSnapshot> _snapshots;
        private PgmDiagnosticTargetSnapshot _last;

        public FakeSafetyProbe(params PgmDiagnosticTargetSnapshot[] snapshots)
        {
            _snapshots = new Queue<PgmDiagnosticTargetSnapshot>(snapshots);
            _last = snapshots.Last();
        }

        public PgmDiagnosticTargetSnapshot Capture(
            PgmConnectDiagnosticRequest request,
            IPAddress parsedHost)
        {
            if (_snapshots.Count > 0)
            {
                _last = _snapshots.Dequeue();
            }

            return _last;
        }
    }

    private sealed class BlockingSecondCaptureSafetyProbe : IPgmDiagnosticSafetyProbe, IDisposable
    {
        private readonly PgmDiagnosticTargetSnapshot _snapshot;
        private int _captureCount;

        public BlockingSecondCaptureSafetyProbe(PgmDiagnosticTargetSnapshot snapshot)
        {
            _snapshot = snapshot;
        }

        public ManualResetEventSlim SecondCaptureEntered { get; } = new(false);

        public ManualResetEventSlim ReleaseSecondCapture { get; } = new(false);

        public PgmDiagnosticTargetSnapshot Capture(
            PgmConnectDiagnosticRequest request,
            IPAddress parsedHost)
        {
            if (Interlocked.Increment(ref _captureCount) == 2)
            {
                SecondCaptureEntered.Set();
                ReleaseSecondCapture.Wait();
            }

            return _snapshot;
        }

        public void Dispose()
        {
            ReleaseSecondCapture.Set();
            SecondCaptureEntered.Dispose();
            ReleaseSecondCapture.Dispose();
        }
    }

    private sealed class FakeConnectOnlySessionFactory : IKtapConnectOnlySessionFactory
    {
        private readonly IKtapConnectOnlySession _session;

        public FakeConnectOnlySessionFactory(IKtapConnectOnlySession session)
        {
            _session = session;
        }

        public int CreateCount { get; private set; }

        public IKtapConnectOnlySession Create()
        {
            CreateCount++;
            return _session;
        }
    }

    private sealed class UnavailableInteropSessionFactory : IKtapConnectOnlySessionFactory
    {
        public IKtapConnectOnlySession Create() =>
            throw new InstalledK3dInteropMetadataUnavailableException();
    }

    private sealed class FakeConnectOnlySession : IKtapConnectOnlySession
    {
        private int _dispatchGate;
        private int _dispatchCount;
        private int _abandonRecorded;

        public ConcurrentQueue<string> Calls { get; } = new();

        public ManualResetEventSlim PreventionAttempted { get; } = new(false);

        public ManualResetEventSlim ConnectCompleted { get; } = new(false);

        public int DispatchCount => Volatile.Read(ref _dispatchCount);

        public bool ComActivationAttempted { get; private set; }

        public PlayoutKtapConnectState LastKtapConnectState { get; private set; }

        public bool TryPreventKtapConnect()
        {
            var previous = Interlocked.CompareExchange(ref _dispatchGate, 2, 0);
            if (previous == 1 && LastKtapConnectState == PlayoutKtapConnectState.NotAttempted)
            {
                LastKtapConnectState = PlayoutKtapConnectState.Attempted;
            }

            // Publish the test synchronization point only after the dispatch gate
            // is closed. Signalling first lets the blocked safety probe resume and
            // race KTAP dispatch before this prevention attempt has taken effect.
            PreventionAttempted.Set();
            return previous == 0;
        }

        public void Connect(string host, int port, Func<bool> finalSafetyCheck)
        {
            Calls.Enqueue("Connect");
            try
            {
                if (Volatile.Read(ref _dispatchGate) != 0)
                {
                    throw new InvalidOperationException();
                }

                ComActivationAttempted = true;
                if (!finalSafetyCheck())
                {
                    throw new PgmDiagnosticSafetyException();
                }

                if (Interlocked.CompareExchange(ref _dispatchGate, 1, 0) != 0)
                {
                    throw new InvalidOperationException();
                }

                Interlocked.Increment(ref _dispatchCount);
                LastKtapConnectState = PlayoutKtapConnectState.AcceptedUnconfirmed;
            }
            finally
            {
                ConnectCompleted.Set();
            }
        }

        public void Disconnect() => Calls.Enqueue("Disconnect");

        public void Abandon()
        {
            TryPreventKtapConnect();
            if (Interlocked.Exchange(ref _abandonRecorded, 1) == 0)
            {
                Calls.Enqueue("Abandon");
            }
        }
    }

    private sealed class RecordingComBinding : IK3dConnectOnlyComBinding
    {
        private readonly ConcurrentQueue<string> _log;

        public RecordingComBinding(ConcurrentQueue<string> log)
        {
            _log = log;
        }

        public object CreateEventHandler()
        {
            _log.Enqueue("CreateEventHandler");
            return new object();
        }

        public object CreateEngine()
        {
            _log.Enqueue("CreateEngine");
            return new object();
        }

        public int KtapConnect(
            object engine,
            string host,
            int port,
            object eventHandler)
        {
            _log.Enqueue("KTAPConnect");
            return 1;
        }

        public void Disconnect(object engine) => _log.Enqueue("Disconnect");

        public void Release(object value) => _log.Enqueue("Release");
    }
}
