"""Tests for ``mgf-fed ping-siblings`` (AP-16 B-lite cornerstone fan-out)."""

from __future__ import annotations

import argparse
from pathlib import Path

import pytest

from mgf.fed._roster import Member
from mgf.fed.cli import _ping


def _member(repo: str, lang: str, deps: tuple[str, ...]) -> Member:
    return Member(repo=repo, lang=lang, registry_version="1.0.0", deps=deps)


def _args(**over: object) -> argparse.Namespace:
    base: dict[str, object] = {
        "dry_run": False,
        "owner": "magogi-admin",
        "branch": "main",
        "url": "https://wp",
        "token": None,
        "stagger": 0.0,  # tests never actually sleep unless they opt in
    }
    base.update(over)
    return argparse.Namespace(**base)


def test_consumers_splits_python_mgf_common_consumers(monkeypatch: pytest.MonkeyPatch) -> None:
    """Keep the Python mgf-common consumers; skip cornerstone / non-Python / non-consumers."""
    roster = [
        _member("mgf-common", "python", ()),  # cornerstone -> skip
        _member("mgf-http", "python", ("mgf-common",)),  # consumer -> ping
        _member("mgf-vm-core", "python", ("mgf-common", "mgf-http")),  # consumer -> ping
        _member("mgf-common-zig", "zig", ("mgf-common",)),  # zig -> skip
        _member("mgf-standard", "python", ()),  # no mgf-common dep -> skip
    ]
    monkeypatch.setattr(_ping, "members", lambda: roster)
    ping, skipped = _ping._consumers()
    assert ping == ["mgf-http", "mgf-vm-core"]
    reasons = dict(skipped)
    assert "cornerstone" in reasons["mgf-common"]
    assert "zig" in reasons["mgf-common-zig"]
    assert "does not depend" in reasons["mgf-standard"]


def test_consumers_falls_back_to_package_data(monkeypatch: pytest.MonkeyPatch) -> None:
    """With no federation-root roster (single-repo CI), it reads mgf.standard package
    data — where deps is a name->spec dict, not a tuple of names."""

    def _raise() -> list[Member]:
        raise FileNotFoundError("no sibling mgf-standard")

    monkeypatch.setattr(_ping, "members", _raise)
    monkeypatch.setattr(
        _ping,
        "_package_registry",
        lambda: {
            "libraries": [
                {"repo": "mgf-common", "lang": "python", "deps": {}},
                {"repo": "mgf-http", "lang": "python", "deps": {"mgf-common": ">=0.41,<0.50"}},
                {"repo": "mgf-common-zig", "lang": "zig", "deps": {"mgf-common": "*"}},
            ]
        },
    )
    ping, skipped = _ping._consumers()
    assert ping == ["mgf-http"]  # deps as name->spec dict still resolves
    assert "cornerstone" in dict(skipped)["mgf-common"]


def test_dry_run_triggers_nothing(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    roster = [
        _member("mgf-common", "python", ()),
        _member("mgf-http", "python", ("mgf-common",)),
    ]
    monkeypatch.setattr(_ping, "members", lambda: roster)
    rc = _ping.run_ping_siblings(_args(dry_run=True))
    out = capsys.readouterr().out
    assert rc == 0
    assert "would trigger  mgf-http" in out
    assert "dry-run" in out


def test_no_token_returns_2(monkeypatch: pytest.MonkeyPatch) -> None:
    """Fail closed when no WP token is resolvable — never a silent no-op."""
    monkeypatch.setattr(_ping, "members", lambda: [_member("mgf-http", "python", ("mgf-common",))])
    monkeypatch.delenv("WOODPECKER_TOKEN", raising=False)
    # point federation_root at an empty dir so the my_stuff/ token file isn't found.
    monkeypatch.setattr(_ping, "federation_root", lambda: Path("/nonexistent-mgf-root-xyz"))
    assert _ping.run_ping_siblings(_args()) == 2


def _three_consumer_roster() -> list[Member]:
    return [
        _member("mgf-common", "python", ()),  # cornerstone -> skip
        _member("mgf-http", "python", ("mgf-common",)),
        _member("mgf-vm-core", "python", ("mgf-common",)),
    ]


def test_stagger_paces_triggers(monkeypatch: pytest.MonkeyPatch) -> None:
    """Triggers are paced by ``time.sleep(stagger)`` BETWEEN each — not after the last.

    A tight burst exhausts the forge's per-build clone-credential minting; pacing
    the fan-out is the fix. Two targets => exactly one inter-trigger sleep.
    """
    monkeypatch.setattr(_ping, "members", _three_consumer_roster)
    monkeypatch.setattr(_ping, "_token", lambda explicit: "tok")
    monkeypatch.setattr(_ping, "_trigger", lambda *a: f"OK    {a[2]}: #1")  # a[2]=repo
    sleeps: list[float] = []
    monkeypatch.setattr(_ping.time, "sleep", sleeps.append)
    rc = _ping.run_ping_siblings(_args(stagger=4.0))
    assert rc == 0
    assert sleeps == [4.0]  # 2 targets -> 1 gap, none trailing


def test_stagger_zero_fires_immediately(monkeypatch: pytest.MonkeyPatch) -> None:
    """``--stagger 0`` disables pacing (the pre-fix fire-all-at-once behaviour)."""
    monkeypatch.setattr(_ping, "members", _three_consumer_roster)
    monkeypatch.setattr(_ping, "_token", lambda explicit: "tok")
    monkeypatch.setattr(_ping, "_trigger", lambda *a: f"OK    {a[2]}: #1")  # a[2]=repo
    sleeps: list[float] = []
    monkeypatch.setattr(_ping.time, "sleep", sleeps.append)
    rc = _ping.run_ping_siblings(_args(stagger=0.0))
    assert rc == 0
    assert sleeps == []


# ── The branch a sibling is actually built on ────────────────────────────────
# `--branch` used to default to the literal "main". Every repo whose default
# branch is `master` was therefore POSTed a request for a branch it does not
# have; Woodpecker answers that with a 500, so it read as a server fault. The
# fan-out reported `36/37 triggered` for two consecutive mgf-common releases and
# the one it dropped was **figrecovery** — the repo that filed the paper the
# release shipped. The lookup response already carries `default_branch`.


def _fake_api(calls: list[tuple[str, object]], default_branch: str):
    def api(url: str, token: str, method: str = "GET", payload: object = None) -> dict:
        calls.append((url, payload))
        if "/lookup/" in url:
            return {"id": 7, "default_branch": default_branch}
        return {"number": 42}

    return api


def test_trigger_uses_the_repos_own_default_branch(monkeypatch: pytest.MonkeyPatch) -> None:
    """A `master`-default repo must be built on `master`, not `main`."""
    calls: list[tuple[str, object]] = []
    monkeypatch.setattr(_ping, "_api", _fake_api(calls, "master"))
    line = _ping._trigger("https://wp", "magogi-admin", "figrecovery", None, "tok")
    posted = [p for u, p in calls if p is not None]
    assert posted == [{"branch": "master"}], posted
    assert "on master" in line


def test_trigger_still_honours_an_explicit_branch(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, object]] = []
    monkeypatch.setattr(_ping, "_api", _fake_api(calls, "master"))
    line = _ping._trigger("https://wp", "magogi-admin", "figrecovery", "release/x", "tok")
    assert [p for u, p in calls if p is not None] == [{"branch": "release/x"}]
    assert "on release/x" in line


def test_trigger_refuses_rather_than_guessing_when_the_forge_gives_no_default(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Guessing "main" is what caused the defect; a missing default must be loud."""
    calls: list[tuple[str, object]] = []

    def api(url: str, token: str, method: str = "GET", payload: object = None) -> dict:
        calls.append((url, payload))
        return {"id": 7} if "/lookup/" in url else {"number": 1}

    monkeypatch.setattr(_ping, "_api", api)
    line = _ping._trigger("https://wp", "magogi-admin", "x", None, "tok")
    assert line.startswith("FAIL")
    assert "default_branch" in line
    assert [p for u, p in calls if p is not None] == [], "must not POST a guessed branch"


def test_branch_flag_defaults_to_None_not_main() -> None:
    """The default lives in the forge, not in argparse — pinned so a future edit
    cannot quietly reintroduce the literal."""
    parser = argparse.ArgumentParser()
    _ping.add_ping_siblings_args(parser)
    assert parser.get_default("branch") is None
