from __future__ import annotations

import subprocess
import sys
import textwrap
import unittest
from pathlib import Path

from fastapi.staticfiles import StaticFiles
from starlette.routing import Mount

sys.path.insert(0, str(Path(__file__).resolve().parents[3]))

from backend.app.main import app


class UiSmokeGoldenTest(unittest.TestCase):
    def _ui_mount(self) -> Mount:
        for route in app.routes:
            if isinstance(route, Mount) and route.path == "/ui":
                return route
        self.fail("Expected /ui mount to be registered")

    def _extract_js_function(self, source: str, name: str) -> str:
        marker = f"function {name}("
        start = source.find(marker)
        if start < 0:
            self.fail(f"Expected function {name} in app.js")
        brace_start = source.find("{", start)
        if brace_start < 0:
            self.fail(f"Expected opening brace for function {name}")
        depth = 0
        for index in range(brace_start, len(source)):
            char = source[index]
            if char == "{":
                depth += 1
            elif char == "}":
                depth -= 1
                if depth == 0:
                    return source[start : index + 1]
        self.fail(f"Expected closing brace for function {name}")

    def _run_app_js_behavior_check(self, app_js: str) -> None:
        functions = "\n\n".join(
            [
                self._extract_js_function(app_js, "paneState"),
                self._extract_js_function(app_js, "currentParentPath"),
                self._extract_js_function(app_js, "baseName"),
                self._extract_js_function(app_js, "prepareParentReturnRestore"),
                self._extract_js_function(app_js, "navigateToParent"),
                self._extract_js_function(app_js, "restoreParentReturnFocus"),
                self._extract_js_function(app_js, "handleKeyboardShortcuts"),
            ]
        )
        script = textwrap.dedent(
            f"""
            const assert = (condition, message) => {{
              if (!condition) {{
                throw new Error(message);
              }}
            }};

            let state = {{
              activePane: "left",
              panes: {{
                left: {{
                  currentPath: "storage1/parent/submap",
                  showHidden: false,
                  selectedItem: null,
                  selectedItems: [],
                  visibleItems: [],
                  currentRowIndex: -1,
                  selectionAnchorIndex: null,
                  pendingSelectionPath: null,
                  returnFocusName: null,
                }},
                right: {{
                  currentPath: "/Volumes",
                  showHidden: false,
                  selectedItem: null,
                  selectedItems: [],
                  visibleItems: [],
                  currentRowIndex: -1,
                  selectionAnchorIndex: null,
                  pendingSelectionPath: null,
                  returnFocusName: null,
                }},
              }},
            }};

            const navigationCalls = [];
            const renderCalls = [];

            function navigateTo(pane, path) {{
              navigationCalls.push({{ pane, path }});
            }}

            function renderPaneItems(pane) {{
              renderCalls.push({{ pane, currentRowIndex: paneState(pane).currentRowIndex }});
              const model = paneState(pane);
              if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {{
                model.currentRowIndex = -1;
                return;
              }}
              if (model.currentRowIndex < 0 || model.currentRowIndex >= model.visibleItems.length) {{
                model.currentRowIndex = 0;
              }}
            }}

            function isContextMenuOpen() {{ return false; }}
            function uploadElements() {{ return {{ menuPopup: {{ classList: {{ contains() {{ return true; }} }} }} }}; }}
            function isFeedbackModalOpen() {{ return false; }}
            function closeFeedbackModal() {{}}
            function isDownloadModalOpen() {{ return false; }}
            function closeDownloadModal() {{}}
            function isInfoOpen() {{ return false; }}
            function closeInfo() {{}}
            function isSearchOpen() {{ return false; }}
            function closeSearch() {{}}
            function submitSearch() {{}}
            function isRenamePopupOpen() {{ return false; }}
            function closeRenamePopup() {{}}
            function submitRenamePopup() {{}}
            function isSettingsOpen() {{ return false; }}
            function closeSettings() {{}}
            function isBatchMovePopupOpen() {{ return false; }}
            function closeBatchMovePopup() {{}}
            function submitBatchMovePopup() {{}}
            function isDeleteConfirmModalOpen() {{ return false; }}
            function closeDeleteConfirmModal() {{}}
            function submitDeleteConfirmModal() {{}}
            function isUploadConflictModalOpen() {{ return false; }}
            function resolveUploadConflict() {{}}
            function isMovePopupOpen() {{ return false; }}
            function closeMovePopup() {{}}
            function submitMovePopup() {{}}
            function isEditorOpen() {{ return false; }}
            function attemptCloseEditor() {{}}
            function isImageOpen() {{ return false; }}
            function closeImageViewer() {{}}
            function isVideoOpen() {{ return false; }}
            function closeVideoViewer() {{}}
            function isPdfOpen() {{ return false; }}
            function closePdfViewer() {{}}
            function isViewerOpen() {{ return false; }}
            function closeViewer() {{}}
            function isWildcardPopupOpen() {{ return false; }}
            function shouldHandleShortcut() {{ return true; }}
            function openInfo() {{}}
            function openSearch() {{}}
            function actionShortcutHandled() {{ return false; }}
            function openWildcardPopup() {{}}
            function jumpCurrentRow() {{}}
            function moveCurrentRow() {{}}
            function extendSelectionByRow() {{}}
            function otherPane(pane) {{ return pane === "left" ? "right" : "left"; }}
            function setActivePane(pane) {{ state.activePane = pane; }}
            function openCurrentDirectory() {{}}
            function toggleCurrentSelection() {{}}
            function clearSelectionForActivePane() {{}}

            {functions}

            let prevented = false;
            handleKeyboardShortcuts({{
              key: "Backspace",
              code: "Backspace",
              shiftKey: false,
              altKey: false,
              metaKey: false,
              ctrlKey: false,
              target: null,
              preventDefault() {{ prevented = true; }},
            }});
            assert(prevented, "Backspace should prevent default");
            assert(navigationCalls.length === 1, "Backspace should trigger parent navigation");
            assert(navigationCalls[0].path === "storage1/parent", "Backspace should navigate to parent path");
            assert(paneState("left").returnFocusName === "submap", "Backspace should prepare return focus by child name");
            assert(paneState("left").pendingSelectionPath === null, "Backspace parent return must not set pending selection path");
            assert(paneState("left").selectedItems.length === 0, "Backspace parent return must not select items");

            state.panes.left.currentPath = "storage1/parent/submap";
            state.panes.left.returnFocusName = null;
            state.panes.left.pendingSelectionPath = null;
            navigationCalls.length = 0;
            navigateToParent("left");
            assert(navigationCalls.length === 1, "Mouse parent-up should trigger parent navigation");
            assert(navigationCalls[0].path === "storage1/parent", "Mouse parent-up should navigate to parent path");
            assert(paneState("left").returnFocusName === "submap", "Mouse parent-up should use same restore flow");
            assert(paneState("left").pendingSelectionPath === null, "Mouse parent-up must not set pending selection path");

            state.panes.left.visibleItems = [
              {{ path: "storage1/parent", name: "..", kind: "directory", isParent: true }},
              {{ path: "storage1/parent/submap", name: "submap", kind: "directory" }},
              {{ path: "storage1/parent/other", name: "other", kind: "directory" }},
            ];
            state.panes.left.currentRowIndex = 0;
            state.panes.left.selectedItems = [];
            state.panes.left.selectedItem = null;
            state.panes.left.selectionAnchorIndex = null;
            state.panes.left.returnFocusName = "submap";
            renderCalls.length = 0;
            restoreParentReturnFocus("left", state.panes.left.visibleItems);
            assert(state.panes.left.currentRowIndex === 1, "Restore should focus the child entry in parent");
            assert(state.panes.left.selectedItems.length === 0, "Restore must not add selection");
            assert(state.panes.left.selectedItem === null, "Restore must not set selectedItem");
            assert(state.panes.left.selectionAnchorIndex === null, "Restore must not set selection anchor");
            assert(state.panes.left.returnFocusName === null, "Restore state should be cleared after success");
            assert(renderCalls.length >= 1, "Restore should re-render focused row");

            state.panes.left.currentRowIndex = 0;
            state.panes.left.selectedItems = [];
            state.panes.left.selectedItem = null;
            state.panes.left.selectionAnchorIndex = null;
            state.panes.left.returnFocusName = "missing";
            renderCalls.length = 0;
            restoreParentReturnFocus("left", state.panes.left.visibleItems);
            assert(state.panes.left.currentRowIndex === 0, "Fallback should keep existing row when match is missing");
            assert(state.panes.left.selectedItems.length === 0, "Fallback must not create selection");
            assert(state.panes.left.selectedItem === null, "Fallback must not set selectedItem");
            assert(state.panes.left.selectionAnchorIndex === null, "Fallback must not set selection anchor");
            assert(state.panes.left.returnFocusName === null, "Restore state should be cleared after failed match");
            """
        )
        result = subprocess.run(
            ["node", "-e", script],
            cwd="/workspace/webmanager-mvp",
            capture_output=True,
            text=True,
            check=False,
        )
        self.assertEqual(result.returncode, 0, msg=result.stderr or result.stdout)

    def _run_header_task_chip_behavior_check(self, app_js: str) -> None:
        functions = "\n\n".join(
            [
                self._extract_js_function(app_js, "headerTaskElements"),
                self._extract_js_function(app_js, "formatTaskStatusLabel"),
                self._extract_js_function(app_js, "inferDownloadTaskContext"),
                self._extract_js_function(app_js, "formatTaskLine"),
                self._extract_js_function(app_js, "isActiveTask"),
                self._extract_js_function(app_js, "activeTasksFromItems"),
                self._extract_js_function(app_js, "taskIsCancellable"),
                self._extract_js_function(app_js, "cancelTaskRequest"),
                self._extract_js_function(app_js, "formatTaskOperationLabel"),
                self._extract_js_function(app_js, "hasMeaningfulItemProgress"),
                self._extract_js_function(app_js, "canShowChipItemProgress"),
                self._extract_js_function(app_js, "compactTaskCurrentItem"),
                self._extract_js_function(app_js, "activeTaskChipLabel"),
                self._extract_js_function(app_js, "taskProgressText"),
                self._extract_js_function(app_js, "taskProgressSubtext"),
                self._extract_js_function(app_js, "headerTaskRenderKey"),
                self._extract_js_function(app_js, "shouldPollHeaderTasks"),
                self._extract_js_function(app_js, "stopHeaderTaskPolling"),
                self._extract_js_function(app_js, "scheduleHeaderTaskPolling"),
                self._extract_js_function(app_js, "setHeaderTaskPopoverOpen"),
                self._extract_js_function(app_js, "renderHeaderTaskPopover"),
                self._extract_js_function(app_js, "renderHeaderTaskChip"),
                self._extract_js_function(app_js, "updateHeaderTaskState"),
            ]
        )
        script = textwrap.dedent(
            f"""
            const assert = (condition, message) => {{
              if (!condition) {{
                throw new Error(message);
              }}
            }};

            function createClassList(initialHidden = false) {{
              const names = new Set(initialHidden ? ["hidden"] : []);
              return {{
                add(name) {{ names.add(name); }},
                remove(name) {{ names.delete(name); }},
                toggle(name, force) {{
                  if (force === undefined) {{
                    if (names.has(name)) {{
                      names.delete(name);
                    }} else {{
                      names.add(name);
                    }}
                    return;
                  }}
                  if (force) {{
                    names.add(name);
                  }} else {{
                    names.delete(name);
                  }}
                }},
                contains(name) {{ return names.has(name); }},
              }};
            }}

            function createElement(initialHidden = false) {{
              return {{
                classList: createClassList(initialHidden),
                textContent: "",
                innerHTML: "",
                children: [],
                disabled: false,
                onclick: null,
                scrollTop: 0,
                title: "",
                attributes: {{}},
                append(...nodes) {{
                  this.children.push(...nodes);
                }},
                setAttribute(name, value) {{
                  this.attributes[name] = value;
                }},
              }};
            }}

            const elements = {{
              "header-task-chip-container": createElement(true),
              "header-task-chip-btn": createElement(false),
              "header-task-chip-label": createElement(false),
              "header-task-popover": createElement(true),
              "header-task-popover-list": createElement(false),
              "header-task-logs-btn": createElement(false),
            }};

            const document = {{
              getElementById(id) {{
                return elements[id] || null;
              }},
              createElement() {{
                return createElement(false);
              }},
            }};

            const window = {{
              setTimeout(fn, delay) {{
                return 1;
              }},
              clearTimeout(id) {{}},
            }};

            function formatModified(value) {{
              return value || "now";
            }}

            async function refreshTasksSnapshot() {{}}
            function setError() {{}}

            let headerTaskState = {{
              activeItems: [],
              popoverOpen: false,
              pollTimer: null,
              lastRenderKey: "",
            }};
            const ACTIVE_TASK_OPERATIONS = new Set(["copy", "move", "duplicate", "delete"]);
            const ACTIVE_TASK_STATUSES = new Set(["queued", "running", "cancelling"]);

            {functions}

            const mixedTasks = [
              {{ id: "a", operation: "copy", status: "queued", source: "/src/a", destination: "/dst/a" }},
              {{ id: "b", operation: "move", status: "running", source: "/src/b", destination: "/dst/b", done_items: 1, total_items: 3, current_item: "b.mkv" }},
              {{ id: "c", operation: "download", status: "requested", source: "/src/c", destination: "kodidownload-20260315-120000.zip" }},
              {{ id: "d", operation: "download", status: "preparing", source: "/src/d", destination: "folder.zip" }},
              {{ id: "dup", operation: "duplicate", status: "queued", source: "/src/dup", destination: "/dst/dup" }},
              {{ id: "del", operation: "delete", status: "running", source: "/src/del", destination: "" }},
              {{ id: "stop", operation: "copy", status: "cancelling", source: "/src/stop", destination: "/dst/stop", done_items: 1, total_items: 4, current_item: "nested/final-file.txt" }},
              {{ id: "e", operation: "copy", status: "completed", source: "/src/e", destination: "/dst/e" }},
              {{ id: "f", operation: "move", status: "failed", source: "/src/f", destination: "/dst/f" }},
              {{ id: "g", operation: "download", status: "ready", source: "/src/g", destination: "folder.zip" }},
              {{ id: "h", operation: "download", status: "cancelled", source: "/src/h", destination: "folder.zip" }},
            ];

            const activeTasks = activeTasksFromItems(mixedTasks);
            assert(activeTasks.length === 5, "Only active task-based file actions should count as active");
            assert(activeTasks.every((task) => isActiveTask(task)), "All filtered tasks should be active");
            assert(activeTasks.some((task) => task.operation === "delete"), "Delete should count once it uses the shared task flow");
            assert(activeTasks.some((task) => task.status === "cancelling"), "Cancelling tasks should remain visible while stopping");
            assert(activeTaskChipLabel(activeTasks) === "5 active tasks", "Chip label should reflect active task count");

            updateHeaderTaskState(mixedTasks);
            assert(!elements["header-task-chip-container"].classList.contains("hidden"), "Chip should be visible with active tasks");
            assert(elements["header-task-chip-label"].textContent === "5 active tasks", "Chip label should render active task count");
            assert(shouldPollHeaderTasks(), "Active tasks should enable header polling");

            setHeaderTaskPopoverOpen(true);
            assert(headerTaskState.popoverOpen, "Popover should open when active tasks exist");
            assert(!elements["header-task-popover"].classList.contains("hidden"), "Popover should be visible when open");
            assert(elements["header-task-chip-btn"].attributes["aria-expanded"] === "true", "Chip button should expose expanded state");
            assert(elements["header-task-popover-list"].children.length === 5, "Popover should render only active file-action tasks");
            const moveRow = elements["header-task-popover-list"].children[1];
            const moveProgress = moveRow.children[3];
            const moveCurrent = moveRow.children[4];
            assert(moveProgress.textContent === "1/3", "Popover should show done/total progress when available");
            assert(moveCurrent.textContent === "b.mkv", "Popover should show compact current item");
            const cancellingRow = elements["header-task-popover-list"].children[4];
            const cancellingProgress = cancellingRow.children[3];
            const cancellingCurrent = cancellingRow.children[4];
            const cancellingSubtext = cancellingRow.children[5];
            assert(cancellingProgress.textContent === "1/4", "Cancelling tasks should keep progress visible");
            assert(cancellingCurrent.textContent === "nested/final-file.txt", "Cancelling tasks should show current item");
            assert(cancellingSubtext.textContent === "Stopping after current item...", "Cancelling tasks should explain stop semantics");
            const firstActionButton = elements["header-task-popover-list"].children[0].children[3].children[0];
            const cancellingActionButton = elements["header-task-popover-list"].children[4].children[6].children[0];
            assert(firstActionButton.textContent === "Stop", "Queued/running tasks should expose a Stop action");
            assert(!firstActionButton.disabled, "Queued/running tasks should be cancellable");
            assert(cancellingActionButton.textContent === "Stopping...", "Cancelling tasks should show stopping state");
            assert(cancellingActionButton.disabled, "Cancelling tasks should not expose a second stop action");

            updateHeaderTaskState([
              {{ id: "single-copy", operation: "copy", status: "running", source: "/src/a", destination: "/dst/a", done_items: 7, total_items: 20, current_item: "season1/episode07.mkv" }},
            ]);
            assert(elements["header-task-chip-label"].textContent === "Copy 7/20", "Single copy task should show compact item progress in chip");

            updateHeaderTaskState([
              {{ id: "single-dup", operation: "duplicate", status: "running", source: "/src/a", destination: "/dst/a copy", done_items: 3, total_items: 12, current_item: "nested/file.txt" }},
            ]);
            assert(elements["header-task-chip-label"].textContent === "Duplicate 3/12", "Single duplicate task should show compact item progress in chip");

            updateHeaderTaskState([
              {{ id: "single-move", operation: "move", status: "running", source: "/src/dir", destination: "/dst/dir", done_items: 0, total_items: 1, current_item: "Folder" }},
            ]);
            assert(elements["header-task-chip-label"].textContent === "Move running", "Single move task should stay coarse in chip");

            updateHeaderTaskState([
              {{ id: "single-cancelling", operation: "copy", status: "cancelling", source: "/src/a", destination: "/dst/a", done_items: 2, total_items: 5, current_item: "nested/file.txt" }},
            ]);
            assert(elements["header-task-chip-label"].textContent === "Copy cancelling", "Single cancelling task should surface cancelling state in chip");

            updateHeaderTaskState([
              {{ id: "z1", operation: "copy", status: "completed", source: "/src/z1", destination: "/dst/z1" }},
              {{ id: "z2", operation: "move", status: "failed", source: "/src/z2", destination: "/dst/z2" }},
              {{ id: "z3", operation: "download", status: "ready", source: "/src/z3", destination: "folder.zip" }},
            ]);
            assert(elements["header-task-chip-container"].classList.contains("hidden"), "Chip should hide when no active tasks remain");
            assert(!headerTaskState.popoverOpen, "Popover should close when no active tasks remain");
            assert(elements["header-task-popover"].classList.contains("hidden"), "Popover should be hidden when no active tasks remain");
            assert(elements["header-task-chip-btn"].attributes["aria-expanded"] === "false", "Chip button should reset expanded state when hidden");
            """
        )
        result = subprocess.run(
            ["node", "-e", script],
            cwd="/workspace/webmanager-mvp",
            capture_output=True,
            text=True,
            check=False,
        )
        self.assertEqual(result.returncode, 0, msg=result.stderr or result.stdout)

    def _run_task_snapshot_sync_behavior_check(self, app_js: str) -> None:
        functions = "\n\n".join(
            [
                self._extract_js_function(app_js, "headerTaskElements"),
                self._extract_js_function(app_js, "formatTaskStatusLabel"),
                self._extract_js_function(app_js, "inferDownloadTaskContext"),
                self._extract_js_function(app_js, "formatTaskLine"),
                self._extract_js_function(app_js, "isActiveTask"),
                self._extract_js_function(app_js, "activeTasksFromItems"),
                self._extract_js_function(app_js, "taskIsCancellable"),
                self._extract_js_function(app_js, "cancelTaskRequest"),
                self._extract_js_function(app_js, "formatTaskOperationLabel"),
                self._extract_js_function(app_js, "hasMeaningfulItemProgress"),
                self._extract_js_function(app_js, "canShowChipItemProgress"),
                self._extract_js_function(app_js, "compactTaskCurrentItem"),
                self._extract_js_function(app_js, "activeTaskChipLabel"),
                self._extract_js_function(app_js, "taskProgressText"),
                self._extract_js_function(app_js, "taskProgressSubtext"),
                self._extract_js_function(app_js, "headerTaskRenderKey"),
                self._extract_js_function(app_js, "shouldPollHeaderTasks"),
                self._extract_js_function(app_js, "stopHeaderTaskPolling"),
                self._extract_js_function(app_js, "scheduleHeaderTaskPolling"),
                self._extract_js_function(app_js, "setHeaderTaskPopoverOpen"),
                self._extract_js_function(app_js, "renderHeaderTaskPopover"),
                self._extract_js_function(app_js, "renderHeaderTaskChip"),
                self._extract_js_function(app_js, "updateHeaderTaskState"),
                self._extract_js_function(app_js, "applyTaskSnapshot"),
            ]
        )
        script = textwrap.dedent(
            f"""
            const assert = (condition, message) => {{
              if (!condition) {{
                throw new Error(message);
              }}
            }};

            function createClassList(initialHidden = false) {{
              const names = new Set(initialHidden ? ["hidden"] : []);
              return {{
                add(name) {{ names.add(name); }},
                remove(name) {{ names.delete(name); }},
                toggle(name, force) {{
                  if (force === undefined) {{
                    if (names.has(name)) {{
                      names.delete(name);
                    }} else {{
                      names.add(name);
                    }}
                    return;
                  }}
                  if (force) {{
                    names.add(name);
                  }} else {{
                    names.delete(name);
                  }}
                }},
                contains(name) {{ return names.has(name); }},
              }};
            }}

            function createElement(initialHidden = false) {{
              return {{
                classList: createClassList(initialHidden),
                textContent: "",
                innerHTML: "",
                children: [],
                disabled: false,
                onclick: null,
                scrollTop: 0,
                attributes: {{}},
                append(...nodes) {{
                  this.children.push(...nodes);
                }},
                setAttribute(name, value) {{
                  this.attributes[name] = value;
                }},
              }};
            }}

            const elements = {{
              "header-task-chip-container": createElement(true),
              "header-task-chip-btn": createElement(false),
              "header-task-chip-label": createElement(false),
              "header-task-popover": createElement(true),
              "header-task-popover-list": createElement(false),
              "header-task-logs-btn": createElement(false),
            }};

            const document = {{
              getElementById(id) {{
                return elements[id] || null;
              }},
              createElement() {{
                return createElement(false);
              }},
            }};

            const window = {{
              setTimeout(fn, delay) {{
                return 1;
              }},
              clearTimeout(id) {{}},
            }};

            function formatModified(value) {{
              return value || "now";
            }}

            async function refreshTasksSnapshot() {{}}
            function setError() {{}}

            let state = {{ lastTaskCount: 0 }};
            let headerTaskState = {{
              activeItems: [],
              popoverOpen: false,
              pollTimer: null,
              lastRenderKey: "",
            }};
            const ACTIVE_TASK_OPERATIONS = new Set(["copy", "move", "duplicate", "delete"]);
            const ACTIVE_TASK_STATUSES = new Set(["queued", "running", "cancelling"]);

            {functions}

            applyTaskSnapshot([
              {{ id: "copy-1", operation: "copy", status: "running", source: "/src", destination: "/dst" }},
            ]);
            assert(!elements["header-task-chip-container"].classList.contains("hidden"), "Running task should make chip visible");
            assert(elements["header-task-chip-label"].textContent === "Copy running", "Single active task should show compact task status");
            assert(headerTaskState.activeItems.length === 1, "Snapshot should store active task state");

            applyTaskSnapshot([
              {{ id: "copy-1", operation: "copy", status: "completed", source: "/src", destination: "/dst" }},
            ]);
            assert(elements["header-task-chip-container"].classList.contains("hidden"), "Chip should hide when latest task snapshot has no active tasks");
            assert(headerTaskState.activeItems.length === 0, "Active task state should be reset when tasks are completed");
            assert(state.lastTaskCount === 1, "Total task snapshot should still reflect fetched tasks list length");
            """
        )
        result = subprocess.run(
            ["node", "-e", script],
            cwd="/workspace/webmanager-mvp",
            capture_output=True,
            text=True,
            check=False,
        )
        self.assertEqual(result.returncode, 0, msg=result.stderr or result.stdout)

    def test_ui_mount_and_index_contains_expected_panels(self) -> None:
        mount = self._ui_mount()
        self.assertIsInstance(mount.app, StaticFiles)
        index_path = Path(mount.app.directory) / "index.html"
        self.assertTrue(index_path.exists())

        body = index_path.read_text(encoding="utf-8")
        self.assertIn('/ui/assets/img/favicon.svg', body)
        self.assertIn('/ui/base.css', body)
        self.assertIn('/ui/theme-default.css', body)
        self.assertIn('/ui/theme-macos-soft.css', body)
        self.assertIn('/ui/theme-midnight.css', body)
        self.assertIn('/ui/theme-graphite.css', body)
        self.assertIn('/ui/theme-windows11.css', body)
        self.assertIn('/ui/theme-commander-electric.css', body)
        self.assertIn('/ui/theme-nord-arctic.css', body)
        self.assertIn('/ui/theme-catppuccin-soft.css', body)
        self.assertIn('/ui/theme-fluent-neon.css', body)
        self.assertIn('id="workspace"', body)
        self.assertIn('id="footer-bar"', body)
        self.assertIn('id="title-brand"', body)
        self.assertIn('id="title-logo"', body)
        self.assertIn('/ui/assets/img/logo.svg', body)
        self.assertIn('id="title-zone-actions"', body)
        self.assertIn('id="status"', body)
        self.assertIn('id="header-task-chip-container"', body)
        self.assertIn('id="header-task-chip-btn"', body)
        self.assertIn('id="header-task-popover"', body)
        self.assertIn('id="header-task-popover-list"', body)
        self.assertIn('id="header-task-logs-btn"', body)
        self.assertIn('id="theme-toggle"', body)
        self.assertIn('id="theme-toggle-icon"', body)
        self.assertIn('id="left-pane"', body)
        self.assertIn('id="right-pane"', body)
        self.assertIn('id="left-items"', body)
        self.assertIn('id="right-items"', body)
        self.assertIn('id="left-focus-line"', body)
        self.assertIn('id="right-focus-line"', body)
        self.assertIn('id="function-bar"', body)
        self.assertIn('id="upload-btn"', body)
        self.assertIn('id="upload-menu-toggle"', body)
        self.assertIn('id="upload-menu-popup"', body)
        self.assertIn('id="upload-folder-btn"', body)
        self.assertIn('id="upload-input"', body)
        self.assertIn('id="upload-menu"', body)
        self.assertIn('id="upload-menu-toggle"', body)
        self.assertIn('id="upload-menu-popup"', body)
        self.assertIn('id="upload-folder-btn"', body)
        self.assertIn('id="upload-modal"', body)
        self.assertIn('id="upload-modal-target"', body)
        self.assertIn('id="upload-modal-current-file"', body)
        self.assertIn('id="upload-modal-progress-bar"', body)
        self.assertIn('id="upload-modal-count"', body)
        self.assertIn('id="upload-modal-status"', body)
        self.assertIn('id="upload-modal-cancel-btn"', body)
        self.assertIn('id="feedback-modal"', body)
        self.assertIn('id="feedback-message"', body)
        self.assertIn('id="feedback-close-btn"', body)
        self.assertIn('id="download-modal"', body)
        self.assertIn('id="download-modal-target"', body)
        self.assertIn('id="download-modal-current-file"', body)
        self.assertIn('id="download-modal-progress-bar"', body)
        self.assertIn('id="download-modal-count"', body)
        self.assertIn('id="download-modal-status"', body)
        self.assertIn('id="download-modal-logs-btn"', body)
        self.assertIn('id="download-modal-cancel-btn"', body)
        self.assertIn('id="download-modal-close-btn"', body)
        self.assertIn('id="context-menu"', body)
        self.assertIn('id="context-menu-scope"', body)
        self.assertIn('id="context-menu-target"', body)
        self.assertIn('id="context-menu-open-btn"', body)
        self.assertIn('id="context-menu-edit-btn"', body)
        self.assertIn('id="context-menu-download-btn"', body)
        self.assertIn('id="context-menu-rename-btn"', body)
        self.assertIn('id="context-menu-duplicate-btn"', body)
        self.assertIn('id="context-menu-copy-btn"', body)
        self.assertIn('id="context-menu-move-btn"', body)
        self.assertIn('id="context-menu-delete-btn"', body)
        self.assertIn('id="context-menu-properties-btn"', body)
        self.assertIn('id="settings-btn"', body)
        self.assertIn('id="rename-btn"', body)
        self.assertIn('id="view-btn"', body)
        self.assertIn('id="edit-btn"', body)
        self.assertIn("F1", body)
        self.assertIn("F2", body)
        self.assertIn("F3", body)
        self.assertIn("F4", body)
        self.assertIn("F5", body)
        self.assertIn("F6", body)
        self.assertIn("F7", body)
        self.assertIn("F8", body)
        self.assertIn('id="viewer-modal"', body)
        self.assertIn('id="video-modal"', body)
        self.assertIn('id="pdf-modal"', body)
        self.assertIn('id="image-modal"', body)
        self.assertIn('id="image-viewer-img"', body)
        self.assertIn('id="image-zoom-in-btn"', body)
        self.assertIn('id="image-zoom-out-btn"', body)
        self.assertIn('id="image-reset-btn"', body)
        self.assertIn('id="pdf-frame"', body)
        self.assertIn('id="pdf-close-btn"', body)
        self.assertIn('id="video-player"', body)
        self.assertIn('id="video-close-btn"', body)
        self.assertIn('id="settings-modal"', body)
        self.assertIn('id="search-modal"', body)
        self.assertIn('id="search-input"', body)
        self.assertIn('id="search-results"', body)
        self.assertIn('id="info-modal"', body)
        self.assertIn('id="rename-popup"', body)
        self.assertIn('id="rename-label"', body)
        self.assertIn('id="rename-input"', body)
        self.assertIn('id="rename-apply-btn"', body)
        self.assertIn('id="settings-general-tab"', body)
        self.assertIn('id="settings-interface-tab"', body)
        self.assertIn('id="settings-downloads-tab"', body)
        self.assertIn('id="settings-logs-tab"', body)
        self.assertIn('id="settings-show-thumbnails"', body)
        self.assertIn("Show thumbnails", body)
        self.assertIn('id="settings-selected-theme"', body)
        self.assertIn("Theme", body)
        self.assertIn('value="default"', body)
        self.assertIn('value="macos-soft"', body)
        self.assertIn('value="midnight"', body)
        self.assertIn('value="graphite"', body)
        self.assertIn('value="windows11"', body)
        self.assertIn('value="commander-electric"', body)
        self.assertIn('value="nord-arctic"', body)
        self.assertIn('value="catppuccin-soft"', body)
        self.assertIn('value="fluent-neon"', body)
        self.assertNotIn('id="settings-selected-color-mode"', body)
        self.assertIn('id="settings-startup-path-left"', body)
        self.assertIn('id="settings-startup-path-right"', body)
        self.assertIn("Preferred startup path (left)", body)
        self.assertIn("Preferred startup path (right)", body)
        self.assertIn('id="settings-general-save-btn"', body)
        self.assertIn('id="settings-interface-save-btn"', body)
        self.assertIn('id="settings-downloads-panel"', body)
        self.assertIn('id="settings-download-max-items"', body)
        self.assertIn('id="settings-download-max-total-size"', body)
        self.assertIn('id="settings-download-max-file-size"', body)
        self.assertIn('id="settings-download-scan-timeout"', body)
        self.assertIn('id="settings-download-symlink-policy"', body)
        self.assertIn("ZIP download limits are shown for reference and cannot be changed here.", body)
        self.assertIn('class="settings-activity-grid"', body)
        self.assertIn('class="settings-activity-panel"', body)
        self.assertIn('id="settings-tasks-title"', body)
        self.assertIn('id="settings-history-title"', body)
        self.assertIn('id="settings-tasks-list"', body)
        self.assertIn('id="settings-logs-list"', body)
        self.assertIn('id="viewer-content"', body)
        self.assertIn('id="editor-modal"', body)
        self.assertIn('id="editor-host"', body)
        self.assertIn('id="editor-save-btn"', body)
        self.assertIn('id="editor-cancel-btn"', body)
        self.assertIn('id="move-popup"', body)
        self.assertIn('id="move-input"', body)
        self.assertIn(">Move</h3>", body)
        self.assertIn(">Target path</label>", body)
        self.assertIn('id="batch-move-popup"', body)
        self.assertIn('id="batch-move-apply-btn"', body)
        self.assertIn('id="delete-confirm-modal"', body)
        self.assertIn('id="delete-confirm-message"', body)
        self.assertIn('id="delete-confirm-apply-btn"', body)
        self.assertIn('id="delete-confirm-cancel-btn"', body)
        self.assertIn("Delete folder and contents?", body)
        self.assertIn('id="mkdir-btn"', body)
        self.assertIn('id="copy-btn"', body)
        self.assertIn('id="move-btn"', body)
        self.assertIn('id="rename-btn"', body)
        self.assertIn('id="delete-btn"', body)
        self.assertIn('id="left-breadcrumbs"', body)
        self.assertIn('id="right-breadcrumbs"', body)
        self.assertIn('id="wildcard-popup"', body)
        self.assertIn('id="wildcard-pattern-input"', body)
        self.assertNotIn('id="search-btn"', body)
        self.assertNotIn('id="info-btn"', body)
        self.assertNotIn('id="bookmarks-panel"', body)
        self.assertNotIn('id="tasks-panel"', body)

        ordered_ids = [
            'id="upload-btn"',
            'id="settings-btn"',
            'id="rename-btn"',
            'id="view-btn"',
            'id="edit-btn"',
            'id="copy-btn"',
            'id="move-btn"',
            'id="mkdir-btn"',
            'id="delete-btn"',
        ]
        positions = [body.index(marker) for marker in ordered_ids]
        self.assertEqual(positions, sorted(positions))

    def test_ui_static_assets_are_present_and_mapped(self) -> None:
        mount = self._ui_mount()
        static_root = Path(mount.app.directory)
        self.assertTrue((static_root / "app.js").exists())
        self.assertTrue((static_root / "base.css").exists())
        self.assertTrue((static_root / "theme-default.css").exists())
        self.assertTrue((static_root / "theme-macos-soft.css").exists())
        self.assertTrue((static_root / "theme-midnight.css").exists())
        self.assertTrue((static_root / "theme-graphite.css").exists())
        self.assertTrue((static_root / "theme-windows11.css").exists())
        self.assertTrue((static_root / "theme-commander-electric.css").exists())
        self.assertTrue((static_root / "theme-nord-arctic.css").exists())
        self.assertTrue((static_root / "theme-catppuccin-soft.css").exists())
        self.assertTrue((static_root / "theme-fluent-neon.css").exists())
        base_css = (static_root / "base.css").read_text(encoding="utf-8")
        self.assertIn(".settings-card {", base_css)
        self.assertIn("#title-brand {", base_css)
        self.assertIn("#title-logo {", base_css)
        self.assertIn("height: 32px;", base_css)
        self.assertIn(".header-task-chip-container {", base_css)
        self.assertIn(".header-task-chip {", base_css)
        self.assertIn(".header-task-popover {", base_css)
        self.assertIn(".header-task-popover-list {", base_css)
        self.assertIn("width: min(1180px, calc(100vw - 32px));", base_css)
        self.assertIn(".settings-activity-grid {", base_css)
        self.assertIn("grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);", base_css)
        self.assertIn(".settings-activity-panel {", base_css)
        self.assertIn("@media (max-width: 900px) {", base_css)
        self.assertIn("@media (max-width: 720px) {", base_css)
        self.assertIn("width: min(1180px, calc(100vw - 20px));", base_css)
        app_js = (static_root / "app.js").read_text(encoding="utf-8")
        self.assertIn('currentPath: "/Volumes"', app_js)
        self.assertIn('returnFocusName: null,', app_js)
        self.assertIn('selectedTheme: "default"', app_js)
        self.assertIn('selectedColorMode: "dark"', app_js)
        self.assertIn('const VALID_THEME_FAMILIES = [', app_js)
        self.assertIn('"commander-electric"', app_js)
        self.assertIn('"nord-arctic"', app_js)
        self.assertIn('"catppuccin-soft"', app_js)
        self.assertIn('"fluent-neon"', app_js)
        self.assertIn('document.documentElement.dataset.themeFamily', app_js)
        self.assertIn('document.documentElement.dataset.colorMode', app_js)
        self.assertIn('function effectiveThemeKey(theme, colorMode)', app_js)
        self.assertIn("document.documentElement.dataset.theme", app_js)
        self.assertIn('document.getElementById("theme-toggle").onclick = toggleTheme;', app_js)
        self.assertIn('document.getElementById("upload-btn").onclick = openUploadPicker;', app_js)
        self.assertIn('function feedbackElements()', app_js)
        self.assertIn('function openFeedbackModal(message)', app_js)
        self.assertIn('function closeFeedbackModal()', app_js)
        self.assertIn('function openConfirmModal({ title, message, path, applyText = "Confirm" })', app_js)
        self.assertIn('function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit })', app_js)
        self.assertIn('function downloadModalElements()', app_js)
        self.assertIn('function isZipDownloadSelection(items)', app_js)
        self.assertIn('function singleFileDownloadRequestKey(path)', app_js)
        self.assertIn('function archiveTaskStatusLabel(status)', app_js)
        self.assertIn('function archiveTaskCountText(task)', app_js)
        self.assertIn('function archiveTaskCurrentItemText(task)', app_js)
        self.assertIn('function archiveTaskProgressPercent(task)', app_js)
        self.assertIn('function formatTaskStatusLabel(task)', app_js)
        self.assertIn('function inferDownloadTaskContext(task)', app_js)
        self.assertIn('function formatTaskLine(task)', app_js)
        self.assertIn('let headerTaskState = {', app_js)
        self.assertIn('const ACTIVE_TASK_OPERATIONS = new Set(["copy", "move", "duplicate", "delete"]);', app_js)
        self.assertIn('const ACTIVE_TASK_STATUSES = new Set(["queued", "running", "cancelling"]);', app_js)
        self.assertIn("The header chip reflects only user-visible file actions that use the shared task system.", app_js)
        self.assertIn('function headerTaskElements()', app_js)
        self.assertIn('function isActiveTask(task)', app_js)
        self.assertIn('function activeTasksFromItems(items)', app_js)
        self.assertIn('function taskIsCancellable(task)', app_js)
        self.assertIn('async function cancelTaskRequest(taskId)', app_js)
        self.assertIn('function formatTaskOperationLabel(task)', app_js)
        self.assertIn('function hasMeaningfulItemProgress(task)', app_js)
        self.assertIn('function canShowChipItemProgress(task)', app_js)
        self.assertIn('function compactTaskCurrentItem(task)', app_js)
        self.assertIn('function activeTaskChipLabel(items)', app_js)
        self.assertIn('function taskProgressText(task)', app_js)
        self.assertIn('function taskProgressSubtext(task)', app_js)
        self.assertIn('function shouldPollHeaderTasks()', app_js)
        self.assertIn('function scheduleHeaderTaskPolling()', app_js)
        self.assertIn('function setHeaderTaskPopoverOpen(nextOpen)', app_js)
        self.assertIn('function renderHeaderTaskPopover(items)', app_js)
        self.assertIn('function renderHeaderTaskChip(items)', app_js)
        self.assertIn('function updateHeaderTaskState(taskItems)', app_js)
        self.assertIn('function applyTaskSnapshot(taskItems)', app_js)
        self.assertIn('return `${count} active task${count === 1 ? "" : "s"}`;', app_js)
        self.assertIn('return task.operation === "copy" || task.operation === "duplicate";', app_js)
        self.assertIn('return `${action} ${task.done_items}/${task.total_items}`;', app_js)
        self.assertIn('return `${action} running`;', app_js)
        self.assertIn('return "Stopping after current item...";', app_js)
        self.assertIn('ACTIVE_TASK_OPERATIONS.has(task.operation)', app_js)
        self.assertIn('headerTaskState.activeItems = activeTasksFromItems(taskItems);', app_js)
        self.assertIn('const open = Boolean(nextOpen) && headerTaskState.activeItems.length > 0;', app_js)
        self.assertIn('const headerTasks = headerTaskElements();', app_js)
        self.assertIn('headerTasks.chipButton.onclick = (event) => {', app_js)
        self.assertIn('headerTasks.logsButton.onclick = () => {', app_js)
        self.assertIn('setHeaderTaskPopoverOpen(!headerTaskState.popoverOpen);', app_js)
        self.assertIn('setHeaderTaskPopoverOpen(false);', app_js)
        self.assertIn('updateHeaderTaskState(items);', app_js)
        self.assertIn('renderTaskItems(applyTaskSnapshot(data.items));', app_js)
        self.assertIn('function renderTaskItems(items)', app_js)
        self.assertIn('async function loadTasksForSettings()', app_js)
        self.assertIn('async function loadLogsAndTasksForSettings()', app_js)
        self.assertIn('function scheduleSettingsLogsPolling()', app_js)
        self.assertIn('lastHistoryRenderKey: ""', app_js)
        self.assertIn('lastTasksRenderKey: ""', app_js)
        self.assertIn('function openZipDownloadModal(selectedItems)', app_js)
        self.assertIn('function openSingleFileDownloadModal(selectedItem)', app_js)
        self.assertIn('function markZipDownloadReady(fileName)', app_js)
        self.assertIn('function markZipDownloadFailed(err)', app_js)
        self.assertIn('function markZipDownloadCancelled()', app_js)
        self.assertIn('function markSingleFileDownloadRequested(fileName, path)', app_js)
        self.assertIn('function markSingleFileDownloadFailed(err, selectedItem)', app_js)
        self.assertIn('function closeDownloadModal()', app_js)
        self.assertIn('function zipDownloadRequestKey(paths)', app_js)
        self.assertIn('async function createArchiveDownloadTask(paths)', app_js)
        self.assertIn('async function getTaskRequest(taskId)', app_js)
        self.assertIn('async function cancelArchiveDownloadTask(taskId)', app_js)
        self.assertIn('function startArchiveDownload(taskId, fileName)', app_js)
        self.assertIn('async function requestArchiveDownloadCancel()', app_js)
        self.assertIn('async function waitForArchiveDownloadReady(taskId)', app_js)
        self.assertIn('function contextMenuElements()', app_js)
        self.assertIn('function openContextMenu(pane, entry, event)', app_js)
        self.assertIn('function closeContextMenu()', app_js)
        self.assertIn('function isOpenableSelection(item)', app_js)
        self.assertIn('async function downloadFileRequest(paths)', app_js)
        self.assertIn('const zipDownload = isZipDownloadSelection(selectedItems);', app_js)
        self.assertIn('openZipDownloadModal(selectedItems);', app_js)
        self.assertIn('openSingleFileDownloadModal(selected);', app_js)
        self.assertIn('const requestKey = zipDownload ? zipDownloadRequestKey(selectedPaths) : singleFileDownloadRequestKey(selected.path);', app_js)
        self.assertIn('targetText: "Archive download requested"', app_js)
        self.assertIn('targetText: `File download requested: ${selectedItem.name}`', app_js)
        self.assertIn('statusText: "Requested"', app_js)
        self.assertIn('statusText: "Requesting download..."', app_js)
        self.assertIn('countText: "Waiting for archive task"', app_js)
        self.assertIn('countText: "Direct file download"', app_js)
        self.assertIn('targetText: "Archive download task"', app_js)
        self.assertIn('statusText: "Ready"', app_js)
        self.assertIn('countText: "Browser download requested"', app_js)
        self.assertIn('countText: "Archive task failed"', app_js)
        self.assertIn('countText: "Archive task cancelled"', app_js)
        self.assertIn('statusText: "Cancelling download..."', app_js)
        self.assertIn('statusText: `Failed: ${err.message || "Archive download failed"}`', app_js)
        self.assertIn('logsVisible: true,', app_js)
        self.assertIn('return `${task.done_items}/${task.total_items} top-level items`;', app_js)
        self.assertIn('return `Current: ${task.current_item}`;', app_js)
        self.assertIn('downloadProgressState.requestKey === requestKey', app_js)
        self.assertIn('setStatus("Preparing download...");', app_js)
        self.assertIn('setStatus("Requesting download...");', app_js)
        self.assertIn('setStatus(zipDownload ? "Preparing download..." : "Requesting download...");', app_js)
        self.assertIn('setStatus(`Download requested: ${anchor.download}`);', app_js)
        self.assertIn('"/api/files/download/archive-prepare"', app_js)
        self.assertIn('"/api/files/duplicate"', app_js)
        self.assertIn('"/api/files/delete"', app_js)
        self.assertIn('`/api/tasks/${encodeURIComponent(taskId)}`', app_js)
        self.assertIn('`/api/files/download/archive/${encodeURIComponent(taskId)}`', app_js)
        self.assertIn('`/api/files/download/archive/${encodeURIComponent(taskId)}/cancel`', app_js)
        self.assertIn('const data = await apiRequest("GET", "/api/tasks");', app_js)
        self.assertIn('return "Ready for download";', app_js)
        self.assertIn('return "Multi-item ZIP";', app_js)
        self.assertIn('details.push(`Current: ${task.current_item}`);', app_js)
        self.assertIn('details.push(`${task.done_items}/${task.total_items} items`);', app_js)
        self.assertIn('if (settingsState.lastHistoryRenderKey === renderKey) {', app_js)
        self.assertIn('if (settingsState.lastTasksRenderKey === renderKey) {', app_js)
        self.assertIn('const scrollTop = elements.logsList.scrollTop;', app_js)
        self.assertIn('const scrollTop = elements.tasksList.scrollTop;', app_js)
        self.assertIn('elements.logsList.scrollTop = scrollTop;', app_js)
        self.assertIn('elements.tasksList.scrollTop = scrollTop;', app_js)
        self.assertIn('if (!settingsState.tasksLoaded) {', app_js)
        self.assertIn('if (!settingsState.logsLoaded) {', app_js)
        self.assertIn('downloadModal.logsButton.onclick = () => {', app_js)
        self.assertIn('openSettings("logs");', app_js)
        self.assertIn('function applyContextMenuSelection()', app_js)
        self.assertIn('function startContextMenuOpen()', app_js)
        self.assertIn('function startContextMenuEdit()', app_js)
        self.assertIn('function startContextMenuDownload()', app_js)
        self.assertIn('function startContextMenuRename()', app_js)
        self.assertIn('function startDuplicateSelected()', app_js)
        self.assertIn('async function deleteSelected()', app_js)
        self.assertIn('const result = await apiRequest("POST", "/api/files/delete", {', app_js)
        self.assertIn('state.selectedTaskId = result.task_id;', app_js)
        self.assertIn('await refreshTasksSnapshot();', app_js)
        self.assertIn('function startContextMenuDuplicate()', app_js)
        self.assertIn('function startContextMenuCopy()', app_js)
        self.assertIn('function startContextMenuMove()', app_js)
        self.assertIn('function startContextMenuDelete()', app_js)
        self.assertIn('function startContextMenuProperties()', app_js)
        self.assertIn('contextMenu.duplicateButton.onclick = startContextMenuDuplicate;', app_js)
        self.assertIn('selectedPathsSet.has(entry.path)', app_js)
        self.assertIn('entry.isParent', app_js)
        self.assertIn('row.oncontextmenu = (event) => {', app_js)
        self.assertIn('event.target.closest("li[data-row-index]")', app_js)
        self.assertIn('if (!row) {', app_js)
        self.assertIn('closeContextMenu();', app_js)
        self.assertIn('elements.openButton.classList.toggle("hidden", isMulti);', app_js)
        self.assertIn('const openableSingle = items.length === 1 && isOpenableSelection(items[0]);', app_js)
        self.assertIn('elements.openButton.disabled = !openableSingle;', app_js)
        self.assertIn('if (item.kind === "directory") {', app_js)
        self.assertIn('return isImageSelection(item) || isVideoSelection(item);', app_js)
        self.assertIn('const editableSingle = items.length === 1 && isEditableSelection(items[0]);', app_js)
        self.assertIn('return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".py", ".css", ".html", ".conf"].some((suffix) => lower.endsWith(suffix));', app_js)
        self.assertIn('if (!item || item.kind !== "file") {', app_js)
        self.assertIn('elements.editButton.classList.toggle("hidden", isMulti || items.length !== 1 || items[0].kind !== "file");', app_js)
        self.assertIn('elements.editButton.disabled = !editableSingle;', app_js)
        self.assertIn('const downloadableSelection = items.length > 0;', app_js)
        self.assertIn('elements.downloadButton.classList.remove("hidden");', app_js)
        self.assertIn('elements.downloadButton.disabled = !downloadableSelection;', app_js)
        self.assertIn('elements.renameButton.classList.toggle("hidden", isMulti);', app_js)
        self.assertIn('elements.duplicateButton.classList.remove("hidden");', app_js)
        self.assertIn('elements.duplicateButton.disabled = items.length === 0;', app_js)
        self.assertIn('elements.copyButton.classList.remove("hidden");', app_js)
        self.assertIn('elements.copyButton.disabled = items.length === 0;', app_js)
        self.assertIn('elements.moveButton.classList.remove("hidden");', app_js)
        self.assertIn('elements.propertiesButton.classList.remove("hidden");', app_js)
        self.assertIn('elements.propertiesButton.disabled = items.length === 0;', app_js)
        self.assertIn('openCurrentDirectory();', app_js)
        self.assertIn('openEditor();', app_js)
        self.assertIn('if (selectedItems.length !== 1 || !isEditableSelection(selectedItems[0])) {', app_js)
        self.assertIn('const created = await createArchiveDownloadTask(selectedPaths);', app_js)
        self.assertIn('const task = await waitForArchiveDownloadReady(created.task_id);', app_js)
        self.assertIn('startArchiveDownload(task.id, task.destination);', app_js)
        self.assertIn('const { blob, fileName } = await downloadFileRequest(selectedPaths);', app_js)
        self.assertIn('anchor.download = fileName || selected.name;', app_js)
        self.assertIn('openRenamePopup();', app_js)
        self.assertIn('const result = await createDuplicateTask(selectedItems.map((item) => item.path));', app_js)
        self.assertIn('showActionSummary("Duplicate", 1, 0, null);', app_js)
        self.assertIn('showActionSummary("Duplicate", 0, 1, err.message);', app_js)
        self.assertIn('startCopySelected();', app_js)
        self.assertIn('openF6Flow();', app_js)
        self.assertIn('deleteSelected();', app_js)
        self.assertIn('const confirmed = await openConfirmModal({', app_js)
        self.assertIn('title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?"', app_js)
        self.assertIn('title: "Discard unsaved changes?"', app_js)
        self.assertIn('title: "Create Folder"', app_js)
        self.assertIn('title: "Add Bookmark"', app_js)
        self.assertIn('openInfo();', app_js)
        self.assertIn('elements.title.textContent = "Properties";', app_js)
        self.assertIn('if (selectedItems.length > 1) {', app_js)
        self.assertIn('renderInfoField("Selected items", selectedItems.length);', app_js)
        self.assertIn('renderInfoField("Files", fileCount);', app_js)
        self.assertIn('renderInfoField("Directories", directoryCount);', app_js)
        self.assertIn('document.getElementById("copy-btn").disabled = !hasSelection;', app_js)
        self.assertNotIn('Only files are supported for copy', app_js)
        self.assertIn('document.getElementById("upload-menu-toggle").onclick = (event) => {', app_js)
        self.assertIn('document.getElementById("upload-folder-btn").onclick = openFolderPicker;', app_js)
        self.assertNotIn('window.confirm(', app_js)
        self.assertNotIn('window.prompt(', app_js)
        self.assertNotIn('window.alert(', app_js)
        self.assertIn('throw createApiError(response, data);', app_js)
        self.assertIn('function closeUploadMenu()', app_js)
        self.assertIn('function toggleUploadMenu()', app_js)
        self.assertNotIn('if (event.altKey) {', app_js)
        self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js)
        self.assertIn('if (event.key === "Backspace") {', app_js)
        self.assertIn('navigateToParent(state.activePane);', app_js)
        self.assertIn('function navigateToParent(pane) {', app_js)
        self.assertIn('prepareParentReturnRestore(pane, childPath);', app_js)
        self.assertIn('function prepareParentReturnRestore(pane, childPath) {', app_js)
        self.assertIn('model.returnFocusName = baseName(childPath);', app_js)
        self.assertNotIn('model.pendingSelectionPath = childPath;', app_js)
        self.assertIn('function collectDeleteRecursivePaths(selectedItems)', app_js)
        self.assertIn('const confirmed = await openConfirmModal({', app_js)
        self.assertIn('recursivePaths.has(item.path)', app_js)
        self.assertIn('Delete selected items and folder contents?', app_js)
        self.assertIn('async function loadSettings()', app_js)
        self.assertIn('await loadSettings();', app_js)
        self.assertIn('await refreshTasksSnapshot();', app_js)
        self.assertIn('settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;', app_js)
        self.assertIn('settings.generalSaveButton.onclick = handlePreferredStartupPathSave;', app_js)
        self.assertIn('settings.interfaceSaveButton.onclick = handleInterfaceSave;', app_js)
        self.assertIn('preferredStartupPathLeft', app_js)
        self.assertIn('preferredStartupPathRight', app_js)
        self.assertIn('zipDownloadLimits', app_js)
        self.assertIn('zip_download_limits', app_js)
        self.assertIn('function renderDownloadSettings()', app_js)
        self.assertIn('function formatBinarySize(bytes)', app_js)
        self.assertIn('function formatSeconds(seconds)', app_js)
        self.assertIn('function formatSymlinkPolicy(policy)', app_js)
        self.assertIn('selected_theme', app_js)
        self.assertIn('selected_color_mode', app_js)
        self.assertNotIn("localStorage", app_js)
        self.assertNotIn("THEME_STORAGE_KEY", app_js)
        self.assertIn('preferred_startup_path_left', app_js)
        self.assertIn('preferred_startup_path_right', app_js)
        self.assertIn('paneState("left").currentPath = settingsState.preferredStartupPathLeft || "/Volumes";', app_js)
        self.assertIn('paneState("right").currentPath = settingsState.preferredStartupPathRight || "/Volumes";', app_js)
        self.assertIn('applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);', app_js)
        self.assertIn('renderPaneItems(pane);', app_js)
        self.assertIn('restoreParentReturnFocus(pane, visibleItems);', app_js)
        self.assertIn('function restoreParentReturnFocus(pane, visibleItems) {', app_js)
        self.assertIn('const returningFromChildName = model.returnFocusName;', app_js)
        self.assertIn('model.returnFocusName = null;', app_js)
        self.assertIn('const returnIndex = visibleItems.findIndex((item) => !item.isParent && item.name === returningFromChildName);', app_js)
        self.assertIn('if (returnIndex < 0) {', app_js)
        self.assertNotIn('setSingleSelectionAtIndex(pane, selectedEntryFromItem(returnItem), returnIndex);', app_js)
        self.assertIn('renderPaneItems(pane);', app_js)
        self.assertIn('settings.interfaceTab.onclick = () => {', app_js)
        self.assertIn('setSettingsTab("interface");', app_js)
        self.assertIn('settings.downloadsTab.onclick = () => {', app_js)
        self.assertIn('setSettingsTab("downloads");', app_js)
        self.assertIn('"/api/settings"', app_js)
        self.assertIn('function uploadElements()', app_js)
        self.assertIn('function openUploadPicker()', app_js)
        self.assertIn('function ensureFolderUploadPicker()', app_js)
        self.assertIn('function openFolderPicker()', app_js)
        self.assertIn('function uploadModalElements()', app_js)
        self._run_header_task_chip_behavior_check(app_js)
        self._run_task_snapshot_sync_behavior_check(app_js)

        self.assertIn('function setUploadModalVisible(', app_js)
        self.assertIn('function updateUploadModalDisplay(', app_js)
        self.assertIn('function buildFolderUploadPlan(files, targetPath)', app_js)
        self.assertIn('function folderDirectoryPaths(plan)', app_js)
        self.assertIn('async function ensureFolderDirectoryExists(path)', app_js)
        self.assertIn('async function executeFolderUploadPlan(plan)', app_js)
        self.assertIn('async function handleFolderSelection(event)', app_js)
        self.assertIn('function deleteConfirmElements()', app_js)
        self.assertIn('function openConfirmModal({ title, message, path, applyText = "Confirm" })', app_js)
        self.assertIn('async function executeDeleteItems(pane, items, recursivePaths)', app_js)
        self.assertIn('async function submitDeleteConfirmModal()', app_js)
        self.assertIn('recursive: recursivePaths.has(item.path)', app_js)
        self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
        self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
        self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js)
        self.assertIn('Folder upload: preparing', app_js)
        self.assertIn('Folder upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped', app_js)
        self.assertIn('async function handleUploadSelection(event)', app_js)
        self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js)
        self.assertIn('"/api/files/upload"', app_js)
        self.assertIn('function ensureUploadConflictModal()', app_js)
        self.assertIn('function promptUploadConflict(', app_js)
        self.assertIn('formData.append("overwrite", overwrite ? "true" : "false")', app_js)
        self.assertIn('createButton("Overwrite"', app_js)
        self.assertIn('createButton("Overwrite all"', app_js)
        self.assertIn('createButton("Skip"', app_js)
        self.assertIn('createButton("Skip all"', app_js)
        self.assertIn('createButton("Cancel"', app_js)
        self.assertIn('if (err.code !== "already_exists") {', app_js)
        self.assertIn('if (choice === "overwrite_all") {', app_js)
        self.assertIn('if (uploadState.skipAll) {', app_js)
        self.assertIn('if (choice === "skip_all") {', app_js)
        self.assertIn('uploadState.skipAll = true;', app_js)
        self.assertIn('Upload to: ${uploadState.targetPath}', app_js)
        self.assertIn('Uploading ${total} file', app_js)
        self.assertIn('`/api/files/thumbnail?', app_js)
        self.assertIn("function iconTypeForEntry(entry)", app_js)
        self.assertIn("function mediaIconSvg(type)", app_js)
        self.assertIn('const iconType = iconTypeForEntry(entry);', app_js)
        self.assertIn('function createMediaSlot(entry)', app_js)
        self.assertIn('function createSelectionSlot(pane, entry, index)', app_js)
        self.assertIn('entry-select-slot', app_js)
        self.assertIn('entry-select-toggle', app_js)
        self.assertIn('toggleSelectionAtIndex(pane, selectedEntryFromItem(entry), index);', app_js)
        self.assertIn('function openSearch()', app_js)
        self.assertIn('async function submitSearch()', app_js)
        self.assertIn('async function openInfo()', app_js)
        self.assertIn('function imageElements()', app_js)
        self.assertIn('function isImageSelection(item)', app_js)
        self.assertIn('async function openImageViewer()', app_js)
        self.assertIn('if (isImageSelection(item)) {', app_js)
        self.assertIn('openImageViewer();', app_js)
        self.assertIn('function isImageOpen()', app_js)
        self.assertIn('requestAnimationFrame(() => {', app_js)
        self.assertIn('if (!fitImageToViewport()) {', app_js)
        self.assertIn('if (!viewport.clientWidth || !viewport.clientHeight) {', app_js)
        self.assertIn("`/api/files/image?", app_js)
        self.assertIn('if (isImageSelection(selected)) {', app_js)
        self.assertIn('document.getElementById("info-modal")', app_js)
        self.assertIn("`/api/files/info?", app_js)
        self.assertIn('document.getElementById("search-input")', app_js)
        self.assertIn("`/api/search?", app_js)
        self.assertIn('event.key.toLowerCase() === "f"', app_js)
        self.assertIn('(event.metaKey || event.ctrlKey)', app_js)
        self.assertIn('const isInfoShortcut = event.key === "Enter"', app_js)
        self.assertIn('if (event.key === "F1") {', app_js)
        self.assertIn('if (event.key === "F2") {', app_js)
        self.assertIn('function openSettings(tab = "general")', app_js)
        self.assertIn('function openRenamePopup()', app_js)
        self.assertIn('document.getElementById("rename-btn").onclick = openRenamePopup;', app_js)
        self.assertIn('return triggerActionButton("rename-btn");', app_js)
        self.assertIn('".py"', app_js)
        self.assertIn('function openVideoViewer()', app_js)
        self.assertIn('function openPdfViewer()', app_js)
        self.assertIn('async function loadMonacoModule()', app_js)
        self.assertIn('async function ensureMonacoEditor(path, content)', app_js)
        self.assertIn('function disposeMonacoEditor()', app_js)
        self.assertIn('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm', app_js)
        self.assertIn('document.getElementById("pdf-modal")', app_js)
        self.assertIn("`/api/files/pdf?", app_js)
        self.assertIn('if (isPdfSelection(selected)) {', app_js)
        self.assertIn('video.player.src = streamUrl;', app_js)
        self.assertIn('document.getElementById("video-close-btn")', app_js)
        self.assertIn('row.ondblclick = (ev) => {', app_js)
        self.assertIn('function openMovePopup()', app_js)
        self.assertIn('document.getElementById("move-btn").onclick = openF6Flow;', app_js)
        self.assertIn('await apiRequest("GET", "/api/history")', app_js)
        self.assertIn('Cross-root directory move is not supported in v1', app_js)
        self.assertIn('Batch directory move is not supported in v1', app_js)
        self.assertIn('Batch move requires all selected items to be in the same root', app_js)
        self.assertIn('destination_base', app_js)
        self.assertIn('sources: selectedItems.map((item) => item.path)', app_js)
        self.assertIn("function rootKeyFromPath(path)", app_js)
        self.assertIn("function isNestedPath(sourcePath, destinationPath)", app_js)
        base_css = (static_root / "base.css").read_text(encoding="utf-8")
        default_theme_css = (static_root / "theme-default.css").read_text(encoding="utf-8")
        macos_theme_css = (static_root / "theme-macos-soft.css").read_text(encoding="utf-8")
        midnight_theme_css = (static_root / "theme-midnight.css").read_text(encoding="utf-8")
        graphite_theme_css = (static_root / "theme-graphite.css").read_text(encoding="utf-8")
        windows_theme_css = (static_root / "theme-windows11.css").read_text(encoding="utf-8")
        commander_theme_css = (static_root / "theme-commander-electric.css").read_text(encoding="utf-8")
        nord_theme_css = (static_root / "theme-nord-arctic.css").read_text(encoding="utf-8")
        catppuccin_theme_css = (static_root / "theme-catppuccin-soft.css").read_text(encoding="utf-8")
        fluent_theme_css = (static_root / "theme-fluent-neon.css").read_text(encoding="utf-8")
        self.assertIn('#theme-toggle', base_css)
        self.assertIn('.settings-card', base_css)
        self.assertIn('.settings-tabs', base_css)
        self.assertIn('.entry-media-slot', base_css)
        self.assertIn('.entry-media-icon.folder', base_css)
        self.assertIn('.entry-media-icon.video', base_css)
        self.assertIn('.entry-media-icon.pdf', base_css)
        self.assertIn('.entry-media-svg', base_css)
        self.assertIn('.entry-media-svg.is-filled', base_css)
        self.assertIn('.entry-media-detail', base_css)
        self.assertIn('.entry-media-icon.file', base_css)
        self.assertIn('.editor-card', base_css)
        self.assertIn('.editor-host', base_css)
        self.assertNotIn('.select-marker', base_css)
        self.assertIn(':root[data-theme-family="default"][data-color-mode="dark"]', default_theme_css)
        self.assertIn(':root[data-theme-family="default"][data-color-mode="light"]', default_theme_css)
        self.assertIn(':root[data-theme-family="macos-soft"][data-color-mode="dark"]', macos_theme_css)
        self.assertIn(':root[data-theme-family="midnight"][data-color-mode="dark"]', midnight_theme_css)
        self.assertIn(':root[data-theme-family="graphite"][data-color-mode="dark"]', graphite_theme_css)
        self.assertIn(':root[data-theme-family="windows11"][data-color-mode="dark"]', windows_theme_css)
        self.assertIn(':root[data-theme-family="commander-electric"][data-color-mode="dark"]', commander_theme_css)
        self.assertIn(':root[data-theme-family="nord-arctic"][data-color-mode="dark"]', nord_theme_css)
        self.assertIn(':root[data-theme-family="catppuccin-soft"][data-color-mode="dark"]', catppuccin_theme_css)
        self.assertIn(':root[data-theme-family="fluent-neon"][data-color-mode="dark"]', fluent_theme_css)

        app_js_url = app.url_path_for("ui", path="/app.js")
        base_css_url = app.url_path_for("ui", path="/base.css")
        theme_default_url = app.url_path_for("ui", path="/theme-default.css")
        self.assertEqual(app_js_url, "/ui/app.js")
        self.assertEqual(base_css_url, "/ui/base.css")
        self.assertEqual(theme_default_url, "/ui/theme-default.css")

    def test_parent_return_restore_focuses_without_selecting(self) -> None:
        mount = self._ui_mount()
        app_js = (Path(mount.app.directory) / "app.js").read_text(encoding="utf-8")
        self._run_app_js_behavior_check(app_js)


if __name__ == "__main__":
    unittest.main()
