using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Runtime;
using System.Text.RegularExpressions;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class PlayoutSafetyValidationTests : IDisposable
{
    private readonly TemporarySceneDirectory _scenes =
        TemporarySceneDirectory.Create("test-scene.t2s", "Video\\background.vrv");

    [Theory]
    [InlineData("Tornado2", true)]
    [InlineData("tornado2-test", true)]
    [InlineData("TORNADO2Preview", true)]
    [InlineData("Tornado20", true)]
    [InlineData("Tornado", false)]
    [InlineData("PreviewTornado2", false)]
    [InlineData(null, false)]
    public void IsTornadoProcessName_UsesCaseInsensitiveTornado2Prefix(
        string? processName,
        bool expected)
    {
        Assert.Equal(expected, TornadoProcessProbe.IsTornadoProcessName(processName));
    }

    [Theory]
    [InlineData("Tornado2 TEST", false)]
    [InlineData("Tornado2", true)]
    [InlineData("Tornado2 PGM", true)]
    [InlineData("PROGRAM OUTPUT", true)]
    [InlineData("preview pgm backup", true)]
    [InlineData("SAFE", false)]
    [InlineData("", true)]
    [InlineData(null, true)]
    public void IsProgramTitle_RejectsPgmAndProgramWindows(string? title, bool expected)
    {
        Assert.Equal(expected, TornadoProcessProbe.IsProgramTitle(title));
    }

    [Theory]
    [InlineData("Tornado2 TEST", true)]
    [InlineData("TEST-1", true)]
    [InlineData("preview_test_output", true)]
    [InlineData("Tornado2", false)]
    [InlineData("CONTEST", false)]
    [InlineData("TEST1", false)]
    [InlineData("", false)]
    [InlineData(null, false)]
    public void HasExplicitTestMarker_RequiresStandaloneTestToken(
        string? title,
        bool expected)
    {
        Assert.Equal(expected, TornadoProcessProbe.HasExplicitTestMarker(title));
    }

    [Fact]
    public void ClassifyWindowTitles_EditorAndPgmRemainTheSameProgramGeneration_WhenFocusOrderChanges()
    {
        string[] editorFirst = ["Tornado2 - [5001.t2s]", "PGM"];
        string[] pgmFirst = ["PGM", "Tornado2 - [5001.t2s]"];

        var first = TornadoProcessProbe.ClassifyWindowTitles(editorFirst, testWindowPattern: null);
        var second = TornadoProcessProbe.ClassifyWindowTitles(pgmFirst, testWindowPattern: null);

        Assert.True(first.IsProgram);
        Assert.True(first.IsEligible);
        Assert.Equal(first, second);

        var identity = new TornadoProcessIdentity(42, 123456789);
        var firstGeneration = TornadoProcessGeneration.FromIdentities(
            "program",
            first.IsProgram ? [identity] : []);
        var secondGeneration = TornadoProcessGeneration.FromIdentities(
            "program",
            second.IsProgram ? [identity] : []);

        Assert.Equal(firstGeneration, secondGeneration);
    }

    [Fact]
    public void ClassifyWindowTitles_TestModeAcceptsExplicitTestTitleFromAnyTopLevelWindow()
    {
        var pattern = new Regex(
            "^Tornado2 TEST$",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100));

        var classification = TornadoProcessProbe.ClassifyWindowTitles(
            ["Tornado2 - [test-editor]", "Tornado2 TEST"],
            pattern);

        Assert.False(classification.IsProgram);
        Assert.True(classification.IsEligible);
    }

    [Fact]
    public void ClassifyWindowTitles_TestModeRejectsProcessThatAlsoOwnsPgmWindow()
    {
        var pattern = new Regex(
            "^Tornado2 TEST$",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100));

        var classification = TornadoProcessProbe.ClassifyWindowTitles(
            ["Tornado2 TEST", "PGM"],
            pattern);

        Assert.True(classification.IsProgram);
        Assert.False(classification.IsEligible);
    }

    [Fact]
    public void ClassifyWindowTitles_NoMeaningfulTitlesFailsClosedAsProgram()
    {
        var pattern = new Regex(
            "^Tornado2 TEST$",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100));

        var classification = TornadoProcessProbe.ClassifyWindowTitles(
            [null, "", "   "],
            pattern);

        Assert.True(classification.IsProgram);
        Assert.False(classification.IsEligible);
    }

    [Fact]
    public void TryClassifyWindowSnapshot_InspectionFailureFailsClosed()
    {
        var snapshot = new TornadoWindowTitleSnapshot(
            ["PGM"],
            InspectionSucceeded: false);

        var succeeded = TornadoProcessProbe.TryClassifyWindowSnapshot(
            snapshot,
            testWindowPattern: null,
            out var classification);

        Assert.False(succeeded);
        Assert.Equal(default, classification);
    }

    [Fact]
    public void TryClassifyWindowSnapshot_NoMeaningfulTitlesFailsIdentityInspection()
    {
        var snapshot = new TornadoWindowTitleSnapshot(
            ["", "   "],
            InspectionSucceeded: true);

        var succeeded = TornadoProcessProbe.TryClassifyWindowSnapshot(
            snapshot,
            testWindowPattern: null,
            out var classification);

        Assert.False(succeeded);
        Assert.Equal(default, classification);
    }

    [Fact]
    public void TryClassifyWindowSnapshot_PgmTitleSucceedsForLiveIdentityInspection()
    {
        var snapshot = new TornadoWindowTitleSnapshot(
            ["Tornado2 - [5001.t2s]", "PGM"],
            InspectionSucceeded: true);

        var succeeded = TornadoProcessProbe.TryClassifyWindowSnapshot(
            snapshot,
            testWindowPattern: null,
            out var classification);

        Assert.True(succeeded);
        Assert.True(classification.IsProgram);
        Assert.True(classification.IsEligible);
    }

    [Fact]
    public void WindowTitleProbe_InvalidProcessIdFailsClosedWithoutNativeInspection()
    {
        var snapshot = new Win32TornadoWindowTitleProbe().Capture(0);

        Assert.False(snapshot.InspectionSucceeded);
        Assert.Empty(snapshot.Titles);
    }

    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    [InlineData(2, false)]
    [InlineData(10, false)]
    public void ProcessSnapshot_RequiresExactlyOneEligibleInstance(
        int eligibleProcessCount,
        bool expected)
    {
        var snapshot = new TornadoProcessSnapshot(
            TotalProcessCount: eligibleProcessCount,
            EligibleProcessCount: eligibleProcessCount,
            ProgramProcessCount: 0);

        Assert.Equal(expected, snapshot.HasSingleEligibleProcess);
    }

    [Fact]
    public void Validate_TestModeRequiresExplicitOutputChannel()
    {
        var options = ValidTestOptions();
        options.OutputChannel = null;

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

        Assert.Contains(nameof(PlayoutOptions.OutputChannel), exception.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Validate_TestModeRequiresSceneAllowlist()
    {
        var options = ValidTestOptions();
        options.TestSceneAllowlist = [];

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

        Assert.Contains(nameof(PlayoutOptions.TestSceneAllowlist), exception.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Validate_LiveModeRequiresClosedSceneAllowlist()
    {
        var options = ValidTestOptions();
        options.Mode = PlayoutMode.Live;
        options.OutputChannel = null;
        options.TestProcessWindowTitlePattern = null;
        options.TestSceneAllowlist = [];

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

        Assert.Contains(
            nameof(PlayoutOptions.TestSceneAllowlist),
            exception.Message,
            StringComparison.Ordinal);
    }

    [Theory]
    [InlineData("localhost")]
    [InlineData("test-tornado.local")]
    [InlineData("192.168.0.10")]
    public void Validate_TestModeRequiresLiteralLoopbackAddress(string host)
    {
        var options = ValidTestOptions();
        options.Host = host;

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

        Assert.Contains(nameof(PlayoutOptions.Host), exception.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Validate_OrdinaryLiveModeRetainsConfiguredRemoteEndpointBehavior()
    {
        var options = ValidTestOptions();
        options.Mode = PlayoutMode.Live;
        options.Host = "playout.dev.invalid";
        options.OutputChannel = null;
        options.TestProcessWindowTitlePattern = null;
        options.TrustedLiveOutputEnabled = true;

        var validated = ValidatedPlayoutOptions.Create(options);

        Assert.Equal("playout.dev.invalid", validated.Host);
    }

    [Theory]
    [InlineData(".*")]
    [InlineData("PGM")]
    [InlineData("PROGRAM")]
    [InlineData("^Tornado2 (TEST|PGM)$")]
    [InlineData("^Tornado2$")]
    [InlineData("^$")]
    [InlineData("^SAFE$")]
    public void Validate_TestWindowPatternThatCanMatchProgramOutput_IsRejected(string pattern)
    {
        var options = ValidTestOptions();
        options.TestProcessWindowTitlePattern = pattern;

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

        Assert.Contains("PROGRAM", exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Validate_InvalidTestWindowPattern_IsRejectedWithoutEchoingPattern()
    {
        const string invalidPattern = "(?<secret>";
        var options = ValidTestOptions();
        options.TestProcessWindowTitlePattern = invalidPattern;

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

        Assert.Equal("테스트 Tornado 창 제목 설정이 올바르지 않습니다.", exception.Message);
        Assert.DoesNotContain(invalidPattern, exception.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Validate_TestSceneAllowlistMatchesSceneNameOnlyAndIgnoresCase()
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());

        Assert.True(validated.IsTestSceneAllowed(
            new PlayoutCue("test-scene.t2s", "TEST-SCENE")));
        Assert.False(validated.IsTestSceneAllowed(
            new PlayoutCue("test-scene.t2s", "OTHER-SCENE")));
    }

    [Fact]
    public void ResolveCue_UsesCanonicalFileUnderConfiguredSceneDirectory()
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());

        var resolved = validated.ResolveCue(new PlayoutCue(
            "test-scene.t2s",
            "TEST-SCENE",
            FadeDuration: 5));

        Assert.Equal(
            System.IO.Path.Combine(_scenes.Path, "test-scene.t2s"),
            resolved.SceneFile,
            ignoreCase: true);
        Assert.Equal("TEST-SCENE", resolved.SceneName);
        Assert.Equal(5, resolved.FadeDuration);
    }

    [Theory]
    [InlineData("..\\outside.t2s", "outside")]
    [InlineData("test-scene.txt", "test-scene")]
    [InlineData("test-scene.t2s", "different-name")]
    [InlineData("missing.t2s", "missing")]
    public void ResolveCue_RejectsUnsafeOrMissingFilesWithoutLeakingInput(
        string sceneFile,
        string sceneName)
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());

        var exception = Assert.Throws<PlayoutRequestException>(
            () => validated.ResolveCue(new PlayoutCue(sceneFile, sceneName)));

        Assert.DoesNotContain(sceneFile, exception.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(_scenes.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void ResolveCue_RejectsRootedScenePathWithoutLeakingIt()
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());
        var rootedPath = System.IO.Path.Combine(_scenes.Path, "test-scene.t2s");

        var exception = Assert.Throws<PlayoutRequestException>(
            () => validated.ResolveCue(new PlayoutCue(rootedPath, "test-scene")));

        Assert.Equal("장면 파일 이름이 올바르지 않습니다.", exception.Message);
        Assert.DoesNotContain(rootedPath, exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void ResolveCue_CanonicalizesAllowlistedBackgroundTextureUnderSceneRoot()
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());

        var resolved = validated.ResolveCue(new PlayoutCue(
            "test-scene.t2s",
            "test-scene",
            Mutations: [new PlayoutSetBackgroundTexture("Video/background.vrv")]));

        var texture = Assert.IsType<PlayoutSetBackgroundTexture>(Assert.Single(resolved.Mutations!));
        Assert.Equal(
            Path.Combine(_scenes.Path, "Video", "background.vrv"),
            texture.AssetPath,
            ignoreCase: true);
    }

    [Fact]
    public void ResolveCue_OperatorBackgroundUsesOnlyTheSeparateTrustedRoot()
    {
        using var backgrounds = TemporarySceneDirectory.Create("studio.vrv");
        var options = ValidTestOptions();
        options.LegacyBackgroundDirectory = backgrounds.Path;
        var validated = ValidatedPlayoutOptions.Create(options);

        var resolved = validated.ResolveCue(new PlayoutCue(
            "test-scene.t2s",
            "test-scene",
            Mutations:
            [
                new PlayoutSetBackgroundVideo(
                    "studio.vrv",
                    LoopCount: 2004,
                    LoopInfinite: 10,
                    AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
            ]));

        var video = Assert.IsType<PlayoutSetBackgroundVideo>(Assert.Single(resolved.Mutations!));
        Assert.Equal(
            Path.Combine(backgrounds.Path, "studio.vrv"),
            video.AssetPath,
            ignoreCase: true);
        Assert.Equal(PlayoutAssetRoot.OperatorBackgroundDirectory, video.AssetRoot);
    }

    [Fact]
    public void ResolveCue_OperatorBackgroundDoesNotFallBackToCutsRoot()
    {
        using var backgrounds = TemporarySceneDirectory.Create();
        var options = ValidTestOptions();
        options.LegacyBackgroundDirectory = backgrounds.Path;
        var validated = ValidatedPlayoutOptions.Create(options);

        Assert.Throws<PlayoutRequestException>(() => validated.ResolveCue(new PlayoutCue(
            "test-scene.t2s",
            "test-scene",
            Mutations:
            [
                new PlayoutSetBackgroundVideo(
                    "Video\\background.vrv",
                    LoopCount: 2004,
                    LoopInfinite: 10,
                    AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
            ])));
    }

    [Fact]
    public void ResolveCue_RevalidatesConfiguredBackgroundRootAtCommandTime()
    {
        var lateRoot = Path.Combine(Directory.GetParent(_scenes.Path)!.FullName, "late-background");
        var options = ValidTestOptions();
        options.LegacyBackgroundDirectory = lateRoot;
        var validated = ValidatedPlayoutOptions.Create(options);

        Directory.CreateDirectory(lateRoot);
        File.WriteAllText(Path.Combine(lateRoot, "studio.vrv"), "late trusted background");

        var resolved = validated.ResolveCue(new PlayoutCue(
            "test-scene.t2s",
            "test-scene",
            Mutations:
            [
                new PlayoutSetBackgroundVideo(
                    "studio.vrv",
                    LoopCount: 2004,
                    LoopInfinite: 10,
                    AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
            ]));

        var video = Assert.IsType<PlayoutSetBackgroundVideo>(Assert.Single(resolved.Mutations!));
        Assert.Equal(
            Path.Combine(lateRoot, "studio.vrv"),
            video.AssetPath,
            ignoreCase: true);
    }

    [Fact]
    public void ResolveCue_AcceptsTheLegacy2004BackgroundVideoLoopValue()
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());

        var resolved = validated.ResolveCue(new PlayoutCue(
            "test-scene.t2s",
            "test-scene",
            Mutations:
            [
                new PlayoutSetBackgroundVideo(
                    "Video/background.vrv",
                    LoopCount: 2004,
                    LoopInfinite: 10)
            ]));

        var video = Assert.IsType<PlayoutSetBackgroundVideo>(Assert.Single(resolved.Mutations!));
        Assert.Equal(2004, video.LoopCount);
        Assert.Equal(
            Path.Combine(_scenes.Path, "Video", "background.vrv"),
            video.AssetPath,
            ignoreCase: true);
    }

    [Theory]
    [InlineData(-1)]
    [InlineData(2)]
    [InlineData(9)]
    [InlineData(11)]
    public void ResolveCue_RejectsBackgroundVideoLoopModesNotUsedByMainForm(int loopMode)
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());

        Assert.Throws<PlayoutRequestException>(() => validated.ResolveCue(new PlayoutCue(
            "test-scene.t2s",
            "test-scene",
            Mutations:
            [
                new PlayoutSetBackgroundVideo(
                    "Video/background.vrv",
                    LoopCount: 2004,
                    LoopInfinite: loopMode)
            ])));
    }

    [Theory]
    [InlineData("../outside.vrv")]
    [InlineData("Video/missing.vrv")]
    [InlineData("Video/background.exe")]
    public void ResolveCue_RejectsUnsafeBackgroundTextureWithoutLeakingInput(string assetPath)
    {
        var validated = ValidatedPlayoutOptions.Create(ValidTestOptions());

        var exception = Assert.Throws<PlayoutRequestException>(() =>
            validated.ResolveCue(new PlayoutCue(
                "test-scene.t2s",
                "test-scene",
                Mutations: [new PlayoutSetBackgroundTexture(assetPath)])));

        Assert.DoesNotContain(assetPath, exception.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(_scenes.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Theory]
    [InlineData(0, 5000, 64, nameof(PlayoutOptions.Port))]
    [InlineData(30001, 99, 64, nameof(PlayoutOptions.OperationTimeoutMilliseconds))]
    [InlineData(30001, 5000, 0, nameof(PlayoutOptions.QueueCapacity))]
    public void Validate_OutOfRangeValuesAreRejected(
        int port,
        int operationTimeoutMilliseconds,
        int queueCapacity,
        string expectedProperty)
    {
        var options = ValidTestOptions();
        options.Port = port;
        options.OperationTimeoutMilliseconds = operationTimeoutMilliseconds;
        options.QueueCapacity = queueCapacity;

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

        Assert.Contains(expectedProperty, exception.Message, StringComparison.Ordinal);
    }

    [Theory]
    [InlineData(-1)]
    [InlineData(1_000_001)]
    public void Validate_OutOfRangeAutomaticRefreshMaximumIsRejected(int value)
    {
        var options = ValidTestOptions();
        options.MaximumAutomaticRefreshesPerTakeIn = value;

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

        Assert.Contains(
            nameof(PlayoutOptions.MaximumAutomaticRefreshesPerTakeIn),
            exception.Message,
            StringComparison.Ordinal);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(1_000_000)]
    public void Validate_AutomaticRefreshMaximumAcceptsClosedSafeRange(int value)
    {
        var options = ValidTestOptions();
        options.MaximumAutomaticRefreshesPerTakeIn = value;

        _ = ValidatedPlayoutOptions.Create(options);
    }

    public void Dispose() => _scenes.Dispose();

    private PlayoutOptions ValidTestOptions() => new()
    {
        Mode = PlayoutMode.Test,
        Host = "127.0.0.1",
        Port = 30001,
        TcpMode = 1,
        ClientPort = 0,
        OutputChannel = 9,
        LayoutIndex = 10,
        SceneDirectory = _scenes.Path,
        TestProcessWindowTitlePattern = "^Tornado2 TEST$",
        TestSceneAllowlist = ["test-scene"],
        TrustedLiveOutputEnabled = false,
        QueueCapacity = 64,
        ConnectTimeoutMilliseconds = 5000,
        OperationTimeoutMilliseconds = 5000,
        DisconnectTimeoutMilliseconds = 3000,
        ProcessPollIntervalMilliseconds = 1000,
        ReconnectDelayMilliseconds = 1000,
        MaximumReconnectAttempts = 3,
        ReconnectEnabled = true
    };
}
