from __future__ import annotations

import json
import re
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
STATIC = ROOT / "static"


class FrontendBoundaryTests(unittest.TestCase):
    def test_shared_api_is_the_only_application_fetch_exit(self) -> None:
        fetch_files = []
        for path in STATIC.rglob("*.js"):
            if "vendor" in path.parts:
                continue
            if re.search(r"\bfetch\s*\(", path.read_text(encoding="utf-8")):
                fetch_files.append(path.relative_to(STATIC).as_posix())
        self.assertEqual(fetch_files, ["shared/api.js"])

    def test_shared_dependencies_load_before_application(self) -> None:
        html = (STATIC / "index.html").read_text(encoding="utf-8")
        ui_position = html.index('/ui-core.js')
        components_position = html.index('/shared/components.js')
        pages_position = html.index('/pages.config.js')
        runtime_position = html.index('/pages/runtime.js')
        state_position = html.index('/shared/state.js')
        api_position = html.index('/shared/api.js')
        shell_position = html.index('/shared/shell.js')
        app_position = html.index('/app.js')
        self.assertLess(ui_position, components_position)
        self.assertLess(components_position, pages_position)
        self.assertLess(pages_position, state_position)
        self.assertLess(pages_position, runtime_position)
        self.assertLess(runtime_position, state_position)
        self.assertLess(state_position, api_position)
        self.assertLess(api_position, shell_position)
        self.assertLess(shell_position, app_position)

    def test_application_state_is_created_through_shared_boundary(self) -> None:
        app = (STATIC / "app.js").read_text(encoding="utf-8")
        self.assertIn("const state = window.XiaobaiState.create({", app)
        self.assertNotIn("const state = {", app)

    def test_runtime_page_registry_matches_governance_registry(self) -> None:
        expected = json.loads(
            (ROOT / "config" / "pages.config.json").read_text(encoding="utf-8")
        )["pages"]
        runtime = (STATIC / "pages.config.js").read_text(encoding="utf-8")
        rows = re.findall(
            r'^\s*\["([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", (true|false)\],$',
            runtime,
            re.MULTILINE,
        )
        actual = [
            {
                "id": row[0],
                "title": row[1],
                "feature": row[2],
                "group": row[3],
                "access": row[4],
                "default": row[5] == "true",
                "desktop_scroll": "page",
                "mobile_layout": "dedicated",
            }
            for row in rows
        ]
        self.assertEqual(actual, expected)

    def test_shell_owns_navigation_and_page_mounting(self) -> None:
        app = (STATIC / "app.js").read_text(encoding="utf-8")
        shell = (STATIC / "shared" / "shell.js").read_text(encoding="utf-8")
        self.assertNotIn("function syncNavigationState", app)
        self.assertNotIn("function initializeApplicationShell", app)
        self.assertIn("function syncNavigation(viewId)", shell)
        self.assertIn("function mount(viewId, mountOptions = {})", shell)
        self.assertIn("function openModalDialog(dialog)", shell)
        self.assertNotIn('document.querySelectorAll(".module-tab").forEach', app)

    def test_every_registered_view_has_one_feature_page_module(self) -> None:
        html = (STATIC / "index.html").read_text(encoding="utf-8")
        runtime_position = html.index('/pages/runtime.js')
        app_position = html.index('/app.js')
        expected = {
            page["id"]: page["feature"]
            for page in json.loads(
                (ROOT / "config" / "pages.config.json").read_text(encoding="utf-8")
            )["pages"]
        }
        expected["screenerTrackingView"] = "screener"
        actual: dict[str, str] = {}
        for path in (STATIC / "pages").glob("*/page.js"):
            script_url = f'/pages/{path.parent.name}/page.js'
            self.assertIn(script_url, html)
            self.assertLess(runtime_position, html.index(script_url))
            self.assertLess(html.index(script_url), app_position)
            script = path.read_text(encoding="utf-8")
            for match in re.finditer(
                r'XiaobaiPageModules\.register\("([^"]+)",\s*\[(.*?)\]',
                script,
                re.DOTALL,
            ):
                feature = match.group(1)
                for view_id in re.findall(r'"([A-Za-z][A-Za-z0-9]+)"', match.group(2)):
                    self.assertNotIn(view_id, actual)
                    actual[view_id] = feature
        self.assertEqual(actual, expected)

    def test_page_lifecycle_is_owned_outside_application_monolith(self) -> None:
        app = (STATIC / "app.js").read_text(encoding="utf-8")
        runtime = (STATIC / "pages" / "runtime.js").read_text(encoding="utf-8")
        start = app.index("function openView(")
        end = app.index("\nfunction initializeAutoTableSorting", start)
        open_view = app[start:end]
        self.assertIn("pageModules.beforeMount(viewId, previousView);", open_view)
        self.assertIn("pageModules.afterMount(viewId, previousView);", open_view)
        self.assertNotRegex(open_view, r'viewId\s*[!=]==?\s*"')
        self.assertIn("function beforeMount(viewId, previousView)", runtime)
        self.assertIn("function afterMount(viewId, previousView)", runtime)

    def test_shared_empty_state_component_is_used_by_multiple_features(self) -> None:
        components = (STATIC / "shared" / "components.js").read_text(encoding="utf-8")
        app = (STATIC / "app.js").read_text(encoding="utf-8")
        self.assertIn("function emptyStateHtml(message, options = {})", components)
        self.assertIn("function renderEmptyState(target, message, options = {})", components)
        self.assertGreaterEqual(app.count("renderEmptyState("), 8)
        self.assertGreaterEqual(app.count("emptyStateHtml("), 8)


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