"""`scripts/ping_sooth_ci.py` — the sooth canary (CIR-22).

The behaviour under test: the canary triggers the designated sooth pipelines,
polls Woodpecker (never Codeberg) to a terminal state, and is non-zero on any
red / unreachable / timed-out build. It fails loudly on a canary repo that is
not a registered sooth member, and never sleeps in --dry-run. All HTTP is
injected — no network.
"""

from __future__ import annotations

import importlib.util
import json
import sys
import urllib.error
from pathlib import Path
from typing import Any

import pytest

_SPEC = importlib.util.spec_from_file_location(
    "ping_sooth_ci", Path(__file__).resolve().parents[2] / "scripts" / "ping_sooth_ci.py"
)
assert _SPEC and _SPEC.loader
canary = importlib.util.module_from_spec(_SPEC)
sys.modules["ping_sooth_ci"] = canary  # dataclasses resolve annotations via sys.modules
_SPEC.loader.exec_module(canary)

REGISTRY = {
    "schema": 1,
    "repos": [
        {"name": "sooth-standard", "slug": "sooth-standard"},
        {"name": "soothfast", "slug": "soothfast"},
        {"name": "soothsmith", "slug": "soothsmith"},
        {"name": "soothweave", "slug": "soothweave"},
    ],
}


# --------------------------------------------------------------------------- #
# Registry + canary-set resolution.
# --------------------------------------------------------------------------- #


def test_default_canary_set_is_the_contract_three() -> None:
    assert canary.DEFAULT_CANARY_SET == ("soothfast", "sooth-standard", "soothsmith")


def test_resolve_canary_set_accepts_registered_members_in_order() -> None:
    assert canary.resolve_canary_set(REGISTRY, ["soothfast", "sooth-standard"]) == [
        "soothfast",
        "sooth-standard",
    ]


def test_resolve_canary_set_rejects_a_non_member_loudly() -> None:
    with pytest.raises(SystemExit) as exc:
        canary.resolve_canary_set(REGISTRY, ["soothfast", "not-a-repo"])
    assert "not-a-repo" in str(exc.value)


def test_registry_names_reads_name_and_slug() -> None:
    assert canary._registry_names(REGISTRY) >= {"soothfast", "sooth-standard", "soothsmith"}


# --------------------------------------------------------------------------- #
# Trigger + poll, driven by an injected API.
# --------------------------------------------------------------------------- #


def _api_factory(
    poll_statuses: dict[str, list[str]],
    *,
    lookup_error: dict[str, int] | None = None,
    unreachable_poll: set[str] | None = None,
) -> Any:
    """Fake Woodpecker API: lookup -> id, POST pipelines -> number, GET one ->
    the next status scripted per repo. Repo id encodes the repo name index."""
    ids = {name: 100 + i for i, name in enumerate(poll_statuses)}
    id_to_name = {v: k for k, v in ids.items()}
    lookup_error = lookup_error or {}
    unreachable_poll = unreachable_poll or set()
    calls: dict[str, int] = {}

    def api(url: str, token: str, method: str = "GET", payload: dict | None = None) -> Any:
        if "/api/repos/lookup/" in url:
            name = url.rsplit("/", 1)[-1]
            if name in lookup_error:
                raise urllib.error.HTTPError(url, lookup_error[name], "e", {}, None)  # type: ignore[arg-type]
            return {"id": ids[name]}
        if method == "POST" and url.endswith("/pipelines"):
            repo_id = int(url.split("/api/repos/")[1].split("/")[0])
            return {"number": 900 + repo_id}
        # GET one pipeline -> next scripted status
        repo_id = int(url.split("/api/repos/")[1].split("/")[0])
        name = id_to_name[repo_id]
        if name in unreachable_poll:
            raise urllib.error.URLError("connection reset")
        seq = poll_statuses[name]
        i = min(calls.get(name, 0), len(seq) - 1)
        calls[name] = calls.get(name, 0) + 1
        return {"status": seq[i]}

    return api


def _run(api: Any, repos: list[str], **kw: Any) -> list[Any]:
    return canary.run_canary(
        "https://wp",
        "magogi-admin",
        repos,
        "main",
        "tok",
        api=api,
        sleep=lambda _s: None,
        poll_interval=0.0,
        **kw,
    )


def test_all_green_is_all_ok() -> None:
    api = _api_factory({"soothfast": ["success"], "sooth-standard": ["success"]})
    results = _run(api, ["soothfast", "sooth-standard"])
    assert all(r.ok for r in results)
    assert [r.repo for r in results] == ["soothfast", "sooth-standard"]


def test_a_red_pipeline_makes_the_repo_not_ok() -> None:
    api = _api_factory({"soothfast": ["success"], "soothsmith": ["failure"]})
    results = _run(api, ["soothfast", "soothsmith"])
    by = {r.repo: r for r in results}
    assert by["soothfast"].ok
    assert not by["soothsmith"].ok
    assert "failure" in by["soothsmith"].detail


def test_poll_waits_through_running_then_terminates() -> None:
    api = _api_factory({"soothfast": ["pending", "running", "running", "success"]})
    results = _run(api, ["soothfast"])
    assert results[0].ok


def test_poll_timeout_is_not_green() -> None:
    clock = {"t": 0.0}

    def now() -> float:
        clock["t"] += 100.0  # each check jumps 100s
        return clock["t"]

    api = _api_factory({"soothfast": ["running"]})  # never terminates
    results = _run(api, ["soothfast"], now=now, timeout=50.0)
    assert not results[0].ok
    assert "timeout" in results[0].detail


def test_unreachable_poll_is_not_green() -> None:
    api = _api_factory({"soothfast": ["success"]}, unreachable_poll={"soothfast"})
    results = _run(api, ["soothfast"])
    assert not results[0].ok
    assert "unreachable" in results[0].detail


def test_404_lookup_is_reported_not_activated() -> None:
    api = _api_factory({"soothfast": ["success"]}, lookup_error={"soothfast": 404})
    results = _run(api, ["soothfast"])
    assert not results[0].ok
    assert "not activated" in results[0].detail


def test_401_lookup_points_at_the_mint_path() -> None:
    api = _api_factory({"soothfast": ["success"]}, lookup_error={"soothfast": 401})
    results = _run(api, ["soothfast"])
    assert not results[0].ok
    assert "MGF_INFRA" in results[0].detail


def test_blocked_is_terminal_and_not_green() -> None:
    api = _api_factory({"soothfast": ["blocked"]})
    results = _run(api, ["soothfast"])
    assert not results[0].ok
    assert "blocked" in results[0].detail


# --------------------------------------------------------------------------- #
# --dry-run through main() calls nothing.
# --------------------------------------------------------------------------- #


def test_dry_run_lists_the_set_and_calls_nothing(tmp_path: Path, capsys: Any) -> None:
    root = tmp_path / "sooth-standard"
    root.mkdir()
    (root / "SOOTH_LIBRARIES.json").write_text(json.dumps(REGISTRY))
    rc = canary.main(["--dry-run", "--sooth-root", str(root)])
    assert rc == 0
    out = capsys.readouterr().out
    assert "would trigger" in out
    for repo in canary.DEFAULT_CANARY_SET:
        assert repo in out


def test_dry_run_with_overridden_repos(tmp_path: Path, capsys: Any) -> None:
    root = tmp_path / "sooth-standard"
    root.mkdir()
    (root / "SOOTH_LIBRARIES.json").write_text(json.dumps(REGISTRY))
    rc = canary.main(["--dry-run", "--sooth-root", str(root), "--repos", "soothweave"])
    assert rc == 0
    assert "soothweave" in capsys.readouterr().out
