using System.Collections.Concurrent;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
using MBN_STOCK_WEBVIEW.Playout.Runtime;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class PgmCutsSequenceRunnerTests
{
    [Fact]
    public async Task StableTarget_ExecutesFixedSequenceWithExactSafetyAndMilestoneOrder()
    {
        using var environment = new EnvironmentScope()
            .Set("MBN_STOCK_PLAYOUT_MODE", "DryRun")
            .Set("MBN_STOCK_PLAYOUT_HOST", "192.0.2.99")
            .Set("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", "77")
            .Set("MBN_STOCK_PLAYOUT_AUTHORIZE_LIVE_OUTPUT", "false");
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var safety = new FakeSafetyProbe(events);
        var engine = new FakeEngine(events);
        var factory = new FakeEngineFactory(events, engine);
        var runner = new PgmCutsSequenceRunner(
            safety,
            new FakeHashPins(true),
            factory,
            new FakeDelay(events),
            new FakeAssetLeases(true));

        var report = await runner.ExecuteAsync(
            Request(scenes.Path),
            stage => events.Enqueue($"Milestone:{stage}"));

        Assert.True(report.Completed);
        Assert.False(report.OutcomeUnknown);
        Assert.False(report.QuarantineAttempted);
        Assert.Equal(
            new[]
            {
                "Safety:1",
                "CreateEngine",
                "Safety:2",
                "Connect",
                "Safety:3",
                "Safety:4",
                "Prepare:5001",
                "Safety:5",
                "Safety:6",
                "TakeIn",
                "Safety:7",
                "Safety:8",
                "Milestone:First",
                "Delay:5000",
                "Safety:9",
                "Safety:10",
                "Next:5006",
                "Safety:11",
                "Safety:12",
                "Milestone:Next",
                "Delay:5000",
                "Safety:13",
                "Safety:14",
                "TakeOut:All",
                "Safety:15",
                "Safety:16",
                "Milestone:TakeOut",
                "Delay:5000",
                "Safety:17",
                "Safety:18",
                "Disconnect",
                "Safety:19",
                "Dispose"
            },
            events);
        Assert.Equal(
            new[]
            {
                PgmCutsSequenceStage.Connect,
                PgmCutsSequenceStage.PrepareFirst,
                PgmCutsSequenceStage.TakeIn,
                PgmCutsSequenceStage.Next,
                PgmCutsSequenceStage.TakeOut,
                PgmCutsSequenceStage.Disconnect
            },
            report.Commands.Select(command => command.Stage));

        var plan = Assert.IsType<PgmCutsSequencePlan>(factory.Plan);
        Assert.Equal(PlayoutMode.Live, plan.Options.Mode);
        Assert.Equal("127.0.0.1", plan.Options.Host);
        Assert.Equal(30_001, plan.Options.Port);
        Assert.Null(plan.Options.OutputChannel);
        Assert.Equal(10, plan.Options.LayoutIndex);
        Assert.Equal(["5001", "5006"], plan.Options.TestSceneAllowlist);
        Assert.True(plan.Options.TrustedLiveOutputEnabled);
        Assert.False(plan.Options.ReconnectEnabled);
        Assert.Equal(0, plan.Options.MaximumReconnectAttempts);
        Assert.Equal(TimeSpan.FromMilliseconds(5_000), plan.ObservationDuration);
        Assert.Equal("5001", plan.FirstCue.SceneName);
        Assert.Equal("5006", plan.NextCue.SceneName);
        Assert.Equal("5001.t2s", plan.FirstCue.SceneFile);
        Assert.Equal("5006.t2s", plan.NextCue.SceneFile);
        Assert.False(Path.IsPathFullyQualified(plan.FirstCue.SceneFile));
        Assert.False(Path.IsPathFullyQualified(plan.NextCue.SceneFile));
        Assert.True(Path.IsPathFullyQualified(plan.FirstAssetPath));
        Assert.True(Path.IsPathFullyQualified(plan.NextAssetPath));
        Assert.Equal("5001.t2s", Path.GetFileName(plan.FirstAssetPath));
        Assert.Equal("5006.t2s", Path.GetFileName(plan.NextAssetPath));
    }

    [Fact]
    public async Task RawPlanCue_IsResolvedExactlyOnceByTornadoEngineBeforeSdkPrepare()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var captureFactory = new FakeEngineFactory(events, new FakeEngine(events));
        var runner = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(events),
            new FakeHashPins(true),
            captureFactory,
            new FakeDelay(events),
            new FakeAssetLeases(true));
        Assert.True((await runner.ExecuteAsync(Request(scenes.Path))).Completed);
        var plan = Assert.IsType<PgmCutsSequencePlan>(captureFactory.Plan);
        PlayoutCue? sdkCue = null;
        var session = new FakeK3dSession
        {
            PrepareAction = (cue, _) => sdkCue = cue
        };
        var dispatcher = new StaDispatcher(plan.Options.QueueCapacity);
        await using var engine = new TornadoPlayoutEngine(
            plan.Options,
            new FakeK3dSessionFactory(() => session),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
            dispatcher,
            new FakeLiveAuthorization(true),
            TimeProvider.System,
            startMonitor: false);

        Assert.False(Path.IsPathFullyQualified(plan.FirstCue.SceneFile));
        Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);

        var prepare = await engine.PrepareAsync(
            plan.FirstCue,
            CancellationToken.None);

        Assert.True(prepare.IsSuccess);
        var resolvedCue = Assert.IsType<PlayoutCue>(sdkCue);
        Assert.True(Path.IsPathFullyQualified(resolvedCue.SceneFile));
        Assert.Equal(
            Path.GetFullPath(plan.FirstAssetPath),
            Path.GetFullPath(resolvedCue.SceneFile),
            StringComparer.OrdinalIgnoreCase);
        Assert.True((await engine.DisconnectAsync(CancellationToken.None)).IsSuccess);
    }

    [Theory]
    [InlineData(2)]
    [InlineData(3)]
    [InlineData(4)]
    [InlineData(5)]
    [InlineData(6)]
    [InlineData(7)]
    [InlineData(8)]
    [InlineData(9)]
    [InlineData(10)]
    [InlineData(11)]
    [InlineData(12)]
    [InlineData(13)]
    [InlineData(14)]
    [InlineData(15)]
    [InlineData(16)]
    [InlineData(17)]
    [InlineData(18)]
    [InlineData(19)]
    public async Task TargetChangeAtEveryBoundary_StopsAndQuarantinesWithoutLaterOutput(
        int changedCapture)
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var engine = new FakeEngine(events);
        var runner = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(events, changedCapture),
            new FakeHashPins(true),
            new FakeEngineFactory(events, engine),
            new FakeDelay(events),
            new FakeAssetLeases(true));

        var report = await runner.ExecuteAsync(
            Request(scenes.Path),
            stage => events.Enqueue($"Milestone:{stage}"));

        Assert.False(report.Completed);
        Assert.Equal("pgm-target-changed", report.ErrorCode);
        Assert.True(report.QuarantineAttempted);
        Assert.True(report.QuarantineCompleted);
        Assert.Equal(1, events.Count(value => value == "Quarantine"));
        var quarantineIndex = Array.IndexOf(events.ToArray(), "Quarantine");
        Assert.DoesNotContain(
            events.Skip(quarantineIndex + 1),
            value => IsOutputCommand(value) || value == "Disconnect");
        if (changedCapture <= 18)
        {
            Assert.DoesNotContain("Disconnect", events);
        }

        Assert.Equal(changedCapture is > 2 and < 19, report.OutcomeUnknown);
    }

    [Theory]
    [InlineData((int)PgmCutsSequenceStage.Connect)]
    [InlineData((int)PgmCutsSequenceStage.PrepareFirst)]
    [InlineData((int)PgmCutsSequenceStage.TakeIn)]
    [InlineData((int)PgmCutsSequenceStage.Next)]
    [InlineData((int)PgmCutsSequenceStage.TakeOut)]
    [InlineData((int)PgmCutsSequenceStage.Disconnect)]
    public async Task OutcomeUnknownAtEveryCommand_QuarantinesWithoutSubsequentCommand(
        int stageValue)
    {
        var stage = (PgmCutsSequenceStage)stageValue;
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var engine = new FakeEngine(events);
        engine.Results[stage] = PlayoutResultCode.OutcomeUnknown;
        var runner = Runner(events, engine);

        var report = await runner.ExecuteAsync(Request(scenes.Path));

        Assert.False(report.Completed);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.QuarantineCompleted);
        Assert.Equal("command-outcome-unknown", report.ErrorCode);
        var quarantineIndex = Array.IndexOf(events.ToArray(), "Quarantine");
        Assert.DoesNotContain(
            events.Skip(quarantineIndex + 1),
            value => IsOutputCommand(value) || value == "Disconnect");
        if (stage != PgmCutsSequenceStage.Disconnect)
        {
            Assert.DoesNotContain("Disconnect", events);
        }
    }

    [Fact]
    public async Task KnownPrepareFailure_PerformsExplicitSameTargetDisconnectCleanup()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var engine = new FakeEngine(events);
        engine.Results[PgmCutsSequenceStage.PrepareFirst] = PlayoutResultCode.Failed;

        var report = await Runner(events, engine).ExecuteAsync(Request(scenes.Path));

        Assert.False(report.Completed);
        Assert.False(report.OutcomeUnknown);
        Assert.False(report.QuarantineAttempted);
        Assert.True(report.DisconnectAttempted);
        Assert.Equal(
            new[] { "Connect", "Prepare:5001", "Disconnect", "Dispose" },
            EngineCalls(events));
    }

    [Fact]
    public async Task QuarantineFailure_SkipsDisposeAndEveryFurtherSdkCommand()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var engine = new FakeEngine(events)
        {
            QuarantineFailure = new InvalidOperationException("fake quarantine failure")
        };
        engine.Results[PgmCutsSequenceStage.Next] = PlayoutResultCode.OutcomeUnknown;

        var report = await Runner(events, engine).ExecuteAsync(Request(scenes.Path));

        Assert.True(report.QuarantineAttempted);
        Assert.False(report.QuarantineCompleted);
        Assert.True(report.OutcomeUnknown);
        Assert.DoesNotContain("Dispose", events);
        Assert.DoesNotContain("Disconnect", events);
        Assert.Equal("Quarantine", events.Last());
    }

    [Fact]
    public async Task TimedOutTakeOut_IsTreatedAsUnknownAndNeverDisconnects()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var engine = new FakeEngine(events);
        engine.Results[PgmCutsSequenceStage.TakeOut] = PlayoutResultCode.TimedOut;

        var report = await Runner(events, engine).ExecuteAsync(Request(scenes.Path));

        Assert.True(report.RenderCommandAttempted);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.QuarantineCompleted);
        Assert.DoesNotContain("Disconnect", events);
        Assert.Equal(1, events.Count(value => value == "TakeOut:All"));
    }

    [Theory]
    [InlineData(1, false)]
    [InlineData(2, true)]
    public async Task InterruptedOnAirObservation_PerformsOneShotTakeOutThenDisconnect(
        int interruptedObservation,
        bool nextWasRendered)
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var delay = new FakeDelay(events)
        {
            FailureOnCall = interruptedObservation
        };
        var engine = new FakeEngine(events);
        var runner = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(events),
            new FakeHashPins(true),
            new FakeEngineFactory(events, engine),
            delay,
            new FakeAssetLeases(true));

        var report = await runner.ExecuteAsync(Request(scenes.Path));

        Assert.False(report.Completed);
        Assert.False(report.OutcomeUnknown);
        Assert.False(report.QuarantineAttempted);
        Assert.Equal("observation-interrupted", report.ErrorCode);
        Assert.Equal(1, events.Count(value => value == "TakeOut:All"));
        Assert.Equal(1, events.Count(value => value == "Disconnect"));
        Assert.Equal(nextWasRendered, events.Contains("Next:5006"));
        Assert.Equal(
            new[] { PgmCutsSequenceStage.TakeOut, PgmCutsSequenceStage.Disconnect },
            report.Commands.TakeLast(2).Select(command => command.Stage));
    }

    [Theory]
    [InlineData(PlayoutResultCode.Failed)]
    [InlineData(PlayoutResultCode.TimedOut)]
    [InlineData(PlayoutResultCode.OutcomeUnknown)]
    public async Task InterruptedObservation_WhenOneShotTakeOutIsNotSuccessful_QuarantinesWithoutDisconnect(
        PlayoutResultCode takeOutCode)
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var engine = new FakeEngine(events);
        engine.Results[PgmCutsSequenceStage.TakeOut] = takeOutCode;
        var delay = new FakeDelay(events)
        {
            FailureOnCall = 1
        };
        var runner = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(events),
            new FakeHashPins(true),
            new FakeEngineFactory(events, engine),
            delay,
            new FakeAssetLeases(true));

        var report = await runner.ExecuteAsync(Request(scenes.Path));

        Assert.False(report.Completed);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.QuarantineCompleted);
        Assert.Equal(1, events.Count(value => value == "TakeOut:All"));
        Assert.DoesNotContain("Disconnect", events);
        var takeOutIndex = Array.IndexOf(events.ToArray(), "TakeOut:All");
        Assert.DoesNotContain(
            events.Skip(takeOutIndex + 1),
            value => value == "TakeOut:All");
    }

    [Fact]
    public async Task InterruptedPostTakeOutObservation_DoesNotRepeatTakeOutAndDisconnects()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var delay = new FakeDelay(events)
        {
            FailureOnCall = 3
        };
        var engine = new FakeEngine(events);
        var runner = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(events),
            new FakeHashPins(true),
            new FakeEngineFactory(events, engine),
            delay,
            new FakeAssetLeases(true));

        var report = await runner.ExecuteAsync(Request(scenes.Path));

        Assert.False(report.Completed);
        Assert.False(report.OutcomeUnknown);
        Assert.False(report.QuarantineAttempted);
        Assert.Equal("observation-interrupted", report.ErrorCode);
        Assert.Equal(1, events.Count(value => value == "TakeOut:All"));
        Assert.Equal(1, events.Count(value => value == "Disconnect"));
    }

    [Fact]
    public async Task MissingPinsOrInvalidAssets_RejectBeforeEngineCreation()
    {
        using var missingScene = TemporarySceneDirectory.Create("5001.t2s");
        using var validScenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var missingPinEvents = new ConcurrentQueue<string>();
        var pinFactory = new FakeEngineFactory(missingPinEvents, new FakeEngine(missingPinEvents));
        var missingPins = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(missingPinEvents),
            new FakeHashPins(false),
            pinFactory,
            new FakeDelay(missingPinEvents));
        var assetEvents = new ConcurrentQueue<string>();
        var assetFactory = new FakeEngineFactory(assetEvents, new FakeEngine(assetEvents));
        var invalidAssets = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(assetEvents),
            new FakeHashPins(true),
            assetFactory,
            new FakeDelay(assetEvents),
            new FakeAssetLeases(true));

        var rejectedAssetEvents = new ConcurrentQueue<string>();
        var rejectedAssetFactory = new FakeEngineFactory(
            rejectedAssetEvents,
            new FakeEngine(rejectedAssetEvents));
        var rejectedAssets = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(rejectedAssetEvents),
            new FakeHashPins(true),
            rejectedAssetFactory,
            new FakeDelay(rejectedAssetEvents),
            new FakeAssetLeases(false));

        var pinReport = await missingPins.ExecuteAsync(Request(validScenes.Path));
        var assetReport = await invalidAssets.ExecuteAsync(Request(missingScene.Path));
        var rejectedAssetReport = await rejectedAssets.ExecuteAsync(Request(validScenes.Path));

        Assert.Equal("hash-pins-required", pinReport.ErrorCode);
        Assert.Equal("request-rejected", assetReport.ErrorCode);
        Assert.Equal("cut-assets-not-approved", rejectedAssetReport.ErrorCode);
        Assert.Equal(0, pinFactory.CreateCount);
        Assert.Equal(0, assetFactory.CreateCount);
        Assert.Equal(0, rejectedAssetFactory.CreateCount);
    }

    [Fact]
    public async Task ReportAndMilestonesContainNoEndpointTitleIdentityPathHashOrSceneCode()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        const string title = "Unique PGM Private Title";
        var events = new ConcurrentQueue<string>();
        var report = await Runner(events, new FakeEngine(events)).ExecuteAsync(
            Request(scenes.Path) with { ExpectedPgmWindowTitle = title });

        var json = JsonSerializer.Serialize(report);
        Assert.DoesNotContain("127.0.0.1", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("30001", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(title, json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(scenes.Path, json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("4242", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(new string('A', 64), json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("5001", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("5006", json, StringComparison.OrdinalIgnoreCase);
        Assert.All(
            events.Where(value => value.StartsWith("Milestone:", StringComparison.Ordinal)),
            value => Assert.DoesNotContain("500", value, StringComparison.Ordinal));
    }

    [Fact]
    public void RunnerCapabilityAndEntryPointsAreNotPublic_AndNormalTestGateIsUnchanged()
    {
        Assert.False(typeof(PgmCutsSequenceRunner).IsPublic);
        Assert.False(typeof(PgmCutsSequenceRequest).IsPublic);
        Assert.False(typeof(PgmCutsSequenceReport).IsPublic);
        Assert.All(
            typeof(FullPgmCutsSequenceEngineFactory).GetNestedTypes(
                System.Reflection.BindingFlags.Public |
                System.Reflection.BindingFlags.NonPublic),
            type => Assert.False(type.IsPublic));

        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var normalTest = new PlayoutOptions
        {
            Mode = PlayoutMode.Test,
            SceneDirectory = scenes.Path,
            OutputChannel = 9,
            TestProcessWindowTitlePattern = "^Tornado2 PGM$",
            TestSceneAllowlist = ["5001", "5006"]
        };

        Assert.Throws<PlayoutConfigurationException>(
            () => ValidatedPlayoutOptions.Create(normalTest));
    }

    [Fact]
    public async Task FullFactoryUsesPrivateCapabilityAndNeverReadsAmbientLiveAuthorization()
    {
        using var environment = new EnvironmentScope()
            .Set("MBN_STOCK_PLAYOUT_AUTHORIZE_LIVE_OUTPUT", "false");
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        var events = new ConcurrentQueue<string>();
        var captureFactory = new FakeEngineFactory(events, new FakeEngine(events));
        var runner = new PgmCutsSequenceRunner(
            new FakeSafetyProbe(events),
            new FakeHashPins(true),
            captureFactory,
            new FakeDelay(events),
            new FakeAssetLeases(true));
        Assert.True((await runner.ExecuteAsync(Request(scenes.Path))).Completed);
        var plan = Assert.IsType<PgmCutsSequencePlan>(captureFactory.Plan);
        var baseline = new PgmDiagnosticTargetSnapshot(
            true,
            1,
            1,
            true,
            new PgmDiagnosticTargetIdentity(4_242, 123_456));

        await using var engine = new FullPgmCutsSequenceEngineFactory(
            new FakeSafetyProbe(new ConcurrentQueue<string>()))
            .Create(plan, baseline);
        var authorizationField = typeof(TornadoPlayoutEngine).GetField(
            "_liveAuthorization",
            System.Reflection.BindingFlags.Instance |
            System.Reflection.BindingFlags.NonPublic);
        Assert.NotNull(authorizationField);
        var authorization = Assert.IsAssignableFrom<MBN_STOCK_WEBVIEW.Playout.Safety.ILiveAuthorization>(
            authorizationField.GetValue(engine));

        Assert.True(authorization.IsAuthorizedForThisLaunch);
        Assert.True(authorization.GetType().IsNestedPrivate);
    }

    [Fact]
    public void ProcessHashPinRequirementRequiresBothProcessScopedHexPins()
    {
        using var environment = new EnvironmentScope()
            .Set(InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable, null)
            .Set(InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable, null);
        var requirement = new ProcessPgmCutsSequenceHashPinRequirement();

        Assert.False(requirement.IsSatisfied());
        environment
            .Set(
                InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable,
                new string('A', 64))
            .Set(
                InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable,
                new string('B', 64));
        Assert.True(requirement.IsSatisfied());
        environment.Set(
            InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable,
            "not-a-hash");
        Assert.False(requirement.IsSatisfied());
    }

    [Fact]
    public void CutAssetRequirementUsesExactApprovedHashesAndFixedTimeComparison()
    {
        Assert.Equal(
            "99CE3B689A42D8C42BEB09A86FA10C2D7C1AEF4F50D324D81276C1A1E4C4D8A7",
            FixedPgmCutsSequenceAssetLeaseFactory.FirstApprovedSha256);
        Assert.Equal(
            "25CD0AE931F51E4E3B84CE3E6FD21A40DB85464F157A23CC3511D63B336D8757",
            FixedPgmCutsSequenceAssetLeaseFactory.NextApprovedSha256);

        byte[] content = [1, 3, 3, 7, 42];
        var contentHash = Convert.ToHexString(
            System.Security.Cryptography.SHA256.HashData(content));
        using var matching = new MemoryStream(content, writable: false);
        using var rejected = new MemoryStream(content, writable: false);

        Assert.True(FixedPgmCutsSequenceAssetLeaseFactory.HasExpectedSha256(
            matching,
            contentHash));
        Assert.False(FixedPgmCutsSequenceAssetLeaseFactory.HasExpectedSha256(
            rejected,
            FixedPgmCutsSequenceAssetLeaseFactory.FirstApprovedSha256));
    }

    [Fact]
    public void SceneTrustRequiresFixedLocalRootAndFilesUnderThatRoot()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
        using var outside = TemporarySceneDirectory.Create("outside.t2s");

        Assert.True(PgmCutsSequenceRunner.IsTrustedLocalScenePaths(
            scenes.Path,
            Path.Combine(scenes.Path, "5001.t2s"),
            Path.Combine(scenes.Path, "5006.t2s")));
        Assert.False(PgmCutsSequenceRunner.IsTrustedLocalScenePaths(
            scenes.Path,
            Path.Combine(scenes.Path, "5001.t2s"),
            Path.Combine(outside.Path, "outside.t2s")));
        Assert.False(PgmCutsSequenceRunner.IsTrustedLocalScenePaths(
            "\\\\server\\share\\Cuts",
            "\\\\server\\share\\Cuts\\5001.t2s"));
    }

    private static PgmCutsSequenceRunner Runner(
        ConcurrentQueue<string> events,
        FakeEngine engine) => new(
        new FakeSafetyProbe(events),
        new FakeHashPins(true),
        new FakeEngineFactory(events, engine),
        new FakeDelay(events),
        new FakeAssetLeases(true));

    private static PgmCutsSequenceRequest Request(string sceneRoot) => new(
        "127.0.0.1",
        30_001,
        "Tornado2 PGM",
        sceneRoot,
        5_000);

    private static bool IsOutputCommand(string value) =>
        value.StartsWith("Prepare:", StringComparison.Ordinal) ||
        value == "TakeIn" ||
        value.StartsWith("Next:", StringComparison.Ordinal) ||
        value.StartsWith("TakeOut:", StringComparison.Ordinal);

    private static string[] EngineCalls(ConcurrentQueue<string> events) =>
        events.Where(value =>
            value == "Connect" ||
            IsOutputCommand(value) ||
            value is "Disconnect" or "Quarantine" or "Dispose").ToArray();

    private sealed class FakeHashPins : IPgmCutsSequenceHashPinRequirement
    {
        private readonly bool _satisfied;

        public FakeHashPins(bool satisfied)
        {
            _satisfied = satisfied;
        }

        public bool IsSatisfied() => _satisfied;
    }

    private sealed class FakeAssetLeases : IPgmCutsSequenceAssetLeaseFactory
    {
        private readonly bool _satisfied;

        public FakeAssetLeases(bool satisfied)
        {
            _satisfied = satisfied;
        }

        public bool TryAcquire(
            PgmCutsSequencePlan plan,
            out IPgmCutsSequenceAssetLease? lease)
        {
            if (!_satisfied)
            {
                lease = null;
                return false;
            }

            lease = new FakeAssetLease();
            return true;
        }

        private sealed class FakeAssetLease : IPgmCutsSequenceAssetLease
        {
            public void Dispose()
            {
            }
        }
    }

    private sealed class FakeSafetyProbe : IPgmDiagnosticSafetyProbe
    {
        private static readonly PgmDiagnosticTargetSnapshot Baseline = new(
            true,
            1,
            1,
            true,
            new PgmDiagnosticTargetIdentity(4_242, 123_456));
        private static readonly PgmDiagnosticTargetSnapshot Changed = new(
            true,
            1,
            1,
            true,
            new PgmDiagnosticTargetIdentity(4_243, 123_457));

        private readonly ConcurrentQueue<string> _events;
        private readonly int? _changedCapture;
        private int _captureCount;

        public FakeSafetyProbe(
            ConcurrentQueue<string> events,
            int? changedCapture = null)
        {
            _events = events;
            _changedCapture = changedCapture;
        }

        public PgmDiagnosticTargetSnapshot Capture(
            PgmConnectDiagnosticRequest request,
            System.Net.IPAddress parsedHost)
        {
            Assert.Equal("127.0.0.1", parsedHost.ToString());
            Assert.Equal(30_001, request.Port);
            var count = Interlocked.Increment(ref _captureCount);
            _events.Enqueue($"Safety:{count}");
            return _changedCapture is { } changed && count >= changed
                ? Changed
                : Baseline;
        }
    }

    private sealed class FakeDelay : IPgmCutsSequenceDelay
    {
        private readonly ConcurrentQueue<string> _events;
        private int _calls;

        public FakeDelay(ConcurrentQueue<string> events)
        {
            _events = events;
        }

        public int? FailureOnCall { get; init; }

        public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
        {
            var call = Interlocked.Increment(ref _calls);
            _events.Enqueue($"Delay:{delay.TotalMilliseconds:0}");
            if (FailureOnCall == call)
            {
                throw new OperationCanceledException("fake observation interruption");
            }

            return Task.CompletedTask;
        }
    }

    private sealed class FakeEngineFactory : IPgmCutsSequenceEngineFactory
    {
        private readonly ConcurrentQueue<string> _events;
        private readonly FakeEngine _engine;

        public FakeEngineFactory(
            ConcurrentQueue<string> events,
            FakeEngine engine)
        {
            _events = events;
            _engine = engine;
        }

        public int CreateCount { get; private set; }

        public PgmCutsSequencePlan? Plan { get; private set; }

        public IPlayoutEngine Create(
            PgmCutsSequencePlan plan,
            PgmDiagnosticTargetSnapshot baseline)
        {
            CreateCount++;
            Plan = plan;
            Assert.True(baseline.IsEligible);
            _events.Enqueue("CreateEngine");
            return _engine;
        }
    }

    private sealed class FakeEngine : IPlayoutEngine
    {
        private readonly ConcurrentQueue<string> _events;

        public FakeEngine(ConcurrentQueue<string> events)
        {
            _events = events;
        }

        public Dictionary<PgmCutsSequenceStage, PlayoutResultCode> Results { get; } = [];

        public Exception? QuarantineFailure { get; init; }

        public PlayoutStatus Status { get; } = new(
            PlayoutMode.Live,
            PlayoutConnectionState.Disconnected,
            false,
            true,
            true,
            true,
            null,
            null,
            "fake",
            DateTimeOffset.UnixEpoch,
            0);

        public event EventHandler<PlayoutStatusChangedEventArgs>? StatusChanged
        {
            add { }
            remove { }
        }

        public Task<PlayoutResult> ConnectAsync(CancellationToken cancellationToken = default)
        {
            _events.Enqueue("Connect");
            return Result(PgmCutsSequenceStage.Connect, PlayoutOperation.Connect);
        }

        public Task<PlayoutResult> DisconnectAsync(CancellationToken cancellationToken = default)
        {
            _events.Enqueue("Disconnect");
            return Result(PgmCutsSequenceStage.Disconnect, PlayoutOperation.Disconnect);
        }

        public Task<PlayoutResult> PrepareAsync(
            PlayoutCue cue,
            CancellationToken cancellationToken = default)
        {
            _events.Enqueue($"Prepare:{cue.SceneName}");
            return Result(PgmCutsSequenceStage.PrepareFirst, PlayoutOperation.Prepare);
        }

        public Task<PlayoutResult> TakeInAsync(CancellationToken cancellationToken = default)
        {
            _events.Enqueue("TakeIn");
            return Result(PgmCutsSequenceStage.TakeIn, PlayoutOperation.TakeIn);
        }

        public Task<PlayoutResult> NextAsync(
            PlayoutCue cue,
            CancellationToken cancellationToken = default)
        {
            _events.Enqueue($"Next:{cue.SceneName}");
            return Result(PgmCutsSequenceStage.Next, PlayoutOperation.Next);
        }

        public Task<PlayoutResult> UpdateOnAirAsync(
            PlayoutCue cue,
            CancellationToken cancellationToken = default)
        {
            _events.Enqueue($"UpdateOnAir:{cue.SceneName}");
            return Task.FromResult(new PlayoutResult(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Success,
                false,
                "fake",
                DateTimeOffset.UnixEpoch));
        }

        public Task<PlayoutResult> TakeOutAsync(
            PlayoutTakeOutScope scope,
            CancellationToken cancellationToken = default)
        {
            _events.Enqueue($"TakeOut:{scope}");
            return Result(PgmCutsSequenceStage.TakeOut, PlayoutOperation.TakeOut);
        }

        public ValueTask QuarantineAsync()
        {
            _events.Enqueue("Quarantine");
            return QuarantineFailure is null
                ? ValueTask.CompletedTask
                : ValueTask.FromException(QuarantineFailure);
        }

        public ValueTask DisposeAsync()
        {
            _events.Enqueue("Dispose");
            return ValueTask.CompletedTask;
        }

        private Task<PlayoutResult> Result(
            PgmCutsSequenceStage stage,
            PlayoutOperation operation)
        {
            var code = Results.GetValueOrDefault(stage, PlayoutResultCode.Success);
            return Task.FromResult(new PlayoutResult(
                operation,
                code,
                false,
                "fake",
                DateTimeOffset.UnixEpoch));
        }
    }
}
