"""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 == []
