"""Forge coverage-sweep budget + TTL cache (SPEC-P4 §7b).

A cron cannot re-fetch ~50 repos every run at a tight cadence. The client caches to
disk with a short TTL (a warm run = 0 live calls) and enforces a per-run live-fetch
budget (once spent, further un-cached reads degrade to ``unverifiable``, never
silently-absent). The per-run Codeberg budget is thereby bounded, not just documented.
"""

from __future__ import annotations

import json
import os
import time

import pytest

from mgf.secrets.doctor import Doctor
from mgf.secrets.errors import SurfaceUnavailableError
from mgf.secrets.forge import CodebergForgeClient
from mgf.secrets.registry import load
from tests._fakes import FakeWoodpecker


def _api_factory(calls: list[str]):
    def api(url, token, method="GET", payload=None):
        calls.append(url)
        return [{"title": "ci-clone", "fingerprint": "SHA256:a", "read_only": True, "key": "ssh-x"}]

    return api


def test_budget_caps_live_reads_and_degrades_to_unverifiable() -> None:
    calls: list[str] = []
    client = CodebergForgeClient(token="x", min_interval=0.0, budget=2, api=_api_factory(calls))
    assert client.repo_deploy_keys("r1")
    assert client.repo_deploy_keys("r2")
    with pytest.raises(SurfaceUnavailableError):
        client.repo_deploy_keys("r3")  # budget exhausted -> unverifiable, not a 3rd call
    assert client.live_calls == 2
    assert len(calls) == 2


def test_warm_cache_costs_zero_live_calls_and_ttl_expiry_refetches(tmp_path) -> None:
    calls: list[str] = []
    cache = tmp_path / "forge-cache"
    # Cold client: one live call, writes the disk cache.
    c1 = CodebergForgeClient(
        token="x", min_interval=0.0, cache_dir=cache, cache_ttl=3600, api=_api_factory(calls)
    )
    c1.repo_deploy_keys("repo-a")
    assert c1.live_calls == 1

    # A fresh client (empty in-mem) served entirely from the WARM disk cache: 0 live calls.
    c2 = CodebergForgeClient(
        token="x", min_interval=0.0, cache_dir=cache, cache_ttl=3600, api=_api_factory(calls)
    )
    c2.repo_deploy_keys("repo-a")
    assert c2.live_calls == 0
    assert len(calls) == 1  # still just the one cold read

    # Age the cache file past the TTL -> the next client re-fetches (bounded staleness).
    cache_file = cache / "repo-a.json"
    old = time.time() - 7200
    os.utime(cache_file, (old, old))
    c3 = CodebergForgeClient(
        token="x", min_interval=0.0, cache_dir=cache, cache_ttl=3600, api=_api_factory(calls)
    )
    c3.repo_deploy_keys("repo-a")
    assert c3.live_calls == 1
    assert len(calls) == 2


FLEET = """\
[fleet_key]
family = "mgf"
kind = "ssh-deploy-key"
purpose = "fleet clone key"
consumers = ["clone"]
recovery = "re-add"
state = "active"
policy = { allowed_repos = "*", blast_radius = "read" }
rotation = { class = "static-key", staged = true }
verify = { probe = "forge-key-fingerprint" }
surfaces = [ { type = "forge-deploy-key", repos = "ALL_MANAGED", readonly = true, title = "ci-clone" } ]
"""


def test_doctor_coverage_respects_the_budget(tmp_path) -> None:
    """A tight budget under a big fleet: the repos it could not afford to read are
    ``unverifiable`` (loud), never falsely reported as missing-the-key."""
    (tmp_path / "SECRETS.toml").write_text(FLEET, encoding="utf-8")
    reg = load([tmp_path / "SECRETS.toml"])
    calls: list[str] = []
    forge = CodebergForgeClient(token="x", min_interval=0.0, budget=1, api=_api_factory(calls))
    wp = FakeWoodpecker(repos=[(1, "repo-a"), (2, "repo-b"), (3, "repo-c")])
    report = Doctor(reg, fed_root=tmp_path, woodpecker=wp, forge=forge).run(["forge"])
    # Exactly one repo was read live; the rest degraded to unverifiable (never a false
    # C5 "missing key" finding).
    assert forge.live_calls == 1
    assert report.exit_code() == 1
    assert not [f for f in report.findings if f.check == "C5"]
    assert any("budget exhausted" in u.reason for u in report.unverifiable)


def test_budget_and_ttl_are_reported_bounded_number(tmp_path) -> None:
    """The per-run Codeberg call count is a readable, bounded number (documented budget)."""
    calls: list[str] = []
    client = CodebergForgeClient(token="x", min_interval=0.0, budget=60, api=_api_factory(calls))
    for i in range(5):
        client.repo_deploy_keys(f"r{i}")
    assert client.live_calls == 5  # <= budget, greppable per-run count
    assert json.dumps({"forge_live_calls": client.live_calls})  # value-free, serialisable
