using System.Security.Cryptography;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Playout.Safety;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class PlayoutLaunchAuthorizationTests
{
    private const string Capability =
        "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";

    [Fact]
    public void Missing_gate_environment_preserves_the_standard_launch_profile()
    {
        using var environment = ClearGateEnvironment();

        var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment();

        Assert.False(authorization.IsGateA);
        Assert.True(authorization.AllowsDatabaseWrites);
        Assert.True(authorization.AllowsCompositionMutation);
        Assert.False(authorization.TryClaimGateAPrepare(Capability));
    }

    [Fact]
    public void Non_gate_capture_preserves_existing_live_authorization_semantics()
    {
        using var environment = ClearGateEnvironment()
            .Set(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);

        using var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment();

        Assert.False(authorization.IsGateA);
        Assert.True(authorization.IsAuthorizedForThisLaunch);
        Assert.Equal(
            PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
            Environment.GetEnvironmentVariable(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
    }

    [Fact]
    public void Gate_capture_consumes_static_live_bearer_and_second_capture_fails_closed()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var scenePath = Path.Combine(scenes.Path, "5001.t2s");
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
        using var environment = GateEnvironment(
                Capability,
                "41308",
                "638878000000000000",
                expiry.ToString(System.Globalization.CultureInfo.InvariantCulture))
            .Set(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);

        using var first = PlayoutLaunchAuthorization.CaptureFromEnvironment(
            clock,
            scenePath,
            SceneHash(scenePath));
        PlayoutLaunchAuthorization? second = null;
        Assert.Throws<PlayoutConfigurationException>(() =>
            second = PlayoutLaunchAuthorization.CaptureFromEnvironment());

        Assert.True(first.IsGateA);
        Assert.True(first.IsAuthorizedForThisLaunch);
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
        Assert.Null(second);
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
    }

    [Fact]
    public void Gate_environment_is_captured_once_and_cleared_before_browser_startup()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var scenePath = Path.Combine(scenes.Path, "5001.t2s");
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
        using var environment = GateEnvironment(
            Capability,
            "41308",
            "638878000000000000",
            expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));

        using var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment(
            clock,
            scenePath,
            SceneHash(scenePath));

        Assert.True(authorization.IsGateA);
        Assert.Equal(41308, authorization.ExpectedPgmProcessId);
        Assert.Equal(638878000000000000, authorization.ExpectedPgmStartTimeUtcTicks);
        Assert.Equal(expiry, authorization.ExpiresAtUtcTicks);
        Assert.False(authorization.AllowsDatabaseWrites);
        Assert.False(authorization.AllowsCompositionMutation);
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));

        environment.Set(
            PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
            new string('F', 64));

        Assert.True(authorization.TryClaimGateAPrepare(Capability));
        Assert.False(authorization.TryClaimGateAPrepare(new string('F', 64)));
    }

    [Theory]
    [InlineData("lowercase0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab")]
    [InlineData("ABCDEF")]
    [InlineData(null)]
    public void Invalid_or_partial_gate_environment_fails_closed_and_is_cleared(
        string? capability)
    {
        using var environment = ClearGateEnvironment()
            .Set(
                PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
                capability)
            .Set(
                PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
                "41308");

        Assert.Throws<PlayoutConfigurationException>(
            PlayoutLaunchAuthorization.CaptureFromEnvironment);
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
    }

    [Fact]
    public void Malformed_first_capture_permanently_blocks_reinjection_and_clears_every_bearer()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var scenePath = Path.Combine(scenes.Path, "5001.t2s");
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
        using var environment = ClearGateEnvironment()
            .Set(
                PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
                "ABCDEF")
            .Set(
                PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
                "41308")
            .Set(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);

        Assert.Throws<PlayoutConfigurationException>(() =>
            PlayoutLaunchAuthorization.CaptureFromEnvironment(
                clock,
                scenePath,
                SceneHash(scenePath)));

        environment
            .Set(
                PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
                Capability)
            .Set(
                PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
                "41308")
            .Set(
                PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable,
                "638878000000000000")
            .Set(
                PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable,
                expiry.ToString(System.Globalization.CultureInfo.InvariantCulture))
            .Set(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);

        PlayoutLaunchAuthorization? second = null;
        Assert.Throws<PlayoutConfigurationException>(() =>
            second = PlayoutLaunchAuthorization.CaptureFromEnvironment(
                clock,
                scenePath,
                SceneHash(scenePath)));

        Assert.Null(second);
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
    }

    [Fact]
    public async Task Concurrent_gate_captures_have_one_success_and_all_later_calls_fail_closed()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var scenePath = Path.Combine(scenes.Path, "5001.t2s");
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
        using var environment = GateEnvironment(
                Capability,
                "41308",
                "638878000000000000",
                expiry.ToString(System.Globalization.CultureInfo.InvariantCulture))
            .Set(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
        using var start = new ManualResetEventSlim();
        var tasks = Enumerable.Range(0, 8)
            .Select(_ => Task.Run(() =>
            {
                start.Wait();
                try
                {
                    return (
                        Authorization: PlayoutLaunchAuthorization.CaptureFromEnvironment(
                            clock,
                            scenePath,
                            SceneHash(scenePath)),
                        Error: (Exception?)null);
                }
                catch (Exception exception)
                {
                    return (Authorization: (PlayoutLaunchAuthorization?)null, Error: exception);
                }
            }))
            .ToArray();

        start.Set();
        var results = await Task.WhenAll(tasks);

        var successful = Assert.Single(results, static result => result.Authorization is not null);
        Assert.True(successful.Authorization!.IsGateA);
        foreach (var failed in results.Where(static result => result.Authorization is null))
        {
            Assert.IsType<PlayoutConfigurationException>(failed.Error);
        }

        successful.Authorization.Dispose();
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
    }

    [Theory]
    [InlineData(0L)]
    [InlineData(-1L)]
    [InlineData(9000000001L)]
    public void Expired_or_overlong_gate_environment_fails_closed_and_clears_every_value(
        long expiryOffsetTicks)
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var scenePath = Path.Combine(scenes.Path, "5001.t2s");
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + expiryOffsetTicks;
        using var environment = GateEnvironment(
            Capability,
            "41308",
            "638878000000000000",
            expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));

        Assert.Throws<PlayoutConfigurationException>(() =>
            PlayoutLaunchAuthorization.CaptureFromEnvironment(
                clock,
                scenePath,
                SceneHash(scenePath)));

        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
        Assert.Null(Environment.GetEnvironmentVariable(
            PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
    }

    [Fact]
    public void Gate_scene_wrong_hash_fails_closed_and_does_not_leave_a_lease()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var scenePath = Path.Combine(scenes.Path, "5001.t2s");
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
        using var environment = GateEnvironment(
            Capability,
            "41308",
            "638878000000000000",
            expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));

        Assert.Throws<PlayoutConfigurationException>(() =>
            PlayoutLaunchAuthorization.CaptureFromEnvironment(
                clock,
                scenePath,
                new string('A', 64)));

        using var write = File.Open(
            scenePath,
            FileMode.Open,
            FileAccess.Write,
            FileShare.ReadWrite | FileShare.Delete);
        Assert.True(write.CanWrite);
    }

    [Fact]
    public void Gate_scene_native_read_lease_blocks_write_and_delete_until_native_shutdown()
    {
        using var scenes = TemporarySceneDirectory.Create("5001.t2s");
        var scenePath = Path.Combine(scenes.Path, "5001.t2s");
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
        using var environment = GateEnvironment(
            Capability,
            "41308",
            "638878000000000000",
            expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));
        var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment(
            clock,
            scenePath,
            SceneHash(scenePath));

        using (var secondReader = File.Open(
                   scenePath,
                   FileMode.Open,
                   FileAccess.Read,
                   FileShare.Read))
        {
            Assert.True(secondReader.CanRead);
        }

        Assert.ThrowsAny<IOException>(() => File.Open(
            scenePath,
            FileMode.Open,
            FileAccess.Write,
            FileShare.ReadWrite | FileShare.Delete));
        Assert.ThrowsAny<IOException>(() => File.Delete(scenePath));

        authorization.CompleteGateAPrepare();
        Assert.ThrowsAny<IOException>(() => File.Delete(scenePath));

        authorization.Dispose();
        File.Delete(scenePath);
        Assert.False(File.Exists(scenePath));
    }

    [Fact]
    public void Native_expiry_at_operator_claim_is_terminal_and_cannot_be_rearmed()
    {
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
        using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            Capability,
            timeProvider: clock,
            expiresAtUtcTicks: expiry);

        clock.Advance(TimeSpan.FromMinutes(1));

        Assert.False(authorization.TryClaimGateAPrepare(Capability));
        clock.SetUtcNow(clock.GetUtcNow().AddHours(-1));
        Assert.False(authorization.TryClaimGateAPrepare(Capability));
        Assert.False(authorization.TryClaimEngineCommand(
            PlayoutOperation.Connect,
            cue: null));
    }

    [Fact]
    public void Expiry_between_operator_claim_and_engine_prepare_blocks_vendor_boundary()
    {
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
        using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            Capability,
            timeProvider: clock,
            expiresAtUtcTicks: expiry);

        clock.Advance(TimeSpan.FromMinutes(1) - TimeSpan.FromTicks(1));
        Assert.True(authorization.TryClaimGateAPrepare(Capability));
        clock.Advance(TimeSpan.FromTicks(1));

        Assert.False(authorization.TryClaimEngineCommand(
            PlayoutOperation.Prepare,
            GateACue()));
        Assert.False(authorization.TryClaimEngineCommand(
            PlayoutOperation.Prepare,
            GateACue()));
    }

    [Fact]
    public void Connect_just_before_expiry_does_not_extend_later_prepare_authority()
    {
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
        using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            Capability,
            timeProvider: clock,
            expiresAtUtcTicks: expiry);

        clock.Advance(TimeSpan.FromMinutes(1) - TimeSpan.FromTicks(1));
        Assert.True(authorization.TryClaimEngineCommand(
            PlayoutOperation.Connect,
            cue: null));
        clock.Advance(TimeSpan.FromTicks(1));

        Assert.False(authorization.TryClaimGateAPrepare(Capability));
        Assert.False(authorization.TryClaimEngineCommand(
            PlayoutOperation.Prepare,
            GateACue()));
    }

    [Fact]
    public void Connect_claim_uses_monotonic_deadline_so_utc_rollback_cannot_extend_gate()
    {
        var clock = new ManualGateTimeProvider();
        var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
        using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
            Capability,
            timeProvider: clock,
            expiresAtUtcTicks: expiry);

        clock.SetUtcNow(clock.GetUtcNow().AddHours(-1));
        clock.AdvanceMonotonic(TimeSpan.FromMinutes(1));

        Assert.False(authorization.TryClaimEngineCommand(
            PlayoutOperation.Connect,
            cue: null));
        Assert.False(authorization.TryClaimGateAPrepare(Capability));
    }

    [Fact]
    public void Capability_and_engine_prepare_are_each_one_shot_and_never_rearm()
    {
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
        var cue = GateACue();

        Assert.False(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));
        Assert.False(authorization.TryClaimGateAPrepare(new string('F', 64)));
        Assert.True(authorization.TryClaimGateAPrepare(Capability));
        Assert.False(authorization.TryClaimGateAPrepare(Capability));
        Assert.True(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));
        Assert.False(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));

        authorization.CompleteGateAPrepare();

        Assert.False(authorization.TryClaimGateAPrepare(Capability));
        Assert.False(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));
    }

    [Fact]
    public void Wrong_prepare_target_consumes_the_engine_phase_and_blocks_correction()
    {
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
        Assert.True(authorization.TryClaimGateAPrepare(Capability));

        Assert.False(authorization.TryClaimEngineCommand(
            PlayoutOperation.Prepare,
            GateACue() with { SceneName = "5006", SceneFile = "5006.t2s" }));
        Assert.False(authorization.TryClaimEngineCommand(
            PlayoutOperation.Prepare,
            GateACue()));
    }

    [Fact]
    public void GateA_rejects_operator_fade_and_background_changes_before_engine_dispatch()
    {
        var blockedCues = new[]
        {
            GateACue() with { FadeDuration = 5 },
            GateACue() with { Mutations = [] },
            GateACue() with
            {
                Mutations = [new PlayoutUseBackground(true)]
            },
            GateACue() with
            {
                Mutations =
                [
                    new PlayoutUseBackground(false),
                    new PlayoutUseBackground(false)
                ]
            },
            GateACue() with
            {
                Mutations = [new PlayoutSetBackgroundTexture("background.png")]
            },
            GateACue() with
            {
                Mutations = [new PlayoutSetBackgroundVideo("background.mp4", 0, 1)]
            }
        };

        foreach (var cue in blockedCues)
        {
            var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
            Assert.True(authorization.TryClaimGateAPrepare(Capability));
            Assert.False(authorization.TryClaimEngineCommand(
                PlayoutOperation.Prepare,
                cue));
            Assert.False(authorization.TryClaimEngineCommand(
                PlayoutOperation.Prepare,
                GateACue()));
        }
    }

    [Fact]
    public async Task Real_5001_provider_cue_with_exact_disabled_background_claims_native_prepare()
    {
        var provider = new RegisteredLegacySceneCueProvider(
            new SinglePageDataSource(new LegacySceneDataPage(
                "s5001",
                new S5001DomesticStockSceneData(
                    LegacyDomesticEquityMarket.Kospi,
                    S5001DomesticStockMode.Current,
                    S5001NxtBadge.None,
                    "Samsung Electronics",
                    ScenePriceDirection.Up,
                    70_000,
                    500,
                    0.72m),
                1)));
        var page = await provider.CreatePageAsync(
            new LegacyPlaylistEntry("gate-a-5001", "5001", FadeDuration: 6),
            pageIndexZeroBased: 0);
        using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);

        Assert.Equal(new PlayoutUseBackground(false), page.Cue.Mutations![0]);
        Assert.True(authorization.TryClaimGateAPrepare(Capability));
        Assert.True(authorization.TryClaimEngineCommand(
            PlayoutOperation.Prepare,
            page.Cue));
    }

    [Fact]
    public async Task Concurrent_operator_claims_have_exactly_one_winner()
    {
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
        using var start = new ManualResetEventSlim();
        var tasks = Enumerable.Range(0, 16)
            .Select(_ => Task.Run(() =>
            {
                start.Wait();
                return authorization.TryClaimGateAPrepare(Capability);
            }))
            .ToArray();

        start.Set();
        var results = await Task.WhenAll(tasks);

        Assert.Single(results, static result => result);
    }

    [Fact]
    public void GateA_options_require_the_exact_closed_profile()
    {
        var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
        var valid = GateAOptions();

        PlayoutEngineFactory.ValidateGateAOptions(valid, authorization);

        foreach (var invalid in new[]
                 {
                     GateAOptions().WithMode(PlayoutMode.DryRun),
                     GateAOptions().WithAllowlist("5001", "5006"),
                     GateAOptions().WithReconnect(true, 0),
                     GateAOptions().WithReconnect(false, 1),
                     GateAOptions().WithRefresh(null),
                     GateAOptions().WithFade(5),
                     GateAOptions().WithBackground(LegacySceneBackgroundKind.Texture),
                     GateAOptions().WithSceneDirectory(Path.GetTempPath()),
                     GateAOptions().WithClientPort(1),
                     GateAOptions().WithOutputChannel(0)
                 })
        {
            Assert.Throws<PlayoutConfigurationException>(() =>
                PlayoutEngineFactory.ValidateGateAOptions(invalid, authorization));
        }
    }

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

    private static PlayoutOptions GateAOptions() => new()
    {
        Mode = PlayoutMode.Live,
        Host = "127.0.0.1",
        Port = 30001,
        TcpMode = 1,
        LayoutIndex = 10,
        TestSceneAllowlist = ["5001"],
        TrustedLiveOutputEnabled = true,
        ReconnectEnabled = false,
        MaximumReconnectAttempts = 0,
        MaximumAutomaticRefreshesPerTakeIn = 0,
        LegacySceneFadeDuration = 6,
        LegacySceneBackgroundKind = LegacySceneBackgroundKind.None,
        SceneDirectory = PlayoutLaunchAuthorization.GateAApprovedSceneDirectory
    };

    private static string SceneHash(string path) =>
        Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));

    private sealed class SinglePageDataSource(LegacySceneDataPage page)
        : ILegacySceneDataSource
    {
        public Task<LegacySceneDataPage> LoadPageAsync(
            LegacyPlaylistEntry entry,
            int pageIndexZeroBased,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(page);
    }

    private static EnvironmentScope ClearGateEnvironment()
    {
        PlayoutLaunchAuthorization.ResetGateACaptureLatchForTests();
        return new EnvironmentScope()
            .Set(PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable, null)
            .Set(PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable, null)
            .Set(PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable, null)
            .Set(PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable, null)
            .Set(PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable, null);
    }

    private static EnvironmentScope GateEnvironment(
        string capability,
        string processId,
        string startTimeUtcTicks,
        string expiresAtUtcTicks) =>
        ClearGateEnvironment()
            .Set(PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable, capability)
            .Set(PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable, processId)
            .Set(PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable, startTimeUtcTicks)
            .Set(PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable, expiresAtUtcTicks);
}

internal sealed class ManualGateTimeProvider : TimeProvider
{
    private DateTimeOffset _utcNow = new(2026, 7, 18, 0, 0, 0, TimeSpan.Zero);
    private long _timestamp;

    public override long TimestampFrequency => TimeSpan.TicksPerSecond;

    public override DateTimeOffset GetUtcNow() => _utcNow;

    public override long GetTimestamp() => _timestamp;

    public void Advance(TimeSpan duration)
    {
        _utcNow = _utcNow.Add(duration);
        _timestamp += duration.Ticks;
    }

    public void AdvanceMonotonic(TimeSpan duration) =>
        _timestamp += duration.Ticks;

    public void SetUtcNow(DateTimeOffset value) => _utcNow = value;
}

internal static class GateAOptionTestExtensions
{
    public static PlayoutOptions WithMode(this PlayoutOptions options, PlayoutMode mode)
    {
        options.Mode = mode;
        return options;
    }

    public static PlayoutOptions WithAllowlist(
        this PlayoutOptions options,
        params string[] scenes)
    {
        options.TestSceneAllowlist = [.. scenes];
        return options;
    }

    public static PlayoutOptions WithReconnect(
        this PlayoutOptions options,
        bool enabled,
        int attempts)
    {
        options.ReconnectEnabled = enabled;
        options.MaximumReconnectAttempts = attempts;
        return options;
    }

    public static PlayoutOptions WithRefresh(this PlayoutOptions options, int? maximum)
    {
        options.MaximumAutomaticRefreshesPerTakeIn = maximum;
        return options;
    }

    public static PlayoutOptions WithFade(this PlayoutOptions options, int fade)
    {
        options.LegacySceneFadeDuration = fade;
        return options;
    }

    public static PlayoutOptions WithBackground(
        this PlayoutOptions options,
        LegacySceneBackgroundKind kind)
    {
        options.LegacySceneBackgroundKind = kind;
        return options;
    }

    public static PlayoutOptions WithSceneDirectory(
        this PlayoutOptions options,
        string sceneDirectory)
    {
        options.SceneDirectory = sceneDirectory;
        return options;
    }

    public static PlayoutOptions WithClientPort(
        this PlayoutOptions options,
        int clientPort)
    {
        options.ClientPort = clientPort;
        return options;
    }

    public static PlayoutOptions WithOutputChannel(
        this PlayoutOptions options,
        int outputChannel)
    {
        options.OutputChannel = outputChannel;
        return options;
    }
}
