"""``pr-health`` — board items 34 (empty PR) and 33 (stale PR).

Every test here drives the forge through a stub, because the defect being pinned
is *what the tool concludes from a forge response*, not the forge itself. The
shapes are taken from the real responses that produced item 34: `mergeable: true`
and CI `success` on a PR with `changed_files: 0`.
"""

from __future__ import annotations

import argparse
import urllib.error
from typing import Any

import pytest

from mgf.fed import _prhealth
from mgf.fed._prhealth import PRFinding, check_repo, run_pr_health


def _pr(num: int, changed: int | None, base: str = "main", head: str = "feat/x") -> dict[str, Any]:
    d: dict[str, Any] = {
        "number": num,
        "title": f"pr {num}",
        "mergeable": True,  # true for an empty PR too — that is the whole point
        "base": {"ref": base},
        "head": {"ref": head},
    }
    if changed is not None:
        d["changed_files"] = changed
    return d


def _stub(monkeypatch: pytest.MonkeyPatch, routes: dict[str, Any]) -> list[str]:
    """Route by URL substring. Returns the list of URLs actually requested."""
    seen: list[str] = []

    def fake(url: str, **_kw: Any) -> Any:
        seen.append(url)
        for frag, val in routes.items():
            if frag in url:
                if isinstance(val, Exception):
                    raise val
                return val
        raise AssertionError(f"unrouted URL: {url}")

    monkeypatch.setattr(_prhealth, "request_json", fake)
    return seen


# --- item 34: the empty PR -----------------------------------------------------


def test_an_empty_pr_is_a_finding(monkeypatch: pytest.MonkeyPatch) -> None:
    """`mergeable: true` and CI green were BOTH true of these — because there was
    nothing in them to fail."""
    _stub(monkeypatch, {"/pulls?state=open": [_pr(46, changed=0)]})
    found, err = check_repo("o", "mgf-livepush", "t", 20)
    assert err == ""
    assert [f.kind for f in found] == ["empty"]
    assert "changed_files: 0" in found[0].detail


def test_a_substantive_current_pr_is_not_a_finding(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub(
        monkeypatch, {"/pulls?state=open": [_pr(1, changed=3)], "/compare/": {"total_commits": 0}}
    )
    assert check_repo("o", "r", "t", 20) == ([], "")


def test_absent_changed_files_is_fetched_not_assumed_zero(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Some forge versions omit `changed_files` from the LIST response. Reading
    absent as zero would report every PR on such a forge as empty."""
    seen = _stub(
        monkeypatch,
        {
            "/pulls?state=open": [_pr(7, changed=None)],
            "/pulls/7": {"changed_files": 5},
            "/compare/": {"total_commits": 0},
        },
    )
    found, _ = check_repo("o", "r", "t", 20)
    assert found == []
    assert any(u.endswith("/pulls/7") for u in seen), "never asked for the detail"


def test_absent_changed_files_that_is_really_zero_still_reports(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _stub(
        monkeypatch,
        {"/pulls?state=open": [_pr(7, changed=None)], "/pulls/7": {"changed_files": 0}},
    )
    found, _ = check_repo("o", "r", "t", 20)
    assert [f.kind for f in found] == ["empty"]


def test_an_empty_pr_is_not_also_reported_stale(monkeypatch: pytest.MonkeyPatch) -> None:
    """One PR, one finding: 'this is empty' is the actionable fact, and adding
    'and also 900 behind' buries it."""
    _stub(
        monkeypatch,
        {"/pulls?state=open": [_pr(9, changed=0)], "/compare/": {"total_commits": 900}},
    )
    found, _ = check_repo("o", "r", "t", 20)
    assert len(found) == 1 and found[0].kind == "empty"


# --- item 33: the stale PR -----------------------------------------------------


def test_a_pr_far_behind_its_base_is_stale(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub(
        monkeypatch,
        {"/pulls?state=open": [_pr(2, changed=1)], "/compare/": {"total_commits": 41}},
    )
    found, _ = check_repo("o", "r", "t", 20)
    assert [f.kind for f in found] == ["stale"]
    assert "41 commits behind main" in found[0].detail


def test_the_limit_is_respected(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub(
        monkeypatch,
        {"/pulls?state=open": [_pr(2, changed=1)], "/compare/": {"total_commits": 20}},
    )
    assert check_repo("o", "r", "t", 20) == ([], "")  # exactly at the limit is fine


def test_compare_falls_back_to_counting_commits(monkeypatch: pytest.MonkeyPatch) -> None:
    _stub(
        monkeypatch,
        {"/pulls?state=open": [_pr(2, changed=1)], "/compare/": {"commits": [{}] * 30}},
    )
    found, _ = check_repo("o", "r", "t", 20)
    assert [f.kind for f in found] == ["stale"]


def test_an_unreadable_compare_is_not_reported_as_current(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """None must not collapse into 0 — 'I could not tell' is not 'it is current'.
    It is also not a finding: inventing staleness from a failed call would be a
    false positive. It simply is not claimed either way."""
    err = urllib.error.HTTPError("u", 500, "boom", {}, None)  # type: ignore[arg-type]
    _stub(monkeypatch, {"/compare/": err})
    # Assert the CONTRACT, not a downstream symptom. An earlier version checked
    # only that `check_repo` produced no finding — but 0 is not greater than the
    # limit either, so that test passed whether the failure returned None or 0,
    # and a sabotage of exactly this line went undetected.
    assert _prhealth._behind_by("o", "r", "main", "feat/x", "t") is None

    _stub(monkeypatch, {"/pulls?state=open": [_pr(2, changed=1)], "/compare/": err})
    found, _ = check_repo("o", "r", "t", 20)
    assert found == []  # ...and it is not invented as a finding either


# --- exit codes ----------------------------------------------------------------


def _args(**kw: Any) -> argparse.Namespace:
    base = {"owner": "o", "behind_limit": 20, "repo": "r", "root": None}
    base.update(kw)
    return argparse.Namespace(**base)


def test_no_token_is_two_not_zero(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.delenv("AGENT_CODEBERG_WRITE", raising=False)
    assert run_pr_health(_args()) == 2
    assert "NOT a pass" in capsys.readouterr().out


def test_clean_is_zero(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("AGENT_CODEBERG_WRITE", "tok")
    monkeypatch.setattr(_prhealth, "check_repo", lambda *a: ([], ""))
    assert run_pr_health(_args()) == 0


def test_findings_are_one(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("AGENT_CODEBERG_WRITE", "tok")
    monkeypatch.setattr(
        _prhealth, "check_repo", lambda *a: ([PRFinding("r", 1, "t", "empty", "d")], "")
    )
    assert run_pr_health(_args()) == 1


def test_a_repo_that_could_not_be_checked_is_two_not_zero(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    """The failure this whole command exists to end, one level up: a sweep that
    could not reach a repo must not report the fleet clean."""
    monkeypatch.setenv("AGENT_CODEBERG_WRITE", "tok")
    monkeypatch.setattr(_prhealth, "check_repo", lambda *a: ([], "r: HTTP 500"))
    assert run_pr_health(_args()) == 2
    assert "not a pass" in capsys.readouterr().out


def test_an_empty_roster_is_two_not_zero(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("AGENT_CODEBERG_WRITE", "tok")
    monkeypatch.setattr(_prhealth, "members", lambda _root=None: [])
    assert run_pr_health(_args(repo=None)) == 2
