from __future__ import annotations

import importlib.util
import sys
from pathlib import Path


MODULE_PATH = Path(__file__).resolve().with_name("build_topology.py")
SPEC = importlib.util.spec_from_file_location("build_topology", MODULE_PATH)
m = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(m)


def _empty_graph(**overrides) -> dict:
    graph = {ntype: [] for ntype in m.NODE_TYPES}
    graph["out_of_scope_capabilities"] = []
    graph.update(overrides)
    return graph


def test_schema_field_ast_check_fails_on_missing_field(tmp_path):
    src = tmp_path / "models.py"
    src.write_text("class BibleProp:\n    attached_to: str\n")
    graph = _empty_graph(
        schema=[
            {
                "id": "schema_bible_prop",
                "model": "BibleProp",
                "file": f"{src}::BibleProp",
                "fields": [
                    {
                        "name": "worn_by",
                        "type": "str",
                        "meaning": "Bogus renamed field.",
                        "semantic": "prop_carrier",
                        "dup_tier": "always",
                    }
                ],
            }
        ]
    )
    errors: list[str] = []

    m.check_symrefs(graph, errors)

    assert any("BibleProp.worn_by" in err and "not found (AST)" in err for err in errors)


def test_check_schema_dups_groups_semantic_cluster():
    graph = _empty_graph(
        schema=[
            {
                "id": "schema_a",
                "model": "A",
                "fields": [{"name": "left", "semantic": "shared", "dup_tier": "always"}],
            },
            {
                "id": "schema_b",
                "model": "B",
                "fields": [{"name": "right", "semantic": "shared", "dup_tier": "always"}],
            },
        ]
    )
    errors: list[str] = []

    clusters = m.check_schema_dups(graph, errors)

    assert clusters["shared"] == ["A.left", "B.right"]
    assert errors == []


def test_reduced_renders_prop_carrier_cluster():
    graph, errors = m.load_graph()
    errors.extend(m._validate_graph(graph))
    assert errors == []

    reduced = m.build_reduced(graph)
    schema_start = reduced.index("## SCHEMA SSOT")
    detail_start = reduced.find("### Per-Model Detail", schema_start)
    schema_end = detail_start if detail_start != -1 else reduced.index("## Entrypoints", schema_start)
    prop_start = reduced.index("- semantic: prop_carrier", schema_start, schema_end)
    next_semantic = reduced.find("- semantic:", prop_start + 1, schema_end)
    prop_block = reduced[prop_start: next_semantic if next_semantic != -1 else schema_end]

    for ref in (
        "BibleProp.attached_to",
        "BibleProp.is_permanent_attachment",
        "BibleProp.carriable",
        "PhaseAppearance.visible_gear",
        "WardrobePiece.state",
    ):
        assert ref in prop_block
    if detail_start != -1:
        assert prop_start < detail_start
    assert "BibleProp.prop_id" not in reduced


def test_reduced_under_cap():
    graph, errors = m.load_graph()
    errors.extend(m._validate_graph(graph))
    assert errors == []

    assert m.token_proxy(m.build_reduced(graph)) <= m.REDUCED_TOKEN_CAP
    assert m.REDUCED_TOKEN_CAP == 7500


def test_unmodeled_warns_not_fails(tmp_path, monkeypatch, capsys):
    def run_case(graph: dict, inventory: list[dict]) -> tuple[int, str, list[str]]:
        outputs = {
            "topology.full.json": "{}\n",
            "TOPOLOGY_REDUCED.md": "## ⚠ DIVERGENCES\n",
            "topology.engineering.md": "ok\n",
            "topology.investor.md": "ok\n",
            "topology.design_brief.md": "ok\n",
            "topology.drift.json": "{}\n",
        }
        gen_dir = tmp_path / f"generated_{len(inventory)}_{len(graph.get('schema', []))}"
        gen_dir.mkdir()
        for name, text in outputs.items():
            (gen_dir / name).write_text(text)

        monkeypatch.setattr(m, "GEN_DIR", gen_dir)
        monkeypatch.setattr(m, "load_graph", lambda: (graph, []))
        monkeypatch.setattr(m, "_validate_graph", lambda g: [])
        monkeypatch.setattr(m, "generate_all", lambda g: outputs)
        monkeypatch.setattr(m, "manifest_capabilities", lambda: {})
        monkeypatch.setattr(m, "unmodeled_entrypoint_warnings", lambda g: [])
        monkeypatch.setattr(m, "unmodeled_surface_warnings", lambda g: [])
        monkeypatch.setattr(m, "_schema_model_inventory", lambda: inventory)
        monkeypatch.setattr(sys, "argv", ["build_topology.py", "--check"])

        rc = m.main()
        captured = capsys.readouterr()
        return rc, captured.err, m.schema_unmodeled_warnings(graph)

    field_graph = _empty_graph(
        schema=[
            {
                "id": "schema_modeled",
                "model": "Modeled",
                "fields": [{"name": "present", "semantic": None, "dup_tier": "never"}],
            }
        ]
    )
    rc, stderr, warnings = run_case(
        field_graph,
        [{"model": "Modeled", "file": "model.py", "fields": ["present", "missing"]}],
    )
    assert rc == 0
    assert warnings == ["schema field missing: Modeled.missing (model.py)"]
    assert "topology WARN: schema field missing: Modeled.missing" in stderr

    class_graph = _empty_graph(schema=[])
    rc, stderr, warnings = run_case(
        class_graph,
        [{"model": "Unmodeled", "file": "model.py", "fields": ["present"]}],
    )
    assert rc == 0
    assert warnings == ["schema class missing: Unmodeled (model.py)"]
    assert "topology WARN: schema class missing: Unmodeled" in stderr
