using System.Text.RegularExpressions;

namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;

public sealed class LegacyWorkspaceLayoutContractTests
{
    private static readonly string RepositoryRoot = FindRepositoryRoot();
    private static readonly string WebRoot = Path.Combine(
        RepositoryRoot,
        "src",
        "MBN_STOCK_WEBVIEW.LegacyParityApp",
        "Web");
    private static readonly string Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html"));
    private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css"));
    private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js"));
    private static readonly string Playout = File.ReadAllText(
        Path.Combine(WebRoot, "playout-ui.js"));
    private static readonly string Navigation = File.ReadAllText(
        Path.Combine(WebRoot, "workspace-navigation.js"));

    [Fact]
    public void ShellUsesSidebarScreensBesidePersistentSchedule()
    {
        var workspaceStart = Markup.IndexOf("id=\"workspace-region\"", StringComparison.Ordinal);
        var navigationStart = Markup.IndexOf("id=\"workspace-navigation\"", StringComparison.Ordinal);
        var marketStart = Markup.IndexOf("id=\"market-tabs\"", StringComparison.Ordinal);
        var settingsTabStart = Markup.IndexOf("id=\"workspace-settings-tab\"", StringComparison.Ordinal);
        var stockStart = Markup.IndexOf("id=\"stock-workspace\"", StringComparison.Ordinal);
        var catalogStart = Markup.IndexOf("id=\"catalog-workspace\"", StringComparison.Ordinal);
        var settingsStart = Markup.IndexOf("id=\"settings-workspace\"", StringComparison.Ordinal);
        var splitterStart = Markup.IndexOf("id=\"workspace-splitter\"", StringComparison.Ordinal);
        var scheduleStart = Markup.IndexOf("class=\"playlist-panel\"", StringComparison.Ordinal);

        Assert.True(workspaceStart >= 0);
        Assert.True(navigationStart > workspaceStart);
        Assert.True(marketStart > navigationStart);
        Assert.True(settingsTabStart > marketStart);
        Assert.True(stockStart > settingsTabStart);
        Assert.True(catalogStart > stockStart);
        Assert.True(settingsStart > catalogStart);
        Assert.True(splitterStart > settingsStart);
        Assert.True(scheduleStart > splitterStart);
        Assert.Matches(
            @"<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*" +
            @"<div id=""workspace-splitter""[^>]*><\/div>\s*" +
            @"<section class=""playlist-panel""",
            Markup);
        Assert.Matches(
            @"id=""workspace-splitter""[^>]*class=""workspace-splitter""" +
            @"[^>]*role=""separator""" +
            @"[^>]*aria-label=""작업 영역과 송출 스케줄 너비 조절""" +
            @"[^>]*aria-orientation=""vertical""" +
            @"[^>]*aria-controls=""workspace-region""" +
            @"[^>]*aria-valuemin=""34""[^>]*aria-valuemax=""48""" +
            @"[^>]*aria-valuenow=""39""[^>]*tabindex=""0""",
            Markup);
        Assert.Matches("id=\"catalog-workspace\"[^>]*hidden inert", Markup);
        Assert.Matches("id=\"settings-workspace\"[^>]*hidden inert", Markup);
        Assert.Contains("<div><h2>송출 스케줄</h2></div>", Markup,
            StringComparison.Ordinal);
        Assert.DoesNotContain("항상 표시", Markup, StringComparison.Ordinal);
    }

    [Fact]
    public void SidebarOwnsEachNativeMarketButtonAndKeepsSettingsOutsideTheirOrder()
    {
        var marketStart = Markup.IndexOf("<div id=\"market-tabs\"", StringComparison.Ordinal);
        var marketEnd = Markup.IndexOf("</div>", marketStart, StringComparison.Ordinal);
        Assert.True(marketStart >= 0 && marketEnd > marketStart);
        var marketMarkup = Markup[marketStart..marketEnd];
        var expected = new[]
        {
            "overseas", "exchange", "index", "kospiIndustry", "kosdaqIndustry",
            "comparison", "theme", "overseasStocks", "expert", "tradingHalt"
        };
        var actual = Regex.Matches(marketMarkup, "data-tab-id=\"([^\"]+)\"")
            .Select(match => match.Groups[1].Value)
            .ToArray();

        Assert.Equal(expected, actual);
        Assert.Equal(expected.Length, Regex.Matches(marketMarkup, "<button\\b").Count);
        Assert.DoesNotContain("workspace-settings-tab", marketMarkup, StringComparison.Ordinal);
        Assert.Contains("workspace-nav-label", marketMarkup, StringComparison.Ordinal);
    }

    [Fact]
    public void WorkAndScheduleExposeAnAccessibleResizableThreeColumnLayout()
    {
        Assert.Matches(
            @"\.operator-shell\s*\{[^}]*grid-template-columns:\s*" +
            @"minmax\(780px,\s*1fr\)\s+var\(--workspace-splitter-width\)\s+" +
            @"minmax\(600px,\s*var\(--schedule-pane-width,[^)]+\)\)",
            Styles);
        Assert.Matches(@"--workspace-splitter-width:\s*8px", Styles);
        Assert.Matches(
            @"\.workspace-splitter\s*\{[^}]*cursor:\s*col-resize" +
            @"[^}]*touch-action:\s*none",
            Styles);
        Assert.Matches(
            @"body\.resizing-workspace\s*\{[^}]*cursor:\s*col-resize" +
            @"[^}]*user-select:\s*none",
            Styles);
        Assert.Contains(".workspace-splitter:focus-visible", Styles,
            StringComparison.Ordinal);
        Assert.Matches(
            @"@media \(forced-colors:\s*active\)[\s\S]*?\.workspace-splitter",
            Styles);
        Assert.Contains(
            "grid-template-rows: 66px minmax(0, 1fr) auto;",
            Styles,
            StringComparison.Ordinal);
        Assert.Contains(".workspace-nav-footer", Styles, StringComparison.Ordinal);
        Assert.Contains(".workspace-navigation .market-tabs button", Styles,
            StringComparison.Ordinal);
        Assert.Matches(
            new Regex(
                @"\.playlist-toolbar\s*\{[^}]*flex-wrap:\s*wrap",
                RegexOptions.CultureInvariant),
            Styles);
        Assert.Matches(
            new Regex(
                @"\.playlist-row\s*\{[^}]*min-width:\s*600px",
                RegexOptions.CultureInvariant),
            Styles);
    }

    [Fact]
    public void SidebarIsOneLabelledAndFullyControlledNavigationRegion()
    {
        Assert.Matches(
            "id=\"workspace-navigation\"[^>]*role=\"navigation\"[^>]*aria-label=",
            Markup);
        Assert.Matches(
            "id=\"workspace-nav-toggle\"[^>]*aria-expanded=\"true\"[\\s\\S]*?" +
            "aria-controls=\"workspace-nav-items workspace-nav-footer\"",
            Markup);
        Assert.Matches(
            "id=\"workspace-nav-items\"[^>]*role=\"group\"[^>]*aria-label=",
            Markup);
        Assert.Matches(
            "id=\"market-tabs\"[^>]*role=\"group\"[^>]*aria-label=",
            Markup);
        Assert.Contains(
            "id=\"workspace-nav-footer\" class=\"workspace-nav-footer\"",
            Markup,
            StringComparison.Ordinal);
    }

    [Fact]
    public void UnifiedOperatorThemePreservesAccessibleAndSemanticStates()
    {
        Assert.Contains(
            "Unified operator theme: visual-only rules; geometry and native contracts stay intact.",
            Styles,
            StringComparison.Ordinal);
        Assert.Contains("Compact broadcast-console components.", Styles,
            StringComparison.Ordinal);
        Assert.Single(
            Regex.Matches(Styles, @"(?m)^:root\s*\{").Cast<Match>());
        Assert.Contains("--ui-canvas:", Styles, StringComparison.Ordinal);
        Assert.Contains("--ui-accent:", Styles, StringComparison.Ordinal);
        Assert.Contains("--ui-success:", Styles, StringComparison.Ordinal);
        Assert.Contains("--ui-danger:", Styles, StringComparison.Ordinal);
        Assert.Contains(
            "font-family: \"Segoe UI Variable\", \"Segoe UI\", \"Noto Sans KR\"",
            Styles,
            StringComparison.Ordinal);
        Assert.Matches(
            new Regex(
                @"button:focus-visible,[\s\S]*?outline:\s*2px solid var\(--ui-accent\)",
                RegexOptions.CultureInvariant),
            Styles);
        Assert.Contains(".playlist-data-row.on-air.selected", Styles, StringComparison.Ordinal);
        Assert.Contains(".playout-actions .take-in:not(:disabled)", Styles, StringComparison.Ordinal);
        Assert.Contains(".playout-actions .next:not(:disabled)", Styles, StringComparison.Ordinal);
        Assert.Contains(".playout-actions .take-out:not(:disabled)", Styles, StringComparison.Ordinal);
        Assert.Contains(".stock-result.selected", Styles, StringComparison.Ordinal);
        Assert.Contains(".playlist-heading-count", Styles, StringComparison.Ordinal);
        Assert.Contains(".playlist-rows:empty::after", Styles, StringComparison.Ordinal);
        Assert.Contains(
            ".playlist-data-row[data-active=\"true\"]:not(.on-air)",
            Styles,
            StringComparison.Ordinal);
        Assert.Contains(
            ".playlist-data-row[aria-selected=\"true\"]:not(.on-air)",
            Styles,
            StringComparison.Ordinal);
        Assert.Contains("@media (forced-colors: active)", Styles, StringComparison.Ordinal);
        Assert.Contains("scrollbar-gutter: stable", Styles, StringComparison.Ordinal);
        Assert.Contains("@media (prefers-reduced-motion: reduce)", Styles, StringComparison.Ordinal);
        Assert.Contains("aria-keyshortcuts=\"F8\"", Markup, StringComparison.Ordinal);
        Assert.Contains("aria-keyshortcuts=\"Escape\"", Markup, StringComparison.Ordinal);
        Assert.Contains(
            ".connection-state.dry-run:not(.outcome-unknown)",
            Styles,
            StringComparison.Ordinal);
        Assert.Contains("accessibleStates.push(\"현재 PGM\")", App, StringComparison.Ordinal);
        Assert.Contains("accessibleStates.push(\"송출 후보\")", App, StringComparison.Ordinal);
        Assert.Contains("accessibleStates.push(\"삭제 선택됨\")", App, StringComparison.Ordinal);
        Assert.Matches(
            new Regex(
                @"id=""stock-search-button""[\s\S]*?<svg[^>]*viewBox=""0 0 24 24""",
                RegexOptions.CultureInvariant),
            Markup);
        Assert.Matches(
            new Regex(
                @"class=""playlist-heading-icon""[\s\S]*?<svg[^>]*viewBox=""0 0 24 24""",
                RegexOptions.CultureInvariant),
            Markup);
        Assert.Matches(
            new Regex(
                @"id=""playlist-heading-count""[^>]*role=""status""[^>]*aria-live=""polite""[^>]*aria-atomic=""true""",
                RegexOptions.CultureInvariant),
            Markup);
        Assert.Contains("const nextHeadingCount = `", App, StringComparison.Ordinal);
        Assert.Contains(
            "if (playlistHeadingCount.textContent !== nextHeadingCount)",
            App,
            StringComparison.Ordinal);
    }

    [Fact]
    public void ScheduleHeaderAndToolbarExposeClearStatesAndExistingShortcuts()
    {
        Assert.Matches(
            @"class=""playlist-state-legend""[^>]*aria-label=""스케줄 상태""[\s\S]*?" +
            @"class=""legend-selected""[\s\S]*?>선택</span>[\s\S]*?" +
            @"class=""legend-next""[\s\S]*?>다음</span>[\s\S]*?" +
            @"class=""legend-on-air""[\s\S]*?>ON AIR</span>",
            Markup);
        Assert.Contains(".playlist-state-legend .legend-selected", Styles,
            StringComparison.Ordinal);
        Assert.Contains(".playlist-state-legend .legend-next", Styles,
            StringComparison.Ordinal);
        Assert.Contains(".playlist-state-legend .legend-on-air", Styles,
            StringComparison.Ordinal);

        var toolbar = Slice(
            Markup,
            "<div class=\"playlist-toolbar\">",
            "<div id=\"playlist-drop-zone\" class=\"playlist-grid\"");
        foreach (var group in new[] { "표시", "순서", "배경", "DB", "삭제" })
        {
            Assert.Matches(
                $"class=\"playlist-toolbar-group[^\"]*\"[^>]*aria-label=\"{group}\"",
                toolbar);
        }

        Assert.Contains("id=\"moving-average-5\"", toolbar, StringComparison.Ordinal);
        Assert.Contains("id=\"moving-average-20\"", toolbar, StringComparison.Ordinal);
        Assert.Contains("id=\"playlist-move-up\"", toolbar, StringComparison.Ordinal);
        Assert.Contains("id=\"playlist-move-down\"", toolbar, StringComparison.Ordinal);
        Assert.Matches("id=\"playout-background-none\"[^>]*aria-keyshortcuts=\"F3\"", toolbar);
        Assert.Matches("id=\"playout-background-file\"[^>]*aria-keyshortcuts=\"F2\"", toolbar);
        Assert.Matches("id=\"playlist-delete-selected\"[^>]*aria-keyshortcuts=\"Delete\"", toolbar);
        Assert.Contains("id=\"db-load-button\"", toolbar, StringComparison.Ordinal);
        Assert.Contains("id=\"db-save-button\"", toolbar, StringComparison.Ordinal);
        Assert.Contains(".playlist-toolbar-group", Styles, StringComparison.Ordinal);
        Assert.Contains(".playlist-toolbar-background", Styles, StringComparison.Ordinal);
        Assert.Contains(".playlist-toolbar-danger", Styles, StringComparison.Ordinal);
        Assert.Contains("aria-keyshortcuts=\"F8\"", Markup, StringComparison.Ordinal);
        Assert.Contains("aria-keyshortcuts=\"Escape\"", Markup, StringComparison.Ordinal);
    }

    [Fact]
    public void SearchBusyAndEmptyFeedbackRemainAccessibleAndNonBlocking()
    {
        Assert.Matches(
            @"id=""stock-search-meta""[^>]*role=""status""[^>]*aria-live=""polite""" +
            @"[^>]*aria-atomic=""true""[^>]*hidden",
            Markup);
        Assert.Contains(".search-meta", Styles, StringComparison.Ordinal);
        Assert.Contains(".stock-results:empty[data-empty-label]::after", Styles,
            StringComparison.Ordinal);
        Assert.Contains(".cut-list:empty[data-empty-label]::after", Styles,
            StringComparison.Ordinal);
        Assert.Contains("content: attr(data-empty-label);", Styles, StringComparison.Ordinal);
        Assert.Contains("body.busy::before", Styles, StringComparison.Ordinal);
        Assert.Contains("@keyframes operator-progress", Styles, StringComparison.Ordinal);
        Assert.Contains("body.busy .operator-shell { pointer-events: none; }", Styles,
            StringComparison.Ordinal);

        Assert.Contains("function renderStockSearchMeta(state)", App,
            StringComparison.Ordinal);
        Assert.Contains("label = \"검색 중\";", App, StringComparison.Ordinal);
        Assert.Contains("stockResults.dataset.emptyLabel = \"결과 없음\";", App,
            StringComparison.Ordinal);
        Assert.Contains("? \"컷 없음\"", App, StringComparison.Ordinal);
        Assert.Contains(": \"종목을 선택하세요\"", App, StringComparison.Ordinal);
        Assert.Contains("status.textContent = isBusy ? \"처리 중\"", App,
            StringComparison.Ordinal);

        Assert.Contains("status.dataset.state = statusState(playout, busy);", Playout,
            StringComparison.Ordinal);
        Assert.Contains("return \"outcome-unknown\";", Playout, StringComparison.Ordinal);
        Assert.Contains(".playout-status[data-state=\"outcome-unknown\"]", Styles,
            StringComparison.Ordinal);
    }

    [Fact]
    public void WorkspaceHeaderUsesModernSemanticLockupAndStableNativeStatusTarget()
    {
        Assert.Matches(
            @"class=""brand""[\s\S]*?class=""brand-wordmark-plate""[\s\S]*?" +
            @"<img class=""brand-wordmark"" src=""Brand/logo\.png"" width=""176"" height=""43""[\s\S]*?" +
            @"alt=""매일경제TV"" draggable=""false"">[\s\S]*?" +
            @"class=""brand-divider"" aria-hidden=""true""[\s\S]*?" +
            @"class=""brand-product"">V-STOCK</span>",
            Markup);
        Assert.DoesNotContain("class=\"brand-symbol\"", Markup, StringComparison.Ordinal);
        Assert.DoesNotContain("class=\"brand-lockup\"", Markup, StringComparison.Ordinal);
        Assert.DoesNotContain(">VRi<", Markup, StringComparison.Ordinal);
        Assert.Matches(
            @"class=""workspace-heading""[\s\S]*?class=""workspace-heading-icon""" +
            @"[\s\S]*?<div class=""workspace-heading-copy"">[\s\S]*?" +
            @"<h1 id=""workspace-title"">종목·컷</h1>",
            Markup);
        var header = Slice(Markup, "<header class=\"workspace-header\">", "</header>");
        Assert.DoesNotContain("<small>", header, StringComparison.Ordinal);
        Assert.DoesNotContain("playout-connection-copy", header, StringComparison.Ordinal);
        Assert.DoesNotContain("<strong>Tornado2</strong>", header, StringComparison.Ordinal);
        Assert.Matches(
            @"id=""playout-connection-state""[^>]*role=""status""" +
            @"[^>]*aria-label=""Tornado2 연결 상태""[^>]*aria-live=""polite""" +
            @"[^>]*aria-atomic=""true"">미연결</div>",
            Markup);
        Assert.Matches(
            @"\.workspace-header::before,\s*\.playlist-heading::before\s*\{\s*display:\s*none;\s*\}",
            Styles);
        Assert.Contains(".connection-state::before", Styles, StringComparison.Ordinal);
        Assert.Contains(".connection-state.connected::before", Styles, StringComparison.Ordinal);
        Assert.Contains(
            ".connection-state.dry-run:not(.outcome-unknown)::before",
            Styles,
            StringComparison.Ordinal);
        Assert.Contains(
            "if (connection.textContent !== nextConnectionText)",
            Playout,
            StringComparison.Ordinal);
    }

    [Fact]
    public void NavigationStoresOnlyApprovedLocalPresentationState()
    {
        Assert.Contains("entry.view.hidden = !isCurrent", Navigation, StringComparison.Ordinal);
        Assert.Contains(
            "entry.view.toggleAttribute(\"inert\", !isCurrent)",
            Navigation,
            StringComparison.Ordinal);
        Assert.Contains("settingsTab.addEventListener(\"click\"", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("syncMarket(button.dataset.tabId)", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("menuButton.click()", Navigation, StringComparison.Ordinal);
        Assert.Contains("pendingKeyboardMarketTabId", Navigation, StringComparison.Ordinal);
        Assert.Contains("pendingButton.focus()", Navigation, StringComparison.Ordinal);
        Assert.Contains("window.requestAnimationFrame(function", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("options.authoritativeNativeRender === true", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("hasPendingKeyboardMarketFocus", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("document.addEventListener(\"pointerdown\"", Navigation,
            StringComparison.Ordinal);
        Assert.DoesNotContain("catalogWorkspace.contains(active)", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("LegacyWorkspaceNavigation", Navigation, StringComparison.Ordinal);
        Assert.Contains("nav-collapsed", Navigation, StringComparison.Ordinal);
        Assert.DoesNotContain("postMessage", Navigation, StringComparison.Ordinal);
        Assert.DoesNotContain("sessionStorage", Navigation, StringComparison.Ordinal);
        var storageKeys = Regex.Matches(
                Navigation,
                "\"(mbn-stock-webview\\.[^\"]+)\"")
            .Select(match => match.Groups[1].Value)
            .ToArray();
        Assert.Equal(
            new[]
            {
                "mbn-stock-webview.last-workspace.v1",
                "mbn-stock-webview.schedule-width.v1"
            },
            storageKeys);
        Assert.Contains("window.localStorage && window.localStorage.getItem(key)", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("window.localStorage.setItem(key, value)", Navigation,
            StringComparison.Ordinal);
    }

    [Fact]
    public void SplitterClampsPointerAndKeyboardResizingAndEscapeRestoresTheGesture()
    {
        foreach (var declaration in new[]
        {
            "const scheduleMinimumPercent = 34",
            "const scheduleMaximumPercent = 48",
            "const workspaceMinimumPixels = 780",
            "const scheduleMinimumPixels = 600",
            "const splitterPixels = 8"
        })
        {
            Assert.Contains(declaration, Navigation, StringComparison.Ordinal);
        }

        Assert.Matches(
            @"const minimum = Math\.max\([\s\S]*?scheduleMinimumPercent," +
            @"[\s\S]*?scheduleMinimumPixels / width \* 100\)",
            Navigation);
        Assert.Matches(
            @"const maximum = Math\.min\([\s\S]*?scheduleMaximumPercent," +
            @"[\s\S]*?\(width - workspaceMinimumPixels - splitterPixels\) / width \* 100\)",
            Navigation);
        Assert.Contains(
            "return Math.min(bounds.maximum, Math.max(bounds.minimum, value))",
            Navigation,
            StringComparison.Ordinal);
        Assert.Contains(
            "shell.style.setProperty(\"--schedule-pane-width\", serialized)",
            Navigation,
            StringComparison.Ordinal);
        foreach (var attribute in new[]
                 {
                     "aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext"
                 })
        {
            Assert.Contains($"splitter.setAttribute(\"{attribute}\"", Navigation,
                StringComparison.Ordinal);
        }
        Assert.Contains(
            "splitter.setAttribute(\"aria-valuemin\", bounds.minimum.toFixed(1))",
            Navigation,
            StringComparison.Ordinal);
        Assert.Contains(
            "splitter.setAttribute(\"aria-valuemax\", bounds.maximum.toFixed(1))",
            Navigation,
            StringComparison.Ordinal);
        Assert.Contains(
            "splitter.setAttribute(\"aria-valuenow\", scheduleWidthPercent.toFixed(1))",
            Navigation,
            StringComparison.Ordinal);
        Assert.DoesNotMatch(
            @"aria-valuemin""[\s\S]{0,80}Math\.ceil|" +
            @"aria-valuemax""[\s\S]{0,80}Math\.floor",
            Navigation);

        Assert.Contains(
            "event.button !== 0 || event.isPrimary === false || splitterGesture",
            Navigation,
            StringComparison.Ordinal);
        Assert.Contains("splitter.setPointerCapture?.(event.pointerId)", Navigation,
            StringComparison.Ordinal);
        foreach (var eventName in new[]
                 {
                     "pointerdown", "pointermove", "pointerup", "pointercancel",
                     "lostpointercapture"
                 })
        {
            Assert.Contains($"splitter.addEventListener(\"{eventName}\"", Navigation,
                StringComparison.Ordinal);
        }

        Assert.Matches(
            @"event\.key === ""Escape"" && splitterGesture[\s\S]*?" +
            @"event\.stopImmediatePropagation\(\)[\s\S]*?finishSplitterGesture\(true\)",
            Navigation);
        Assert.Contains("event.key === \"ArrowLeft\") next += 2", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("event.key === \"ArrowRight\") next -= 2", Navigation,
            StringComparison.Ordinal);
        Assert.Contains("event.key === \"Home\") next = numericScheduleBounds().minimum",
            Navigation,
            StringComparison.Ordinal);
        Assert.Contains("event.key === \"End\") next = numericScheduleBounds().maximum",
            Navigation,
            StringComparison.Ordinal);
        Assert.Matches(
            @"applyScheduleWidth\(\s*restore === true \? gesture\.startPercent" +
            @" : scheduleWidthPercent,[\s\S]*?restore !== true\)",
            Navigation);
    }

    [Fact]
    public void ExplicitMenuInputWinsOverDeferredLastWorkspaceRestore()
    {
        Assert.Matches(
            @"stockTab\.addEventListener\(""click"", function \(\) \{\s*" +
            @"startPreferenceApplied = true;\s*setWorkspace\(""stock""\);\s*\}\)",
            Navigation);
        Assert.Matches(
            @"settingsTab\.addEventListener\(""click"", function \(\) \{\s*" +
            @"startPreferenceApplied = true;\s*setWorkspace\(""settings""\);\s*\}\)",
            Navigation);
        Assert.Matches(
            @"marketTabs\.addEventListener\(""click"", function \(event\) \{" +
            @"[\s\S]*?if \(!button \|\| !marketTabs\.contains\(button\)\) return;\s*" +
            @"startPreferenceApplied = true;\s*syncMarket\(button\.dataset\.tabId\);\s*" +
            @"setWorkspace\(""catalog""\);",
            Navigation);
    }

    [Fact]
    public void VisualCardModeReflowsOnlyGraphicActionsAndKeepsNativeIntentButtons()
    {
        var viewModes = Slice(
            Styles,
            "/* View modes affect graphic actions only.",
            "/* End view-mode presentation. */");

        Assert.Contains("[data-visual-card-list=\"true\"]", viewModes,
            StringComparison.Ordinal);
        Assert.Contains(
            "@container operator-workspace (min-width: 520px)",
            viewModes,
            StringComparison.Ordinal);
        Assert.Contains("grid-template-columns: repeat(2, minmax(0, 1fr))", viewModes,
            StringComparison.Ordinal);
        Assert.Contains("@container operator-workspace (min-width: 820px)", viewModes,
            StringComparison.Ordinal);
        Assert.Contains("grid-template-columns: repeat(3, minmax(0, 1fr))", viewModes,
            StringComparison.Ordinal);
        Assert.Contains("button.visual-card", viewModes, StringComparison.Ordinal);
        Assert.Contains("min-height: 62px", viewModes, StringComparison.Ordinal);
        Assert.DoesNotContain("grid-auto-rows: minmax(62px", viewModes,
            StringComparison.Ordinal);
        Assert.DoesNotContain(".visual-card-preview", viewModes,
            StringComparison.Ordinal);
        Assert.DoesNotContain(".visual-card-kicker", viewModes,
            StringComparison.Ordinal);
        Assert.DoesNotContain(".visual-card-state", viewModes,
            StringComparison.Ordinal);
        Assert.Matches(@"\.comparison-actions\s*\{\s*display:\s*block;", viewModes);
        Assert.Contains(
            "summary[data-tree-root].tree-node-selected",
            viewModes,
            StringComparison.Ordinal);
        Assert.Contains("@container operator-workspace (max-width: 759px)", viewModes,
            StringComparison.Ordinal);
        Assert.DoesNotContain(".stock-result", viewModes, StringComparison.Ordinal);
        Assert.DoesNotContain(".cut-row", viewModes, StringComparison.Ordinal);
        Assert.DoesNotContain(".playlist-data-row", viewModes, StringComparison.Ordinal);
        Assert.DoesNotContain(".operator-catalog-draft", viewModes, StringComparison.Ordinal);
        Assert.DoesNotContain(".overseas-period-actions", viewModes, StringComparison.Ordinal);
        Assert.Matches(@"\.industry-action-tree\s*\{\s*overflow:\s*auto;", viewModes);

        Assert.Contains("function setVisualCardContent(button, options)", App,
            StringComparison.Ordinal);
        Assert.Contains("button.dataset.actionId = action.id", App,
            StringComparison.Ordinal);
        Assert.Contains("button.dataset.industryActionId = action.actionId", App,
            StringComparison.Ordinal);
        Assert.Contains("button.dataset.comparisonActionId = action.actionId", App,
            StringComparison.Ordinal);
        Assert.Contains("button.dataset.themeActionId = action.actionId", App,
            StringComparison.Ordinal);
        Assert.Contains("button.dataset.expertActionId = action.actionId", App,
            StringComparison.Ordinal);
        Assert.Contains("button.dataset.haltActionId = action.actionId", App,
            StringComparison.Ordinal);
        Assert.Matches(
            @"button\.dataset\.overseasActionId = action\.actionId;\s*" +
            @"button\.textContent = label;",
            App);
    }

    private static string Slice(string source, string startMarker, string endMarker)
    {
        var start = source.IndexOf(startMarker, StringComparison.Ordinal);
        Assert.True(start >= 0, $"Missing start marker: {startMarker}");
        var end = source.IndexOf(endMarker, start, StringComparison.Ordinal);
        Assert.True(end > start, $"Missing end marker: {endMarker}");
        return source[start..end];
    }

    private static string FindRepositoryRoot()
    {
        var directory = new DirectoryInfo(AppContext.BaseDirectory);
        while (directory is not null)
        {
            if (File.Exists(Path.Combine(directory.FullName, "MBN_STOCK_WEBVIEW.sln")))
            {
                return directory.FullName;
            }

            directory = directory.Parent;
        }

        throw new DirectoryNotFoundException("Repository root could not be located.");
    }
}
