using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class PlayoutOptionsLoaderTests
{
    private static readonly string[] EnvironmentNames =
    [
        "MBN_STOCK_PLAYOUT_MODE",
        "MBN_STOCK_PLAYOUT_HOST",
        "MBN_STOCK_PLAYOUT_PORT",
        "MBN_STOCK_PLAYOUT_TCP_MODE",
        "MBN_STOCK_PLAYOUT_CLIENT_PORT",
        "MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL",
        "MBN_STOCK_PLAYOUT_LAYOUT_INDEX",
        "MBN_STOCK_PLAYOUT_LEGACY_FADE_DURATION",
        "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND",
        "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET",
        "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT",
        "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE",
        "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
        "MBN_STOCK_PLAYOUT_SCENE_DIRECTORY",
        "MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN",
        "MBN_STOCK_PLAYOUT_QUEUE_CAPACITY",
        "MBN_STOCK_PLAYOUT_CONNECT_TIMEOUT_MS",
        "MBN_STOCK_PLAYOUT_OPERATION_TIMEOUT_MS",
        "MBN_STOCK_PLAYOUT_DISCONNECT_TIMEOUT_MS",
        "MBN_STOCK_PLAYOUT_PROCESS_POLL_INTERVAL_MS",
        "MBN_STOCK_PLAYOUT_RECONNECT_DELAY_MS",
        "MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS",
        "MBN_STOCK_PLAYOUT_RECONNECT_ENABLED",
        "MBN_STOCK_PLAYOUT_MAXIMUM_AUTOMATIC_REFRESHES_PER_TAKE_IN",
        PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
        "MBN_STOCK_PLAYOUT_TEST_SCENE_ALLOWLIST",
        "MBN_STOCK_PLAYOUT_TRUSTED_LIVE_OUTPUT_ENABLED"
    ];

    [Fact]
    public void Load_WhenFileIsMissing_UsesSafeDryRunDefaults()
    {
        using var environment = ClearedEnvironment();
        using var file = TemporaryJsonFile.Missing();

        var options = PlayoutOptionsLoader.Load(file.Path);

        Assert.Equal(PlayoutMode.DryRun, options.Mode);
        Assert.Equal("127.0.0.1", options.Host);
        Assert.Equal(30001, options.Port);
        Assert.Equal(1, options.TcpMode);
        Assert.Equal(0, options.ClientPort);
        Assert.Null(options.OutputChannel);
        Assert.Null(options.SceneDirectory);
        Assert.Equal(6, options.LegacySceneFadeDuration);
        Assert.Equal(LegacySceneBackgroundKind.None, options.LegacySceneBackgroundKind);
        Assert.Null(options.LegacySceneBackgroundAssetPath);
        Assert.Equal(2004, options.LegacySceneBackgroundVideoLoopCount);
        Assert.True(options.LegacySceneBackgroundVideoLoopInfinite);
        Assert.Null(options.LegacyBackgroundDirectory);
        Assert.Empty(options.TestSceneAllowlist);
        Assert.False(options.TrustedLiveOutputEnabled);
        Assert.True(options.ReconnectEnabled);
        Assert.Null(options.MaximumAutomaticRefreshesPerTakeIn);
    }

    [Fact]
    public void Load_SourceOnlyIgnoresLocalProfileOperatorRootsAndEnvironment()
    {
        using var environment = ClearedEnvironment()
            .Set("MBN_STOCK_PLAYOUT_MODE", "Live")
            .Set("MBN_STOCK_PLAYOUT_HOST", "configured-live-host")
            .Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\configured-live-scenes")
            .Set(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
        using var file = TemporaryJsonFile.Create("{ intentionally-invalid-json");

        var options = PlayoutOptionsLoader.Load(
            file.Path,
            operatorSceneDirectory: "C:\\operator-scenes",
            operatorBackgroundDirectory: "C:\\operator-backgrounds",
            forceSafeDryRun: true);

        Assert.Equal(PlayoutMode.DryRun, options.Mode);
        Assert.Equal("127.0.0.1", options.Host);
        Assert.Null(options.SceneDirectory);
        Assert.Null(options.LegacyBackgroundDirectory);
        Assert.False(options.TrustedLiveOutputEnabled);
        Assert.Empty(options.TestSceneAllowlist);
    }

    [Fact]
    public void Load_RejectedDevelopmentLiveRequestCannotInheritLiveEnvironment()
    {
        using var environment = ClearedEnvironment()
            .Set("MBN_STOCK_PLAYOUT_MODE", "Live")
            .Set("MBN_STOCK_PLAYOUT_HOST", "127.0.0.1")
            .Set(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
        using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());

        var options = PlayoutOptionsLoader.Load(
            file.Path,
            operatorSceneDirectory: "C:\\operator-scenes",
            operatorBackgroundDirectory: "C:\\operator-backgrounds",
            forceSafeDryRun: true);
        var validated = ValidatedPlayoutOptions.Create(options);

        Assert.Equal(PlayoutMode.DryRun, validated.Mode);
        Assert.False(validated.TrustedLiveOutputEnabled);
        Assert.Null(validated.SceneDirectory);
        Assert.Empty(validated.TestSceneAllowlist);
    }

    [Fact]
    public void LoadDevelopmentLive_IgnoresEnvironmentAndPinsExecutableCuts()
    {
        using var environment = ClearedEnvironment()
            .Set("MBN_STOCK_PLAYOUT_MODE", "Disabled")
            .Set("MBN_STOCK_PLAYOUT_HOST", "stale-remote-host.invalid")
            .Set("MBN_STOCK_PLAYOUT_PORT", "32001")
            .Set("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", "99")
            .Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\stale-scenes")
            .Set(
                "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
                "C:\\stale-backgrounds")
            .Set("MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", "true")
            .Set("MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS", "9");
        using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
        var executableRoot = Path.Combine(
            Path.GetTempPath(),
            Guid.NewGuid().ToString("N"));
        var cuts = Path.Combine(executableRoot, "Cuts");
        Directory.CreateDirectory(cuts);
        try
        {
            var options = PlayoutOptionsLoader.LoadDevelopmentLive(
                file.Path,
                executableRoot);

            Assert.True(options.DevelopmentLiveBootstrapApplied);
            Assert.Equal(PlayoutMode.Live, options.Mode);
            Assert.Equal("127.0.0.1", options.Host);
            Assert.Equal(31001, options.Port);
            Assert.Equal(7, options.OutputChannel);
            Assert.Equal(Path.GetFullPath(cuts), options.SceneDirectory);
            Assert.Equal(
                Path.GetFullPath(cuts),
                options.DevelopmentLiveExecutableSceneDirectory);
            Assert.Null(options.LegacyBackgroundDirectory);
            Assert.False(options.ReconnectEnabled);
            Assert.Equal(0, options.MaximumReconnectAttempts);

            var validated = ValidatedPlayoutOptions.Create(options);
            Assert.Equal("127.0.0.1", validated.Host);
            Assert.Equal(Path.GetFullPath(cuts), validated.SceneDirectory);
            Assert.False(validated.ReconnectEnabled);
            Assert.Equal(0, validated.MaximumReconnectAttempts);
        }
        finally
        {
            Directory.Delete(executableRoot, recursive: true);
        }
    }

    [Fact]
    public void LoadDevelopmentLive_UsesTheFirstRunCutsPathWithoutCopyingIt()
    {
        using var environment = ClearedEnvironment()
            .Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\stale-scenes");
        using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
        var executableRoot = Path.Combine(
            Path.GetTempPath(),
            Guid.NewGuid().ToString("N"),
            "app");
        var selectedCuts = Path.Combine(
            Path.GetTempPath(),
            Guid.NewGuid().ToString("N"),
            "Cuts");
        Directory.CreateDirectory(executableRoot);
        Directory.CreateDirectory(selectedCuts);
        try
        {
            var options = PlayoutOptionsLoader.LoadDevelopmentLive(
                file.Path,
                executableRoot,
                selectedCuts);

            Assert.Equal(Path.GetFullPath(selectedCuts), options.SceneDirectory);
            Assert.Equal(
                Path.GetFullPath(selectedCuts),
                options.DevelopmentLiveExecutableSceneDirectory);
            Assert.False(Directory.Exists(Path.Combine(executableRoot, "Cuts")));

            var validated = ValidatedPlayoutOptions.Create(options);
            Assert.Equal(Path.GetFullPath(selectedCuts), validated.SceneDirectory);
        }
        finally
        {
            Directory.Delete(
                Path.GetDirectoryName(executableRoot)!,
                recursive: true);
            Directory.Delete(
                Path.GetDirectoryName(selectedCuts)!,
                recursive: true);
        }
    }

    [Fact]
    public void LoadDevelopmentLive_RejectsBaseProfileThatCanReconnect()
    {
        using var environment = ClearedEnvironment();
        using var file = TemporaryJsonFile.Create(
            DevelopmentLiveBaseJson().Replace(
                "\"reconnectEnabled\": false",
                "\"reconnectEnabled\": true",
                StringComparison.Ordinal));

        var exception = Assert.Throws<PlayoutConfigurationException>(
            () => PlayoutOptionsLoader.LoadDevelopmentLive(file.Path, "C:\\runtime"));

        Assert.Contains(
            "local configuration is invalid",
            exception.Message,
            StringComparison.Ordinal);
    }

    [Fact]
    public void Validate_DevelopmentLiveRequiresLiteralLoopbackAtFinalBoundary()
    {
        using var environment = ClearedEnvironment();
        using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
        var executableRoot = Path.Combine(
            Path.GetTempPath(),
            Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(Path.Combine(executableRoot, "Cuts"));
        try
        {
            var options = PlayoutOptionsLoader.LoadDevelopmentLive(
                file.Path,
                executableRoot);
            options.Host = "playout.dev.invalid";

            var exception = Assert.Throws<PlayoutConfigurationException>(
                () => ValidatedPlayoutOptions.Create(options));

            Assert.Contains(
                nameof(PlayoutOptions.Host),
                exception.Message,
                StringComparison.Ordinal);
        }
        finally
        {
            Directory.Delete(executableRoot, recursive: true);
        }
    }

    [Fact]
    public void LoadDevelopmentLive_RejectsWrongModeNonCanonicalJsonAndAllowlistExpansion()
    {
        using var environment = ClearedEnvironment();
        var canonical = DevelopmentLiveBaseJson();
        var closingBrace = canonical.LastIndexOf('}');
        Assert.True(closingBrace > 0);
        string[] invalidConfigurations =
        [
            canonical.Replace(
                "\"mode\": \"Live\"",
                "\"mode\": \"DryRun\"",
                StringComparison.Ordinal),
            canonical.Replace(
                "\"mode\": \"Live\"",
                "\"Mode\": \"Live\"",
                StringComparison.Ordinal),
            canonical.Replace(
                "\"mode\": \"Live\"",
                "\"mode\": \"Live\", \"mode\": \"Live\"",
                StringComparison.Ordinal),
            canonical.Replace(
                "\"port\": 31001",
                "\"port\": \"31001\"",
                StringComparison.Ordinal),
            canonical[..closingBrace] + ",\n}",
            canonical.Replace("{", "{/* comment */", StringComparison.Ordinal),
            canonical[..closingBrace] + ",\n  \"unexpected\": true\n}",
            canonical.Replace(
                "\"N5001\"",
                "\"N5001\", \"9999\"",
                StringComparison.Ordinal)
        ];

        foreach (var json in invalidConfigurations)
        {
            using var file = TemporaryJsonFile.Create(json);

            var exception = Assert.Throws<PlayoutConfigurationException>(
                () => PlayoutOptionsLoader.LoadDevelopmentLive(
                    file.Path,
                    "C:\\runtime"));

            Assert.Equal(
                "Development Live local configuration is invalid.",
                exception.Message);
        }
    }

    [Fact]
    public void Validate_DevelopmentLiveRejectsSceneRootReplacementAfterLoading()
    {
        using var environment = ClearedEnvironment();
        using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
        var executableRoot = Path.Combine(
            Path.GetTempPath(),
            Guid.NewGuid().ToString("N"));
        var replacementRoot = Path.Combine(
            Path.GetTempPath(),
            Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(Path.Combine(executableRoot, "Cuts"));
        Directory.CreateDirectory(replacementRoot);
        try
        {
            var options = PlayoutOptionsLoader.LoadDevelopmentLive(
                file.Path,
                executableRoot);
            options.SceneDirectory = replacementRoot;

            var exception = Assert.Throws<PlayoutConfigurationException>(
                () => ValidatedPlayoutOptions.Create(options));

            Assert.Contains(
                "protected startup contract",
                exception.Message,
                StringComparison.Ordinal);
        }
        finally
        {
            Directory.Delete(executableRoot, recursive: true);
            Directory.Delete(replacementRoot, recursive: true);
        }
    }

    [Fact]
    public void ApplyExecutableAssetDefaults_UsesCutsBesideExecutable()
    {
        var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
        var cuts = Path.Combine(directory, "Cuts");
        Directory.CreateDirectory(cuts);
        try
        {
            var options = new PlayoutOptions();

            PlayoutOptionsLoader.ApplyExecutableAssetDefaults(options, directory);

            Assert.Equal(Path.GetFullPath(cuts), options.SceneDirectory);
        }
        finally
        {
            Directory.Delete(directory, recursive: true);
        }
    }

    [Fact]
    public void ApplyExecutableAssetDefaults_DoesNotReplaceExplicitSceneDirectory()
    {
        var options = new PlayoutOptions
        {
            SceneDirectory = "C:\\explicit-cuts"
        };

        PlayoutOptionsLoader.ApplyExecutableAssetDefaults(options, "C:\\runtime");

        Assert.Equal("C:\\explicit-cuts", options.SceneDirectory);
    }

    [Fact]
    public void ApplyOperatorAssetOverrides_ChangesOnlyNativeAssetRoots()
    {
        var options = new PlayoutOptions
        {
            Mode = PlayoutMode.Live,
            Host = "protected-host",
            SceneDirectory = "C:\\json-cuts",
            LegacyBackgroundDirectory = "C:\\json-backgrounds",
            TrustedLiveOutputEnabled = true,
            TestSceneAllowlist = ["5001"]
        };

        PlayoutOptionsLoader.ApplyOperatorAssetOverrides(
            options,
            " C:\\operator-cuts ",
            " C:\\operator-backgrounds ");

        Assert.Equal("C:\\operator-cuts", options.SceneDirectory);
        Assert.Equal("C:\\operator-backgrounds", options.LegacyBackgroundDirectory);
        Assert.Equal(PlayoutMode.Live, options.Mode);
        Assert.Equal("protected-host", options.Host);
        Assert.True(options.TrustedLiveOutputEnabled);
        Assert.Equal(["5001"], options.TestSceneAllowlist);
    }

    [Fact]
    public void Load_EnvironmentAssetRootsOverrideNativeOperatorSelection()
    {
        using var environment = ClearedEnvironment()
            .Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\environment-cuts")
            .Set(
                "MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
                "C:\\environment-backgrounds");
        using var file = TemporaryJsonFile.Missing();

        var options = PlayoutOptionsLoader.Load(
            file.Path,
            "C:\\operator-cuts",
            "C:\\operator-backgrounds");

        Assert.Equal("C:\\environment-cuts", options.SceneDirectory);
        Assert.Equal("C:\\environment-backgrounds", options.LegacyBackgroundDirectory);
        Assert.Equal(PlayoutMode.DryRun, options.Mode);
        Assert.False(options.TrustedLiveOutputEnabled);
    }

    [Fact]
    public void Load_EnvironmentOverridesJson_ExceptFileOnlySafetyGates()
    {
        using var environment = ClearedEnvironment()
            .Set("MBN_STOCK_PLAYOUT_MODE", "DryRun")
            .Set("MBN_STOCK_PLAYOUT_HOST", "env-host")
            .Set("MBN_STOCK_PLAYOUT_PORT", "32001")
            .Set("MBN_STOCK_PLAYOUT_TCP_MODE", "2")
            .Set("MBN_STOCK_PLAYOUT_CLIENT_PORT", "32002")
            .Set("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", "12")
            .Set("MBN_STOCK_PLAYOUT_LAYOUT_INDEX", "13")
            .Set("MBN_STOCK_PLAYOUT_LEGACY_FADE_DURATION", "9")
            .Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND", "Video")
            .Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET", "Video\\studio.vrv")
            .Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT", "2004")
            .Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE", "false")
            .Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY", "C:\\env-backgrounds")
            .Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\env-scenes")
            .Set("MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN", "ENV TEST")
            .Set("MBN_STOCK_PLAYOUT_QUEUE_CAPACITY", "14")
            .Set("MBN_STOCK_PLAYOUT_CONNECT_TIMEOUT_MS", "1501")
            .Set("MBN_STOCK_PLAYOUT_OPERATION_TIMEOUT_MS", "1502")
            .Set("MBN_STOCK_PLAYOUT_DISCONNECT_TIMEOUT_MS", "1503")
            .Set("MBN_STOCK_PLAYOUT_PROCESS_POLL_INTERVAL_MS", "1504")
            .Set("MBN_STOCK_PLAYOUT_RECONNECT_DELAY_MS", "1505")
            .Set("MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS", "4")
            .Set("MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", "false")
            .Set("MBN_STOCK_PLAYOUT_MAXIMUM_AUTOMATIC_REFRESHES_PER_TAKE_IN", "999")
            .Set("MBN_STOCK_PLAYOUT_TEST_SCENE_ALLOWLIST", "C:\\env\\must-not-apply.t2s")
            .Set("MBN_STOCK_PLAYOUT_TRUSTED_LIVE_OUTPUT_ENABLED", "false");
        using var file = TemporaryJsonFile.Create(
            """
            {
              "mode": "Test",
              "host": "json-host",
              "port": 31001,
              "tcpMode": 1,
              "clientPort": 31002,
              "outputChannel": 3,
              "layoutIndex": 10,
              "sceneDirectory": "C:\\json-scenes",
              "testProcessWindowTitlePattern": "JSON TEST",
              "testSceneAllowlist": ["C:\\test-scenes\\allowed.t2s"],
              "trustedLiveOutputEnabled": true,
              "queueCapacity": 64,
              "connectTimeoutMilliseconds": 5000,
              "operationTimeoutMilliseconds": 5000,
              "disconnectTimeoutMilliseconds": 3000,
              "processPollIntervalMilliseconds": 1000,
              "reconnectDelayMilliseconds": 1000,
              "maximumReconnectAttempts": 3,
              "reconnectEnabled": true,
              "maximumAutomaticRefreshesPerTakeIn": 1
            }
            """);

        var options = PlayoutOptionsLoader.Load(file.Path);

        Assert.Equal(PlayoutMode.DryRun, options.Mode);
        Assert.Equal("env-host", options.Host);
        Assert.Equal(32001, options.Port);
        Assert.Equal(2, options.TcpMode);
        Assert.Equal(32002, options.ClientPort);
        Assert.Equal(12, options.OutputChannel);
        Assert.Equal(13, options.LayoutIndex);
        Assert.Equal(9, options.LegacySceneFadeDuration);
        Assert.Equal(LegacySceneBackgroundKind.Video, options.LegacySceneBackgroundKind);
        Assert.Equal("Video\\studio.vrv", options.LegacySceneBackgroundAssetPath);
        Assert.Equal(2004, options.LegacySceneBackgroundVideoLoopCount);
        Assert.False(options.LegacySceneBackgroundVideoLoopInfinite);
        Assert.Equal("C:\\env-backgrounds", options.LegacyBackgroundDirectory);
        Assert.Equal("C:\\env-scenes", options.SceneDirectory);
        Assert.Equal("ENV TEST", options.TestProcessWindowTitlePattern);
        Assert.Equal(14, options.QueueCapacity);
        Assert.Equal(1501, options.ConnectTimeoutMilliseconds);
        Assert.Equal(1502, options.OperationTimeoutMilliseconds);
        Assert.Equal(1503, options.DisconnectTimeoutMilliseconds);
        Assert.Equal(1504, options.ProcessPollIntervalMilliseconds);
        Assert.Equal(1505, options.ReconnectDelayMilliseconds);
        Assert.Equal(4, options.MaximumReconnectAttempts);
        Assert.False(options.ReconnectEnabled);
        Assert.Equal(1, options.MaximumAutomaticRefreshesPerTakeIn);
        Assert.Equal(["C:\\test-scenes\\allowed.t2s"], options.TestSceneAllowlist);
        Assert.True(options.TrustedLiveOutputEnabled);
    }

    [Fact]
    public void LoadFileOnly_IgnoresEnvironmentForReproducibleExplicitDiagnostics()
    {
        using var environment = ClearedEnvironment()
            .Set("MBN_STOCK_PLAYOUT_MODE", "Live")
            .Set("MBN_STOCK_PLAYOUT_HOST", "sensitive-host");
        using var file = TemporaryJsonFile.Create(
            """
            {
              "mode": "Test",
              "host": "127.0.0.1"
            }
            """);

        var options = PlayoutOptionsLoader.LoadFileOnly(file.Path);

        Assert.Equal(PlayoutMode.Test, options.Mode);
        Assert.Equal("127.0.0.1", options.Host);
    }

    [Fact]
    public void Load_InvalidJson_ReturnsOnlySafeConfigurationMessage()
    {
        using var environment = ClearedEnvironment();
        using var file = TemporaryJsonFile.Create("{ invalid-json: true }");

        var exception = Assert.Throws<PlayoutConfigurationException>(
            () => PlayoutOptionsLoader.Load(file.Path));

        Assert.Equal("송출 설정 파일 형식이 올바르지 않습니다.", exception.Message);
        Assert.DoesNotContain(file.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("invalid-json", exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Load_StringAutomaticRefreshMaximumFailsClosed()
    {
        using var environment = ClearedEnvironment();
        using var file = TemporaryJsonFile.Create(
            """
            {
              "maximumAutomaticRefreshesPerTakeIn": "1"
            }
            """);

        var exception = Assert.Throws<PlayoutConfigurationException>(
            () => PlayoutOptionsLoader.Load(file.Path));

        Assert.DoesNotContain(file.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("\"1\"", exception.Message, StringComparison.Ordinal);
    }

    [Theory]
    [InlineData("MBN_STOCK_PLAYOUT_MODE", "not-a-mode")]
    [InlineData("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND", "not-a-background")]
    [InlineData("MBN_STOCK_PLAYOUT_PORT", "not-a-number")]
    [InlineData("MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", "not-a-bool")]
    public void Load_InvalidEnvironmentValue_IdentifiesVariableWithoutLeakingValue(
        string name,
        string invalidValue)
    {
        using var environment = ClearedEnvironment().Set(name, invalidValue);
        using var file = TemporaryJsonFile.Missing();

        var exception = Assert.Throws<PlayoutConfigurationException>(
            () => PlayoutOptionsLoader.Load(file.Path));

        Assert.Contains(name, exception.Message, StringComparison.Ordinal);
        Assert.DoesNotContain(invalidValue, exception.Message, StringComparison.Ordinal);
    }

    private static EnvironmentScope ClearedEnvironment()
    {
        var result = new EnvironmentScope();
        foreach (var name in EnvironmentNames)
        {
            result.Set(name, null);
        }

        return result;
    }

    private static string DevelopmentLiveBaseJson() =>
        """
        {
          "mode": "Live",
          "host": "127.0.0.1",
          "port": 31001,
          "tcpMode": 1,
          "clientPort": 0,
          "sceneDirectory": null,
          "outputChannel": 7,
          "layoutIndex": 10,
          "legacySceneFadeDuration": 6,
          "legacySceneBackgroundKind": "None",
          "legacySceneBackgroundAssetPath": null,
          "legacySceneBackgroundVideoLoopCount": 2004,
          "legacySceneBackgroundVideoLoopInfinite": true,
          "legacyBackgroundDirectory": null,
          "testProcessWindowTitlePattern": null,
          "testSceneAllowlist": [
            "5001", "5006", "5011", "5016", "50160", "5023", "5024", "5025",
            "5026", "5029", "5032", "5037", "5068", "5070", "5072", "5074",
            "5076", "5077", "5078", "5079", "5080", "5081", "5082", "5083",
            "5084", "5085", "5086", "50860", "5087", "5088", "6001", "6067",
            "8001", "8002", "8003", "8018", "8032", "8035", "8040", "8046",
            "8051", "8056", "8061", "8067", "N5001"
          ],
          "trustedLiveOutputEnabled": true,
          "queueCapacity": 64,
          "connectTimeoutMilliseconds": 5000,
          "operationTimeoutMilliseconds": 5000,
          "disconnectTimeoutMilliseconds": 3000,
          "processPollIntervalMilliseconds": 1000,
          "reconnectDelayMilliseconds": 1000,
          "maximumReconnectAttempts": 0,
          "reconnectEnabled": false,
          "maximumAutomaticRefreshesPerTakeIn": 0
        }
        """;
}
