using System.Text.Json;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MBN_STOCK_WEBVIEW.LegacyBridge;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;

public sealed class LegacyBridgeProtocolTests
{
    [Fact]
    public void ParseSearch_PreservesBlankEnterAndRejectsExtraAuthority()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"search-stocks\",\"payload\":{\"text\":\"\",\"trigger\":\"enter\"}}",
            out var parsed,
            out var error));
        Assert.Null(error);
        var search = Assert.IsType<LegacySearchStocksIntent>(parsed);
        Assert.Equal(string.Empty, search.Text);
        Assert.Equal(LegacySearchTrigger.Enter, search.Trigger);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"search-stocks\",\"payload\":{\"text\":\"삼성\",\"trigger\":\"button\",\"sceneCode\":\"5001\"}}",
            out _,
            out _));
    }

    [Fact]
    public void ParsePointer_RequiresExactBoundedInput()
    {
        const string json =
            "{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400,\"allowAppend\":false}}";

        Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
        var pointer = Assert.IsType<LegacyCutPointerDownIntent>(parsed);
        Assert.Equal(1, pointer.PhysicalIndex);
        Assert.True(pointer.Shift);
        Assert.Equal(1_400, pointer.TimestampMilliseconds);
        Assert.False(pointer.AllowAppend);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400,\"allowAppend\":0}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400,\"allowAppend\":true,\"sceneCode\":\"5001\"}}",
            out _,
            out _));
    }

    [Theory]
    [InlineData(49, true)]
    [InlineData(50, false)]
    public void ParsePointer_UsesTheFullFiftyRowRuntimeMenuBoundary(
        int physicalIndex,
        bool expected)
    {
        var json =
            "{\"type\":\"cut-pointer-down\",\"payload\":{" +
            $"\"physicalIndex\":{physicalIndex},\"control\":false," +
            "\"shift\":false,\"x\":0,\"y\":0,\"timestampMilliseconds\":0," +
            "\"allowAppend\":true}}";

        Assert.Equal(expected, LegacyBridgeProtocol.TryParseIntent(
            json,
            out var parsed,
            out _));
        if (expected)
        {
            Assert.Equal(
                physicalIndex,
                Assert.IsType<LegacyCutPointerDownIntent>(parsed).PhysicalIndex);
        }
    }

    [Theory]
    [InlineData("arrow-up", LegacyCutKeyboardKey.ArrowUp)]
    [InlineData("arrow-down", LegacyCutKeyboardKey.ArrowDown)]
    [InlineData("home", LegacyCutKeyboardKey.Home)]
    [InlineData("end", LegacyCutKeyboardKey.End)]
    [InlineData("space", LegacyCutKeyboardKey.Space)]
    public void ParseCutKeyDown_AcceptsOnlyClosedKeysAndModifiers(
        string key,
        LegacyCutKeyboardKey expected)
    {
        var json =
            "{\"type\":\"cut-key-down\",\"payload\":{" +
            $"\"key\":\"{key}\",\"control\":true,\"shift\":false}}}}";

        Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
        var intent = Assert.IsType<LegacyCutKeyDownIntent>(parsed);
        Assert.Equal(expected, intent.Key);
        Assert.True(intent.Control);
        Assert.False(intent.Shift);
    }

    [Theory]
    [InlineData("page-down", "\"control\":false,\"shift\":false")]
    [InlineData("ArrowUp", "\"control\":false,\"shift\":false")]
    [InlineData("home", "\"control\":0,\"shift\":false")]
    [InlineData("home", "\"control\":false,\"shift\":false,\"physicalIndex\":0")]
    public void ParseCutKeyDown_RejectsUnknownOrAdditionalAuthority(
        string key,
        string fields)
    {
        var json =
            "{\"type\":\"cut-key-down\",\"payload\":{" +
            $"\"key\":\"{key}\",{fields}}}}}";

        Assert.False(LegacyBridgeProtocol.TryParseIntent(json, out _, out _));
    }

    [Fact]
    public void ParseClearCutSelection_AcceptsOnlyAnEmptyPayload()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"clear-cut-selection\",\"payload\":{}}",
            out var parsed,
            out _));
        Assert.IsType<LegacyClearCutSelectionIntent>(parsed);
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"clear-cut-selection\",\"payload\":{\"physicalIndex\":0}}",
            out _,
            out _));
    }

    [Fact]
    public void ParsePlaylistSelection_PreservesModifiersAndRejectsUnsafeAuthority()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\",\"control\":true,\"shift\":false}}",
            out var parsed,
            out _));

        var selection = Assert.IsType<LegacySelectPlaylistRowIntent>(parsed);
        Assert.Equal("legacy-stock-00000003", selection.RowId);
        Assert.True(selection.Control);
        Assert.False(selection.Shift);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-playlist-row\",\"payload\":{\"rowId\":\"../row\",\"control\":false,\"shift\":false}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\",\"control\":false,\"shift\":false,\"rowIndex\":2}}",
            out _,
            out _));
    }

    [Fact]
    public void ParsePlaylistActivation_AcceptsOnlyOneOpaqueRowId()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\"}}",
            out var parsed,
            out _));
        Assert.Equal(
            "legacy-stock-00000003",
            Assert.IsType<LegacyActivatePlaylistRowIntent>(parsed).RowId);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"../row\"}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\",\"enabled\":false}}",
            out _,
            out _));
    }

    [Fact]
    public void ParsePlaylistReorder_UsesOnlyOpaqueRowsAndClosedDropPosition()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"before\"}}",
            out var parsed,
            out _));

        var reorder = Assert.IsType<LegacyReorderPlaylistRowsIntent>(parsed);
        Assert.Equal("legacy-stock-00000001", reorder.SourceRowId);
        Assert.Equal("legacy-stock-00000004", reorder.TargetRowId);
        Assert.Equal(LegacyDragDropPosition.Before, reorder.Position);

        foreach (var invalid in new[]
                 {
                     "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"../row\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"before\"}}",
                     "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000001\",\"position\":\"before\"}}",
                     "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"middle\"}}",
                     "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"before\",\"targetIndex\":3}}"
                 })
        {
            Assert.False(LegacyBridgeProtocol.TryParseIntent(invalid, out _, out _));
        }
    }

    [Fact]
    public void ParseAllPlaylistEnabled_RequiresOnlyBooleanState()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-all-playlist-enabled\",\"payload\":{\"enabled\":false}}",
            out var parsed,
            out _));
        var enabled = Assert.IsType<LegacySetAllPlaylistEnabledIntent>(parsed);
        Assert.False(enabled.Enabled);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-all-playlist-enabled\",\"payload\":{\"enabled\":1}}",
            out _,
            out _));
    }

    [Fact]
    public void ParseTabs_UsesClosedIdentitySet()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-tab\",\"payload\":{\"tabId\":\"kospiIndustry\"}}",
            out var selection,
            out _));
        Assert.Equal(
            LegacyOperatorTabId.KospiIndustry,
            Assert.IsType<LegacySelectTabIntent>(selection).TabId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"swap-tabs\",\"payload\":{\"source\":\"theme\",\"target\":\"exchange\"}}",
            out var swap,
            out _));
        var parsedSwap = Assert.IsType<LegacySwapTabsIntent>(swap);
        Assert.Equal(LegacyOperatorTabId.Theme, parsedSwap.Source);
        Assert.Equal(LegacyOperatorTabId.Exchange, parsedSwap.Target);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"hover-swap-tabs\",\"payload\":{\"source\":\"theme\",\"target\":\"exchange\",\"expectedSourceIndex\":6,\"expectedTargetIndex\":1}}",
            out var hoverSwap,
            out _));
        var parsedHover = Assert.IsType<LegacyHoverSwapTabsIntent>(hoverSwap);
        Assert.Equal(6, parsedHover.ExpectedSourceIndex);
        Assert.Equal(1, parsedHover.ExpectedTargetIndex);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"hover-swap-tabs\",\"payload\":{\"source\":\"theme\",\"target\":\"exchange\",\"expectedSourceIndex\":6,\"expectedTargetIndex\":10}}",
            out _,
            out _));

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-tab\",\"payload\":{\"tabId\":\"scene5001\"}}",
            out _,
            out _));
    }

    [Theory]
    [InlineData("first", false)]
    [InlineData("last", true)]
    public void ParsePlaylistBoundary_UsesClosedValues(string boundary, bool last)
    {
        var json = $"{{\"type\":\"select-playlist-boundary\",\"payload\":{{\"boundary\":\"{boundary}\"}}}}";
        Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
        Assert.Equal(last, Assert.IsType<LegacySelectPlaylistBoundaryIntent>(parsed).Last);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-playlist-boundary\",\"payload\":{\"boundary\":\"middle\"}}",
            out _,
            out _));
    }

    [Fact]
    public void ParseToggleActiveEnabled_RequiresEmptyPayload()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"toggle-active-playlist-enabled\",\"payload\":{}}",
            out var parsed,
            out _));
        Assert.IsType<LegacyToggleActivePlaylistEnabledIntent>(parsed);
    }

    [Fact]
    public void ParseFixedCatalogIntents_DoesNotAcceptSceneAuthority()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-fixed-action\",\"payload\":{\"actionId\":\"fixed-001\"}}",
            out var action,
            out _));
        Assert.Equal("fixed-001", Assert.IsType<LegacyActivateFixedActionIntent>(action).ActionId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-fixed-section\",\"payload\":{\"sectionIndex\":0}}",
            out var section,
            out _));
        Assert.Equal(0, Assert.IsType<LegacyActivateFixedSectionIntent>(section).SectionIndex);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-fixed-action\",\"payload\":{\"actionId\":\"fixed-001\",\"sceneCode\":\"5001\"}}",
            out _,
            out _));
    }

    [Fact]
    public void ParseMovingAverages_RequiresBothBooleans()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-moving-averages\",\"payload\":{\"ma5\":true,\"ma20\":false}}",
            out var parsed,
            out _));
        var intent = Assert.IsType<LegacySetMovingAveragesIntent>(parsed);
        Assert.True(intent.Ma5);
        Assert.False(intent.Ma20);
    }

    [Fact]
    public void ParseIndustryIntents_AcceptsOnlyClosedSelectionTextAndOpaqueActionId()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-industry\",\"payload\":{\"index\":2}}",
            out var selected,
            out _));
        var selectIntent = Assert.IsType<LegacySelectIndustryIntent>(selected);
        Assert.Equal(2, selectIntent.Index);
        Assert.False(selectIntent.DoubleClick);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"double-click-industry\",\"payload\":{\"index\":3}}",
            out var doubled,
            out _));
        Assert.True(Assert.IsType<LegacySelectIndustryIntent>(doubled).DoubleClick);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-industry-action\",\"payload\":{\"actionId\":\"yield-5일\"}}",
            out var action,
            out _));
        Assert.Equal(
            "yield-5일",
            Assert.IsType<LegacyActivateIndustryActionIntent>(action).ActionId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"swap-industries\",\"payload\":{}}",
            out var swap,
            out _));
        Assert.IsType<LegacySwapIndustriesIntent>(swap);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"직접 입력_%\"}}",
            out var edit,
            out _));
        var editIntent = Assert.IsType<LegacySetIndustryComparisonTextIntent>(edit);
        Assert.Equal(LegacyIndustryComparisonSlot.First, editIntent.Slot);
        Assert.Equal("직접 입력_%", editIntent.Value);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"second\",\"value\":\"\"}}",
            out var clear,
            out _));
        Assert.Equal(string.Empty,
            Assert.IsType<LegacySetIndustryComparisonTextIntent>(clear).Value);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-industry-action\",\"payload\":{\"actionId\":\"../5001\"}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"third\",\"value\":\"업종\"}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"업종\\n이름\"}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"업종\",\"actionId\":\"two-column\"}}",
            out _,
            out _));

        var oversized = new string('가',
            LegacyIndustrySelectionWorkflow.MaximumComparisonTextLength + 1);
        var oversizedJson = "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":" +
            System.Text.Json.JsonSerializer.Serialize(oversized) + "}}";
        Assert.False(LegacyBridgeProtocol.TryParseIntent(oversizedJson, out _, out _));
    }

    [Fact]
    public void ParseThemeIntents_UsesQueryOnlySearchSortAndOpaqueIds()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\"}}",
            out var search,
            out _));
        Assert.Equal("AI", Assert.IsType<LegacySearchThemesIntent>(search).Query);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-theme\",\"payload\":{\"resultId\":\"theme-result-001\"}}",
            out var selection,
            out _));
        Assert.Equal(
            "theme-result-001",
            Assert.IsType<LegacySelectThemeIntent>(selection).ResultId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"set-theme-sort\",\"payload\":{\"sortId\":\"GAIN_DESC\"}}",
            out var sort,
            out _));
        Assert.Equal("GAIN_DESC", Assert.IsType<LegacySetThemeSortIntent>(sort).SortId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-theme-action\",\"payload\":{\"actionId\":\"five-row-current\"}}",
            out var action,
            out _));
        Assert.Equal(
            "five-row-current",
            Assert.IsType<LegacyActivateThemeActionIntent>(action).ActionId);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\",\"nxtSession\":\"preMarket\"}}",
            out _,
            out _));
    }

    [Fact]
    public void ParseExpertAndTradingHaltIntents_AreClosed()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"search-experts\",\"payload\":{\"query\":\"김\"}}",
            out var expertSearch,
            out _));
        Assert.Equal("김", Assert.IsType<LegacySearchExpertsIntent>(expertSearch).Query);
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-expert\",\"payload\":{\"selectionId\":\"expert-result-0001-0001\"}}",
            out var expertSelect,
            out _));
        Assert.IsType<LegacySelectExpertIntent>(expertSelect);
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-expert-action\",\"payload\":{\"actionId\":\"five-row-current\"}}",
            out var expertAction,
            out _));
        Assert.IsType<LegacyActivateExpertActionIntent>(expertAction);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"search-trading-halts\",\"payload\":{\"query\":\"삼성\"}}",
            out var haltSearch,
            out _));
        Assert.IsType<LegacySearchTradingHaltsIntent>(haltSearch);
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-trading-halt\",\"payload\":{\"selectionId\":\"trading-halt-result-0001-0001\"}}",
            out var haltSelect,
            out _));
        Assert.IsType<LegacySelectTradingHaltIntent>(haltSelect);
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"toggle-trading-halt-sort\",\"payload\":{}}",
            out var haltSort,
            out _));
        Assert.IsType<LegacyToggleTradingHaltSortIntent>(haltSort);
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"activate-trading-halt-action\",\"payload\":{\"actionId\":\"candle-120d\"}}",
            out var haltAction,
            out _));
        Assert.IsType<LegacyActivateTradingHaltActionIntent>(haltAction);
    }

    [Fact]
    public void ExpertProjection_PreservesNullBuyAmountAndAvailableCompositionAction()
    {
        var expert = new LegacyExpertWorkflowSnapshot(
            1,
            new LegacyExpertSearchSnapshot(
                string.Empty,
                LegacyExpertRequestStatus.Ready,
                [],
                false,
                string.Empty),
            null,
            new LegacyExpertPreviewSnapshot(
                LegacyExpertRequestStatus.Ready,
                [new LegacyExpertRecommendationView(0, "005930", "Samsung", null)],
                false,
                5,
                1,
                20,
                string.Empty),
            [new LegacyExpertActionView(
                LegacyExpertWorkflowController.ActionId,
                "Expert",
                true)],
            string.Empty,
            LegacyOperatorStatusKind.Warning);
        var snapshot = new LegacyOperatorSnapshot(
            1, string.Empty, [], null, [], [], null, string.Empty,
            LegacyOperatorStatusKind.Warning, Expert: expert);

        using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
        var projected = document.RootElement.GetProperty("payload").GetProperty("expert");
        var preview = Assert.Single(
            projected.GetProperty("preview").GetProperty("items").EnumerateArray());

        Assert.Equal(JsonValueKind.Null, preview.GetProperty("buyAmount").ValueKind);
        Assert.True(Assert.Single(projected.GetProperty("actions").EnumerateArray())
            .GetProperty("isAvailable").GetBoolean());
    }

    [Fact]
    public void ParseExpertInitialConsonantQuery_PreservesCompatibilityJamo()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"search-experts\",\"payload\":{\"query\":\"ㄱㅈㅁ\"}}",
            out var intent,
            out _));

        Assert.Equal("ㄱㅈㅁ", Assert.IsType<LegacySearchExpertsIntent>(intent).Query);
    }

    [Theory]
    [InlineData("up", LegacyPlaylistMoveDirection.Up)]
    [InlineData("down", LegacyPlaylistMoveDirection.Down)]
    public void ParsePlaylistMove_UsesStrictDirection(
        string direction,
        LegacyPlaylistMoveDirection expected)
    {
        var json = $"{{\"type\":\"move-selected-playlist-rows\",\"payload\":{{\"direction\":\"{direction}\"}}}}";

        Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
        var move = Assert.IsType<LegacyMoveSelectedPlaylistRowsIntent>(parsed);
        Assert.Equal(expected, move.Direction);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"move-selected-playlist-rows\",\"payload\":{\"direction\":\"first\"}}",
            out _,
            out _));
    }

    [Theory]
    [InlineData("delete-selected-playlist-rows", typeof(LegacyDeleteSelectedPlaylistRowsIntent))]
    [InlineData("delete-all-playlist-rows", typeof(LegacyDeleteAllPlaylistRowsIntent))]
    public void ParsePlaylistDelete_RequiresEmptyPayload(string type, Type expectedType)
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            $"{{\"type\":\"{type}\",\"payload\":{{}}}}",
            out var parsed,
            out _));
        Assert.IsType(expectedType, parsed);

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            $"{{\"type\":\"{type}\",\"payload\":{{\"rowId\":\"legacy-stock-00000001\"}}}}",
            out _,
            out _));
    }

    [Fact]
    public void ParseNamedPlaylistIntents_AcceptsOnlyOpaqueIdsTitlesAndDispatchIds()
    {
        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"refresh-named-playlists\",\"payload\":{\"requestId\":\"named-ui-1\"}}",
            out var refresh,
            out _));
        Assert.Equal(
            "named-ui-1",
            Assert.IsType<LegacyRefreshNamedPlaylistsIntent>(refresh).RequestId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-named-playlist\",\"payload\":{\"definitionId\":\"named-definition-AAAAAAAAAAAB\",\"requestId\":\"named-ui-2\"}}",
            out var selection,
            out _));
        var selectionIntent = Assert.IsType<LegacySelectNamedPlaylistIntent>(selection);
        Assert.Equal("named-definition-AAAAAAAAAAAB", selectionIntent.DefinitionId);
        Assert.Equal("named-ui-2", selectionIntent.RequestId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"create-named-playlist\",\"payload\":{\"title\":\"아침 방송 A^B\",\"requestId\":\"named-ui-2\"}}",
            out var create,
            out _));
        var createIntent = Assert.IsType<LegacyCreateNamedPlaylistIntent>(create);
        Assert.Equal("아침 방송 A^B", createIntent.Title);
        Assert.Equal("named-ui-2", createIntent.RequestId);

        Assert.True(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"delete-selected-named-playlist\",\"payload\":{\"requestId\":\"named-ui-3\"}}",
            out var delete,
            out _));
        Assert.Equal(
            "named-ui-3",
            Assert.IsType<LegacyDeleteSelectedNamedPlaylistIntent>(delete).RequestId);

        foreach (var type in new[]
                 {
                     "load-selected-named-playlist",
                     "save-current-named-playlist"
                 })
        {
            Assert.True(LegacyBridgeProtocol.TryParseIntent(
                $"{{\"type\":\"{type}\",\"payload\":{{}}}}",
                out var emptyIntent,
                out _));
            Assert.NotNull(emptyIntent);
        }

        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-named-playlist\",\"payload\":{\"definitionId\":\"named-definition-AAAAAAAAAAAB\",\"programCode\":\"00000001\"}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"select-named-playlist\",\"payload\":{\"definitionId\":\"named-definition-AAAAAAAAAAAB\"}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"save-current-named-playlist\",\"payload\":{\"rows\":[]}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"create-named-playlist\",\"payload\":{\"title\":\" padded \",\"requestId\":\"named-ui-4\"}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"refresh-named-playlists\",\"payload\":{}}",
            out _,
            out _));
        Assert.False(LegacyBridgeProtocol.TryParseIntent(
            "{\"type\":\"delete-selected-named-playlist\",\"payload\":{}}",
            out _,
            out _));
    }

    [Theory]
    [InlineData("https://legacy-parity.mbn.local/index.html", true)]
    [InlineData("https://legacy-parity.mbn.local.evil/index.html", false)]
    [InlineData("http://legacy-parity.mbn.local/index.html", false)]
    [InlineData("file:///C:/index.html", false)]
    public void TrustedSource_IsExactHttpsOrigin(string source, bool expected)
    {
        Assert.Equal(expected, LegacyBridgeProtocol.IsTrustedSource(source));
    }

    [Fact]
    public void SerializedIndustryStateProjectsAuthoritativeEditedTextNotTypedIdentityName()
    {
        var identity = new IndustrySelection(IndustryMarket.Kospi, "DB 업종명", "013");
        var industry = new LegacyIndustrySelectionSnapshot(
            3,
            IndustryMarket.Kospi,
            [identity],
            LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi),
            0,
            identity,
            identity,
            null,
            "직접 입력 첫째",
            "직접 입력 둘째");
        var snapshot = new LegacyOperatorSnapshot(
            1,
            string.Empty,
            [],
            null,
            [],
            [],
            null,
            string.Empty,
            LegacyOperatorStatusKind.Neutral,
            Industry: industry);

        using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
        var projected = document.RootElement.GetProperty("payload").GetProperty("industry");

        Assert.Equal("직접 입력 첫째", projected.GetProperty("firstName").GetString());
        Assert.Equal("직접 입력 둘째", projected.GetProperty("secondName").GetString());
        Assert.NotEqual("DB 업종명", projected.GetProperty("firstName").GetString());
    }

    [Fact]
    public void SerializedState_DoesNotExposeHiddenDbOrSceneAuthority()
    {
        var playlistRow = LegacyStockCutCatalog.CreatePlaylistRow(
            "legacy-stock-00000001",
            "삼성전자",
            new LegacyStockIdentity(LegacyDomesticStockMarket.Kospi, "삼성전자", "005930"),
            LegacyStockCutCatalog.Rows[1]);
        var snapshot = new LegacyOperatorSnapshot(
            1,
            "삼성",
            [new LegacyStockResultRow(0, "삼성전자")],
            0,
            [new LegacyCutViewRow(1, 2, "1열판기본_현재가", false, true, true)],
            [playlistRow],
            null,
            string.Empty,
            LegacyOperatorStatusKind.Neutral);

        var json = LegacyBridgeProtocol.SerializeState(snapshot, isBusy: true);

        Assert.DoesNotContain("005930", json, StringComparison.Ordinal);
        Assert.DoesNotContain("5001", json, StringComparison.Ordinal);
        Assert.DoesNotContain("rawCutLabel", json, StringComparison.Ordinal);

        using var document = JsonDocument.Parse(json);
        var payload = document.RootElement.GetProperty("payload");
        Assert.Equal(
            "삼성전자",
            payload.GetProperty("searchResults")[0].GetProperty("displayName").GetString());
        Assert.Equal(
            "삼성전자",
            payload.GetProperty("playlist")[0].GetProperty("stockName").GetString());
        Assert.Equal(
            "1열판기본_현재가",
            payload.GetProperty("cutRows")[0].GetProperty("rawLabel").GetString());
        Assert.True(payload.GetProperty("cutRows")[0].GetProperty("isFocused").GetBoolean());
        Assert.False(payload.GetProperty("playlist")[0].GetProperty("isSelected").GetBoolean());
        Assert.False(payload.GetProperty("playlist")[0].GetProperty("isActive").GetBoolean());
        Assert.True(payload.GetProperty("isBusy").GetBoolean());
    }

    [Fact]
    public void SerializedState_PreservesInjectedRuntimeCutMenuOrderAndSeparators()
    {
        var controller = new LegacyOperatorController(
            new EmptyStockLookup(),
            cutRows:
            [
                new LegacyCutCatalogRow(
                    0,
                    1,
                    LegacyStockCutCatalog.Rows[1].RawLabel,
                    IsSeparator: false),
                new LegacyCutCatalogRow(1, null, string.Empty, IsSeparator: true),
                new LegacyCutCatalogRow(
                    2,
                    2,
                    LegacyStockCutCatalog.Rows[0].RawLabel,
                    IsSeparator: false)
            ]);

        var json = LegacyBridgeProtocol.SerializeState(controller.Current);

        using var document = JsonDocument.Parse(json);
        var rows = document.RootElement.GetProperty("payload").GetProperty("cutRows");
        Assert.Equal(3, rows.GetArrayLength());
        Assert.Equal("1열판기본_현재가", rows[0].GetProperty("rawLabel").GetString());
        Assert.Equal(0, rows[0].GetProperty("physicalIndex").GetInt32());
        Assert.True(rows[1].GetProperty("isSeparator").GetBoolean());
        Assert.Equal(string.Empty, rows[1].GetProperty("rawLabel").GetString());
        Assert.Equal("1열판기본_예상체결가", rows[2].GetProperty("rawLabel").GetString());
        Assert.Equal(2, rows[2].GetProperty("displayOrdinal").GetInt32());
    }

    [Fact]
    public async Task SerializedNamedPlaylist_ProjectsOnlyOpaqueListAndSafetyState()
    {
        var workflow = new LegacyNamedPlaylistWorkflowController(
            new BridgeNamedPlaylistPersistence());
        await workflow.RefreshAsync();
        workflow.SelectDefinition(workflow.Current.Definitions.Single().DefinitionId);
        await workflow.LoadSelectedAsync();
        var snapshot = new LegacyOperatorSnapshot(
            1,
            string.Empty,
            [],
            null,
            [],
            [],
            null,
            string.Empty,
            LegacyOperatorStatusKind.Neutral,
            NamedPlaylist: workflow.Current);

        var json = LegacyBridgeProtocol.SerializeState(snapshot);

        Assert.DoesNotContain("87654321", json, StringComparison.Ordinal);
        Assert.DoesNotContain("SECRET-SUBJECT", json, StringComparison.Ordinal);
        Assert.DoesNotContain("SECRET-DATA", json, StringComparison.Ordinal);
        Assert.DoesNotContain("programCode", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("listText", json, StringComparison.OrdinalIgnoreCase);

        using var document = JsonDocument.Parse(json);
        var named = document.RootElement.GetProperty("payload").GetProperty("namedPlaylist");
        Assert.Equal("저녁 방송", named.GetProperty("definitions")[0]
            .GetProperty("title").GetString());
        Assert.Equal(1, named.GetProperty("rowCount").GetInt32());
        Assert.False(named.TryGetProperty("rows", out _));
        Assert.True(named.GetProperty("canLoad").GetBoolean());
    }

    private sealed class BridgeNamedPlaylistPersistence : INamedPlaylistPersistenceService
    {
        private static readonly NamedPlaylistSummary Summary = new("87654321", "저녁 방송");

        public bool CanMutate => true;

        public Task<NamedPlaylistListResult> ListAsync(
            int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new NamedPlaylistListResult(
                DateTimeOffset.UtcNow,
                [Summary],
                false));

        public Task<string> SuggestNextProgramCodeAsync(
            CancellationToken cancellationToken = default) =>
            Task.FromResult("87654322");

        public Task<NamedPlaylistDocument> LoadAsync(
            string programCode,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new NamedPlaylistDocument(
                Summary,
                DateTimeOffset.UtcNow,
                [new NamedPlaylistStoredItem(
                    0,
                    true,
                    new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
                        "SECRET-GROUP",
                        "SECRET-SUBJECT",
                        "SECRET-GRAPHIC",
                        "SECRET-SUBTYPE",
                        "SECRET-DATA"),
                    new NamedPlaylistPageState(1, 1))]));

        public Task CreateDefinitionAsync(
            string programCode,
            string title,
            CancellationToken cancellationToken = default) => Task.CompletedTask;

        public Task ReplaceItemsAsync(
            string programCode,
            IReadOnlyList<NamedPlaylistStoredItem> items,
            CancellationToken cancellationToken = default) => Task.CompletedTask;

        public Task DeleteAsync(
            string programCode,
            CancellationToken cancellationToken = default) => Task.CompletedTask;
    }

    private sealed class EmptyStockLookup : ILegacyStockLookup
    {
        public Task<LegacyStockSearchData> SearchAsync(
            string query,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new LegacyStockSearchData(query, [], []));
    }
}
