"""Unit tests for ``mgf-fed ci-stats`` (CIR-11 — the standing CI-health metric).

No real network: the collection layer's ``request_json`` is monkeypatched with a scripted fake, and
the metric functions (``classify_red`` / ``percentile`` / ``compute``) are exercised directly so the
numbers are pinned + reproducible.
"""

from __future__ import annotations

import argparse
import json
from typing import Any

import pytest

from mgf.fed.cli import _ci_stats
from mgf.fed.cli._ci_stats import (
    PipelineRec,
    add_ci_stats_args,
    classify_red,
    collect_repo,
    compute,
    percentile,
    run_ci_stats,
)


def _rec(
    repo: str = "mgf-x",
    number: int = 1,
    status: str = "success",
    commit: str = "aaa",
    event: str = "push",
    duration: int | None = 100,
    failed_steps: tuple[str, ...] = (),
    error_text: str = "",
) -> PipelineRec:
    return PipelineRec(
        repo=repo,
        number=number,
        status=status,
        commit=commit,
        event=event,
        duration=duration,
        failed_steps=failed_steps,
        error_text=error_text,
    )


class TestPercentile:
    def test_nearest_rank_is_deterministic(self) -> None:
        vals = list(range(1, 41))  # 1..40
        assert percentile(vals, 50) == 20  # ceil(0.5*40)=20 -> 20th value
        assert percentile(vals, 90) == 36  # ceil(0.9*40)=36 -> 36th value
        assert percentile(vals, 100) == 40
        assert percentile(vals, 0) == 1  # rank floored to 1

    def test_empty_is_none(self) -> None:
        assert percentile([], 50) is None


class TestClassify:
    def test_green_is_none(self) -> None:
        assert classify_red(_rec(status="success")) is None
        assert classify_red(_rec(status="skipped")) is None

    def test_clone_only_failure(self) -> None:
        assert classify_red(_rec(status="failure", failed_steps=("clone",))) == "clone"

    def test_first_failed_step_wins(self) -> None:
        assert classify_red(_rec(status="failure", failed_steps=("test", "coverage"))) == "test"

    def test_failure_without_detail(self) -> None:
        assert classify_red(_rec(status="failure", failed_steps=())) == "failure"

    def test_error_forge_config_signature(self) -> None:
        rec = _rec(status="error", error_text="could not load config from forge: %!w(<nil>)")
        assert classify_red(rec) == "error: forge config-fetch"

    def test_error_other(self) -> None:
        assert classify_red(_rec(status="error", error_text="some other boom")) == "error"

    def test_killed(self) -> None:
        assert classify_red(_rec(status="killed")) == "killed"


class TestCompute:
    def test_fleet_red_rate_and_classes(self) -> None:
        recs = [
            _rec("a", 1, "success", commit="c1"),
            _rec("a", 2, "success", commit="c2"),
            _rec("a", 3, "failure", commit="c3", failed_steps=("test",)),
            _rec("b", 1, "failure", commit="c4", failed_steps=("clone",)),
            _rec("b", 2, "error", commit="c5", error_text="could not load config from forge"),
            _rec("b", 3, "killed", commit="c6"),
        ]
        s = compute(recs)
        assert s.total == 6 and s.red == 4
        assert round(s.red_rate, 3) == round(4 / 6, 3)
        assert s.by_status["success"] == 2 and s.by_status["failure"] == 2
        assert s.red_by_class["test"] == 1
        assert s.red_by_class["clone"] == 1
        assert s.red_by_class["error: forge config-fetch"] == 1
        assert s.red_by_class["killed"] == 1

    def test_restart_tax_counts_repeated_commit_event_pairs(self) -> None:
        recs = [
            _rec(number=1, commit="c1", event="push"),
            _rec(number=2, commit="c1", event="push"),  # a re-run of c1/push
            _rec(number=3, commit="c1", event="push"),  # a second re-run
            _rec(number=4, commit="c1", event="cron"),  # same commit, DIFFERENT event -> not a pair
            _rec(number=5, commit="c2", event="push"),  # a lone run
        ]
        s = compute(recs)
        assert s.restart_pairs == 1  # only (c1, push) ran >1
        assert s.restart_extra == 2  # 3 runs of it -> 2 extra

    def test_durations_p50_p90(self) -> None:
        recs = [_rec(number=i, duration=i * 10) for i in range(1, 41)]  # 10..400
        s = compute(recs)
        assert s.duration_n == 40
        assert s.duration_p50 == 200  # 20th of 10..400
        assert s.duration_p90 == 360  # 36th
        # None durations are excluded from the timing set
        recs.append(_rec(number=99, duration=None))
        assert compute(recs).duration_n == 40

    def test_per_repo_outliers(self) -> None:
        # fleet red rate is 4/8 = 50%. `hot` is 3/3 (100%), `cool` is 0/3, `mid` 1/2 (50%, not >).
        recs = [
            _rec("hot", 1, "failure", commit="h1", failed_steps=("test",)),
            _rec("hot", 2, "failure", commit="h2", failed_steps=("test",)),
            _rec("hot", 3, "failure", commit="h3", failed_steps=("test",)),
            _rec("cool", 1, "success", commit="k1"),
            _rec("cool", 2, "success", commit="k2"),
            _rec("cool", 3, "success", commit="k3"),
            _rec("mid", 1, "failure", commit="m1", failed_steps=("test",)),
            _rec("mid", 2, "success", commit="m2"),
        ]
        s = compute(recs)
        names = [rs.repo for rs in s.outliers]
        assert names == ["hot"]  # only repo above the 50% fleet rate with >=2 reds

    def test_lone_single_red_is_not_an_outlier(self) -> None:
        # a repo with 1 run that is red is 100% but must NOT be paraded (>=2 reds guard)
        recs = [_rec("lonely", 1, "failure", commit="x", failed_steps=("test",))]
        assert compute(recs).outliers == []


class _FakeApi:
    """Scripted request_json keyed on URL suffix (mirrors test_ci_heal's fake)."""

    def __init__(self, listing: list[dict[str, Any]], details: dict[int, dict[str, Any]]) -> None:
        self.listing = listing
        self.details = details

    def __call__(self, url: str, *, method: str = "GET", **kw: Any) -> Any:
        if "/api/repos/lookup/" in url:
            return {"id": 42}
        if "pipelines?perPage=" in url:
            return self.listing
        number = int(url.rsplit("/", 1)[1])
        return self.details.get(number, {})


def test_collect_repo_classifies_reds_via_detail(monkeypatch: pytest.MonkeyPatch) -> None:
    listing = [
        {
            "number": 5,
            "status": "success",
            "commit": "c1",
            "event": "push",
            "started": 100,
            "finished": 250,
        },
        {
            "number": 6,
            "status": "failure",
            "commit": "c2",
            "event": "push",
            "started": 100,
            "finished": 300,
        },
        {
            "number": 7,
            "status": "error",
            "commit": "c3",
            "event": "cron",
            "errors": [{"message": "could not load config from forge: %!w(<nil>)"}],
        },
    ]
    details = {
        6: {
            "workflows": [
                {
                    "children": [
                        {"name": "clone", "state": "success"},
                        {"name": "test", "state": "failure"},
                    ]
                }
            ]
        },
    }
    fake = _FakeApi(listing, details)
    monkeypatch.setattr(_ci_stats, "request_json", fake)
    recs, warnings = collect_repo("https://wp", "magogi-admin", "mgf-x", "tok", 40)
    assert warnings == []
    assert len(recs) == 3
    by_num = {r.number: r for r in recs}
    assert by_num[5].duration == 150 and by_num[5].status == "success"
    assert by_num[6].failed_steps == ("test",)  # from the detail
    assert classify_red(by_num[6]) == "test"
    assert classify_red(by_num[7]) == "error: forge config-fetch"


def test_collect_repo_skips_unactivated(monkeypatch: pytest.MonkeyPatch) -> None:
    import urllib.error

    def _raise(url: str, **kw: Any) -> Any:
        raise urllib.error.HTTPError(url, 404, "no", {}, None)  # type: ignore[arg-type]

    monkeypatch.setattr(_ci_stats, "request_json", _raise)
    recs, warnings = collect_repo("https://wp", "magogi-admin", "mgf-x", "tok", 40)
    assert recs == []
    assert warnings and "not activated" in warnings[0]


def _args(**over: object) -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    add_ci_stats_args(parser)
    ns = parser.parse_args(["--repo", "mgf-x"])
    for k, v in over.items():
        setattr(ns, k, v)
    return ns


def test_run_ci_stats_requires_token(monkeypatch: pytest.MonkeyPatch, capsys: Any) -> None:
    monkeypatch.setattr(_ci_stats, "_token", lambda explicit: None)
    assert run_ci_stats(_args()) == 2
    assert "no token" in capsys.readouterr().out


def test_run_ci_stats_end_to_end_human(monkeypatch: pytest.MonkeyPatch, capsys: Any) -> None:
    listing = [
        {
            "number": 5,
            "status": "success",
            "commit": "c1",
            "event": "push",
            "started": 0,
            "finished": 189,
        },
        {
            "number": 6,
            "status": "failure",
            "commit": "c2",
            "event": "push",
            "started": 10,
            "finished": 412,
        },
    ]
    monkeypatch.setattr(
        _ci_stats,
        "request_json",
        _FakeApi(
            listing, {6: {"workflows": [{"children": [{"name": "test", "state": "failure"}]}]}}
        ),
    )
    monkeypatch.setattr(_ci_stats, "_token", lambda explicit: "tok")
    assert run_ci_stats(_args()) == 0
    out = capsys.readouterr().out
    assert "ci-stats" in out and "red rate 50.0%" in out
    assert "test 1" in out  # red-by-class


def test_run_ci_stats_json(monkeypatch: pytest.MonkeyPatch, capsys: Any) -> None:
    listing = [
        {
            "number": 5,
            "status": "success",
            "commit": "c1",
            "event": "push",
            "started": 100,
            "finished": 200,
        }
    ]
    monkeypatch.setattr(_ci_stats, "request_json", _FakeApi(listing, {}))
    monkeypatch.setattr(_ci_stats, "_token", lambda explicit: "tok")
    assert run_ci_stats(_args(json=True)) == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["stats"]["total"] == 1
    assert payload["stats"]["red"] == 0
    assert payload["limit"] == 40
