"""Tests for the markdown, JSON and JUnit report formats.

These are the outputs that leave the terminal — pasted into PRs, parsed by
scripts, read by CI. The invariants that matter are the ones that stop a
number travelling further than its evidence: every format states the MDE, no
format turns a missing interval into a zero.
"""

from __future__ import annotations

import json
from pathlib import Path
from xml.etree import ElementTree as ET

import pytest
from click.testing import CliRunner

from ablate.cli import main
from ablate.config.variant import ConfigVariant
from ablate.record import AssertionRecord, TelemetryRecord, TrialRecord
from ablate.report.analysis import analyze
from ablate.report.formats import (
    render_json,
    render_junit,
    render_markdown,
    should_fail,
)
from ablate.stats import Verdict

ARMS = ("A", "B")
VARIANTS = [
    ConfigVariant("A", "# Conventions\n\nUse snake_case.\n", "CLAUDE.md"),
    ConfigVariant("B", None, "/dev/null"),
]


def _record(arm: str, task: str, passed: bool, replicate: int, tokens: int = 1200):
    return TrialRecord(
        trial_id=f"{task}-{arm}-{replicate}",
        run_id="run1",
        task_id=task,
        variant_id=arm,
        variant_hash="sha256:x",
        replicate=replicate,
        agent="claude-code",
        agent_version="2.1.220",
        model="claude-sonnet-5",
        started_at="2026-07-27T00:00:00Z",
        duration_s=1.0,
        telemetry=TelemetryRecord(input_tokens=tokens, turns=6, cost_usd=0.3),
        assertions=[AssertionRecord(id="a", type="exit_code", passed=passed)],
        passed=passed,
    )


def _analysis(
    n_tasks: int = 10,
    a_passes: int = 4,
    b_passes: int = 2,
    a_tokens: int = 1200,
    b_tokens: int = 900,
    **kw,
):
    """By default arm A both passes more *and* costs more tokens.

    The token direction is explicit because the gate checks every metric, not
    just pass rate — a config that holds pass rate steady while inflating token
    cost is a regression, and a fixture that ignored that would quietly hide it.
    """
    records = []
    for i in range(n_tasks):
        for r in range(5):
            records.append(_record("A", f"task-{i:02d}", r < a_passes, r, tokens=a_tokens))
            records.append(_record("B", f"task-{i:02d}", r < b_passes, r, tokens=b_tokens))
    return analyze(
        records, ARMS, run_id="run1", seed=7, agent="claude-code",
        agent_version="2.1.220", **kw,
    )


# --- markdown ---------------------------------------------------------------


def test_markdown_has_a_method_block():
    """Not optional: it is the difference between a screenshot people believe
    and one they argue with."""
    md = render_markdown(_analysis(), VARIANTS, command="ablate compare CLAUDE.md /dev/null")
    assert "## Method" in md
    assert "paired cluster bootstrap" in md.lower()
    assert "α = 0.05" in md


def test_markdown_has_a_reproduce_block_with_the_command():
    md = render_markdown(_analysis(), VARIANTS, command="ablate compare CLAUDE.md /dev/null --seed 7")
    assert "## Reproduce" in md
    assert "ablate compare CLAUDE.md /dev/null --seed 7" in md


def test_markdown_states_the_mde():
    md = render_markdown(_analysis(), VARIANTS)
    assert "Minimum detectable effect" in md
    assert "would not be visible here" in md


def test_markdown_scopes_the_estimand():
    assert "not a general claim about the config" in render_markdown(_analysis(), VARIANTS)


def test_markdown_labels_per_task_rows_descriptive():
    md = render_markdown(_analysis(), VARIANTS)
    assert "No per-task tests are performed" in md


def test_markdown_records_provenance():
    md = render_markdown(_analysis(), VARIANTS, version="0.4.0", repo="3f9a1c2")
    assert "claude-code 2.1.220" in md
    assert "claude-sonnet-5" in md
    assert "3f9a1c2" in md
    assert "0.4.0" in md


def test_markdown_states_a_withheld_interval_rather_than_omitting_it():
    md = render_markdown(_analysis(n_tasks=3), VARIANTS)
    assert "no interval" in md
    assert "at least 8" in md


def test_markdown_carries_warnings():
    md = render_markdown(_analysis(a_passes=5, b_passes=5), VARIANTS)
    assert "Warnings" in md
    assert "no signal" in md or "effectively" in md


# --- json -------------------------------------------------------------------


def test_json_is_valid_and_complete():
    payload = json.loads(render_json(_analysis(), VARIANTS, command="x", version="0.4.0"))
    assert payload["ablate_version"] == "0.4.0"
    assert payload["design"]["tasks"] == 10
    assert {m["key"] for m in payload["metrics"]} == {"pass_rate", "tokens_per_turn", "turns"}
    assert len(payload["tasks"]) == 10


def test_json_reports_the_mde():
    payload = json.loads(render_json(_analysis(), VARIANTS))
    assert payload["design"]["mde_pass_rate"] > 0
    assert payload["design"]["mde_tokens"] > 0


def test_json_nulls_a_withheld_interval_and_explains_it():
    """A consumer must not be able to read a missing interval as zero."""
    payload = json.loads(render_json(_analysis(n_tasks=3), VARIANTS))
    metric = next(m for m in payload["metrics"] if m["key"] == "pass_rate")
    assert metric["lo"] is None and metric["hi"] is None
    assert "at least 8" in metric["ci_withheld"]
    assert metric["verdict"] == "INCONCLUSIVE"


def test_json_includes_a_plain_statement_per_metric():
    payload = json.loads(render_json(_analysis(), VARIANTS))
    assert all(m["statement"] for m in payload["metrics"])


def test_json_cost_is_null_when_unknown():
    records = [_record("A", "t", True, 0), _record("B", "t", True, 0)]
    for record in records:
        record.telemetry.cost_usd = None
    payload = json.loads(render_json(analyze(records, ARMS), VARIANTS))
    assert payload["cost_usd"] is None


# --- junit ------------------------------------------------------------------


def test_junit_is_valid_xml_with_a_case_per_metric():
    root = ET.fromstring(render_junit(_analysis()))
    assert root.tag == "testsuite"
    assert {c.get("name") for c in root.findall("testcase")} == {
        "pass_rate", "tokens_per_turn", "turns"
    }


def test_junit_passes_when_nothing_trips_the_gate():
    root = ET.fromstring(
        render_junit(_analysis(a_tokens=900, b_tokens=900), fail_on="regression")
    )
    assert root.get("failures") == "0"


def test_junit_fails_on_a_token_regression_even_when_pass_rate_is_fine():
    """A config that holds pass rate steady while doubling token cost is a
    regression. A gate watching only pass rate would wave it through."""
    root = ET.fromstring(
        render_junit(
            _analysis(a_passes=3, b_passes=3, a_tokens=2400, b_tokens=900),
            fail_on="regression",
        )
    )
    failing = {
        c.get("name") for c in root.findall("testcase") if c.find("failure") is not None
    }
    assert "tokens_per_turn" in failing
    assert "pass_rate" not in failing


def test_junit_fails_on_a_regression():
    """A is worse than B on pass rate."""
    root = ET.fromstring(render_junit(_analysis(a_passes=1, b_passes=5), fail_on="regression"))
    assert int(root.get("failures")) >= 1


def test_junit_strict_mode_fails_on_inconclusive():
    lenient = ET.fromstring(render_junit(_analysis(n_tasks=3), fail_on="regression"))
    strict = ET.fromstring(render_junit(_analysis(n_tasks=3), fail_on="inconclusive"))
    assert int(lenient.get("failures")) == 0
    assert int(strict.get("failures")) == 3


def test_junit_discloses_the_mde():
    """A green check that does not say how much reassurance it is worth is
    theatre. Every other CI eval integration omits this."""
    output = ET.fromstring(render_junit(_analysis())).find("system-out").text
    assert "MDE for this design" in output
    assert "would not be visible here" in output


# --- fail-on semantics ------------------------------------------------------


@pytest.mark.parametrize("verdict", list(Verdict))
def test_never_fails_on_nothing(verdict):
    assert not should_fail(verdict, "never")


def test_regression_gate_fails_only_on_regression():
    assert should_fail(Verdict.REGRESSION, "regression")
    assert not should_fail(Verdict.INCONCLUSIVE, "regression")
    assert not should_fail(Verdict.EQUIVALENT, "regression")
    assert not should_fail(Verdict.IMPROVEMENT, "regression")


def test_inconclusive_gate_demands_proof_of_no_harm():
    assert should_fail(Verdict.REGRESSION, "inconclusive")
    assert should_fail(Verdict.INCONCLUSIVE, "inconclusive")
    assert not should_fail(Verdict.EQUIVALENT, "inconclusive")


def test_unknown_gate_is_rejected():
    with pytest.raises(ValueError, match="fail-on"):
        should_fail(Verdict.EQUIVALENT, "sometimes")


# --- through the CLI --------------------------------------------------------


def _stored_run(repo: Path) -> None:
    from ablate.store.log import TrialLog
    from ablate.store.run import RunManifest, VariantRef

    run_dir = repo / ".ablate" / "runs" / "2026-07-27T10-00-00Z"
    log = TrialLog(run_dir)
    for i in range(10):
        for r in range(5):
            log.append(_record("A", f"task-{i:02d}", r < 4, r))
            log.append(_record("B", f"task-{i:02d}", r < 2, r, tokens=900))
    RunManifest(
        run_id="2026-07-27T10-00-00Z",
        command="ablate compare CLAUDE.md /dev/null --trials 5",
        agent="claude-code", agent_version="2.1.220", model="claude-sonnet-5",
        variants=[
            VariantRef(id="A", source="CLAUDE.md", text="# Conventions\n"),
            VariantRef(id="B", source="/dev/null", text=None),
        ],
        n_tasks=10, n_trials=5, seed=7,
    ).save(run_dir)


@pytest.mark.parametrize(
    ("fmt", "filename"), [("md", "report.md"), ("json", "report.json"), ("junit", "junit.xml")]
)
def test_report_writes_the_requested_format(git_repo: Path, monkeypatch, fmt, filename):
    _stored_run(git_repo)
    monkeypatch.chdir(git_repo)

    result = CliRunner().invoke(main, ["report", "--last", "--format", fmt])

    assert result.exit_code == 0, result.output
    assert (git_repo / ".ablate" / "runs" / "2026-07-27T10-00-00Z" / filename).exists()


def test_text_format_writes_no_file(git_repo: Path, monkeypatch):
    _stored_run(git_repo)
    monkeypatch.chdir(git_repo)
    CliRunner().invoke(main, ["report", "--last"])
    run_dir = git_repo / ".ablate" / "runs" / "2026-07-27T10-00-00Z"
    assert not (run_dir / "report.md").exists()


def test_fail_on_sets_a_non_zero_exit(git_repo: Path, monkeypatch):
    _stored_run(git_repo)
    monkeypatch.chdir(git_repo)

    result = CliRunner().invoke(main, ["report", "--last", "--fail-on", "inconclusive"])

    assert result.exit_code == 1
    assert "--fail-on inconclusive" in result.output


def test_default_gate_never_fails(git_repo: Path, monkeypatch):
    _stored_run(git_repo)
    monkeypatch.chdir(git_repo)
    assert CliRunner().invoke(main, ["report", "--last"]).exit_code == 0


# --- sticky PR comment ------------------------------------------------------


def test_pr_comment_carries_the_sticky_marker():
    """The Action finds its own previous comment by this marker and edits it,
    so a long-running PR does not accumulate one comment per push."""
    from ablate.report.formats import STICKY_MARKER, render_pr_comment

    assert render_pr_comment(_analysis(), VARIANTS).startswith(STICKY_MARKER)


def test_pr_comment_discloses_the_mde():
    """The detail that makes the Action trustworthy rather than theatrical."""
    from ablate.report.formats import render_pr_comment

    body = render_pr_comment(_analysis(), VARIANTS)
    assert "MDE for this design" in body
    assert "would not be visible here" in body


def test_pr_comment_shows_an_interval_on_every_row():
    from ablate.report.formats import render_pr_comment

    body = render_pr_comment(_analysis(), VARIANTS)
    assert body.count("95% CI") == 1  # header
    assert body.count("●") == 3  # one verdict per metric


def test_pr_comment_status_reflects_the_gate():
    from ablate.report.formats import render_pr_comment

    clean = render_pr_comment(_analysis(a_tokens=900, b_tokens=900), VARIANTS)
    assert "✅ no regression detected" in clean

    dirty = render_pr_comment(_analysis(a_passes=1, b_passes=5), VARIANTS)
    assert "❌" in dirty


def test_pr_comment_orients_columns_as_base_versus_pr():
    """Arm A is the PR's config, arm B is the base branch."""
    from ablate.report.formats import render_pr_comment

    body = render_pr_comment(_analysis(), VARIANTS)
    assert "| base | this PR |" in body
