from __future__ import annotations

import importlib.util
import subprocess
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 _graph(*, symbols: list[dict] | None = None, entrypoints: list[dict] | None = None) -> dict:
    return {
        "capabilities": [],
        "entrypoints": entrypoints or [],
        "phases": [],
        "routes": [],
        "loops": [],
        "flags": [],
        "artifacts": [],
        "divergences": [],
        "symbols": symbols or [],
    }


def _touch_py(tmp_path: Path, name: str, body: str) -> Path:
    path = tmp_path / name
    path.write_text(body)
    return path


def test_flags_unmodeled_public_function(tmp_path):
    src = _touch_py(tmp_path, "surface.py", "def visible_fork():\n    pass\n\ndef _private():\n    pass\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert any(f"{src}::visible_fork" in w for w in warnings)
    assert not any("_private" in w for w in warnings)


def test_modeled_function_not_flagged(tmp_path):
    src = _touch_py(tmp_path, "surface.py", "def visible_fork():\n    pass\n")
    graph = _graph(symbols=[{"id": "sym", "file": str(src), "symbol": "visible_fork"}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert not any("visible_fork" in w for w in warnings)


def test_allowlist_suppresses(tmp_path):
    src = _touch_py(tmp_path, "surface.py", "def visible_fork():\n    pass\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist={f"{src}::visible_fork"})

    assert not any("visible_fork" in w for w in warnings)


def test_flags_fastapi_route_handler(tmp_path):
    src = _touch_py(
        tmp_path,
        "route.py",
        "from fastapi import FastAPI\napp = FastAPI()\n\n@app.get('/x')\ndef get_x():\n    pass\n",
    )
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert any(f"{src}::get_x" in w for w in warnings)


def test_flags_cli_main_marker(tmp_path):
    src = _touch_py(
        tmp_path,
        "cli.py",
        "import argparse\n\nif __name__ == \"__main__\":\n    argparse.ArgumentParser()\n",
    )
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert any(f"{src}::__main__" in w for w in warnings)


def test_skips_methods_and_nested(tmp_path):
    src = _touch_py(
        tmp_path,
        "nested.py",
        "class C:\n    def method(self):\n        pass\n\n"
        "def outer():\n    def inner():\n        pass\n    return inner\n",
    )
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert any(f"{src}::outer" in w for w in warnings)
    assert not any(f"{src}::method" in w for w in warnings)
    assert not any(f"{src}::inner" in w for w in warnings)


def test_constants_not_enumerated(tmp_path):
    src = _touch_py(tmp_path, "constants.py", "DEFAULT_X = 1\n\ndef fn():\n    pass\n")
    graph = _graph(symbols=[{"id": "fn", "file": str(src), "symbol": "fn"}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert not any("DEFAULT_X" in w for w in warnings)


def test_default_allowlist_file_loaded(tmp_path, monkeypatch):
    src = _touch_py(tmp_path, "surface.py", "def visible_fork():\n    pass\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])
    allowlist = tmp_path / "allowlist.txt"
    allowlist.write_text(f"# comment\n\n{src}::visible_fork\n")
    monkeypatch.setattr(m, "UNMODELED_ALLOWLIST_PATH", allowlist)

    warnings = m.unmodeled_surface_warnings(graph)

    assert not any("visible_fork" in w for w in warnings)

    monkeypatch.setattr(m, "UNMODELED_ALLOWLIST_PATH", tmp_path / "missing.txt")
    warnings = m.unmodeled_surface_warnings(graph)
    assert any(f"{src}::visible_fork" in w for w in warnings)


def test_main_advisory_with_forced_warning(monkeypatch, capsys):
    monkeypatch.setattr(
        m,
        "unmodeled_surface_warnings",
        lambda graph: ["unmodeled surface (modeled file, surface absent from topology): x.py::synthetic"],
    )
    monkeypatch.setattr(sys, "argv", ["build_topology.py", "--check"])

    try:
        rc = m.main()
    except SystemExit as exc:
        rc = exc.code

    captured = capsys.readouterr()
    assert (rc or 0) == 0
    assert "x.py::synthetic" in captured.err
    assert "unmodeled surface warning(s)" in captured.err


def test_seeded_baseline_channel_clean():
    result = subprocess.run(
        [sys.executable, str(MODULE_PATH), "--check"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        text=True,
        check=False,
    )

    assert result.returncode == 0
    assert "topology: 0 unmodeled surface warning(s)" in result.stderr


def test_deterministic_order(tmp_path):
    src = _touch_py(tmp_path, "order.py", "def zed():\n    pass\n\ndef alpha():\n    pass\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert warnings == sorted(warnings)


def test_same_name_different_file_not_suppressed(tmp_path):
    src_a = _touch_py(tmp_path, "a.py", "def main():\n    pass\n")
    src_b = _touch_py(tmp_path, "b.py", "def main():\n    pass\n")
    graph = _graph(
        symbols=[
            {"id": "a-main", "file": str(src_a), "symbol": "main"},
            {"id": "b-file", "file": str(src_b)},
        ]
    )

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert not any(f"{src_a}::main" in w for w in warnings)
    assert any(f"{src_b}::main" in w for w in warnings)


def test_file_with_colon_symbol_normalized(tmp_path):
    src = _touch_py(tmp_path, "c.py", "def handler():\n    pass\n")
    graph = _graph(symbols=[{"id": "handler", "file": f"{src}::handler", "symbol": "handler"}])

    keys, _ = m._surface_keys(graph)
    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert f"{src}::handler" in keys
    assert f"{src}::handler::handler" not in keys
    assert not any("handler" in w for w in warnings)


def test_write_baseline_zeroes_channel(tmp_path, monkeypatch):
    src = _touch_py(tmp_path, "surface.py", "def visible_fork():\n    pass\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])
    allowlist = tmp_path / "allowlist.txt"
    monkeypatch.setattr(m, "UNMODELED_ALLOWLIST_PATH", allowlist)

    count = m.write_surface_baseline(graph)

    assert count >= 1
    assert m.unmodeled_surface_warnings(graph) == []


def test_new_surface_warns_against_baseline(tmp_path, monkeypatch):
    src = _touch_py(tmp_path, "surface.py", "def visible_fork():\n    pass\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])
    allowlist = tmp_path / "allowlist.txt"
    monkeypatch.setattr(m, "UNMODELED_ALLOWLIST_PATH", allowlist)
    assert m.write_surface_baseline(graph) >= 1
    src.write_text("def visible_fork():\n    pass\n\ndef new_fork():\n    pass\n")

    warnings = m.unmodeled_surface_warnings(graph)

    assert any(f"{src}::new_fork" in w for w in warnings)
    assert not any(f"{src}::visible_fork" in w for w in warnings)


def test_unscannable_file_surfaced_not_silent(tmp_path):
    src = _touch_py(tmp_path, "broken.py", "def (:\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])

    warnings = m.unmodeled_surface_warnings(graph, allowlist=set())

    assert warnings == [f"unmodeled-surface scan skipped: {src} (SyntaxError)"]


def test_write_baseline_aborts_on_invalid_topology(tmp_path, monkeypatch, capsys):
    allowlist = tmp_path / "allowlist.txt"
    monkeypatch.setattr(m, "UNMODELED_ALLOWLIST_PATH", allowlist)
    monkeypatch.setattr(m, "load_graph", lambda: ({}, ["boom"]))
    monkeypatch.setattr(m, "_validate_graph", lambda graph: [])
    monkeypatch.setattr(sys, "argv", ["build_topology.py", "--write-surface-baseline"])

    rc = m.main()

    captured = capsys.readouterr()
    assert rc == 1
    assert "TOPOLOGY ERRORS" in captured.err
    assert "boom" in captured.err
    assert not allowlist.exists()


def test_write_baseline_aborts_on_unscannable(tmp_path, monkeypatch, capsys):
    src = _touch_py(tmp_path, "broken.py", "def (:\n")
    graph = _graph(symbols=[{"id": "file", "file": str(src)}])
    allowlist = tmp_path / "allowlist.txt"
    monkeypatch.setattr(m, "UNMODELED_ALLOWLIST_PATH", allowlist)

    assert m.write_surface_baseline(graph) == -1
    assert not allowlist.exists()

    monkeypatch.setattr(m, "load_graph", lambda: (graph, []))
    monkeypatch.setattr(m, "_validate_graph", lambda graph: [])
    monkeypatch.setattr(sys, "argv", ["build_topology.py", "--write-surface-baseline"])
    rc = m.main()

    captured = capsys.readouterr()
    assert rc == 1
    assert "cannot baseline: unscannable modeled file(s):" in captured.err
    assert str(src) in captured.err
    assert not allowlist.exists()
