using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class PlayoutBridgeProtocolTests
{
    [Theory]
    [InlineData("https://app.mbn.local/index.html", true)]
    [InlineData("HTTPS://APP.MBN.LOCAL/path", true)]
    [InlineData("http://app.mbn.local/index.html", false)]
    [InlineData("https://app.mbn.local:444/index.html", false)]
    [InlineData("https://app.mbn.local@evil.example/index.html", false)]
    [InlineData("https://evil.example/index.html", false)]
    [InlineData("about:blank", false)]
    [InlineData("not-a-uri", false)]
    public void IsTrustedSource_RequiresTheExactHttpsDefaultPortOrigin(
        string source,
        bool expected)
    {
        Assert.Equal(
            expected,
            PlayoutBridgeProtocol.IsTrustedSource(source, "app.mbn.local"));
    }

    [Theory]
    [InlineData("prepare", true)]
    [InlineData("next", true)]
    [InlineData("take-in", false)]
    [InlineData("take-out", false)]
    public void ParseCommand_AcceptsOnlyTheCommandSpecificCueShape(
        string command,
        bool hasCue)
    {
        var cue = hasCue ? ",\"cue\":{\"code\":\"5001\"}" : string.Empty;
        using var document = JsonDocument.Parse(
            $"{{\"requestId\":\"REQ_1\",\"command\":\"{command}\"{cue}}}");

        var result = PlayoutBridgeProtocol.ParseCommand(document.RootElement);

        Assert.True(result.IsValid);
        Assert.Equal("REQ_1", result.Request.RequestId);
        Assert.Equal(command, result.Request.Command);
        if (hasCue)
        {
            Assert.Equal("5001", result.Request.Cue?.SceneName);
            Assert.Equal("5001.t2s", result.Request.Cue?.SceneFile);
        }
        else
        {
            Assert.Null(result.Request.Cue);
        }
    }

    [Theory]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"cue\":{\"code\":\"../5001\"}}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"cue\":{\"code\":\"5001\",\"title\":\"ignored\"}}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"take-in\",\"cue\":{\"code\":\"5001\"}}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"next\"}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"unknown\"}")]
    [InlineData("{\"requestId\":\"REQ\",\"requestId\":\"REQ2\",\"command\":\"take-out\"}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"take-out\",\"liveAuthorization\":true}")]
    public void ParseCommand_RejectsUnknownDuplicateOrUnsafeInput(string json)
    {
        using var document = JsonDocument.Parse(json);

        var result = PlayoutBridgeProtocol.ParseCommand(document.RootElement);

        Assert.False(result.IsValid);
        Assert.NotEmpty(result.Error);
        Assert.Null(result.Request.Cue);
    }

    [Fact]
    public void ParseCommand_DoesNotReflectAnUnsafeRequestIdentifier()
    {
        const string unsafeRequestId = "../../private-value";
        using var document = JsonDocument.Parse(
            $"{{\"requestId\":\"{unsafeRequestId}\",\"command\":\"take-out\"}}");

        var result = PlayoutBridgeProtocol.ParseCommand(document.RootElement);

        Assert.False(result.IsValid);
        Assert.Equal(string.Empty, result.Request.RequestId);
        Assert.DoesNotContain(unsafeRequestId, result.Error, StringComparison.Ordinal);
    }

    [Fact]
    public void ParseWorkflowCommand_AcceptsPreparePlaylistAndNativeLookupFields()
    {
        using var document = JsonDocument.Parse("""
            {
              "requestId":"REQ_1",
              "command":"prepare",
              "playlist":[
                {
                  "id":"entry-1",
                  "code":"5001",
                  "enabled":true,
                  "fadeDuration":3,
                  "selection":{
                    "groupCode":"코스피",
                    "subject":"삼성전자",
                    "graphicType":"1열판 기본",
                    "subtype":"현재가",
                    "dataCode":"005930"
                  }
                },
                {"id":"entry-2","code":"5074","enabled":false}
              ],
              "selectedIndex":0
            }
            """);

        var result = PlayoutBridgeProtocol.ParseWorkflowCommand(document.RootElement);

        Assert.True(result.IsValid);
        Assert.Equal("REQ_1", result.Request.RequestId);
        Assert.Equal("prepare", result.Request.Command);
        Assert.Equal(0, result.Request.SelectedIndexZeroBased);
        var playlist = Assert.IsAssignableFrom<IReadOnlyList<LegacyPlaylistEntry>>(
            result.Request.Playlist);
        Assert.Equal(2, playlist.Count);
        Assert.Equal("005930", playlist[0].Selection?.DataCode);
        Assert.Equal(3, playlist[0].FadeDuration);
        Assert.False(playlist[1].IsEnabled);
    }

    [Theory]
    [InlineData("take-in")]
    [InlineData("next")]
    [InlineData("take-out")]
    public void ParseWorkflowCommand_AcceptsStateCommandsWithoutCallerSelectedCue(string command)
    {
        using var document = JsonDocument.Parse(
            $"{{\"requestId\":\"REQ\",\"command\":\"{command}\"}}");

        var result = PlayoutBridgeProtocol.ParseWorkflowCommand(document.RootElement);

        Assert.True(result.IsValid);
        Assert.Null(result.Request.Playlist);
        Assert.Equal(-1, result.Request.SelectedIndexZeroBased);
    }

    [Theory]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"next\",\"playlist\":[],\"selectedIndex\":0}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[],\"selectedIndex\":0}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"same\",\"code\":\"5001\",\"enabled\":true},{\"id\":\"same\",\"code\":\"5006\",\"enabled\":true}],\"selectedIndex\":0}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true,\"fadeDuration\":20}],\"selectedIndex\":0}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true,\"selection\":{\"groupCode\":\"코스피\",\"subject\":\"bad\\nvalue\",\"graphicType\":\"\",\"subtype\":\"\",\"dataCode\":\"005930\"}}],\"selectedIndex\":0}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true,\"selection\":{\"groupCode\":\"코스피\",\"subject\":\"삼성전자\",\"graphicType\":\"\",\"subtype\":\"\",\"dataCode\":\"005930\",\"objectName\":\"title\"}}],\"selectedIndex\":0}")]
    public void ParseWorkflowCommand_RejectsCueOverrideDuplicateOrUnsafeShapes(string json)
    {
        using var document = JsonDocument.Parse(json);

        var result = PlayoutBridgeProtocol.ParseWorkflowCommand(document.RootElement);

        Assert.False(result.IsValid);
        Assert.Null(result.Request.Playlist);
    }

    [Fact]
    public void ParsePagePlanRequest_AcceptsOnlyPagedEntriesWithoutStoredPageInput()
    {
        using var document = JsonDocument.Parse("""
            {
              "requestId":"PAGE_PLAN_1",
              "entries":[
                {
                  "id":"entry-5074",
                  "code":"5074",
                  "enabled":true,
                  "fadeDuration":6,
                  "selection":{
                    "groupCode":"PAGED_DOMESTIC_KOSPI",
                    "subject":"INDEX",
                    "graphicType":"",
                    "subtype":"MARKET_CAP",
                    "dataCode":"2026-07-12"
                  }
                },
                {"id":"entry-5088","code":"5088","enabled":false}
              ]
            }
            """);

        var result = PlayoutBridgeProtocol.ParsePagePlanRequest(document.RootElement);

        Assert.True(result.IsValid);
        Assert.Equal("PAGE_PLAN_1", result.Request.RequestId);
        Assert.Equal(["5074", "5088"], result.Request.Entries!.Select(entry => entry.CutCode));
        Assert.Equal("2026-07-12", result.Request.Entries![0].Selection?.DataCode);
    }

    [Theory]
    [InlineData("{\"requestId\":\"REQ\",\"entries\":[]}")]
    [InlineData("{\"requestId\":\"REQ\",\"entries\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true}]}")]
    [InlineData("{\"requestId\":\"REQ\",\"entries\":[{\"id\":\"one\",\"code\":\"5074\",\"enabled\":true,\"pageText\":\"1/24\"}]}")]
    [InlineData("{\"requestId\":\"REQ\",\"entries\":[{\"id\":\"same\",\"code\":\"5074\",\"enabled\":true},{\"id\":\"same\",\"code\":\"5077\",\"enabled\":true}]}")]
    [InlineData("{\"requestId\":\"REQ\",\"entries\":[{\"id\":\"one\",\"code\":\"5088\",\"enabled\":true}],\"cue\":{\"code\":\"5088\"}}")]
    public void ParsePagePlanRequest_RejectsNonPagedStoredPageOrExtraControlInput(string json)
    {
        using var document = JsonDocument.Parse(json);

        var result = PlayoutBridgeProtocol.ParsePagePlanRequest(document.RootElement);

        Assert.False(result.IsValid);
        Assert.Equal("REQ", result.Request.RequestId);
        Assert.Null(result.Request.Entries);
        Assert.NotEmpty(result.Error);
    }

    [Theory]
    [InlineData("prepare")]
    [InlineData("take-in")]
    [InlineData("next")]
    [InlineData("take-out")]
    public void ParseTimeoutQuarantine_AcceptsOnlyKnownTimedOutCommands(string command)
    {
        using var document = JsonDocument.Parse(
            $"{{\"requestId\":\"REQ_1\",\"command\":\"{command}\"}}");

        var result = PlayoutBridgeProtocol.ParseTimeoutQuarantine(document.RootElement);

        Assert.True(result.IsValid);
        Assert.Equal("REQ_1", result.Request.RequestId);
        Assert.Equal(command, result.Request.Command);
        Assert.Empty(result.Error);
    }

    [Theory]
    [InlineData("{}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"unknown\"}")]
    [InlineData("{\"requestId\":\"../REQ\",\"command\":\"next\"}")]
    [InlineData("{\"requestId\":\"REQ\",\"command\":\"next\",\"retry\":true}")]
    [InlineData("{\"requestId\":\"REQ\",\"requestId\":\"REQ2\",\"command\":\"next\"}")]
    public void ParseTimeoutQuarantine_RejectsUnknownDuplicateOrUnsafeInput(string json)
    {
        using var document = JsonDocument.Parse(json);

        var result = PlayoutBridgeProtocol.ParseTimeoutQuarantine(document.RootElement);

        Assert.False(result.IsValid);
        Assert.NotEmpty(result.Error);
    }
}
