"""Meta-test on `scripts/check_adapter_budget.py`.

The budget script is itself a guardrail protecting premise 3 of the design
doc. If the counter silently miscounts (e.g. starts counting docstring lines
or stops counting comment-terminated lines), the guardrail drifts without
anyone noticing. These tests pin the counting rules.
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

import pytest

_REPO_ROOT = Path(__file__).resolve().parents[1]
_SCRIPT_PATH = _REPO_ROOT / "scripts" / "check_adapter_budget.py"


def _load_script() -> object:
    spec = importlib.util.spec_from_file_location("check_adapter_budget", _SCRIPT_PATH)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


@pytest.fixture(scope="module")
def script() -> object:
    return _load_script()


# ---------------------------------------------------------------------------
# count_logical_lines
# ---------------------------------------------------------------------------


class TestCountLogicalLines:
    def test_blank_lines_not_counted(self, script: object) -> None:
        assert script.count_logical_lines("\n\n\n") == 0  # type: ignore[attr-defined]

    def test_comments_not_counted(self, script: object) -> None:
        src = "# comment\n# another\n# still a comment\n"
        assert script.count_logical_lines(src) == 0  # type: ignore[attr-defined]

    def test_module_docstring_not_counted(self, script: object) -> None:
        src = '"""Module docstring.\nspans\nmultiple lines."""\n'
        assert script.count_logical_lines(src) == 0  # type: ignore[attr-defined]

    def test_function_docstring_not_counted(self, script: object) -> None:
        src = 'def f():\n    """docstring"""\n    return 1\n'
        # 2 logical lines: `def f():` and `return 1`
        assert script.count_logical_lines(src) == 2  # type: ignore[attr-defined]

    def test_class_docstring_not_counted(self, script: object) -> None:
        src = 'class Foo:\n    """Foo docstring"""\n    x: int = 1\n'
        assert script.count_logical_lines(src) == 2  # type: ignore[attr-defined]

    def test_inline_comment_does_not_decrement(self, script: object) -> None:
        src = "x = 1  # trailing comment\ny = 2\n"
        assert script.count_logical_lines(src) == 2  # type: ignore[attr-defined]

    def test_real_code_counted(self, script: object) -> None:
        src = "import os\nimport sys\nprint(os.getcwd())\n"
        assert script.count_logical_lines(src) == 3  # type: ignore[attr-defined]


# ---------------------------------------------------------------------------
# main() exit codes on fixture adapter trees
# ---------------------------------------------------------------------------


def _write_adapter(path: Path, n_logical_lines: int) -> None:
    """Create an adapter file with roughly `n_logical_lines` code lines."""
    # Docstring doesn't count; pad with simple assignments.
    body = ['"""Fixture adapter."""', "from __future__ import annotations", ""]
    body.extend(f"x_{i} = {i}" for i in range(n_logical_lines - 1))
    path.write_text("\n".join(body) + "\n")


class TestMainExitCodes:
    def test_under_budget_exits_zero(
        self, script: object, tmp_path: Path, capsys: pytest.CaptureFixture[str]
    ) -> None:
        _write_adapter(tmp_path / "under.py", n_logical_lines=249)
        rc = script.main(["--path", str(tmp_path), "--budget", "300"])  # type: ignore[attr-defined]
        assert rc == 0
        out = capsys.readouterr().out
        assert "PASS" in out
        assert "249" in out

    def test_at_budget_exits_zero(
        self, script: object, tmp_path: Path, capsys: pytest.CaptureFixture[str]
    ) -> None:
        _write_adapter(tmp_path / "exact.py", n_logical_lines=300)
        rc = script.main(["--path", str(tmp_path), "--budget", "300"])  # type: ignore[attr-defined]
        assert rc == 0

    def test_over_budget_exits_one(
        self, script: object, tmp_path: Path, capsys: pytest.CaptureFixture[str]
    ) -> None:
        _write_adapter(tmp_path / "huge.py", n_logical_lines=350)
        rc = script.main(["--path", str(tmp_path), "--budget", "300"])  # type: ignore[attr-defined]
        assert rc == 1
        err = capsys.readouterr().err
        assert "over budget" in err

    def test_init_py_is_skipped(self, script: object, tmp_path: Path) -> None:
        # Even a giant __init__.py shouldn't trip the check.
        _write_adapter(tmp_path / "__init__.py", n_logical_lines=10_000)
        _write_adapter(tmp_path / "ok.py", n_logical_lines=50)
        rc = script.main(["--path", str(tmp_path), "--budget", "300"])  # type: ignore[attr-defined]
        assert rc == 0

    def test_missing_path_exits_two(self, script: object) -> None:
        rc = script.main(["--path", "/nonexistent/does/not/exist"])  # type: ignore[attr-defined]
        assert rc == 2

    def test_empty_dir_warns_but_passes(
        self, script: object, tmp_path: Path, capsys: pytest.CaptureFixture[str]
    ) -> None:
        rc = script.main(["--path", str(tmp_path)])  # type: ignore[attr-defined]
        assert rc == 0
        err = capsys.readouterr().err
        assert "no adapters found" in err

    def test_syntax_error_exits_two(self, script: object, tmp_path: Path) -> None:
        (tmp_path / "broken.py").write_text("def f(:\n    pass\n")
        rc = script.main(["--path", str(tmp_path)])  # type: ignore[attr-defined]
        assert rc == 2


class TestRealAdaptersFitBudget:
    """Integration: run the script against the actual `src/acaua/adapters/`
    tree. If this fails, an adapter crossed the 300 logical-line budget and
    the abstraction is leaking per premise 3."""

    def test_real_adapters_pass(self, script: object) -> None:
        rc = script.main(  # type: ignore[attr-defined]
            ["--path", str(_REPO_ROOT / "src" / "acaua" / "adapters"), "--budget", "300"]
        )
        assert rc == 0


# Clean up after module-level sys.modules pollution from importlib.util.
def teardown_module() -> None:
    sys.modules.pop("check_adapter_budget", None)
