"""Deterministic tournament lifecycle, checkpoint, and rendering contracts."""

from __future__ import annotations

import json
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock

import pytest

from obliteratus import tourney


def _contender(
    method: str,
    score: float,
    *,
    output_dir: str = "",
    error: str | None = None,
    direction: str = "mean_diff",
    cert: str = "GREEN",
) -> tourney.Contender:
    return tourney.Contender(
        method=method,
        score=score,
        metrics={
            "refusal_rate": max(0.0, 1.0 - score),
            "coherence": max(0.0, score),
            "kl_divergence": 0.1,
            "perplexity": 12.0,
            "direction_method": direction,
            "spectral_certification": cert,
        },
        output_dir=output_dir,
        time_s=2.5,
        error=error,
        direction_method=direction,
        spectral_cert=cert,
    )


@pytest.mark.parametrize(
    ("certification", "expected"),
    [("GREEN", 1.0), ("YELLOW", 0.975), ("RED", 0.95), (None, 0.975)],
)
def test_composite_score_preserves_documented_weighting(certification, expected):
    metrics = {
        "refusal_rate": 0.0,
        "coherence": 1.0,
        "kl_divergence": 0.0,
        "perplexity": 0.0,
        "spectral_certification": certification,
        "degenerate_count": 0,
    }

    assert tourney.composite_score(metrics) == pytest.approx(expected)


def test_composite_score_missing_metrics_and_degenerate_outputs_fail_safe():
    assert tourney.composite_score({}) == pytest.approx(0.225)
    assert tourney.composite_score({"degenerate_count": 3}) == pytest.approx(0.1875)


def test_result_dictionary_sorts_contenders_without_mutating_round_order():
    low = _contender("low", 0.2)
    high = _contender("high", 0.9)
    rnd = tourney.TourneyRound(
        round_num=1,
        name="Qualifier",
        contenders=[low, high],
        prompt_volume=64,
        advanced_to=["high"],
        eliminated=["low"],
    )
    result = tourney.TourneyResult(
        model="org/model",
        winner=high,
        rounds=[rnd],
        total_time_s=12.5,
        timestamp="2026-08-16T00:00:00+00:00",
    )

    payload = result.to_dict()

    assert payload["winner"]["method"] == "high"
    assert [item["method"] for item in payload["rounds"][0]["contenders"]] == [
        "high",
        "low",
    ]
    assert rnd.contenders == [low, high]


def test_checkpoint_round_trip_preserves_completed_and_partial_metadata(tmp_path):
    completed = _contender("complete", 0.8, direction="svd", cert="YELLOW")
    completed.round_eliminated = 1
    partial = _contender("partial", 0.7, direction="pca", cert="RED")
    result = tourney.TourneyResult(
        model="org/model",
        rounds=[
            tourney.TourneyRound(
                round_num=1,
                name="Qualifier",
                contenders=[completed],
                prompt_volume=64,
                advanced_to=[],
                eliminated=["complete"],
            )
        ],
    )

    path = tourney._save_checkpoint(
        output_dir=tmp_path,
        result=result,
        current_round_num=2,
        current_round_name="Semifinals",
        current_round_volume=128,
        current_round_advance=1,
        current_round_verify=30,
        completed_methods=[partial],
        remaining_methods=["remaining"],
        alive=["partial", "remaining"],
        model_name="org/model",
        dataset_key="builtin",
        quantization="4bit",
        methods=["complete", "partial", "remaining"],
    )

    checkpoint = tourney._load_checkpoint(tmp_path)
    restored, partials, remaining, interrupted = tourney._restore_rounds(checkpoint)

    assert path == tmp_path / tourney.CHECKPOINT_FILENAME
    assert tourney._checkpoint_matches(checkpoint, "org/model", "builtin", "4bit")
    assert not tourney._checkpoint_matches(checkpoint, "other/model", "builtin", "4bit")
    assert restored.rounds[0].contenders[0].direction_method == "svd"
    assert restored.rounds[0].contenders[0].spectral_cert == "YELLOW"
    assert partials[0].direction_method == "pca"
    assert partials[0].spectral_cert == "RED"
    assert partials[0].round_eliminated == 0
    assert remaining == ["remaining"]
    assert interrupted["verify_sample_size"] == 30


@pytest.mark.parametrize("payload", ["[]", "null", "{}", '{"version": 2}', "not-json"])
def test_checkpoint_loader_rejects_malformed_or_unsupported_roots(tmp_path, payload):
    (tmp_path / tourney.CHECKPOINT_FILENAME).write_text(payload, encoding="utf-8")

    assert tourney._load_checkpoint(tmp_path) is None


def test_checkpoint_loader_returns_none_when_absent(tmp_path):
    assert tourney._load_checkpoint(tmp_path) is None


@pytest.mark.parametrize("error", [OSError("unreadable"), UnicodeError("invalid encoding")])
def test_checkpoint_loader_fails_closed_when_document_cannot_be_read(
    monkeypatch,
    tmp_path,
    error,
):
    path = tmp_path / tourney.CHECKPOINT_FILENAME
    path.write_text('{"version": 1}', encoding="utf-8")
    monkeypatch.setattr(Path, "read_text", Mock(side_effect=error))

    assert tourney._load_checkpoint(tmp_path) is None


def test_markdown_and_html_render_all_outcomes_and_escape_html():
    winner = _contender("<winner>", 0.85, direction="<dir>")
    middle = _contender("middle", 0.55, cert="YELLOW")
    low = _contender("low", 0.2, cert="INCONCLUSIVE")
    failed = _contender("failed", -1.0, error="boom")
    rnd = tourney.TourneyRound(
        round_num=1,
        name="<script>alert(1)</script>",
        contenders=[failed, low, middle, winner],
        prompt_volume=64,
        advanced_to=[winner.method, middle.method],
        eliminated=[low.method, failed.method],
    )
    result = tourney.TourneyResult(
        model="org/<model>",
        winner=winner,
        rounds=[rnd],
        total_time_s=120,
        hub_repo="org/result",
    )

    markdown = tourney.render_bracket(result)
    html = tourney.render_bracket_html(result)

    assert "Pushed to: [org/result]" in markdown
    assert "| 4 | failed | — | ERROR" in markdown
    assert "*out*" not in markdown  # the only round is final
    assert "&lt;model&gt;" in html
    assert "&lt;winner&gt;" in html
    assert "&lt;script&gt;alert(1)&lt;/script&gt;" in html
    assert "<script>alert(1)</script>" not in html
    assert "card-score good" in html
    assert "card-score mid" in html
    assert "card-score bad" in html
    assert "badge-err" in html


def test_renderers_handle_no_winner_and_model_card_requires_winner():
    result = tourney.TourneyResult(model="model", total_time_s=0)

    assert "**No winner**" in tourney.render_bracket(result)
    assert "No winner determined" in tourney.render_bracket_html(result)
    assert tourney.generate_model_card(result) == ""


def test_model_card_includes_winner_metrics_and_bracket():
    winner = _contender("advanced", 0.9)
    result = tourney.TourneyResult(
        model="org/base-model",
        winner=winner,
        rounds=[
            tourney.TourneyRound(
                round_num=1,
                name="Final",
                contenders=[winner],
                prompt_volume=64,
                advanced_to=["advanced"],
            )
        ],
        timestamp="2026-08-16T00:00:00+00:00",
    )

    card = tourney.generate_model_card(result)

    assert "base_model: org/base-model" in card
    assert "Winning Method: `advanced`" in card
    assert "# OBLITERATUS TOURNEY" in card


def test_runner_initialization_cleans_fresh_output_and_preserves_resume(tmp_path):
    output = tmp_path / "tourney"
    output.mkdir()
    (output / "stale.txt").write_text("stale", encoding="utf-8")

    fresh = tourney.TourneyRunner("model", output_dir=str(output), methods=["a"])
    assert fresh.output_dir == output
    assert not (output / "stale.txt").exists()

    (output / "checkpoint.txt").write_text("keep", encoding="utf-8")
    resumed = tourney.TourneyRunner(
        "model",
        output_dir=str(output),
        methods=["a"],
        resume=True,
    )
    assert (resumed.output_dir / "checkpoint.txt").read_text(encoding="utf-8") == "keep"


def test_runner_prompt_loading_is_bounded_by_shortest_source(monkeypatch, tmp_path):
    runner = tourney.TourneyRunner("model", output_dir=str(tmp_path), methods=["a"])
    loader = Mock(return_value=(["h1", "h2", "h3"], ["s1", "s2"]))
    monkeypatch.setattr("obliteratus.prompts.load_dataset_source", loader)

    harmful, harmless = runner._load_prompts(10)

    assert harmful == ["h1", "h2"]
    assert harmless == ["s1", "s2"]
    loader.assert_called_once_with("builtin")


@pytest.mark.parametrize(
    ("message", "expected"),
    [
        ("GPU quota exceeded for this session", True),
        ("ZeroGPU token expired", True),
        ("ordinary model failure", False),
    ],
)
def test_quota_error_classification_is_narrow(message, expected):
    assert tourney.TourneyRunner._is_quota_error(RuntimeError(message)) is expected


def test_run_one_method_uses_optional_gpu_wrapper(monkeypatch, tmp_path):
    runner = tourney.TourneyRunner("model", output_dir=str(tmp_path), methods=["a"])
    direct = Mock(return_value=_contender("a", 0.8))
    monkeypatch.setattr(runner, "_run_method", direct)

    assert runner._run_one_method("a", ["h"], ["s"], "out", 20, None).method == "a"
    wrapper = Mock(side_effect=lambda fn, *args: fn(*args))
    assert runner._run_one_method("a", ["h"], ["s"], "out", 20, wrapper).method == "a"
    wrapper.assert_called_once()


def test_full_runner_ranks_rounds_cleans_losers_and_writes_results(monkeypatch, tmp_path):
    logs: list[str] = []
    rounds: list[tourney.TourneyRound] = []
    runner = tourney.TourneyRunner(
        "org/model",
        methods=["alpha", "beta", "gamma", "delta"],
        output_dir=str(tmp_path / "run"),
        on_log=logs.append,
        on_round=rounds.append,
    )
    scores = {"alpha": 0.9, "beta": 0.7, "gamma": 0.4, "delta": 0.2}
    monkeypatch.setattr(runner, "_load_prompts", lambda volume: (["h"] * volume, ["s"] * volume))
    monkeypatch.setattr(
        tourney.shutil,
        "disk_usage",
        lambda _path: SimpleNamespace(free=int(4.5e9)),
    )

    def run_method(method, _harmful, _harmless, save_dir, _verify):
        Path(save_dir).mkdir(parents=True, exist_ok=True)
        return _contender(method, scores[method], output_dir=save_dir)

    monkeypatch.setattr(runner, "_run_method", run_method)

    result = runner.run()

    assert [rnd.name for rnd in result.rounds] == ["Qualifiers", "Semifinals", "Championship"]
    assert result.winner.method == "alpha"
    assert rounds == result.rounds
    assert (runner.output_dir / "tourney_results.json").exists()
    assert (runner.output_dir / "tourney_bracket.md").exists()
    assert (runner.output_dir / "r3_alpha").exists()
    assert not (runner.output_dir / "r3_beta").exists()
    assert any("Low disk space" in line for line in logs)


def test_full_runner_does_not_crown_an_errored_only_contender(monkeypatch, tmp_path):
    runner = tourney.TourneyRunner("model", methods=["broken"], output_dir=str(tmp_path))
    monkeypatch.setattr(runner, "_load_prompts", lambda _volume: (["h"], ["s"]))
    monkeypatch.setattr(
        runner,
        "_run_method",
        lambda method, *_args: _contender(method, -1.0, error="broken"),
    )

    result = runner.run()

    assert result.winner is None
    assert json.loads((tmp_path / "tourney_results.json").read_text())["winner"] is None


def test_run_iter_saves_exact_resume_point_on_quota_exhaustion(monkeypatch, tmp_path):
    runner = tourney.TourneyRunner(
        "model",
        methods=["alpha", "beta"],
        output_dir=str(tmp_path),
    )
    monkeypatch.setattr(runner, "_load_prompts", lambda _volume: (["h"], ["s"]))

    def run_one(method, *_args):
        if method == "beta":
            raise RuntimeError("GPU quota exceeded")
        return _contender(method, 0.8)

    monkeypatch.setattr(runner, "_run_one_method", run_one)
    iterator = runner.run_iter()

    assert "running `alpha`" in next(iterator)[0]
    assert "running `beta`" in next(iterator)[0]
    with pytest.raises(RuntimeError, match="GPU quota exceeded"):
        next(iterator)

    checkpoint = tourney._load_checkpoint(tmp_path)
    interrupted = checkpoint["interrupted_round"]
    assert [item["method"] for item in interrupted["completed_methods"]] == ["alpha"]
    assert interrupted["remaining_methods"] == ["beta"]


def test_run_iter_resumes_partial_round_without_repeating_completed_method(monkeypatch, tmp_path):
    partial = _contender("alpha", 0.9, direction="pca", cert="YELLOW")
    tourney._save_checkpoint(
        output_dir=tmp_path,
        result=tourney.TourneyResult(model="model"),
        current_round_num=1,
        current_round_name="Qualifiers",
        current_round_volume=64,
        current_round_advance=1,
        current_round_verify=20,
        completed_methods=[partial],
        remaining_methods=["beta"],
        alive=["alpha", "beta"],
        model_name="model",
        dataset_key="builtin",
        quantization=None,
        methods=["alpha", "beta"],
    )
    runner = tourney.TourneyRunner(
        "model",
        methods=["alpha", "beta"],
        output_dir=str(tmp_path),
        resume=True,
    )
    monkeypatch.setattr(runner, "_load_prompts", lambda _volume: (["h"], ["s"]))
    called: list[str] = []

    def run_one(method, *_args):
        called.append(method)
        return _contender(method, 0.7)

    monkeypatch.setattr(runner, "_run_one_method", run_one)

    events = list(runner.run_iter())

    assert events[0][0].startswith("**Resuming tournament**")
    assert events[-1][0] == "Tournament complete"
    result = events[-1][1]
    assert called == ["beta"]
    assert result.winner.method == "alpha"
    assert result.winner.direction_method == "pca"
    assert result.winner.spectral_cert == "YELLOW"
    assert not (tmp_path / tourney.CHECKPOINT_FILENAME).exists()
