from __future__ import annotations

import re
import unittest
from collections import defaultdict
from pathlib import Path

from tests.frontend_test_helpers import assembled_frontend_document


ROOT = Path(__file__).resolve().parents[1]
STATIC = ROOT / "frontend"
TOKENS = STATIC / "shared" / "tokens.css"

MODULE_STYLESHEETS = (
    "shared/base.css",
    "shared/shell.css",
    "shared/auth.css",
    "shared/components/controls.css",
    "shared/components/navigation.css",
    "shared/components/cards.css",
    "shared/components/tables.css",
    "shared/components/dialogs.css",
    "shared/components/feedback.css",
    "pages/market/foundation.css",
    "pages/sentiment/foundation.css",
    "pages/pools/foundation.css",
    "pages/ladder/foundation.css",
    "pages/rotation/foundation.css",
    "pages/auction/foundation.css",
    "pages/themes/foundation.css",
    "pages/popularity/foundation.css",
    "pages/dragon-tiger/foundation.css",
    "pages/screener/foundation.css",
    "pages/mentor/foundation.css",
    "pages/heaven/foundation.css",
    "pages/review/foundation.css",
)

RETIRED_STYLESHEETS = (
    "styles/styles.css",
    "styles/renovation.css",
    "styles/redesign-v2.css",
    "styles/design-system.css",
    "styles/theme.css",
    "pages/heaven/page.css",
)

PAGE_OWNERS = {
    "sentimentCycleView": "pages/sentiment/foundation.css",
    "limitPool": "pages/pools/foundation.css",
    "brokenView": "pages/pools/foundation.css",
    "downView": "pages/pools/foundation.css",
    "yesterdayView": "pages/pools/foundation.css",
    "performanceView": "pages/pools/foundation.css",
    "ladderView": "pages/ladder/foundation.css",
    "rotationView": "pages/rotation/foundation.css",
    "auctionView": "pages/auction/foundation.css",
    "themeLibraryView": "pages/themes/foundation.css",
    "popularityView": "pages/popularity/foundation.css",
    "dragonView": "pages/dragon-tiger/foundation.css",
    "screenerView": "pages/screener/foundation.css",
    "screenerTrackingView": "pages/screener/foundation.css",
    "mentorView": "pages/mentor/foundation.css",
    "heavenView": "pages/heaven/foundation.css",
    "reviewWorkspaceView": "pages/review/foundation.css",
}

SHARED_ROOT_OWNERS = {
    ".app-header": "shared/shell.css",
    ".app-main": "shared/shell.css",
    ".module-nav": "shared/shell.css",
    ".overview-strip": "shared/shell.css",
    ".sidebar": "shared/shell.css",
    ".status-bar": "shared/shell.css",
    ".workspace-view": "shared/shell.css",
    ".button": "shared/components/controls.css",
    ".icon-button": "shared/components/controls.css",
    ".segment": "shared/components/navigation.css",
    ".data-table": "shared/components/tables.css",
    ".card": "shared/components/cards.css",
    ".dialog": "shared/components/dialogs.css",
    ".toast": "shared/components/feedback.css",
}

HISTORICAL_RUNTIME_CSS_BYTES = 861_341
HISTORICAL_SELECTOR_CONTEXT_RULES = 8_569


def normalize(value: str) -> str:
    return re.sub(r"\s+", " ", re.sub(r"/\*.*?\*/", "", value, flags=re.DOTALL)).strip()


def normalize_context(value: str) -> str:
    """Normalize equivalent at-rule spellings before ownership checks."""
    return re.sub(r"\s*([():,])\s*", r"\1", normalize(value))


def skip_space_and_comments(text: str, position: int) -> int:
    while position < len(text):
        if text[position].isspace():
            position += 1
        elif text.startswith("/*", position):
            closing = text.find("*/", position + 2)
            position = len(text) if closing < 0 else closing + 2
        else:
            break
    return position


def find_delimiter(text: str, position: int) -> tuple[int, str]:
    quote = ""
    escaped = False
    depth = 0
    in_comment = False
    while position < len(text):
        char = text[position]
        pair = text[position : position + 2]
        if in_comment:
            if pair == "*/":
                in_comment = False
                position += 2
                continue
            position += 1
            continue
        if quote:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = ""
            position += 1
            continue
        if pair == "/*":
            in_comment = True
            position += 2
            continue
        if char in "\"'":
            quote = char
        elif char in "([":
            depth += 1
        elif char in ")]":
            depth = max(0, depth - 1)
        elif depth == 0 and char in "{;":
            return position, char
        position += 1
    return len(text), ""


def find_closing_brace(text: str, opening: int) -> int:
    depth = 1
    position = opening + 1
    quote = ""
    escaped = False
    in_comment = False
    while position < len(text):
        char = text[position]
        pair = text[position : position + 2]
        if in_comment:
            if pair == "*/":
                in_comment = False
                position += 2
                continue
            position += 1
            continue
        if quote:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = ""
            position += 1
            continue
        if pair == "/*":
            in_comment = True
            position += 2
            continue
        if char in "\"'":
            quote = char
        elif char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                return position
        position += 1
    raise AssertionError(f"unclosed CSS block at offset {opening}")


def split_selectors(prelude: str) -> list[str]:
    selectors: list[str] = []
    start = 0
    quote = ""
    escaped = False
    depth = 0
    for index, char in enumerate(prelude):
        if quote:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = ""
            continue
        if char in "\"'":
            quote = char
        elif char in "([":
            depth += 1
        elif char in ")]":
            depth = max(0, depth - 1)
        elif char == "," and depth == 0:
            selectors.append(normalize(prelude[start:index]))
            start = index + 1
    selectors.append(normalize(prelude[start:]))
    return [selector for selector in selectors if selector]


def stylesheet_rule_keys(text: str, contexts: tuple[str, ...] = ()) -> list[tuple[tuple[str, ...], str]]:
    keys: list[tuple[tuple[str, ...], str]] = []
    position = 0
    while True:
        position = skip_space_and_comments(text, position)
        if position >= len(text):
            return keys
        delimiter, kind = find_delimiter(text, position)
        prelude = normalize(text[position:delimiter])
        if not kind:
            raise AssertionError(f"unparsed CSS tail: {prelude[:80]}")
        if kind == ";":
            position = delimiter + 1
            continue
        closing = find_closing_brace(text, delimiter)
        body = text[delimiter + 1 : closing]
        lowered = prelude.lower()
        if lowered.startswith(("@media", "@supports", "@container", "@layer", "@starting-style", "@scope")):
            keys.extend(stylesheet_rule_keys(body, contexts + (normalize_context(prelude),)))
        elif lowered.startswith(("@keyframes", "@-webkit-keyframes")):
            keys.append((contexts, prelude))
        elif not prelude.startswith("@"):
            keys.extend((contexts, selector) for selector in split_selectors(prelude))
        position = closing + 1


def declaration_properties(body: str) -> frozenset[str]:
    properties: set[str] = set()
    start = 0
    position = 0
    quote = ""
    escaped = False
    depth = 0
    in_comment = False
    while position <= len(body):
        char = body[position] if position < len(body) else ";"
        pair = body[position : position + 2]
        if in_comment:
            if pair == "*/":
                in_comment = False
                position += 2
                continue
            position += 1
            continue
        if quote:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = ""
            position += 1
            continue
        if pair == "/*":
            in_comment = True
            position += 2
            continue
        if char in "\"'":
            quote = char
        elif char in "([":
            depth += 1
        elif char in ")]":
            depth = max(0, depth - 1)
        elif char == ";" and depth == 0:
            declaration = normalize(body[start:position])
            match = re.match(r"^(--[-a-zA-Z0-9_]+|[-a-zA-Z][-_a-zA-Z0-9]*)\s*:", declaration)
            if match:
                properties.add(match.group(1).lower())
            start = position + 1
        position += 1
    return frozenset(properties)


def stylesheet_rules(
    text: str,
    contexts: tuple[str, ...] = (),
) -> list[tuple[tuple[str, ...], str, frozenset[str]]]:
    rules: list[tuple[tuple[str, ...], str, frozenset[str]]] = []
    position = 0
    while True:
        position = skip_space_and_comments(text, position)
        if position >= len(text):
            return rules
        delimiter, kind = find_delimiter(text, position)
        prelude = normalize(text[position:delimiter])
        if not kind:
            raise AssertionError(f"unparsed CSS tail: {prelude[:80]}")
        if kind == ";":
            position = delimiter + 1
            continue
        closing = find_closing_brace(text, delimiter)
        body = text[delimiter + 1 : closing]
        lowered = prelude.lower()
        if lowered.startswith(("@media", "@supports", "@container", "@layer", "@starting-style", "@scope")):
            rules.extend(stylesheet_rules(body, contexts + (normalize_context(prelude),)))
        elif not prelude.startswith("@"):
            properties = declaration_properties(body)
            rules.extend((contexts, selector, properties) for selector in split_selectors(prelude))
        position = closing + 1


def remove_page_scope(selector: str, view_id: str) -> str:
    prefixes = (
        rf":where\(#{re.escape(view_id)}\)\s+",
        rf"#{re.escape(view_id)}\s+",
        rf"body\[data-active-view=[\"']{re.escape(view_id)}[\"']\]\s+",
    )
    semantic = selector
    for prefix in prefixes:
        semantic = re.sub(rf"^(?P<theme>:root(?:\[[^\]]+\])?\s+)?{prefix}", r"\g<theme>", semantic)
    return normalize(semantic)


def find_matching_parenthesis(value: str, opening: int) -> int:
    depth = 1
    quote = ""
    escaped = False
    for position in range(opening + 1, len(value)):
        char = value[position]
        if quote:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = ""
            continue
        if char in "\"'":
            quote = char
        elif char == "(":
            depth += 1
        elif char == ")":
            depth -= 1
            if depth == 0:
                return position
    raise AssertionError(f"unclosed selector function: {value}")


def prune_selector_consumers(
    selector: str,
    source: str,
    dynamic_prefixes: set[str],
) -> tuple[str, bool, bool]:
    output: list[str] = []
    cursor = 0
    changed = False
    function_pattern = re.compile(r":(is|where|has|not)\(", re.IGNORECASE)
    while match := function_pattern.search(selector, cursor):
        opening = match.end() - 1
        closing = find_matching_parenthesis(selector, opening)
        output.append(selector[cursor : match.start()])
        name = match.group(1).lower()
        content = selector[opening + 1 : closing]
        arguments = split_selectors(content)
        retained: list[str] = []
        nested_changed = False
        for argument in arguments:
            pruned, reachable, argument_changed = prune_selector_consumers(argument, source, dynamic_prefixes)
            nested_changed |= argument_changed
            if reachable:
                retained.append(pruned)
            else:
                nested_changed = True
        if name != "not" and not retained:
            return selector, False, True
        if name == "not" and not retained:
            changed = True
        elif nested_changed or len(retained) != len(arguments):
            output.append(f":{name}({', '.join(retained)})")
            changed = True
        else:
            output.append(selector[match.start() : closing + 1])
        cursor = closing + 1
    output.append(selector[cursor:])
    pruned_selector = "".join(output)
    tokens = {
        match.group(2)
        for match in re.finditer(r"([.#])([_a-zA-Z][-_a-zA-Z0-9]*)", pruned_selector)
    }
    reachable = all(
        token in source or any(token.startswith(prefix) for prefix in dynamic_prefixes)
        for token in tokens
    )
    return pruned_selector, reachable, changed or not reachable


class CssGovernanceTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.html = assembled_frontend_document()
        cls.tokens = TOKENS.read_text(encoding="utf-8")
        cls.modules = {
            relative: (STATIC / relative).read_text(encoding="utf-8")
            for relative in MODULE_STYLESHEETS
        }
        cls.rule_keys = {
            relative: stylesheet_rule_keys(stylesheet)
            for relative, stylesheet in cls.modules.items()
        }
        cls.rules = {
            relative: stylesheet_rules(stylesheet)
            for relative, stylesheet in cls.modules.items()
        }
        cls.runtime_source = "\n".join(
            path.read_text(encoding="utf-8")
            for path in STATIC.rglob("*")
            if path.is_file()
            and path.suffix in {".html", ".js", ".mjs"}
            and "_css_core_before" not in path.parts
            and path.name != "css-core-before.html"
        )
        cls.dynamic_class_prefixes = {
            *re.findall(r"([_a-zA-Z][-_a-zA-Z0-9]*)\$\{", cls.runtime_source),
            *re.findall(r'["\']([_a-zA-Z][-_a-zA-Z0-9]*)["\']\s*\+', cls.runtime_source),
        }

    def test_runtime_loads_only_the_canonical_stylesheet_stack(self) -> None:
        expected = ("shared/tokens.css", *MODULE_STYLESHEETS)
        positions = [self.html.index(f'/{path}') for path in expected]
        self.assertEqual(positions, sorted(positions))
        self.assertEqual(self.html.count('<link rel="stylesheet"'), len(expected))
        for path in expected:
            self.assertEqual(self.html.count(f'/{path}'), 1, path)

    def test_historical_layers_and_patch_file_names_are_absent(self) -> None:
        for relative in RETIRED_STYLESHEETS:
            self.assertFalse((STATIC / relative).exists(), relative)
            self.assertNotIn(f'/{relative}', self.html)
        for name in ("legacy.css", "override.css", "fix.css"):
            self.assertFalse(any(STATIC.rglob(name)), name)

    def test_every_module_declares_its_owner_and_is_nonempty(self) -> None:
        for relative, stylesheet in self.modules.items():
            owner = Path(relative).parent.name if "/pages/" in f"/{relative}" else Path(relative).stem
            if relative.startswith("shared/components/"):
                owner = Path(relative).stem
            self.assertTrue(
                stylesheet.startswith(
                    f"/* Canonical CSS owner: {owner}. Historical layers consolidated 2026-08-02. */"
                ),
                relative,
            )
            self.assertIn("{", stylesheet, relative)

    def test_modules_do_not_contain_empty_declarations(self) -> None:
        for relative, stylesheet in self.modules.items():
            self.assertNotRegex(stylesheet, r"(?m)^\s*[-_a-zA-Z0-9]+:\s*;\s*$", relative)

    def test_selector_and_context_have_exactly_one_owner(self) -> None:
        owners: dict[tuple[tuple[str, ...], str], list[str]] = defaultdict(list)
        for relative, keys in self.rule_keys.items():
            for key in keys:
                owners[key].append(relative)
        duplicates = {
            f"{' > '.join(contexts) or '<root>'} :: {selector}": paths
            for (contexts, selector), paths in owners.items()
            if len(paths) != 1
        }
        self.assertEqual(duplicates, {})

    def test_page_ids_only_appear_in_their_owner_stylesheet(self) -> None:
        for relative, stylesheet in self.modules.items():
            for view_id, owner in PAGE_OWNERS.items():
                if re.search(rf"(?:#{re.escape(view_id)}|data-active-view=[\"']{re.escape(view_id)})", stylesheet):
                    self.assertIn(relative, (owner, "shared/shell.css"), f"{view_id} leaked into {relative}")

    def test_unscoped_shared_roots_stay_in_their_shared_owner(self) -> None:
        for relative, keys in self.rule_keys.items():
            for contexts, selector in keys:
                owner = SHARED_ROOT_OWNERS.get(selector)
                page_scoped = any(any(f"#{view_id}" in context for view_id in PAGE_OWNERS) for context in contexts)
                if owner and not page_scoped:
                    self.assertEqual(relative, owner, f"{selector} leaked into {relative}")

    def test_historical_shell_aliases_cannot_return(self) -> None:
        class_tokens = {
            token
            for value in re.findall(r'class=["\']([^"\']*)["\']', self.html)
            for token in value.split()
        }
        self.assertTrue({"app-header", "module-nav", "overview-strip", "status-bar"} <= class_tokens)
        self.assertTrue({"topbar", "statusbar", "mktstrip", "brand", "logo"}.isdisjoint(class_tokens))
        selector_text = "\n".join(
            selector
            for keys in self.rule_keys.values()
            for _contexts, selector in keys
        )
        for alias in ("topbar", "statusbar", "mktstrip"):
            self.assertNotRegex(selector_text, rf"(?<![-_a-zA-Z0-9])\.{alias}(?![-_a-zA-Z0-9])")

    def test_special_shared_and_page_rules_have_one_owner(self) -> None:
        exact_owners = {
            ".lucide": "shared/components/controls.css",
            ".visually-hidden": "shared/base.css",
        }
        for selector, expected_owner in exact_owners.items():
            owners = [
                relative
                for relative, keys in self.rule_keys.items()
                if any(candidate == selector for _contexts, candidate in keys)
            ]
            self.assertEqual(owners, [expected_owner], selector)

        for relative, keys in self.rule_keys.items():
            for _contexts, selector in keys:
                if ".wentian-v2-dialog" in selector:
                    self.assertEqual(relative, "pages/heaven/foundation.css", selector)
                if re.search(r"\.admin-dialog\s+\.model-", selector):
                    self.assertEqual(relative, "shared/components/dialogs.css", selector)

    def test_page_scope_variants_never_override_the_same_property(self) -> None:
        conflicts: dict[str, list[str]] = defaultdict(list)
        for view_id, owner in PAGE_OWNERS.items():
            variants: dict[tuple[tuple[str, ...], str], list[tuple[str, frozenset[str]]]] = defaultdict(list)
            for contexts, selector, properties in self.rules[owner]:
                semantic = remove_page_scope(selector, view_id)
                if semantic != selector:
                    variants[(contexts, semantic)].append((selector, properties))
            for (contexts, semantic), rows in variants.items():
                unique_selectors = {selector for selector, _properties in rows}
                if len(unique_selectors) < 2:
                    continue
                for index, (left_selector, left_properties) in enumerate(rows):
                    for right_selector, right_properties in rows[index + 1 :]:
                        if left_selector == right_selector:
                            continue
                        overlap = sorted(left_properties & right_properties)
                        if overlap:
                            context = " > ".join(contexts) or "<root>"
                            conflicts[owner].append(
                                f"{context} :: {semantic} :: {left_selector} <> {right_selector}: {overlap}"
                            )
        details = "\n".join(
            f"{owner}: {detail}"
            for owner, owner_conflicts in conflicts.items()
            for detail in owner_conflicts
        )
        self.assertFalse(conflicts, details)

    def test_every_selector_has_a_runtime_consumer(self) -> None:
        stale: list[str] = []
        for relative, keys in self.rule_keys.items():
            for contexts, selector in keys:
                if selector.lower().startswith(("@keyframes", "@-webkit-keyframes")):
                    continue
                _pruned, reachable, changed = prune_selector_consumers(
                    selector,
                    self.runtime_source,
                    self.dynamic_class_prefixes,
                )
                if not reachable or changed:
                    context = " > ".join(contexts) or "<root>"
                    stale.append(f"{relative}: {context} :: {selector}")
        self.assertEqual(stale, [])

    def test_runtime_css_is_smaller_and_has_fewer_rules(self) -> None:
        total_bytes = sum((STATIC / path).stat().st_size for path in MODULE_STYLESHEETS)
        total_rules = sum(len(keys) for keys in self.rule_keys.values())
        self.assertLess(total_bytes, HISTORICAL_RUNTIME_CSS_BYTES)
        self.assertLess(total_rules, HISTORICAL_SELECTOR_CONTEXT_RULES)

    def test_token_file_has_three_layer_contract(self) -> None:
        for heading in (
            "/* Primitive tokens */",
            "/* Semantic tokens */",
            "/* Component tokens */",
            "/* Compatibility aliases.",
        ):
            self.assertIn(heading, self.tokens)
        for variable in (
            "--color-action:",
            "--surface-canvas:",
            "--text-primary:",
            "--card-bg:",
            "--control-height:",
            "--sidebar-width:",
        ):
            self.assertIn(variable, self.tokens)

    def test_light_and_dark_semantics_share_one_owner(self) -> None:
        self.assertIn(':root[data-theme="dark"] {', self.tokens)
        global_root = re.compile(r'(?m)^:root(?:\[data-theme="dark"\])?\s*\{')
        for filename, stylesheet in self.modules.items():
            self.assertIsNone(global_root.search(stylesheet), filename)

    def test_wentian_tokens_remain_isolated(self) -> None:
        self.assertNotRegex(self.tokens, r"--wt-[a-z0-9-]+\s*:")
        wentian = self.modules["pages/heaven/foundation.css"]
        self.assertRegex(wentian, r"--wt-[a-z0-9-]+\s*:")

    def test_compatibility_aliases_cover_historical_layers(self) -> None:
        for variable in (
            "--blue:",
            "--up:",
            "--xb-blue-500:",
            "--r2-blue:",
            "--chart-background:",
            "--dragon-profile-list-width:",
        ):
            self.assertIn(variable, self.tokens)


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