using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Runtime;
using MBN_STOCK_WEBVIEW.Playout.Safety;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class TornadoPlayoutEngineTests
{
    private const string GateACapability =
        "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";

    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    public async Task DefaultDryRun_MonitorsProcessWithoutCreatingComSession(
        int totalProcessCount,
        bool expectedRunning)
    {
        var sessionFactory = new FakeK3dSessionFactory();
        var registration = new FakeRegistrationProbe();
        var process = new FakeTornadoProcessProbe(
            new TornadoProcessSnapshot(totalProcessCount, totalProcessCount, 0));
        await using var engine = CreateEngine(
            new PlayoutOptions(),
            sessionFactory,
            registration,
            process,
            liveAuthorized: false);

        await engine.PollProcessOnceAsync(CancellationToken.None);
        var connect = await engine.ConnectAsync(CancellationToken.None);
        var prepare = await engine.PrepareAsync(
            new PlayoutCue("never-resolved.t2s", "dry-run-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutMode.DryRun, engine.Status.Mode);
        Assert.Equal(expectedRunning, engine.Status.IsProcessRunning);
        Assert.Equal(PlayoutResultCode.Success, connect.Code);
        Assert.Equal(PlayoutResultCode.Success, prepare.Code);
        Assert.Equal(0, sessionFactory.CreateCount);
        Assert.Equal(0, registration.ProbeCount);
        Assert.Equal(
            PlayoutKtapConnectState.NotAttempted,
            engine.Status.LastKtapConnectState);
        Assert.False(engine.Status.NetworkMonitoringRecordExpected);
        Assert.False(engine.Status.NetworkMonitoringCheckRequired);
        Assert.Contains("Network Monitoring", engine.Status.Message, StringComparison.Ordinal);
    }

    [Fact]
    public async Task CancelledCommandBeforeSerialization_DoesNotProbeOrCreateSession()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var sessionFactory = new FakeK3dSessionFactory();
        var registration = new FakeRegistrationProbe();
        var process = new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0));
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            registration,
            process,
            liveAuthorized: false);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        var result = await engine.ConnectAsync(cancellation.Token);

        Assert.Equal(PlayoutResultCode.Cancelled, result.Code);
        Assert.Equal(0, sessionFactory.CreateCount);
        Assert.Equal(0, registration.ProbeCount);
        Assert.Equal(0, process.CaptureCount);
    }

    [Theory]
    [InlineData(0, 0, 0, false, "찾을 수 없습니다")]
    [InlineData(2, 1, 0, false, "여러 개")]
    [InlineData(1, 0, 1, false, "PROGRAM")]
    [InlineData(1, 1, 0, true, null)]
    public async Task TestConnect_RequiresExactlyOneEligibleNonProgramInstance(
        int totalProcessCount,
        int eligibleProcessCount,
        int programProcessCount,
        bool expectedSuccess,
        string? expectedFailureText)
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var sessionFactory = new FakeK3dSessionFactory();
        var registration = new FakeRegistrationProbe();
        var process = new FakeTornadoProcessProbe(new TornadoProcessSnapshot(
            totalProcessCount,
            eligibleProcessCount,
            programProcessCount));
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            registration,
            process,
            liveAuthorized: false);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(expectedSuccess, result.IsSuccess);
        Assert.Equal(expectedSuccess ? 1 : 0, sessionFactory.CreateCount);
        Assert.Equal(expectedSuccess ? 1 : 0, registration.ProbeCount);
        Assert.Equal(
            expectedSuccess
                ? PlayoutKtapConnectState.AcceptedUnconfirmed
                : PlayoutKtapConnectState.NotAttempted,
            engine.Status.LastKtapConnectState);
        Assert.Equal(expectedSuccess, engine.Status.NetworkMonitoringRecordExpected);
        Assert.Equal(expectedSuccess, engine.Status.NetworkMonitoringCheckRequired);
        Assert.NotNull(process.LastPattern);
        if (expectedSuccess)
        {
            Assert.Equal(PlayoutConnectionState.Connected, engine.Status.State);
            Assert.True(engine.Status.LiveTakeInAllowed);
        }
        else
        {
            Assert.Equal(PlayoutResultCode.Unavailable, result.Code);
            Assert.Contains(expectedFailureText!, result.Message, StringComparison.OrdinalIgnoreCase);
            Assert.False(engine.Status.LiveTakeInAllowed);
        }
    }

    [Fact]
    public async Task TestConnect_WhenKtapCallFails_ReportsAttemptedFailureEvidence()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            ConnectAction = _ => throw new InvalidOperationException("fake KTAP failure")
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Failed, result.Code);
        Assert.Equal(
            PlayoutKtapConnectState.Failed,
            engine.Status.LastKtapConnectState);
        Assert.True(engine.Status.KtapConnectAttempted);
        Assert.Null(engine.Status.KtapConnectAccepted);
        Assert.Null(engine.Status.NetworkMonitoringRecordExpected);
        Assert.True(engine.Status.NetworkMonitoringCheckRequired);
        Assert.Null(engine.Status.KtapHelloObserved);
    }

    [Fact]
    public async Task TestConnect_WhenKtapCallTimesOut_PreservesAttemptedUnknownEvidence()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var connectStarted = new ManualResetEventSlim();
        using var releaseConnect = new ManualResetEventSlim();
        var session = new FakeK3dSession
        {
            ConnectAction = _ =>
            {
                connectStarted.Set();
                if (!releaseConnect.Wait(TimeSpan.FromSeconds(5)))
                {
                    throw new TimeoutException("fake test coordination timeout");
                }
            }
        };
        var options = TestOptions(scenes.Path, "test-scene");
        options.ConnectTimeoutMilliseconds = 100;
        await using var engine = CreateEngine(
            options,
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        try
        {
            var connectTask = engine.ConnectAsync(CancellationToken.None);
            Assert.True(connectStarted.Wait(TimeSpan.FromSeconds(2)));

            var result = await connectTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
            Assert.Equal(
                PlayoutKtapConnectState.Attempted,
                engine.Status.LastKtapConnectState);
            Assert.True(engine.Status.KtapConnectAttempted);
            Assert.Null(engine.Status.KtapConnectAccepted);
            Assert.Null(engine.Status.NetworkMonitoringRecordExpected);
            Assert.True(engine.Status.NetworkMonitoringCheckRequired);
            Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        }
        finally
        {
            releaseConnect.Set();
        }
    }

    [Fact]
    public async Task TestConnect_TimeoutDuringActivation_PreventsLateKtapDispatch()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var activationStarted = new ManualResetEventSlim();
        using var releaseActivation = new ManualResetEventSlim();
        var session = new FakeK3dSession
        {
            BeforeKtapDispatchAction = () =>
            {
                activationStarted.Set();
                if (!releaseActivation.Wait(TimeSpan.FromSeconds(5)))
                {
                    throw new TimeoutException("fake activation coordination timeout");
                }
            }
        };
        var options = TestOptions(scenes.Path, "test-scene");
        options.ConnectTimeoutMilliseconds = 100;
        await using var engine = CreateEngine(
            options,
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        try
        {
            var connectTask = engine.ConnectAsync(CancellationToken.None);
            Assert.True(activationStarted.Wait(TimeSpan.FromSeconds(2)));

            var result = await connectTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
            Assert.Equal(
                PlayoutKtapConnectState.NotAttempted,
                engine.Status.LastKtapConnectState);
            Assert.False(engine.Status.KtapConnectAttempted);
            Assert.False(engine.Status.NetworkMonitoringRecordExpected);
            Assert.False(engine.Status.NetworkMonitoringCheckRequired);
            Assert.Equal(0, session.KtapDispatchCount);
            Assert.DoesNotContain("Dispose", session.Calls);

            releaseActivation.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Dispose"),
                TimeSpan.FromSeconds(2)));
            Assert.Equal(0, session.KtapDispatchCount);
            Assert.Equal(new[] { "Connect", "Dispose" }, session.Calls);
            Assert.Single(session.ThreadIds.Distinct());
            Assert.All(
                session.ApartmentStates,
                state => Assert.Equal(ApartmentState.STA, state));
        }
        finally
        {
            releaseActivation.Set();
        }
    }

    [Fact]
    public async Task TestConnect_BlockingFactoryThenConnectTimeout_PreventsLateKtapDispatch()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var factoryStarted = new ManualResetEventSlim();
        using var releaseFactory = new ManualResetEventSlim();
        using var beforeKtapDispatch = new ManualResetEventSlim();
        using var releaseConnect = new ManualResetEventSlim();
        var session = new FakeK3dSession
        {
            BeforeKtapDispatchAction = () =>
            {
                beforeKtapDispatch.Set();
                if (!releaseConnect.Wait(TimeSpan.FromSeconds(5)))
                {
                    throw new TimeoutException("fake connect coordination timeout");
                }
            }
        };
        var sessionFactory = new FakeK3dSessionFactory(() =>
        {
            factoryStarted.Set();
            if (!releaseFactory.Wait(TimeSpan.FromSeconds(5)))
            {
                throw new TimeoutException("fake factory coordination timeout");
            }

            return session;
        });
        var options = TestOptions(scenes.Path, "test-scene");
        options.ConnectTimeoutMilliseconds = 100;
        await using var engine = CreateEngine(
            options,
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        try
        {
            var connectTask = Task.Run(() => engine.ConnectAsync(CancellationToken.None));
            Assert.True(factoryStarted.Wait(TimeSpan.FromSeconds(2)));

            await Task.Delay(250);
            Assert.False(connectTask.IsCompleted);
            Assert.Equal(0, session.KtapDispatchCount);

            releaseFactory.Set();
            Assert.True(beforeKtapDispatch.Wait(TimeSpan.FromSeconds(2)));
            var result = await connectTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
            Assert.Equal(PlayoutKtapConnectState.NotAttempted, engine.Status.LastKtapConnectState);
            Assert.Equal(0, session.KtapDispatchCount);
            Assert.DoesNotContain("Dispose", session.Calls);

            releaseConnect.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Dispose"),
                TimeSpan.FromSeconds(2)));
            Assert.Equal(0, session.KtapDispatchCount);
        }
        finally
        {
            releaseFactory.Set();
            releaseConnect.Set();
        }
    }

    [Fact]
    public async Task GuardedConnect_FinalTargetFailure_PreventsKtapAndAbandonsWithoutDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => false,
            abandonOnTargetMismatch: true);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Failed, result.Code);
        Assert.Equal(0, session.KtapDispatchCount);
        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GuardedConnect_KtapFailure_IsOutcomeUnknownAndNeverReconnectable()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            ConnectAction = _ => throw new COMException("fake ambiguous KTAP failure")
        };
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.False(engine.Status.IsCommandAvailable);
        Assert.Equal(1, session.KtapDispatchCount);
        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
        await engine.PollProcessOnceAsync(CancellationToken.None);
        Assert.Equal(1, sessionFactory.CreateCount);
    }

    [Fact]
    public async Task GuardedConnect_CallbackCancellationAfterKtap_IsOutcomeUnknown()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            ConnectAction = _ => throw new OperationCanceledException(
                "fake cancellation after KTAP dispatch")
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Equal(1, session.KtapDispatchCount);
        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GuardedConnect_CancelledBeforeSerialization_RemainsKnownAndDoesNotCreateSession()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var sessionFactory = new FakeK3dSessionFactory();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        var result = await engine.ConnectAsync(cancellation.Token);

        Assert.Equal(PlayoutResultCode.Cancelled, result.Code);
        Assert.Equal(0, sessionFactory.CreateCount);
        Assert.Equal(PlayoutKtapConnectState.NotAttempted, engine.Status.LastKtapConnectState);
    }

    [Fact]
    public async Task GuardedConnect_SlowSafetyCheckPastTimeoutNeverDispatchesKtap()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var safetyStarted = new ManualResetEventSlim();
        using var releaseSafety = new ManualResetEventSlim();
        var session = new FakeK3dSession();
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false,
            finalConnectSafetyCheck: () =>
            {
                safetyStarted.Set();
                Assert.True(releaseSafety.Wait(TimeSpan.FromSeconds(5)));
                return true;
            },
            abandonOnTargetMismatch: true);

        try
        {
            var connectTask = engine.ConnectAsync(CancellationToken.None);
            Assert.True(safetyStarted.Wait(TimeSpan.FromSeconds(2)));

            var result = await connectTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
            Assert.Equal(0, session.KtapDispatchCount);
            releaseSafety.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(2)));
            Assert.Equal(0, session.KtapDispatchCount);
            await Assert.ThrowsAsync<InvalidOperationException>(
                () => engine.QuarantineAsync().AsTask());
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releaseSafety.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task GuardedConnect_TimeoutWinningAtomicClaimPreventsKtapDispatch()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var claimStarted = new ManualResetEventSlim();
        using var releaseClaim = new ManualResetEventSlim();
        var session = new FakeK3dSession();
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true,
            beforeSdkDispatchClaim: () =>
            {
                claimStarted.Set();
                Assert.True(releaseClaim.Wait(TimeSpan.FromSeconds(5)));
            });

        try
        {
            var connectTask = engine.ConnectAsync(CancellationToken.None);
            Assert.True(claimStarted.Wait(TimeSpan.FromSeconds(2)));

            var connect = await connectTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, connect.Code);
            Assert.Equal(0, session.KtapDispatchCount);
            releaseClaim.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(2)));
            Assert.Equal(0, session.KtapDispatchCount);
            await Assert.ThrowsAsync<InvalidOperationException>(
                () => engine.QuarantineAsync().AsTask());
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releaseClaim.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task GuardedConnect_PostGenerationChange_AbandonsWithoutDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var original = ProcessSnapshot("pgm-generation-a");
        var changed = ProcessSnapshot("pgm-generation-b");
        var session = new FakeK3dSession();
        var finalChecks = 0;
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(original, changed),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => Interlocked.Increment(ref finalChecks) == 1,
            abandonOnTargetMismatch: true);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
        Assert.Equal(1, finalChecks);
        Assert.Equal(1, session.KtapDispatchCount);
        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GuardedTargetPolicy_PreCommandGenerationChange_AbandonsWithoutDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var original = ProcessSnapshot("pgm-generation-a");
        var changed = ProcessSnapshot("pgm-generation-b");
        var session = new FakeK3dSession();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(original, original, changed),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        var prepare = await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, prepare.Code);
        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
        Assert.DoesNotContain(session.Calls, call =>
            call.StartsWith("Prepare:", StringComparison.Ordinal));
    }

    [Fact]
    public async Task GuardedTargetPolicy_AmbiguousPrepareFailure_AbandonsWithoutDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var stable = ProcessSnapshot("pgm-generation-a");
        var session = new FakeK3dSession
        {
            PrepareAction = (_, _) => throw new COMException("fake ambiguous prepare failure")
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(stable),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        var prepare = await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, prepare.Code);
        Assert.Equal(
            new[] { "Connect", "Prepare:test-scene", "Abandon" },
            session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GuardedTargetPolicy_NextChecksTargetBetweenPrepareAndPlay()
    {
        using var scenes = TemporarySceneDirectory.Create(
            "test-scene.t2s",
            "next-scene.t2s");
        var stable = ProcessSnapshot("pgm-generation-a");
        var session = new FakeK3dSession();
        var safetyChecks = 0;
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene", "next-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(stable),
            liveAuthorized: false,
            finalConnectSafetyCheck: () =>
                Interlocked.Increment(ref safetyChecks) != 7,
            abandonOnTargetMismatch: true);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        var next = await engine.NextAsync(
            Cue("next-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, next.Code);
        Assert.Equal(7, safetyChecks);
        Assert.Equal(1, session.Calls.Count(call => call == "Play"));
        Assert.Equal(
            new[]
            {
                "Connect",
                "Prepare:test-scene",
                "Play",
                "Prepare:next-scene",
                "Abandon"
            },
            session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GuardedTargetPolicy_DisconnectPrecheckFailureAbandonsWithoutDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var stable = ProcessSnapshot("pgm-generation-a");
        var session = new FakeK3dSession();
        var safetyChecks = 0;
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(stable),
            liveAuthorized: false,
            finalConnectSafetyCheck: () =>
                Interlocked.Increment(ref safetyChecks) == 1,
            abandonOnTargetMismatch: true);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        var disconnect = await engine.DisconnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, disconnect.Code);
        Assert.Equal(2, safetyChecks);
        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GuardedTargetPolicy_SlowCommandSafetyCheckPastTimeoutNeverDispatchesSdkCall()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var safetyStarted = new ManualResetEventSlim();
        using var releaseSafety = new ManualResetEventSlim();
        var stable = ProcessSnapshot("pgm-generation-a");
        var session = new FakeK3dSession();
        var safetyChecks = 0;
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(stable),
            liveAuthorized: false,
            finalConnectSafetyCheck: () =>
            {
                if (Interlocked.Increment(ref safetyChecks) == 2)
                {
                    safetyStarted.Set();
                    Assert.True(releaseSafety.Wait(TimeSpan.FromSeconds(5)));
                }

                return true;
            },
            abandonOnTargetMismatch: true);

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            var prepareTask = engine.PrepareAsync(
                Cue("test-scene"),
                CancellationToken.None);
            Assert.True(safetyStarted.Wait(TimeSpan.FromSeconds(2)));

            var prepare = await prepareTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, prepare.Code);
            releaseSafety.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(2)));
            Assert.DoesNotContain(
                session.Calls,
                call => call.StartsWith("Prepare:", StringComparison.Ordinal));
            await Assert.ThrowsAsync<InvalidOperationException>(
                () => engine.QuarantineAsync().AsTask());
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releaseSafety.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task GuardedTargetPolicy_SlowDisconnectSafetyCheckPastTimeoutNeverDisconnects()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var safetyStarted = new ManualResetEventSlim();
        using var releaseSafety = new ManualResetEventSlim();
        var stable = ProcessSnapshot("pgm-generation-a");
        var session = new FakeK3dSession();
        var safetyChecks = 0;
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(stable),
            liveAuthorized: false,
            finalConnectSafetyCheck: () =>
            {
                if (Interlocked.Increment(ref safetyChecks) == 2)
                {
                    safetyStarted.Set();
                    Assert.True(releaseSafety.Wait(TimeSpan.FromSeconds(5)));
                }

                return true;
            },
            abandonOnTargetMismatch: true);

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            var disconnectTask = engine.DisconnectAsync(CancellationToken.None);
            Assert.True(safetyStarted.Wait(TimeSpan.FromSeconds(2)));

            var disconnect = await disconnectTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, disconnect.Code);
            releaseSafety.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(2)));
            await Assert.ThrowsAsync<InvalidOperationException>(
                () => engine.QuarantineAsync().AsTask());
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releaseSafety.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task GuardedTargetPolicy_TimeoutWinningAtomicCommandClaimPreventsPrepare()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var claimStarted = new ManualResetEventSlim();
        using var releaseClaim = new ManualResetEventSlim();
        var session = new FakeK3dSession();
        var claims = 0;
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("pgm-generation-a")),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true,
            beforeSdkDispatchClaim: () =>
            {
                if (Interlocked.Increment(ref claims) == 2)
                {
                    claimStarted.Set();
                    Assert.True(releaseClaim.Wait(TimeSpan.FromSeconds(5)));
                }
            });

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            var prepareTask = engine.PrepareAsync(
                Cue("test-scene"),
                CancellationToken.None);
            Assert.True(claimStarted.Wait(TimeSpan.FromSeconds(2)));

            var prepare = await prepareTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, prepare.Code);
            releaseClaim.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(2)));
            Assert.DoesNotContain(
                session.Calls,
                call => call.StartsWith("Prepare:", StringComparison.Ordinal));
            await Assert.ThrowsAsync<InvalidOperationException>(
                () => engine.QuarantineAsync().AsTask());
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releaseClaim.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task GuardedTargetPolicy_NextUsesSeparateAtomicClaimsForPrepareAndPlay()
    {
        using var scenes = TemporarySceneDirectory.Create(
            "test-scene.t2s",
            "next-scene.t2s");
        using var playClaimStarted = new ManualResetEventSlim();
        using var releasePlayClaim = new ManualResetEventSlim();
        var session = new FakeK3dSession();
        var claims = 0;
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene", "next-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("pgm-generation-a")),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true,
            beforeSdkDispatchClaim: () =>
            {
                if (Interlocked.Increment(ref claims) == 5)
                {
                    playClaimStarted.Set();
                    Assert.True(releasePlayClaim.Wait(TimeSpan.FromSeconds(5)));
                }
            });

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            Assert.True((await engine.PrepareAsync(
                Cue("test-scene"),
                CancellationToken.None)).IsSuccess);
            Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
            var nextTask = engine.NextAsync(
                Cue("next-scene"),
                CancellationToken.None);
            Assert.True(playClaimStarted.Wait(TimeSpan.FromSeconds(2)));

            var next = await nextTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, next.Code);
            Assert.Contains("Prepare:next-scene", session.Calls);
            Assert.Equal(1, session.Calls.Count(call => call == "Play"));
            releasePlayClaim.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(2)));
            Assert.Equal(1, session.Calls.Count(call => call == "Play"));
            await Assert.ThrowsAsync<InvalidOperationException>(
                () => engine.QuarantineAsync().AsTask());
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releasePlayClaim.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task GuardedTargetPolicy_TimeoutWinningAtomicDisconnectClaimPreventsDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var claimStarted = new ManualResetEventSlim();
        using var releaseClaim = new ManualResetEventSlim();
        var session = new FakeK3dSession();
        var claims = 0;
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("pgm-generation-a")),
            liveAuthorized: false,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true,
            beforeSdkDispatchClaim: () =>
            {
                if (Interlocked.Increment(ref claims) == 2)
                {
                    claimStarted.Set();
                    Assert.True(releaseClaim.Wait(TimeSpan.FromSeconds(5)));
                }
            });

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            var disconnectTask = engine.DisconnectAsync(CancellationToken.None);
            Assert.True(claimStarted.Wait(TimeSpan.FromSeconds(2)));

            var disconnect = await disconnectTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, disconnect.Code);
            releaseClaim.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(2)));
            await Assert.ThrowsAsync<InvalidOperationException>(
                () => engine.QuarantineAsync().AsTask());
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releaseClaim.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task LastKtapEvidence_SurvivesLaterFailureBeforeAnotherDispatch()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        var createCount = 0;
        var sessionFactory = new FakeK3dSessionFactory(() =>
            Interlocked.Increment(ref createCount) == 1
                ? session
                : throw new InvalidOperationException("fake activation-stage failure"));
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.DisconnectAsync(CancellationToken.None)).IsSuccess);

        var secondConnect = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Failed, secondConnect.Code);
        Assert.Equal(PlayoutConnectionState.Faulted, engine.Status.State);
        Assert.Equal(
            PlayoutKtapConnectState.AcceptedUnconfirmed,
            engine.Status.LastKtapConnectState);
        Assert.True(engine.Status.NetworkMonitoringRecordExpected);
        Assert.True(engine.Status.NetworkMonitoringCheckRequired);
        Assert.Equal(2, sessionFactory.CreateCount);
    }

    [Fact]
    public async Task TestWorkflow_ExecutesFakeSessionInCommandOrderOnSta()
    {
        using var scenes = TemporarySceneDirectory.Create("first.t2s", "next.t2s");
        var session = new FakeK3dSession();
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        var process = new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0));
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "first", "next"),
            sessionFactory,
            new FakeRegistrationProbe(),
            process,
            liveAuthorized: false);

        var connect = await engine.ConnectAsync(CancellationToken.None);
        Assert.True(engine.Status.LiveTakeInAllowed);
        var prepare = await engine.PrepareAsync(Cue("first"), CancellationToken.None);
        var takeIn = await engine.TakeInAsync(CancellationToken.None);
        var next = await engine.NextAsync(Cue("next"), CancellationToken.None);
        var takeOut = await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None);
        var disconnect = await engine.DisconnectAsync(CancellationToken.None);

        Assert.All(
            new[] { connect, prepare, takeIn, next, takeOut, disconnect },
            result => Assert.Equal(PlayoutResultCode.Success, result.Code));
        Assert.Equal(
            new[]
            {
                "Connect",
                "Prepare:first",
                "Play",
                "Prepare:next",
                "Play",
                "TakeOut:All",
                "Disconnect",
                "Dispose"
            },
            session.Calls);
        Assert.Single(session.ThreadIds.Distinct());
        Assert.All(session.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
        Assert.Equal(PlayoutConnectionState.Disconnected, engine.Status.State);
        Assert.False(engine.Status.LiveTakeInAllowed);
        Assert.Null(engine.Status.PreparedSceneName);
        Assert.Null(engine.Status.OnAirSceneName);
    }

    [Fact]
    public async Task UpdateOnAir_TestMode_UsesSerializedSessionPathAndKeepsSceneOnAir()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        var result = await engine.UpdateOnAirAsync(
            Cue("test-scene") with
            {
                Fields = [new PlayoutField("headline", "page two", true)]
            },
            CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(PlayoutOperation.UpdateOnAir, result.Operation);
        Assert.False(result.IsDryRun);
        Assert.Equal("test-scene", engine.Status.OnAirSceneName);
        Assert.Null(engine.Status.PreparedSceneName);
        Assert.Equal(
            new[]
            {
                "Connect",
                "Prepare:test-scene",
                "Play",
                "UpdateOnAir:test-scene"
            },
            session.Calls);
        Assert.Single(session.ThreadIds.Distinct());
        Assert.All(session.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
    }

    [Fact]
    public async Task UpdateOnAir_RejectsIdleOrDifferentSceneBeforeSessionDispatch()
    {
        using var scenes = TemporarySceneDirectory.Create("first.t2s", "other.t2s");
        var session = new FakeK3dSession();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "first", "other"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        var idle = await engine.UpdateOnAirAsync(Cue("first"), CancellationToken.None);
        Assert.True((await engine.PrepareAsync(Cue("first"), CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        var different = await engine.UpdateOnAirAsync(
            Cue("other"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, idle.Code);
        Assert.Equal(PlayoutResultCode.Rejected, different.Code);
        Assert.Equal("first", engine.Status.OnAirSceneName);
        Assert.DoesNotContain(session.Calls, call =>
            call.StartsWith("UpdateOnAir:", StringComparison.Ordinal));
    }

    [Fact]
    public async Task UpdateOnAir_AmbiguousFailureLatchesOutcomeUnknownAndIsNeverRetried()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            UpdateOnAirAction = (_, _) =>
                throw new COMException("fake ambiguous on-air update failure")
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        var failed = await engine.UpdateOnAirAsync(
            Cue("test-scene"),
            CancellationToken.None);
        var later = await engine.UpdateOnAirAsync(
            Cue("test-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, failed.Code);
        Assert.Equal(PlayoutResultCode.OutcomeUnknown, later.Code);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Equal(
            1,
            session.Calls.Count(call => call == "UpdateOnAir:test-scene"));
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task UnchangedProcessGeneration_DoesNotReconnectAnExistingSession()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        var unchanged = ProcessSnapshot("eligible-generation-a");
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(unchanged),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.Equal(1, sessionFactory.CreateCount);
        Assert.Equal(new[] { "Connect" }, session.Calls);
        Assert.Equal(PlayoutConnectionState.Connected, engine.Status.State);
    }

    [Fact]
    public async Task ChangedProcessGeneration_CleansOldSessionBeforeReconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var calls = new ConcurrentQueue<string>();
        var first = new FakeK3dSession
        {
            CallObserver = call => calls.Enqueue($"first:{call}")
        };
        var second = new FakeK3dSession
        {
            CallObserver = call => calls.Enqueue($"second:{call}")
        };
        var created = 0;
        var sessionFactory = new FakeK3dSessionFactory(
            () => Interlocked.Increment(ref created) == 1 ? first : second);
        var original = ProcessSnapshot("eligible-generation-a");
        var replacement = ProcessSnapshot("eligible-generation-b");
        var options = TestOptions(scenes.Path, "test-scene");
        options.ReconnectDelayMilliseconds = 0;
        await using var engine = CreateEngine(
            options,
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(original, original, replacement),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.Equal(2, sessionFactory.CreateCount);
        Assert.Equal(
            new[]
            {
                "first:Connect",
                "first:Disconnect",
                "first:Dispose",
                "second:Connect"
            },
            calls);
        Assert.Equal(PlayoutConnectionState.Connected, engine.Status.State);
        Assert.DoesNotContain("eligible-generation-a", engine.Status.Message, StringComparison.Ordinal);
        Assert.DoesNotContain("eligible-generation-b", engine.Status.Message, StringComparison.Ordinal);
    }

    [Fact]
    public async Task UnreliableProcessIdentity_FailsClosedBeforeRegistrationOrCom()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var sessionFactory = new FakeK3dSessionFactory();
        var registration = new FakeRegistrationProbe();
        var snapshot = ProcessSnapshot("unreliable") with
        {
            IdentityInspectionSucceeded = false
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            registration,
            new FakeTornadoProcessProbe(snapshot),
            liveAuthorized: false);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Unavailable, result.Code);
        Assert.Equal(0, registration.ProbeCount);
        Assert.Equal(0, sessionFactory.CreateCount);
    }

    [Fact]
    public void ProcessGeneration_TracksEligibleAndProgramIdentitiesIndependently()
    {
        var baseline = ProcessSnapshot("eligible-generation-a") with
        {
            ProgramProcessGeneration = new TornadoProcessGeneration("program-generation-a")
        };
        var same = ProcessSnapshot("eligible-generation-a") with
        {
            ProgramProcessGeneration = new TornadoProcessGeneration("program-generation-a")
        };
        var changedProgram = same with
        {
            ProgramProcessGeneration = new TornadoProcessGeneration("program-generation-b")
        };

        Assert.True(baseline.HasSameProcessGeneration(same));
        Assert.False(baseline.HasSameProcessGeneration(changedProgram));
    }

    [Fact]
    public async Task TestNextWithoutOnAirScene_IsRejectedBeforeNextSessionCalls()
    {
        using var scenes = TemporarySceneDirectory.Create("first.t2s", "next.t2s");
        var session = new FakeK3dSession();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "first", "next"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("eligible-generation-a")),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        var callsBeforeIdleNext = session.Calls.ToArray();

        var idleNext = await engine.NextAsync(Cue("next"), CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, idleNext.Code);
        Assert.Equal(callsBeforeIdleNext, session.Calls);

        Assert.True((await engine.PrepareAsync(Cue("first"), CancellationToken.None)).IsSuccess);
        var callsBeforeNext = session.Calls.ToArray();

        var next = await engine.NextAsync(Cue("next"), CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, next.Code);
        Assert.Equal(callsBeforeNext, session.Calls);
        Assert.DoesNotContain("Play", session.Calls);
        Assert.Equal("first", engine.Status.PreparedSceneName);
        Assert.Null(engine.Status.OnAirSceneName);
    }

    [Theory]
    [InlineData(100)]
    [InlineData(837)]
    [InlineData(300_000)]
    public async Task Status_ExposesValidatedOperationTimeoutMilliseconds(
        int operationTimeoutMilliseconds)
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var options = TestOptions(scenes.Path, "test-scene");
        options.OperationTimeoutMilliseconds = operationTimeoutMilliseconds;
        await using var engine = CreateEngine(
            options,
            new FakeK3dSessionFactory(),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("eligible-generation-a")),
            liveAuthorized: false);

        Assert.Equal(operationTimeoutMilliseconds, engine.Status.OperationTimeoutMilliseconds);
    }

    [Fact]
    public async Task TestSceneOutsideAllowlist_IsRejectedBeforeSessionCall()
    {
        using var scenes = TemporarySceneDirectory.Create("allowed.t2s", "blocked.t2s");
        var session = new FakeK3dSession();
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "allowed"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);
        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);

        var prepare = await engine.PrepareAsync(Cue("blocked"), CancellationToken.None);
        var next = await engine.NextAsync(Cue("blocked"), CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, prepare.Code);
        Assert.Equal(PlayoutResultCode.Rejected, next.Code);
        Assert.DoesNotContain(session.Calls, call => call.StartsWith("Prepare:", StringComparison.Ordinal));
        Assert.DoesNotContain("blocked.t2s", prepare.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(scenes.Path, prepare.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    public async Task LiveWithoutBothAuthorizations_RejectsCommandsBeforeCom(
        bool trustedLiveOutputEnabled,
        bool launchAuthorized)
    {
        using var scenes = TemporarySceneDirectory.Create("live-scene.t2s");
        var sessionFactory = new FakeK3dSessionFactory();
        var registration = new FakeRegistrationProbe();
        await using var engine = CreateEngine(
            LiveOptions(scenes.Path, trustedLiveOutputEnabled),
            sessionFactory,
            registration,
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            launchAuthorized);

        var results = new[]
        {
            await engine.ConnectAsync(CancellationToken.None),
            await engine.PrepareAsync(Cue("live-scene"), CancellationToken.None),
            await engine.TakeInAsync(CancellationToken.None),
            await engine.NextAsync(Cue("live-scene"), CancellationToken.None),
            await engine.UpdateOnAirAsync(Cue("live-scene"), CancellationToken.None),
            await engine.TakeOutAsync(PlayoutTakeOutScope.All, CancellationToken.None)
        };

        Assert.All(results, result => Assert.Equal(PlayoutResultCode.Rejected, result.Code));
        Assert.Equal(0, sessionFactory.CreateCount);
        Assert.Equal(0, registration.ProbeCount);
        Assert.False(engine.Status.LiveTakeInAllowed);
    }

    [Fact]
    public async Task LiveWithBothAuthorizations_CanUseFakeSessionOnly()
    {
        using var scenes = TemporarySceneDirectory.Create("live-scene.t2s");
        var session = new FakeK3dSession();
        await using var engine = CreateEngine(
            LiveOptions(scenes.Path, trustedLiveOutputEnabled: true),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: true);

        var connect = await engine.ConnectAsync(CancellationToken.None);
        var prepare = await engine.PrepareAsync(Cue("live-scene"), CancellationToken.None);
        var takeIn = await engine.TakeInAsync(CancellationToken.None);
        var update = await engine.UpdateOnAirAsync(
            Cue("live-scene"),
            CancellationToken.None);

        Assert.True(connect.IsSuccess);
        Assert.True(prepare.IsSuccess);
        Assert.True(takeIn.IsSuccess);
        Assert.True(update.IsSuccess);
        Assert.True(engine.Status.LiveTakeInAllowed);
        Assert.Contains("Play", session.Calls);
        Assert.Contains("UpdateOnAir:live-scene", session.Calls);
    }

    [Fact]
    public async Task LiveWithBothAuthorizations_RejectsSceneOutsideClosedAllowlist()
    {
        using var scenes = TemporarySceneDirectory.Create(
            "live-scene.t2s",
            "blocked-scene.t2s");
        var session = new FakeK3dSession();
        await using var engine = CreateEngine(
            LiveOptions(scenes.Path, trustedLiveOutputEnabled: true),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: true);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);

        var prepare = await engine.PrepareAsync(
            Cue("blocked-scene"),
            CancellationToken.None);
        var next = await engine.NextAsync(
            Cue("blocked-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, prepare.Code);
        Assert.Equal(PlayoutResultCode.Rejected, next.Code);
        Assert.DoesNotContain(
            session.Calls,
            call => call.Contains("blocked-scene", StringComparison.Ordinal));
    }

    [Fact]
    public async Task UpdateOnAir_LiveMode_RechecksAuthorizationBeforeSessionDispatch()
    {
        using var scenes = TemporarySceneDirectory.Create("live-scene.t2s");
        var session = new FakeK3dSession();
        var authorization = new FakeLiveAuthorization(true);
        await using var engine = CreateEngine(
            LiveOptions(scenes.Path, trustedLiveOutputEnabled: true),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: true,
            liveAuthorization: authorization);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("live-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        authorization.IsAuthorizedForThisLaunch = false;

        var result = await engine.UpdateOnAirAsync(
            Cue("live-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, result.Code);
        Assert.DoesNotContain("UpdateOnAir:live-scene", session.Calls);
        Assert.Equal("live-scene", engine.Status.OnAirSceneName);
    }

    [Fact]
    public async Task TimedOutTakeIn_WithProcessGenerationChange_NeverReconnectsOrReplays()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var playStarted = new ManualResetEventSlim();
        using var releasePlay = new ManualResetEventSlim();
        var session = new FakeK3dSession
        {
            PlayAction = _ =>
            {
                playStarted.Set();
                Assert.True(releasePlay.Wait(TimeSpan.FromSeconds(5)));
            }
        };
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        var options = TestOptions(scenes.Path, "test-scene");
        options.OperationTimeoutMilliseconds = 100;
        options.ReconnectDelayMilliseconds = 0;
        var originalProcess = ProcessSnapshot("eligible-generation-a");
        var restartedProcess = ProcessSnapshot("eligible-generation-b");
        await using var engine = CreateEngine(
            options,
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(
                originalProcess,
                originalProcess,
                originalProcess,
                originalProcess,
                restartedProcess),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(Cue("test-scene"), CancellationToken.None)).IsSuccess);
        try
        {
            var takeInTask = engine.TakeInAsync(CancellationToken.None);
            Assert.True(playStarted.Wait(TimeSpan.FromSeconds(5)));
            var takeIn = await takeInTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, takeIn.Code);
            Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
            Assert.False(engine.Status.IsCommandAvailable);
            Assert.Equal(1, session.Calls.Count(call => call == "Play"));

            releasePlay.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(5)));
            await engine.PollProcessOnceAsync(CancellationToken.None);
            var takeOut = await engine.TakeOutAsync(
                PlayoutTakeOutScope.All,
                CancellationToken.None);

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, takeOut.Code);
            Assert.Equal(1, sessionFactory.CreateCount);
            Assert.Equal(1, session.Calls.Count(call => call == "Play"));
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain(session.Calls, call => call.StartsWith("TakeOut:", StringComparison.Ordinal));
        }
        finally
        {
            releasePlay.Set();
        }
    }

    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task FailedTakeOut_AbandonsSessionWithoutSdkDisconnect(
        bool callbackThrowsCancellation)
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            TakeOutAction = (_, _) =>
            {
                if (callbackThrowsCancellation)
                {
                    throw new OperationCanceledException("fake callback cancellation");
                }

                throw new COMException("fake take-out failure");
            }
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        var takeOut = await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, takeOut.Code);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Contains("TakeOut:All", session.Calls);
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task FailedPrepareWhileOnAir_AbandonsWithoutSdkDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var prepareCount = 0;
        var session = new FakeK3dSession
        {
            PrepareAction = (_, _) =>
            {
                if (Interlocked.Increment(ref prepareCount) > 1)
                {
                    throw new InvalidOperationException("fake on-air prepare failure");
                }
            }
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        var prepare = await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, prepare.Code);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task TimedOutTakeOut_LateCompletionAbandonsWithoutSdkDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var takeOutStarted = new ManualResetEventSlim();
        using var releaseTakeOut = new ManualResetEventSlim();
        var session = new FakeK3dSession
        {
            TakeOutAction = (_, _) =>
            {
                takeOutStarted.Set();
                Assert.True(releaseTakeOut.Wait(TimeSpan.FromSeconds(5)));
            }
        };
        var options = TestOptions(scenes.Path, "test-scene");
        options.OperationTimeoutMilliseconds = 100;
        await using var engine = CreateEngine(
            options,
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        try
        {
            var takeOutTask = engine.TakeOutAsync(
                PlayoutTakeOutScope.All,
                CancellationToken.None);
            Assert.True(takeOutStarted.Wait(TimeSpan.FromSeconds(5)));
            var takeOut = await takeOutTask;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, takeOut.Code);
            releaseTakeOut.Set();
            Assert.True(SpinWait.SpinUntil(
                () => session.Calls.Contains("Abandon"),
                TimeSpan.FromSeconds(5)));
            Assert.DoesNotContain("Disconnect", session.Calls);
            Assert.DoesNotContain("Dispose", session.Calls);
        }
        finally
        {
            releaseTakeOut.Set();
        }
    }

    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task TakeOut_WhenOnAirProcessBecomesUnsafe_AbandonsWithoutDisconnect(
        bool generationChanged)
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        var original = ProcessSnapshot("eligible-generation-a");
        var unsafeSnapshot = generationChanged
            ? ProcessSnapshot("eligible-generation-b")
            : new TornadoProcessSnapshot(1, 0, 1);
        var snapshots = Enumerable.Repeat(original, 4)
            .Append(unsafeSnapshot)
            .ToArray();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(snapshots),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        var takeOut = await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, takeOut.Code);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain(session.Calls, call => call.StartsWith("TakeOut:", StringComparison.Ordinal));
    }

    [Fact]
    public async Task ProcessMonitor_WhenOutputIsOnAir_AbandonsWithoutDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        var original = ProcessSnapshot("eligible-generation-a");
        var snapshots = Enumerable.Repeat(original, 4)
            .Append(new TornadoProcessSnapshot(1, 0, 1))
            .ToArray();
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(snapshots),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);

        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
    }

    [Fact]
    public async Task OutcomeUnknown_PreservesLatchAndNeverReconnectsOrReplays()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var secretPath = System.IO.Path.Combine(scenes.Path, "test-scene.t2s");
        var first = new FakeK3dSession
        {
            PlayAction = _ => throw new COMException(
                $"vendor HRESULT 0x80004005 at {secretPath}",
                unchecked((int)0x80004005))
        };
        var second = new FakeK3dSession();
        var created = 0;
        var sessionFactory = new FakeK3dSessionFactory(
            () => Interlocked.Increment(ref created) == 1 ? first : second);
        var options = TestOptions(scenes.Path, "test-scene");
        options.ReconnectDelayMilliseconds = 0;
        await using var engine = CreateEngine(
            options,
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(Cue("test-scene"), CancellationToken.None)).IsSuccess);
        var takeIn = await engine.TakeInAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, takeIn.Code);
        Assert.DoesNotContain("0x80004005", takeIn.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(secretPath, takeIn.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("0x80004005", engine.Status.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(secretPath, engine.Status.Message, StringComparison.OrdinalIgnoreCase);
        Assert.Equal(1, first.Calls.Count(call => call == "Play"));

        await engine.PollProcessOnceAsync(CancellationToken.None);
        var laterCommand = await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None);
        var laterTakeIn = await engine.TakeInAsync(CancellationToken.None);
        var laterNext = await engine.NextAsync(
            Cue("test-scene"),
            CancellationToken.None);
        using var cancelled = new CancellationTokenSource();
        cancelled.Cancel();
        var cancelledResults = new[]
        {
            await engine.ConnectAsync(cancelled.Token),
            await engine.DisconnectAsync(cancelled.Token),
            await engine.PrepareAsync(Cue("test-scene"), cancelled.Token),
            await engine.TakeInAsync(cancelled.Token),
            await engine.NextAsync(Cue("test-scene"), cancelled.Token),
            await engine.TakeOutAsync(PlayoutTakeOutScope.All, cancelled.Token)
        };

        Assert.Equal(1, sessionFactory.CreateCount);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Equal(PlayoutResultCode.OutcomeUnknown, laterCommand.Code);
        Assert.Equal(PlayoutResultCode.OutcomeUnknown, laterTakeIn.Code);
        Assert.Equal(PlayoutResultCode.OutcomeUnknown, laterNext.Code);
        Assert.All(cancelledResults, result =>
            Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code));
        Assert.False(engine.Status.IsCommandAvailable);
        Assert.DoesNotContain("Play", second.Calls);
        Assert.DoesNotContain(second.Calls, call => call.StartsWith("Prepare:", StringComparison.Ordinal));
    }

    [Fact]
    public async Task CancelledGateWait_WhenLeadingCommandLatchesUnknown_ReturnsOutcomeUnknown()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var playStarted = new ManualResetEventSlim();
        using var releasePlay = new ManualResetEventSlim();
        var session = new FakeK3dSession
        {
            PlayAction = _ =>
            {
                playStarted.Set();
                Assert.True(releasePlay.Wait(TimeSpan.FromSeconds(5)));
                throw new COMException("fake ambiguous play failure");
            }
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);

        try
        {
            var takeInTask = engine.TakeInAsync(CancellationToken.None);
            Assert.True(playStarted.Wait(TimeSpan.FromSeconds(5)));
            using var cancellation = new CancellationTokenSource();
            var waitingCommand = engine.NextAsync(Cue("test-scene"), cancellation.Token);
            cancellation.Cancel();
            releasePlay.Set();

            var takeIn = await takeInTask;
            var next = await waitingCommand;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, takeIn.Code);
            Assert.Equal(PlayoutResultCode.OutcomeUnknown, next.Code);
        }
        finally
        {
            releasePlay.Set();
        }
    }

    [Fact]
    public async Task CancelledGateWaitBehindDispose_DoesNotReverseDisposedStateOrThrow()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        using var playStarted = new ManualResetEventSlim();
        using var releasePlay = new ManualResetEventSlim();
        var session = new FakeK3dSession
        {
            PlayAction = _ =>
            {
                playStarted.Set();
                Assert.True(releasePlay.Wait(TimeSpan.FromSeconds(5)));
            }
        };
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);

        try
        {
            var takeInTask = engine.TakeInAsync(CancellationToken.None);
            Assert.True(playStarted.Wait(TimeSpan.FromSeconds(5)));
            var disposeTask = engine.DisposeAsync().AsTask();
            using var cancellation = new CancellationTokenSource();
            var waitingCommand = engine.NextAsync(Cue("test-scene"), cancellation.Token);
            cancellation.Cancel();
            releasePlay.Set();

            Assert.True((await takeInTask).IsSuccess);
            await disposeTask;
            var next = await waitingCommand;

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, next.Code);
            Assert.Equal(PlayoutConnectionState.Disposed, engine.Status.State);
            await engine.DisposeAsync();
        }
        finally
        {
            releasePlay.Set();
            await engine.DisposeAsync();
        }
    }

    [Fact]
    public async Task FailedPrepare_ReconnectsWithoutReplayingTheFailedCommand()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var first = new FakeK3dSession
        {
            PrepareAction = (_, _) => throw new InvalidOperationException("fake prepare failure")
        };
        var second = new FakeK3dSession();
        var created = 0;
        var sessionFactory = new FakeK3dSessionFactory(
            () => Interlocked.Increment(ref created) == 1 ? first : second);
        var options = TestOptions(scenes.Path, "test-scene");
        options.ReconnectDelayMilliseconds = 0;
        await using var engine = CreateEngine(
            options,
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        var prepare = await engine.PrepareAsync(Cue("test-scene"), CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Failed, prepare.Code);
        Assert.Equal(1, first.Calls.Count(call => call == "Prepare:test-scene"));
        Assert.Equal(PlayoutConnectionState.Reconnecting, engine.Status.State);

        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.Equal(2, sessionFactory.CreateCount);
        Assert.Equal(PlayoutConnectionState.Connected, engine.Status.State);
        Assert.DoesNotContain(second.Calls, call => call.StartsWith("Prepare:", StringComparison.Ordinal));
        Assert.DoesNotContain("Play", second.Calls);
    }

    [Fact]
    public async Task FailedPrepare_WhenSessionCleanupFails_ReturnsOutcomeUnknown()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            PrepareAction = (_, _) => throw new InvalidOperationException("fake prepare failure"),
            DisconnectAction = () => throw new InvalidOperationException("fake cleanup failure")
        };
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);

        var prepare = await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.OutcomeUnknown, prepare.Code);
        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.False(engine.Status.IsCommandAvailable);
        Assert.Contains("Disconnect", session.Calls);
        Assert.Contains("Dispose", session.Calls);
        Assert.Equal(1, sessionFactory.CreateCount);
    }

    [Fact]
    public async Task Quarantine_ReleasesLocalSessionWithoutSdkDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        await engine.QuarantineAsync();
        await engine.DisposeAsync();

        Assert.Equal(PlayoutConnectionState.Disposed, engine.Status.State);
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
        Assert.Equal(1, sessionFactory.CreateCount);
    }

    [Fact]
    public async Task Quarantine_WhenLocalAbandonFails_ReportsFailureAndNeverDisconnectsOrDisposes()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            AbandonAction = () => throw new InvalidOperationException(
                "fake local abandon failure")
        };
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => engine.QuarantineAsync().AsTask());

        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
        Assert.Equal(1, sessionFactory.CreateCount);
    }

    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CancelledTakeOutThenShutdown_AbandonsWithoutSdkDisconnect(
        bool callDisconnectFirst)
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession();
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("test-scene"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        var takeOut = await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            cancellation.Token);

        Assert.Equal(PlayoutResultCode.Cancelled, takeOut.Code);
        if (callDisconnectFirst)
        {
            var disconnect = await engine.DisconnectAsync(CancellationToken.None);
            Assert.Equal(PlayoutResultCode.OutcomeUnknown, disconnect.Code);
        }

        await engine.DisposeAsync();

        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
        Assert.DoesNotContain(session.Calls, call => call.StartsWith("TakeOut:", StringComparison.Ordinal));
        Assert.Equal(PlayoutConnectionState.Disposed, engine.Status.State);
    }

    [Fact]
    public async Task ProcessMonitor_DrainsLifecycleCallbacksOnTheStaAndPublishesHello()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            KtapHelloObserved = false,
            HasPendingLifecycleCallbacks = true,
            ProcessPendingCallbacksAction = _ => new K3dCallbackDrainResult(2, 1, true)
        };
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        var process = new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0));
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            process,
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.False(engine.Status.KtapHelloObserved);
        session.KtapHelloObserved = true;

        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.True(engine.Status.KtapHelloObserved);
        Assert.Contains("ProcessPendingCallbacks", session.Calls);
        Assert.All(session.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
        Assert.Equal(PlayoutConnectionState.Connected, engine.Status.State);
    }

    [Fact]
    public async Task Disconnect_DoesNotDispatchWhileTakeOutCompletionCallbackIsPending()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.TakeOutAction = (_, _) => session.HasPendingLifecycleCallbacks = true;
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(Cue("test-scene"), CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None)).IsSuccess);

        var disconnect = await engine.DisconnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Unavailable, disconnect.Code);
        Assert.Contains("ProcessPendingCallbacks", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Abandon", session.Calls);
        Assert.True(session.IsConnected);

        session.HasPendingLifecycleCallbacks = false;
    }

    [Fact]
    public async Task Dispose_AbandonsWithoutDisconnectWhenTakeOutCallbackNeverArrives()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.TakeOutAction = (_, _) => session.HasPendingLifecycleCallbacks = true;
        var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(Cue("test-scene"), CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None)).IsSuccess);

        await engine.DisposeAsync();

        Assert.Contains("ProcessPendingCallbacks", session.Calls);
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
        Assert.Equal(PlayoutConnectionState.Disposed, engine.Status.State);
    }

    [Fact]
    public async Task LifecycleCallbackFailure_LatchesOutcomeUnknownWithoutDisconnectOrReplay()
    {
        using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            HasPendingLifecycleCallbacks = true,
            ProcessPendingCallbacksAction = _ =>
                throw new InvalidOperationException("fake callback failure")
        };
        var sessionFactory = new FakeK3dSessionFactory(() => session);
        var process = new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0));
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "test-scene"),
            sessionFactory,
            new FakeRegistrationProbe(),
            process,
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
        Assert.Contains("ProcessPendingCallbacks", session.Calls);
        Assert.Contains("Abandon", session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
        var later = await engine.PrepareAsync(Cue("test-scene"), CancellationToken.None);
        Assert.Equal(PlayoutResultCode.OutcomeUnknown, later.Code);
        Assert.Equal(1, session.Calls.Count(call => call == "ProcessPendingCallbacks"));
    }

    [Fact]
    public async Task PlayCompletionPending_StatusClearsOnlyAfterCallbackDrain()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var callbackReady = false;
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.PlayAction = _ =>
        {
            session.HasPendingPlayCallbacks = true;
            session.HasPendingLifecycleCallbacks = true;
        };
        session.ProcessPendingCallbacksAction = _ =>
        {
            if (!callbackReady)
            {
                return new K3dCallbackDrainResult(0, 0, false);
            }

            session.HasPendingPlayCallbacks = false;
            session.HasPendingLifecycleCallbacks = false;
            return new K3dCallbackDrainResult(1, 1, false);
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "5001"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("5001"),
            CancellationToken.None)).IsSuccess);
        var observedPendingStates = new List<bool>();
        engine.StatusChanged += (_, args) =>
            observedPendingStates.Add(args.Current.IsPlayCompletionPending);

        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.True(engine.Status.IsPlayCompletionPending);
        Assert.Equal([true, true], observedPendingStates);

        callbackReady = true;
        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.False(engine.Status.IsPlayCompletionPending);
        Assert.Equal([true, true, false], observedPendingStates);
        Assert.True((await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.DisconnectAsync(CancellationToken.None)).IsSuccess);
    }

    [Fact]
    public async Task PendingPlayCallback_BlocksDelayedMultiNextUntilEachCompletionIsDrained()
    {
        using var scenes = TemporarySceneDirectory.Create(
            "5001.t2s",
            "5006.t2s",
            "5011.t2s");
        var callbackReady = false;
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.PlayAction = _ =>
        {
            session.HasPendingPlayCallbacks = true;
            session.HasPendingLifecycleCallbacks = true;
        };
        session.ProcessPendingCallbacksAction = _ =>
        {
            if (!callbackReady)
            {
                return new K3dCallbackDrainResult(0, 0, false);
            }

            callbackReady = false;
            session.HasPendingPlayCallbacks = false;
            session.HasPendingLifecycleCallbacks = false;
            return new K3dCallbackDrainResult(1, 1, false);
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "5001", "5006", "5011"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("5001"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        Assert.True(session.HasPendingPlayCallbacks);
        Assert.True(engine.Status.IsPlayCompletionPending);

        var firstEarlyNext = await engine.NextAsync(
            Cue("5006"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Unavailable, firstEarlyNext.Code);
        Assert.Contains("completion callback", firstEarlyNext.Message, StringComparison.Ordinal);
        Assert.DoesNotContain("Prepare:5006", session.Calls);

        callbackReady = true;
        Assert.True((await engine.NextAsync(
            Cue("5006"),
            CancellationToken.None)).IsSuccess);
        Assert.True(session.HasPendingPlayCallbacks);
        Assert.Equal(1, session.Calls.Count(call => call == "Prepare:5006"));

        var secondEarlyNext = await engine.NextAsync(
            Cue("5011"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Unavailable, secondEarlyNext.Code);
        Assert.DoesNotContain("Prepare:5011", session.Calls);

        callbackReady = true;
        Assert.True((await engine.NextAsync(
            Cue("5011"),
            CancellationToken.None)).IsSuccess);
        Assert.Equal(1, session.Calls.Count(call => call == "Prepare:5011"));

        callbackReady = true;
        await engine.PollProcessOnceAsync(CancellationToken.None);
        Assert.False(session.HasPendingPlayCallbacks);
        Assert.False(engine.Status.IsPlayCompletionPending);
        Assert.True((await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.DisconnectAsync(CancellationToken.None)).IsSuccess);
        Assert.Contains("Disconnect", session.Calls);
    }

    [Theory]
    [InlineData("prepare")]
    [InlineData("take-in")]
    [InlineData("next")]
    [InlineData("update-on-air")]
    public async Task PendingPlayCallback_RejectsEveryNonTakeOutSceneMutation(string operation)
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.PlayAction = _ =>
        {
            session.HasPendingPlayCallbacks = true;
            session.HasPendingLifecycleCallbacks = true;
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "5001", "5006"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        if (operation != "prepare")
        {
            Assert.True((await engine.PrepareAsync(
                Cue("5001"),
                CancellationToken.None)).IsSuccess);
        }

        if (operation is "next" or "update-on-air")
        {
            Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        }
        else
        {
            session.HasPendingPlayCallbacks = true;
            session.HasPendingLifecycleCallbacks = true;
        }

        var result = operation switch
        {
            "prepare" => await engine.PrepareAsync(
                Cue("5006"),
                CancellationToken.None),
            "take-in" => await engine.TakeInAsync(CancellationToken.None),
            "next" => await engine.NextAsync(
                Cue("5006"),
                CancellationToken.None),
            "update-on-air" => await engine.UpdateOnAirAsync(
                Cue("5001"),
                CancellationToken.None),
            _ => throw new InvalidOperationException("Unknown test operation.")
        };

        Assert.Equal(PlayoutResultCode.Unavailable, result.Code);
        Assert.Contains("completion callback", result.Message, StringComparison.Ordinal);
        if (operation == "prepare")
        {
            Assert.DoesNotContain("Prepare:5006", session.Calls);
        }
        else if (operation == "take-in")
        {
            Assert.DoesNotContain("Play", session.Calls);
        }
        else if (operation == "next")
        {
            Assert.DoesNotContain("Prepare:5006", session.Calls);
        }
        else
        {
            Assert.DoesNotContain("UpdateOnAir:5001", session.Calls);
        }
    }

    [Fact]
    public async Task PendingPlayCallback_DoesNotBlockEmergencyTakeOut()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.PlayAction = _ =>
        {
            session.HasPendingPlayCallbacks = true;
            session.HasPendingLifecycleCallbacks = true;
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "5001"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("5001"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        Assert.True(session.HasPendingPlayCallbacks);
        Assert.True(engine.Status.IsPlayCompletionPending);

        var result = await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Contains("TakeOut:All", session.Calls);
        Assert.True(engine.Status.IsPlayCompletionPending);
    }

    [Fact]
    public async Task PendingTakeOutCallback_BlocksEveryFurtherSdkCommandUntilCompletion()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var completionReady = false;
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.TakeOutAction = (_, _) =>
        {
            session.HasPendingLifecycleCallbacks = true;
            session.HasPendingTakeOutCallbacks = true;
        };
        session.ProcessPendingCallbacksAction = _ =>
        {
            if (!completionReady)
            {
                return new K3dCallbackDrainResult(0, 0, false);
            }

            session.HasPendingLifecycleCallbacks = false;
            session.HasPendingTakeOutCallbacks = false;
            return new K3dCallbackDrainResult(1, 1, false);
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "5001", "5006"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("5001"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None)).IsSuccess);
        Assert.True(engine.Status.IsTakeOutCompletionPending);
        Assert.False(engine.Status.IsCommandAvailable);

        var prepare = await engine.PrepareAsync(Cue("5006"), CancellationToken.None);
        var repeatedTakeOut = await engine.TakeOutAsync(
            PlayoutTakeOutScope.All,
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Unavailable, prepare.Code);
        Assert.Equal(PlayoutResultCode.Unavailable, repeatedTakeOut.Code);
        Assert.Contains("CutOut/StopAll", prepare.Message, StringComparison.Ordinal);
        Assert.Equal(1, session.Calls.Count(call => call == "TakeOut:All"));
        Assert.DoesNotContain("Prepare:5006", session.Calls);

        completionReady = true;
        await engine.PollProcessOnceAsync(CancellationToken.None);

        Assert.False(engine.Status.IsTakeOutCompletionPending);
        Assert.True(engine.Status.IsCommandAvailable);
        Assert.True((await engine.PrepareAsync(
            Cue("5006"),
            CancellationToken.None)).IsSuccess);
        Assert.Equal(1, session.Calls.Count(call => call == "Prepare:5006"));
    }

    [Theory]
    [InlineData(PlayoutTakeOutScope.All)]
    [InlineData(PlayoutTakeOutScope.Layout)]
    public async Task TakeOutCompletion_ClearsInterruptedPlayAndAllowsDisconnect(
        PlayoutTakeOutScope scope)
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var takeOutCompletionReady = false;
        var session = new FakeK3dSession
        {
            SupportsLifecycleCallbacks = true,
            ClearPendingLifecycleCallbacksAfterProcess = false
        };
        session.PlayAction = _ =>
        {
            session.HasPendingPlayCallbacks = true;
            session.HasPendingLifecycleCallbacks = true;
        };
        session.TakeOutAction = (_, _) => session.HasPendingLifecycleCallbacks = true;
        session.ProcessPendingCallbacksAction = _ =>
        {
            if (!takeOutCompletionReady)
            {
                return new K3dCallbackDrainResult(0, 0, false);
            }

            session.HasPendingPlayCallbacks = false;
            session.HasPendingLifecycleCallbacks = false;
            return new K3dCallbackDrainResult(1, 1, false);
        };
        await using var engine = CreateEngine(
            TestOptions(scenes.Path, "5001"),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            liveAuthorized: false);

        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
        Assert.True((await engine.PrepareAsync(
            Cue("5001"),
            CancellationToken.None)).IsSuccess);
        Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
        Assert.True(session.HasPendingPlayCallbacks);
        Assert.True((await engine.TakeOutAsync(scope, CancellationToken.None)).IsSuccess);

        takeOutCompletionReady = true;
        var disconnect = await engine.DisconnectAsync(CancellationToken.None);

        Assert.True(disconnect.IsSuccess);
        Assert.False(session.HasPendingPlayCallbacks);
        Assert.Contains($"TakeOut:{scope}", session.Calls);
        Assert.Contains("ProcessPendingCallbacks", session.Calls);
        Assert.Contains("Disconnect", session.Calls);
    }

    [Fact]
    public async Task GateA_AllowsExactlyOneConnectAndClaimed5001Prepare_ThenAbandonsWithoutBye()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var session = new FakeK3dSession();
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            GateACapability);
        var engine = CreateEngine(
            GateAOptions(scenes.Path),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("approved-pgm")),
            liveAuthorized: true,
            launchAuthorization: authorization);

        try
        {
            var cue = GateACue();

            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.ConnectAsync(CancellationToken.None)).Code);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.PrepareAsync(cue, CancellationToken.None)).Code);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.TakeInAsync(CancellationToken.None)).Code);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.NextAsync(cue, CancellationToken.None)).Code);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.UpdateOnAirAsync(cue, CancellationToken.None)).Code);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.TakeOutAsync(
                    PlayoutTakeOutScope.All,
                    CancellationToken.None)).Code);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.DisconnectAsync(CancellationToken.None)).Code);
            Assert.False(engine.Status.LiveTakeInAllowed);

            Assert.True(authorization.TryClaimGateAPrepare(GateACapability));
            Assert.True((await engine.PrepareAsync(cue, CancellationToken.None)).IsSuccess);
            Assert.Equal(
                PlayoutResultCode.Rejected,
                (await engine.PrepareAsync(cue, CancellationToken.None)).Code);
        }
        finally
        {
            authorization.CompleteGateAPrepare();
            await engine.DisposeAsync();
        }

        Assert.Equal(new[] { "Connect", "Prepare:5001", "Abandon" }, session.Calls);
        Assert.Equal(1, session.KtapDispatchCount);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GateA_WrongPrepareTargetPermanentlyConsumesPrepareWithoutSdkMutation()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var session = new FakeK3dSession();
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            GateACapability);
        var engine = CreateEngine(
            GateAOptions(scenes.Path),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("approved-pgm")),
            liveAuthorized: true,
            launchAuthorization: authorization);

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            Assert.True(authorization.TryClaimGateAPrepare(GateACapability));

            var wrong = await engine.PrepareAsync(
                new PlayoutCue("5006.t2s", "5006", FadeDuration: 6),
                CancellationToken.None);
            var correction = await engine.PrepareAsync(
                GateACue(),
                CancellationToken.None);

            Assert.Equal(PlayoutResultCode.Rejected, wrong.Code);
            Assert.Equal(PlayoutResultCode.Rejected, correction.Code);
        }
        finally
        {
            authorization.CompleteGateAPrepare();
            await engine.DisposeAsync();
        }

        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain(
            session.Calls,
            static call => call.StartsWith("Prepare:", StringComparison.Ordinal));
        Assert.DoesNotContain("Disconnect", session.Calls);
    }

    [Fact]
    public async Task GateA_PgmGenerationChangeAbandonsWithoutReconnectOrBye()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var sessionFactory = new FakeK3dSessionFactory();
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            GateACapability);
        var options = GateAOptions(scenes.Path);
        options.ReconnectEnabled = true;
        options.MaximumReconnectAttempts = 3;
        var engine = CreateEngine(
            options,
            sessionFactory,
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(
                ProcessSnapshot("approved-pgm"),
                ProcessSnapshot("approved-pgm"),
                ProcessSnapshot("replacement-pgm")),
            liveAuthorized: true,
            launchAuthorization: authorization);

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);

            await engine.PollProcessOnceAsync(CancellationToken.None);

            Assert.Equal(PlayoutConnectionState.OutcomeUnknown, engine.Status.State);
            Assert.Equal(1, sessionFactory.CreateCount);
            var session = Assert.Single(sessionFactory.Sessions);
            Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
            Assert.DoesNotContain("Disconnect", session.Calls);
        }
        finally
        {
            authorization.CompleteGateAPrepare();
            await engine.DisposeAsync();
        }

        Assert.Equal(1, sessionFactory.CreateCount);
    }

    [Fact]
    public async Task GateA_ExpiredConnectIsRejectedBeforeProcessRegistrationOrVendorBoundary()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var clock = new ManualGateTimeProvider();
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            GateACapability,
            timeProvider: clock,
            expiresAtUtcTicks:
                clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks);
        clock.Advance(TimeSpan.FromMinutes(1));
        var sessionFactory = new FakeK3dSessionFactory();
        var registration = new FakeRegistrationProbe();
        var process = new FakeTornadoProcessProbe(ProcessSnapshot("approved-pgm"));
        var engine = CreateEngine(
            GateAOptions(scenes.Path),
            sessionFactory,
            registration,
            process,
            liveAuthorized: true,
            launchAuthorization: authorization);

        try
        {
            var result = await engine.ConnectAsync(CancellationToken.None);

            Assert.Equal(PlayoutResultCode.Rejected, result.Code);
            Assert.Equal(0, process.CaptureCount);
            Assert.Equal(0, registration.ProbeCount);
            Assert.Equal(0, sessionFactory.CreateCount);
        }
        finally
        {
            await engine.DisposeAsync();
            authorization.Dispose();
        }
    }

    [Fact]
    public async Task GateA_PrepareFailureBecomesOutcomeUnknownAndCannotRetryOrDisconnect()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var session = new FakeK3dSession
        {
            PrepareAction = (_, _) => throw new COMException(
                "fake ambiguous Gate A prepare failure")
        };
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            GateACapability);
        var engine = CreateEngine(
            GateAOptions(scenes.Path),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("approved-pgm")),
            liveAuthorized: true,
            abandonOnTargetMismatch: true,
            launchAuthorization: authorization);

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            Assert.True(authorization.TryClaimGateAPrepare(GateACapability));

            var failed = await engine.PrepareAsync(GateACue(), CancellationToken.None);
            var retry = await engine.PrepareAsync(GateACue(), CancellationToken.None);

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, failed.Code);
            Assert.Equal(PlayoutResultCode.OutcomeUnknown, retry.Code);
        }
        finally
        {
            authorization.CompleteGateAPrepare();
            await engine.DisposeAsync();
            authorization.Dispose();
        }

        Assert.Equal(new[] { "Connect", "Prepare:5001", "Abandon" }, session.Calls);
        Assert.Equal(1, session.Calls.Count(call => call == "Prepare:5001"));
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GateA_ConnectExpiryAtSdkBoundaryPreventsKtapDispatch()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var clock = new ManualGateTimeProvider();
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            GateACapability,
            timeProvider: clock,
            expiresAtUtcTicks:
                clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks);
        var session = new FakeK3dSession
        {
            BeforeKtapDispatchAction = () => clock.Advance(TimeSpan.FromMinutes(1))
        };
        var engine = CreateEngine(
            GateAOptions(scenes.Path),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("approved-pgm")),
            liveAuthorized: true,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true,
            beforeSdkDispatchClaim: authorization.EnsureGateASdkDispatchAuthorized,
            launchAuthorization: authorization);

        try
        {
            var result = await engine.ConnectAsync(CancellationToken.None);

            Assert.False(result.IsSuccess);
            Assert.Equal(0, session.KtapDispatchCount);
        }
        finally
        {
            await engine.DisposeAsync();
            authorization.Dispose();
        }

        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    [Fact]
    public async Task GateA_PrepareExpiryAtSdkBoundaryPreventsPrepareDispatchAndQuarantines()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var clock = new ManualGateTimeProvider();
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            GateACapability,
            timeProvider: clock,
            expiresAtUtcTicks:
                clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks);
        var dispatchBoundaryCount = 0;
        void DelayedDispatchGuard()
        {
            if (Interlocked.Increment(ref dispatchBoundaryCount) == 2)
            {
                clock.Advance(TimeSpan.FromMinutes(1));
            }

            authorization.EnsureGateASdkDispatchAuthorized();
        }

        var session = new FakeK3dSession();
        var engine = CreateEngine(
            GateAOptions(scenes.Path),
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(ProcessSnapshot("approved-pgm")),
            liveAuthorized: true,
            finalConnectSafetyCheck: () => true,
            abandonOnTargetMismatch: true,
            beforeSdkDispatchClaim: DelayedDispatchGuard,
            launchAuthorization: authorization);

        try
        {
            Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
            Assert.True(authorization.TryClaimGateAPrepare(GateACapability));

            var result = await engine.PrepareAsync(GateACue(), CancellationToken.None);
            var retry = await engine.PrepareAsync(GateACue(), CancellationToken.None);

            Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
            Assert.Equal(PlayoutResultCode.OutcomeUnknown, retry.Code);
            Assert.Equal(2, dispatchBoundaryCount);
        }
        finally
        {
            authorization.CompleteGateAPrepare();
            await engine.DisposeAsync();
            authorization.Dispose();
        }

        Assert.Equal(new[] { "Connect", "Abandon" }, session.Calls);
        Assert.DoesNotContain(
            session.Calls,
            static call => call.StartsWith("Prepare:", StringComparison.Ordinal));
        Assert.DoesNotContain("Disconnect", session.Calls);
        Assert.DoesNotContain("Dispose", session.Calls);
    }

    private static TornadoPlayoutEngine CreateEngine(
        PlayoutOptions rawOptions,
        FakeK3dSessionFactory sessionFactory,
        FakeRegistrationProbe registrationProbe,
        FakeTornadoProcessProbe processProbe,
        bool liveAuthorized,
        Func<bool>? finalConnectSafetyCheck = null,
        bool abandonOnTargetMismatch = false,
        Action? beforeSdkDispatchClaim = null,
        FakeLiveAuthorization? liveAuthorization = null,
        PlayoutLaunchAuthorization? launchAuthorization = null)
    {
        var options = ValidatedPlayoutOptions.Create(rawOptions);
        IStaDispatcher? dispatcher = options.Mode is PlayoutMode.Test or PlayoutMode.Live
            ? new StaDispatcher(options.QueueCapacity)
            : null;
        return new TornadoPlayoutEngine(
            options,
            sessionFactory,
            registrationProbe,
            processProbe,
            dispatcher,
            liveAuthorization ?? new FakeLiveAuthorization(liveAuthorized),
            TimeProvider.System,
            startMonitor: false,
            finalConnectSafetyCheck: finalConnectSafetyCheck,
            abandonOnTargetMismatch: abandonOnTargetMismatch,
            beforeSdkDispatchClaim: beforeSdkDispatchClaim,
            launchAuthorization: launchAuthorization);
    }

    private static PlayoutOptions TestOptions(
        string sceneDirectory,
        params string[] allowedScenes) => new()
    {
        Mode = PlayoutMode.Test,
        SceneDirectory = sceneDirectory,
        OutputChannel = 9,
        TestProcessWindowTitlePattern = "^Tornado2 TEST$",
        TestSceneAllowlist = [.. allowedScenes],
        ConnectTimeoutMilliseconds = 500,
        OperationTimeoutMilliseconds = 500,
        DisconnectTimeoutMilliseconds = 500,
        ProcessPollIntervalMilliseconds = 100,
        ReconnectDelayMilliseconds = 0,
        MaximumReconnectAttempts = 3
    };

    private static PlayoutOptions LiveOptions(
        string sceneDirectory,
        bool trustedLiveOutputEnabled) => new()
    {
        Mode = PlayoutMode.Live,
        SceneDirectory = sceneDirectory,
        TestSceneAllowlist = ["live-scene"],
        TrustedLiveOutputEnabled = trustedLiveOutputEnabled,
        ConnectTimeoutMilliseconds = 500,
        OperationTimeoutMilliseconds = 500,
        DisconnectTimeoutMilliseconds = 500,
        ProcessPollIntervalMilliseconds = 100,
        ReconnectDelayMilliseconds = 0,
        MaximumReconnectAttempts = 3
    };

    private static PlayoutOptions GateAOptions(string sceneDirectory) => new()
    {
        Mode = PlayoutMode.Live,
        Host = "127.0.0.1",
        Port = 30001,
        TcpMode = 1,
        LayoutIndex = 10,
        SceneDirectory = sceneDirectory,
        TestSceneAllowlist = ["5001"],
        TrustedLiveOutputEnabled = true,
        ReconnectEnabled = false,
        MaximumReconnectAttempts = 0,
        MaximumAutomaticRefreshesPerTakeIn = 0,
        LegacySceneFadeDuration = 6,
        ConnectTimeoutMilliseconds = 500,
        OperationTimeoutMilliseconds = 500,
        DisconnectTimeoutMilliseconds = 500,
        ProcessPollIntervalMilliseconds = 100,
        ReconnectDelayMilliseconds = 0
    };

    private static PlayoutCue GateACue() => new(
        "5001.t2s",
        "5001",
        FadeDuration: 6,
        Mutations: [new PlayoutUseBackground(false)]);

    private static PlayoutCue Cue(string sceneName) => new(
        $"{sceneName}.t2s",
        sceneName,
        [new PlayoutField("headline", "fake", true)]);

    private static TornadoProcessSnapshot ProcessSnapshot(string generation) =>
        new(1, 1, 0)
        {
            EligibleProcessGeneration = new TornadoProcessGeneration(generation)
        };
}
