"""Individual doctor checks C1/C6/C7/C8 + report shape (SPEC §5)."""

from __future__ import annotations

from mgf.secrets.doctor import Doctor
from mgf.secrets.hosts import HostStat
from mgf.secrets.registry import load
from tests._fakes import FakeHost, FakeWoodpecker, write_pipeline


def _reg(tmp_path, text):
    p = tmp_path / "SECRETS.toml"
    p.write_text(text, encoding="utf-8")
    return load([p])


ORG = """\
[s1]
family = "mgf"
kind = "api-token"
purpose = "p"
consumers = ["c"]
recovery = "r"
state = "active"
policy = { allowed_repos = "*", blast_radius = "b" }
rotation = { class = "token", staged = true }
verify = { probe = "auth" }
surfaces = [ { type = "woodpecker-org-secret", events = ["push", "tag"] } ]
"""


def test_c1_missing_secret(tmp_path) -> None:
    reg = _reg(tmp_path, ORG)
    wp = FakeWoodpecker(org_secrets={}, repos=[])
    findings = Doctor(reg, fed_root=tmp_path, woodpecker=wp).c1_declared_actual()
    assert findings and findings[0].check == "C1" and "MISSING" in findings[0].finding


def test_c1_scope_mismatch(tmp_path) -> None:
    reg = _reg(tmp_path, ORG)
    wp = FakeWoodpecker(org_secrets={"s1": {"push"}}, repos=[])  # missing 'tag'
    findings = Doctor(reg, fed_root=tmp_path, woodpecker=wp).c1_declared_actual()
    assert findings and "!= declared" in findings[0].finding


def test_c1_clean_when_exact(tmp_path) -> None:
    reg = _reg(tmp_path, ORG)
    wp = FakeWoodpecker(org_secrets={"s1": {"push", "tag"}}, repos=[])
    assert Doctor(reg, fed_root=tmp_path, woodpecker=wp).c1_declared_actual() == []


C8 = """\
[scoped_secret]
family = "mgf"
kind = "api-token"
purpose = "p"
consumers = ["c"]
recovery = "r"
state = "active"
policy = { allowed_repos = ["mgf-cost-watch"], blast_radius = "b" }
rotation = { class = "token", staged = true }
verify = { probe = "auth" }
surfaces = [ { type = "woodpecker-org-secret", events = ["push"] } ]
"""


def test_c8_policy_out_of_allowed_repos(tmp_path) -> None:
    reg = _reg(tmp_path, C8)
    write_pipeline(
        tmp_path,
        "mgf-website",
        """\
when:
  - event: push
steps:
  - name: x
    environment:
      E:
        from_secret: scoped_secret
    commands: [echo x]
""",
    )
    wp = FakeWoodpecker(org_secrets={"scoped_secret": {"push"}}, repos=[(1, "mgf-website")])
    findings = Doctor(reg, fed_root=tmp_path, woodpecker=wp).run(["woodpecker"])
    c8 = [f for f in findings.findings if f.check == "C8"]
    assert c8 and c8[0].evidence["repo"] == "mgf-website"


HOSTFILE = """\
[oo_root]
family = "mgf"
kind = "service-account"
purpose = "p"
consumers = ["c"]
recovery = "r"
state = "active"
policy = { allowed_repos = "*", blast_radius = "b" }
rotation = { class = "sa", staged = true }
verify = { probe = "oo" }
surfaces = [ { type = "host-file", host = "h1", path = "/etc/mgf/x", mode = "0600", owner = "root" } ]
"""


def test_c7_missing_and_mode_owner_drift(tmp_path) -> None:
    reg = _reg(tmp_path, HOSTFILE)
    # missing
    hosts = FakeHost({("h1", "/etc/mgf/x"): HostStat("missing")})
    f = Doctor(reg, fed_root=tmp_path, hosts=hosts).c7_hosts()
    assert f and "MISSING" in f[0].finding and f[0].severity == "high"
    # mode + owner drift
    hosts = FakeHost({("h1", "/etc/mgf/x"): HostStat("present", mode="644", owner="ubuntu")})
    f = Doctor(reg, fed_root=tmp_path, hosts=hosts).c7_hosts()
    kinds = {x.finding for x in f}
    assert any("mode" in m for m in kinds) and any("owner" in m for m in kinds)


def test_c7_clean(tmp_path) -> None:
    reg = _reg(tmp_path, HOSTFILE)
    hosts = FakeHost({("h1", "/etc/mgf/x"): HostStat("present", mode="600", owner="root")})
    assert Doctor(reg, fed_root=tmp_path, hosts=hosts).c7_hosts() == []


def test_c7_unreachable_is_unverifiable_not_missing(tmp_path) -> None:
    """A present-but-unverifiable host-file (e.g. /opt/steward 0750 traversal) must
    NEVER page as MISSING — but "couldn't look" is now LOUD (SPEC §4): an
    ``unverifiable`` record (exit 1), distinct from both clean and a real finding."""
    reg = _reg(tmp_path, HOSTFILE)
    hosts = FakeHost({("h1", "/etc/mgf/x"): HostStat("unreachable", detail="permission denied")})
    doc = Doctor(reg, fed_root=tmp_path, hosts=hosts)
    findings = doc.c7_hosts()
    assert findings == []  # never a MISSING finding
    assert any(u.check == "C7" and "permission denied" in u.reason for u in doc.unverifiable)
    # the whole run reds on unverifiable (exit 1), tellable from clean.
    report = Doctor(reg, fed_root=tmp_path, hosts=hosts).run(["hosts"])
    assert report.exit_code() == 1
    assert not report.findings and report.unverifiable


LOCAL = """\
[dpw]
family = "mgf"
kind = "password"
purpose = "p"
consumers = ["c"]
recovery = "r"
state = "active"
policy = { allowed_repos = "*", blast_radius = "b" }
rotation = { class = "password", staged = true }
verify = { probe = "login" }
surfaces = [ { type = "local-file", repo = "mgf", path = "tokens/dpw.txt", mode = "0600" } ]
"""


def test_c6_mode_drift_and_leftover_and_gitignore(tmp_path) -> None:
    reg = _reg(tmp_path, LOCAL)
    base = tmp_path / "mgf" / "my_stuff"
    (base / "tokens").mkdir(parents=True)
    f = base / "tokens" / "dpw.txt"
    f.write_text("single-line-value", encoding="utf-8")
    f.chmod(0o644)  # mode drift vs declared 0600
    (base / "old_leftover.txt").write_text("x", encoding="utf-8")  # .bak/old_ leftover
    # no .gitignore covering my_stuff -> a finding
    findings = Doctor(reg, fed_root=tmp_path).c6_local_hygiene()
    msgs = [x.finding for x in findings]
    assert any("mode 644" in m for m in msgs)
    assert any("leftover" in m for m in msgs)
    assert any("gitignore" in m for m in msgs)


CLOUDS = """\
[oscred]
family = "mgf"
kind = "openstack-appcred"
purpose = "p"
consumers = ["c"]
recovery = "r"
state = "active"
policy = { allowed_repos = "*", blast_radius = "b" }
rotation = { class = "appcred", staged = true }
verify = { probe = "openstack" }
surfaces = [ { type = "local-file", repo = "mgf", path = "infomaniak/clouds.yaml", mode = "0600" } ]
"""


def test_c6_openstack_clouds_yaml_is_not_doc_form(tmp_path) -> None:
    """A clouds.yaml is legitimately multi-line YAML — never the doc-form leak class."""
    reg = _reg(tmp_path, CLOUDS)
    f = tmp_path / "mgf" / "my_stuff" / "infomaniak" / "clouds.yaml"
    f.parent.mkdir(parents=True)
    f.write_text("clouds:\n  mycloud:\n    auth:\n      username: x\n", encoding="utf-8")
    (tmp_path / "mgf" / ".gitignore").write_text("my_stuff/\n", encoding="utf-8")
    (tmp_path / "mgf" / "my_stuff").chmod(0o700)
    findings = Doctor(reg, fed_root=tmp_path).c6_local_hygiene()
    assert not any("DOC-FORM" in x.finding for x in findings)


# --- C11: canon path reconciliation ---------------------------------------- #

#: The counterfeit: an entry whose ONLY surface is the org secret, exactly as
#: [woodpecker_token] was declared while a live token sat on disk at a path the
#: canon told every agent to use.
C11_UNDECLARED = """\
[woodpecker_token]
family = "mgf"
kind = "jwt-minted"
purpose = "p"
consumers = ["c"]
recovery = "r"
state = "active"
policy = { allowed_repos = "*", blast_radius = "b" }
rotation = { class = "jwt", staged = true }
verify = { probe = "wp" }
surfaces = [ { type = "woodpecker-org-secret", events = ["tag"] } ]
"""

C11_DECLARED = C11_UNDECLARED.replace(
    'surfaces = [ { type = "woodpecker-org-secret", events = ["tag"] } ]',
    'surfaces = [ { type = "woodpecker-org-secret", events = ["tag"] },\n'
    '  { type = "local-file", repo = "mgf-standard", path = "woodpecker_token.txt", '
    'mode = "0600" } ]',
)

CANON = """\
## Access & recovery
Use `$WOODPECKER_TOKEN` or `~/PycharmProjects/mgf-standard/my_stuff/woodpecker_token.txt`.
Never read `my_stuff/**` contents. Host facts: `mgf-standard/my_stuff/infomaniak/FACTS.md`.
"""


def _canon_fed(tmp_path, registry_text):
    """A fed root whose MGF_INFRA.md names a token path, plus the sooth mirror."""
    for repo, name in (("mgf-standard", "MGF_INFRA.md"), ("sooth-standard", "SOOTH_INFRA.md")):
        d = tmp_path / repo
        d.mkdir(parents=True, exist_ok=True)
        (d / name).write_text(CANON if repo == "mgf-standard" else "# no paths\n", encoding="utf-8")
    return _reg(tmp_path, registry_text)


def test_c11_canon_path_with_no_declared_surface_is_a_finding(tmp_path, monkeypatch) -> None:
    """The negative control: reproduce the trap and prove the gate fires on it.

    This is the state the estate was actually in on 2026-08-01 — canon naming a token
    path, registry declaring only the org secret — and `mgf-secrets` reported clean.
    """
    monkeypatch.setenv("HOME", str(tmp_path))  # no host card in the fixture
    reg = _canon_fed(tmp_path, C11_UNDECLARED)
    findings = Doctor(reg, fed_root=tmp_path).c11_canon_path_reconciliation()
    assert len(findings) == 1
    f = findings[0]
    assert f.check == "C11" and f.severity == "high"
    assert f.evidence["rel"] == "woodpecker_token.txt"
    assert "no registry entry declares it" in f.finding
    # and it reds the whole canon run, not just this check
    assert Doctor(reg, fed_root=tmp_path).run(["canon"]).exit_code() == 1


def test_c11_declared_surface_is_clean(tmp_path, monkeypatch) -> None:
    """Declaring the surface — the Phase 1 fix — silences it. A gate that cannot pass
    is as useless as one that cannot fail."""
    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _canon_fed(tmp_path, C11_DECLARED)
    assert Doctor(reg, fed_root=tmp_path).c11_canon_path_reconciliation() == []


def test_c11_doc_reference_is_exempt_but_recorded(tmp_path, monkeypatch) -> None:
    """`infomaniak/FACTS.md` is documentation, not a credential — exempt, and SAID so."""
    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _canon_fed(tmp_path, C11_DECLARED)
    doc = Doctor(reg, fed_root=tmp_path)
    doc.c11_canon_path_reconciliation()
    assert any("FACTS.md" in s and "exempt" in s for s in doc.skipped)


def test_c11_globs_and_directories_are_not_credentials(tmp_path, monkeypatch) -> None:
    """An ordinary documentation edit must not red the credential gate (court P1-F1/F2).

    `my_stuff/tokens/*.txt` and `my_stuff/ssh_key/` name a SET, not a credential. Both
    used to produce HIGH findings against a CORRECT registry: `*` was absent from the
    match class, so the glob was silently truncated to its parent directory `tokens` and
    reported as an undeclared surface — and the `"*" in rel` guard written to prevent
    exactly that could never fire, because `*` never reached it.
    """
    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _canon_fed(tmp_path, C11_DECLARED)
    (tmp_path / "mgf-standard" / "MGF_INFRA.md").write_text(
        "Release tokens live in `my_stuff/tokens/*.txt`; keys in `my_stuff/ssh_key/`.\n"
        "Never read `my_stuff/**` contents.\n",
        encoding="utf-8",
    )
    doc = Doctor(reg, fed_root=tmp_path)
    assert doc.c11_canon_path_reconciliation() == []
    # all three exempted, and RECORDED — `**` counts too, now that `*` is matched
    assert sum("directory or glob" in s for s in doc.skipped) == 3


def test_c11_still_fires_on_a_real_path_beside_globs(tmp_path, monkeypatch) -> None:
    """The glob skip must not become a blanket amnesty for the file beside it."""
    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _canon_fed(tmp_path, C11_UNDECLARED)
    (tmp_path / "mgf-standard" / "MGF_INFRA.md").write_text(
        "Globs `my_stuff/tokens/*.txt` and the real one: "
        "`mgf-standard/my_stuff/woodpecker_token.txt`.\n",
        encoding="utf-8",
    )
    findings = Doctor(reg, fed_root=tmp_path).c11_canon_path_reconciliation()
    assert [f.evidence["rel"] for f in findings] == ["woodpecker_token.txt"]


def test_c11_runs_under_the_canon_surface_not_local(tmp_path, monkeypatch) -> None:
    """C11 must be reachable WITHOUT the local surface (court P1-F1).

    It shipped registered on `local`, which the estate's only scheduled doctor never
    requests — and `local` cannot be enabled there, because that host's shallow fed root
    makes C6 permanently red. So the gate ran nowhere.
    """
    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _canon_fed(tmp_path, C11_UNDECLARED)
    report = Doctor(reg, fed_root=tmp_path).run(["canon"])
    assert [f.check for f in report.findings] == ["C11"]
    assert report.exit_code() == 1
    # ...and the local surface no longer drags it along
    assert not [
        f for f in Doctor(reg, fed_root=tmp_path).run(["local"]).findings if f.check == "C11"
    ]


def test_c11_is_in_the_scheduled_watch_surfaces() -> None:
    """The gate must be in what the cron actually runs, or it does not exist.

    This asserts the wiring, not the check: `mgf/CLAUDE.md` — 'a verifier that isn't in
    a gate's exit code does not exist'.
    """
    from mgf.secrets.cli import WATCH_DEFAULT_SURFACES

    assert "canon" in WATCH_DEFAULT_SURFACES


def test_c11_reads_canon_from_the_forge_when_there_is_no_checkout(tmp_path, monkeypatch) -> None:
    """forge-raw canon: the shallow-fed-root case the scheduled doctor actually runs in."""
    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _reg(tmp_path, C11_UNDECLARED)  # NO INFRA files on disk at all

    class _Forge:
        def repo_file(self, repo, path=".woodpecker.yml"):
            return CANON if repo == "mgf-standard" else "# no paths\n"

    doc = Doctor(reg, fed_root=tmp_path, forge=_Forge(), config_source="forge-raw")
    findings = doc.c11_canon_path_reconciliation()
    assert [f.evidence["rel"] for f in findings] == ["woodpecker_token.txt"]
    assert not doc.unverifiable  # the shallow root is NOT an excuse here


def test_c11_forge_canon_unreadable_is_unverifiable(tmp_path, monkeypatch) -> None:
    """A 429/404 on the canon fetch must never read as 'reconciled'."""
    from mgf.secrets.errors import SurfaceUnavailableError

    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _reg(tmp_path, C11_DECLARED)

    class _Forge:
        def repo_file(self, repo, path=".woodpecker.yml"):
            raise SurfaceUnavailableError(f"{repo}/{path}", "HTTP 429 (rate budget)")

    doc = Doctor(reg, fed_root=tmp_path, forge=_Forge(), config_source="forge-raw")
    doc.c11_canon_path_reconciliation()
    assert len(doc.unverifiable) == 2
    assert all(u.check == "C11" and "429" in u.reason for u in doc.unverifiable)


def test_c11_missing_fed_canon_is_unverifiable_not_clean(tmp_path, monkeypatch) -> None:
    """A canon file it could not read must never share an exit code with 'reconciled'."""
    monkeypatch.setenv("HOME", str(tmp_path))
    reg = _reg(tmp_path, C11_DECLARED)  # no INFRA files written at all
    doc = Doctor(reg, fed_root=tmp_path)
    doc.c11_canon_path_reconciliation()
    assert {u.target for u in doc.unverifiable} == {
        "mgf-standard/MGF_INFRA.md",
        "sooth-standard/SOOTH_INFRA.md",
    }
    assert Doctor(reg, fed_root=tmp_path).run(["local"]).exit_code() == 1


def test_c11_absent_host_card_is_skipped_not_loud(tmp_path, monkeypatch) -> None:
    """Off this workstation `~/.claude/CLAUDE.md` is legitimately absent — recorded,
    never a failure, or every other host learns to ignore the check."""
    monkeypatch.setenv("HOME", str(tmp_path / "elsewhere"))
    reg = _canon_fed(tmp_path, C11_DECLARED)
    doc = Doctor(reg, fed_root=tmp_path)
    doc.c11_canon_path_reconciliation()
    assert any("host-local canon absent" in s for s in doc.skipped)
    assert not [u for u in doc.unverifiable if "CLAUDE.md" in u.target]


def test_report_json_shape_and_severity_sort(tmp_path) -> None:
    reg = _reg(tmp_path, ORG)
    wp = FakeWoodpecker(org_secrets={}, repos=[])
    report = Doctor(reg, fed_root=tmp_path, woodpecker=wp).run(["woodpecker"])
    js = report.to_json()
    assert set(js) == {"surfaces", "clean", "findings", "unverifiable", "skipped"}
    for f in js["findings"]:
        assert set(f) == {
            "id",
            "check",
            "severity",
            "finding",
            "evidence",
            "acknowledged",
            "ack_reason",
        }
        assert f["acknowledged"] is False and f["ack_reason"] is None
