"""Tests for the component dependency analyzer."""

import pytest
from shared.sx.parser import parse_all
from shared.sx.types import Component, Macro, Symbol
from shared.sx.deps import (
    _scan_ast,
    transitive_deps,
    compute_all_deps,
    scan_components_from_sx,
    components_needed,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_env(*sx_sources: str) -> dict:
    """Parse and evaluate component definitions into an env dict."""
    from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
    env: dict = {}
    for source in sx_sources:
        exprs = parse_all(source)
        for expr in exprs:
            _trampoline(_eval(expr, env))
    return env


# ---------------------------------------------------------------------------
# _scan_ast
# ---------------------------------------------------------------------------

class TestScanAst:
    def test_simple_component_ref(self):
        env = make_env('(defcomp ~card (&key title) (div (~shared:misc/badge :label title)))')
        comp = env["~card"]
        refs = _scan_ast(comp.body)
        assert refs == {"~shared:misc/badge"}

    def test_no_refs(self):
        env = make_env('(defcomp ~plain (&key text) (div :class "p-4" text))')
        comp = env["~plain"]
        refs = _scan_ast(comp.body)
        assert refs == set()

    def test_multiple_refs(self):
        env = make_env(
            '(defcomp ~page (&key title) (div (~header :title title) (~footer)))'
        )
        comp = env["~page"]
        refs = _scan_ast(comp.body)
        assert refs == {"~header", "~footer"}

    def test_nested_in_control_flow(self):
        env = make_env(
            '(defcomp ~card (&key big) '
            '  (if big (~big-card) (~small-card)))'
        )
        comp = env["~card"]
        refs = _scan_ast(comp.body)
        assert refs == {"~big-card", "~small-card"}

    def test_refs_in_dict(self):
        env = make_env(
            '(defcomp ~wrap (&key) (div {:slot (~inner)}))'
        )
        comp = env["~wrap"]
        refs = _scan_ast(comp.body)
        assert refs == {"~inner"}


# ---------------------------------------------------------------------------
# transitive_deps
# ---------------------------------------------------------------------------

class TestTransitiveDeps:
    def test_direct_dep(self):
        env = make_env(
            '(defcomp ~card (&key) (div (~shared:misc/badge)))',
            '(defcomp ~shared:misc/badge (&key) (span "★"))',
        )
        deps = transitive_deps("~card", env)
        assert deps == {"~shared:misc/badge"}

    def test_transitive(self):
        env = make_env(
            '(defcomp ~page (&key) (div (~layout)))',
            '(defcomp ~layout (&key) (div (~header) (~footer)))',
            '(defcomp ~header (&key) (nav "header"))',
            '(defcomp ~footer (&key) (footer "footer"))',
        )
        deps = transitive_deps("~page", env)
        assert deps == {"~layout", "~header", "~footer"}

    def test_circular(self):
        """Circular deps should not cause infinite recursion."""
        env = make_env(
            '(defcomp ~a (&key) (div (~b)))',
            '(defcomp ~b (&key) (div (~a)))',
        )
        deps = transitive_deps("~a", env)
        assert deps == {"~b"}

    def test_no_deps(self):
        env = make_env('(defcomp ~leaf (&key) (span "hi"))')
        deps = transitive_deps("~leaf", env)
        assert deps == set()

    def test_missing_component(self):
        """Referencing a component not in env should not crash."""
        env = make_env('(defcomp ~card (&key) (div (~unknown)))')
        deps = transitive_deps("~card", env)
        assert "~unknown" in deps

    def test_island_deps_scanned(self):
        """Island bodies must be scanned for component dependencies."""
        env = make_env(
            '(defcomp ~leaf (&key) (span "leaf"))',
            '(defcomp ~branch (&key) (div (~leaf)))',
            '(defisland ~my-island () (div (~branch) "island"))',
        )
        deps = transitive_deps("~my-island", env)
        assert "~branch" in deps
        assert "~leaf" in deps

    def test_without_tilde_prefix(self):
        env = make_env(
            '(defcomp ~card (&key) (div (~shared:misc/badge)))',
            '(defcomp ~shared:misc/badge (&key) (span "★"))',
        )
        deps = transitive_deps("card", env)
        assert deps == {"~shared:misc/badge"}


# ---------------------------------------------------------------------------
# compute_all_deps
# ---------------------------------------------------------------------------

class TestComputeAllDeps:
    def test_sets_deps_on_components(self):
        env = make_env(
            '(defcomp ~page (&key) (div (~card)))',
            '(defcomp ~card (&key) (div (~shared:misc/badge)))',
            '(defcomp ~shared:misc/badge (&key) (span "★"))',
        )
        compute_all_deps(env)
        assert env["~page"].deps == {"~card", "~shared:misc/badge"}
        assert env["~card"].deps == {"~shared:misc/badge"}
        assert env["~shared:misc/badge"].deps == set()


# ---------------------------------------------------------------------------
# scan_components_from_sx
# ---------------------------------------------------------------------------

class TestScanComponentsFromSx:
    def test_basic(self):
        source = '(~card :title "hi" (~shared:misc/badge :label "new"))'
        refs = scan_components_from_sx(source)
        assert refs == {"~card", "~shared:misc/badge"}

    def test_no_components(self):
        source = '(div :class "p-4" (p "hello"))'
        refs = scan_components_from_sx(source)
        assert refs == set()


# ---------------------------------------------------------------------------
# components_needed
# ---------------------------------------------------------------------------

class TestComponentsNeeded:
    def test_page_with_deps(self):
        env = make_env(
            '(defcomp ~page-layout (&key) (div (~plans/environment-images/nav) (~footer)))',
            '(defcomp ~plans/environment-images/nav (&key) (nav "nav"))',
            '(defcomp ~footer (&key) (footer "footer"))',
            '(defcomp ~unused (&key) (div "not needed"))',
        )
        compute_all_deps(env)
        page_sx = '(~page-layout)'
        needed = components_needed(page_sx, env)
        assert "~page-layout" in needed
        assert "~plans/environment-images/nav" in needed
        assert "~footer" in needed
        assert "~unused" not in needed
