"""Tests for eval run resume-from-progress and deduplication logic."""

from __future__ import annotations

import time
from pathlib import Path
from types import SimpleNamespace

from evals import run_eval
from llmdebug import _json as json


def _run_eval_main(argv: list[str]) -> int:
    return run_eval.main([*argv, "--executor", "local"])


def _make_case(repo_root: Path, case_id: str) -> run_eval.CaseSpec:
    case_dir = repo_root / "synthetic_cases" / case_id
    case_dir.mkdir(parents=True, exist_ok=True)
    (case_dir / "buggy.py").write_text("def f() -> int:\n    return 1\n", encoding="utf-8")
    (case_dir / "test_buggy.py").write_text(
        "from buggy import f\n\n\ndef test_f() -> None:\n    assert f() == 2\n",
        encoding="utf-8",
    )
    return run_eval.CaseSpec(
        case_id=case_id,
        path=case_dir,
        category="traceback_controls",
        difficulty="easy",
        snapshot_dependency="none",
        expected_exception="AssertionError",
        tags=["resume"],
        python_args=["-m", "pytest", "-q"],
        timeout_sec=30,
    )


def _result_record(
    *,
    run_id: str,
    case: run_eval.CaseSpec,
    condition: run_eval.Condition,
    run_metadata: dict[str, object],
    success: bool,
    attempt: int = 1,
) -> dict[str, object]:
    return {
        "run_id": run_id,
        "case_id": case.case_id,
        "condition": condition.value,
        "attempt": attempt,
        "success": success,
        "run_metadata": dict(run_metadata),
    }


def test_run_eval_resume_skips_completed_units(tmp_path, monkeypatch, capsys) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    cases = [_make_case(repo_root, "case_a"), _make_case(repo_root, "case_b")]
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: cases)
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run_first(*, case, condition, run_id, run_metadata, **_kwargs):
        if case.case_id == "case_a":
            return [
                _result_record(
                    run_id=run_id,
                    case=case,
                    condition=condition,
                    run_metadata=run_metadata,
                    success=False,
                )
            ]
        raise RuntimeError("simulated interruption")

    monkeypatch.setattr(run_eval, "run_one_condition", _run_first)
    exit_first = _run_eval_main(
        [
            "--run-id",
            "resume-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "1",
        ]
    )
    assert exit_first == 2
    capsys.readouterr()

    resumed_calls: list[tuple[str, str]] = []

    def _run_resume(*, case, condition, run_id, run_metadata, **_kwargs):
        resumed_calls.append((case.case_id, condition.value))
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=True,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_resume)
    exit_resume = _run_eval_main(
        [
            "--run-id",
            "resume-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "1",
        ]
    )
    assert exit_resume == 0
    assert resumed_calls == [("case_b", "traceback_only")]

    out = capsys.readouterr().out
    assert "SKIP (resumed) case_a/traceback_only" in out
    assert "DONE case_b/traceback_only" in out


def test_run_eval_resume_warns_on_settings_mismatch_and_continues(
    tmp_path, monkeypatch, capsys
) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "case_only")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run_once(*, case, condition, run_id, run_metadata, **_kwargs):
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=False,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_once)
    first_exit = _run_eval_main(
        [
            "--run-id",
            "mismatch-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "1",
        ]
    )
    assert first_exit == 0
    capsys.readouterr()

    resumed_calls: list[tuple[str, str]] = []

    def _run_resume(*, case, condition, run_id, run_metadata, **_kwargs):
        resumed_calls.append((case.case_id, condition.value))
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=False,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_resume)
    resume_exit = _run_eval_main(
        [
            "--run-id",
            "mismatch-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only,with_snapshot",
            "--k",
            "1",
        ]
    )
    assert resume_exit == 0
    assert resumed_calls == [("case_only", "with_snapshot")]
    out = capsys.readouterr().out
    assert "Resume warning: run settings fingerprint mismatch for existing run_id" in out
    assert "SKIP (resumed) case_only/traceback_only" in out


def test_run_eval_resume_warns_on_retry_context_mode_mismatch(
    tmp_path, monkeypatch, capsys
) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "retry_mode_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run_once(*, case, condition, run_id, run_metadata, **_kwargs):
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=False,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_once)
    first_exit = _run_eval_main(
        [
            "--run-id",
            "retry-mode-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--retry-context-mode",
            "history",
            "--k",
            "1",
        ]
    )
    assert first_exit == 0
    capsys.readouterr()

    calls = {"count": 0}

    def _should_not_run(**_kwargs):
        calls["count"] += 1
        return []

    monkeypatch.setattr(run_eval, "run_one_condition", _should_not_run)
    resume_exit = _run_eval_main(
        [
            "--run-id",
            "retry-mode-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--retry-context-mode",
            "fresh",
            "--k",
            "1",
        ]
    )
    assert resume_exit == 0
    assert calls["count"] == 0
    assert (
        "Resume warning: run settings fingerprint mismatch for existing run_id"
        in capsys.readouterr().out
    )


def test_run_eval_resume_warns_on_retry_history_budget_mismatch(
    tmp_path, monkeypatch, capsys
) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "retry_history_budget_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run_once(*, case, condition, run_id, run_metadata, **_kwargs):
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=False,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_once)
    first_exit = _run_eval_main(
        [
            "--run-id",
            "retry-history-budget-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--prompt-budget-retry-history-chars",
            "12000",
            "--k",
            "1",
        ]
    )
    assert first_exit == 0
    capsys.readouterr()

    calls = {"count": 0}

    def _should_not_run(**_kwargs):
        calls["count"] += 1
        return []

    monkeypatch.setattr(run_eval, "run_one_condition", _should_not_run)
    resume_exit = _run_eval_main(
        [
            "--run-id",
            "retry-history-budget-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--prompt-budget-retry-history-chars",
            "2000",
            "--k",
            "1",
        ]
    )
    assert resume_exit == 0
    assert calls["count"] == 0
    assert (
        "Resume warning: run settings fingerprint mismatch for existing run_id"
        in capsys.readouterr().out
    )


def test_run_eval_progress_prints_start_finish_and_eta(tmp_path, monkeypatch, capsys) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "progress_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run_progress(*, case, condition, run_id, run_metadata, **_kwargs):
        success = condition == run_eval.Condition.TRACEBACK_ONLY
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=success,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_progress)
    exit_code = _run_eval_main(
        [
            "--run-id",
            "progress-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only,with_snapshot",
        ]
    )
    assert exit_code == 0

    out = capsys.readouterr().out
    assert "START progress_case/traceback_only" in out
    assert "DONE progress_case/traceback_only status=success" in out
    assert "DONE progress_case/with_snapshot status=failed" in out
    assert "eta=" in out
    assert "Progress: finished" in out


def test_run_eval_resume_repairs_malformed_trailing_jsonl_line_durably(
    tmp_path, monkeypatch, capsys
) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "broken_json_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    results_dir = repo_root / "evals" / "results"
    results_dir.mkdir(parents=True, exist_ok=True)
    fingerprint = "broken-fingerprint"
    valid_line = {
        "run_id": "broken-run",
        "case_id": "broken_json_case",
        "condition": "traceback_only",
        "attempt": 1,
        "success": False,
        "run_metadata": {
            "run_id": "broken-run",
            "run_settings_fingerprint": fingerprint,
        },
    }
    (results_dir / "broken-run.jsonl").write_text(
        f"{json.dumps(valid_line)}\n{{not-json}}\n",
        encoding="utf-8",
    )

    monkeypatch.setattr(
        run_eval,
        "_build_run_settings_fingerprint",
        lambda **_kwargs: fingerprint,
    )
    calls = {"count": 0}

    def _run(*, case, condition, run_id, run_metadata, **_kwargs):
        calls["count"] += 1
        return [
            {
                "run_id": run_id,
                "case_id": case.case_id,
                "condition": condition.value,
                "attempt": 2,
                "success": True,
                "run_metadata": dict(run_metadata),
            }
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run)

    first_exit = _run_eval_main(
        [
            "--run-id",
            "broken-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "3",
        ]
    )
    assert first_exit == 0
    assert calls["count"] == 1
    first_out = capsys.readouterr().out
    assert "Resume repair:" in first_out
    assert "truncated malformed trailing JSON record" in first_out
    sidecars = sorted(results_dir.glob("broken-run.jsonl.corrupt-tail-*.log"))
    assert len(sidecars) == 1
    sidecar_bytes = sidecars[0].read_bytes()
    assert b"{not-json}" in sidecar_bytes

    second_exit = _run_eval_main(
        [
            "--run-id",
            "broken-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "3",
        ]
    )
    assert second_exit == 0
    assert calls["count"] == 1
    second_out = capsys.readouterr().out
    assert "Resume aborted" not in second_out
    assert "Nothing to do: all planned case/condition units are already complete." in second_out


def test_run_eval_resume_invalid_non_trailing_jsonl_still_fails_fast(
    tmp_path, monkeypatch, capsys
) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "broken_non_trailing_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])

    results_dir = repo_root / "evals" / "results"
    results_dir.mkdir(parents=True, exist_ok=True)
    good_line = {
        "run_id": "broken-non-trailing-run",
        "case_id": case.case_id,
        "condition": "traceback_only",
        "attempt": 1,
        "success": False,
        "run_metadata": {
            "run_id": "broken-non-trailing-run",
            "run_settings_fingerprint": "fp",
        },
    }
    (results_dir / "broken-non-trailing-run.jsonl").write_text(
        "{not-json}\n" + json.dumps(good_line) + "\n",
        encoding="utf-8",
    )

    exit_code = _run_eval_main(
        [
            "--run-id",
            "broken-non-trailing-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
        ]
    )
    assert exit_code == 2
    assert "invalid JSON" in capsys.readouterr().out


def test_run_eval_resume_reruns_partially_written_unit(tmp_path, monkeypatch, capsys) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "partial_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    results_dir = repo_root / "evals" / "results"
    results_dir.mkdir(parents=True, exist_ok=True)
    fingerprint = "run-fp"
    existing_line = {
        "run_id": "partial-run",
        "case_id": "partial_case",
        "condition": "traceback_only",
        "attempt": 1,
        "success": False,
        "run_metadata": {
            "run_id": "partial-run",
            "run_settings_fingerprint": fingerprint,
        },
    }
    (results_dir / "partial-run.jsonl").write_text(
        f"{json.dumps(existing_line)}\n",
        encoding="utf-8",
    )

    monkeypatch.setattr(
        run_eval,
        "_build_run_settings_fingerprint",
        lambda **_kwargs: fingerprint,
    )

    calls = {"count": 0}

    def _run_partial(*, case, condition, run_id, run_metadata, **_kwargs):
        calls["count"] += 1
        return [
            {
                "run_id": run_id,
                "case_id": case.case_id,
                "condition": condition.value,
                "attempt": 2,
                "success": True,
                "run_metadata": dict(run_metadata),
            }
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_partial)

    exit_code = _run_eval_main(
        [
            "--run-id",
            "partial-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "3",
        ]
    )
    assert exit_code == 0
    assert calls["count"] == 1
    assert "SKIP (resumed) partial_case/traceback_only" not in capsys.readouterr().out


def test_run_eval_streamed_record_append_avoids_duplicate_lines(
    tmp_path, monkeypatch, capsys
) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "stream_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run_streamed(
        *,
        case,
        condition,
        run_id,
        run_metadata,
        record_sink,
        progress_update,
        **_kwargs,
    ):
        progress_update("attempt_start", {"attempt": 1, "max_attempts": 1})
        record = _result_record(
            run_id=run_id,
            case=case,
            condition=condition,
            run_metadata=run_metadata,
            success=True,
            attempt=1,
        )
        record_sink(record)
        return [record]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_streamed)
    exit_code = _run_eval_main(
        [
            "--run-id",
            "streamed-no-dup",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--progress-verbosity",
            "detailed",
        ]
    )
    assert exit_code == 0
    results_path = repo_root / "evals" / "results" / "streamed-no-dup.jsonl"
    rows = [json.loads(line) for line in results_path.read_text(encoding="utf-8").splitlines()]
    assert len(rows) == 1
    assert rows[0]["case_id"] == "stream_case"
    assert rows[0]["attempt"] == 1
    assert "[ATTEMPT] stream_case/traceback_only" in capsys.readouterr().out


def test_run_eval_detailed_progress_emits_heartbeat_while_unit_is_running(
    tmp_path, monkeypatch, capsys
) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "heartbeat_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run_slow(
        *,
        case,
        condition,
        run_id,
        run_metadata,
        progress_update,
        **_kwargs,
    ):
        progress_update("attempt_start", {"attempt": 1, "max_attempts": 1})
        time.sleep(0.08)
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=True,
                attempt=1,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run_slow)
    exit_code = _run_eval_main(
        [
            "--run-id",
            "heartbeat-run",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--progress-verbosity",
            "detailed",
            "--progress-heartbeat-sec",
            "0.02",
        ]
    )
    assert exit_code == 0
    out = capsys.readouterr().out
    assert "[HEARTBEAT] heartbeat_case/traceback_only" in out


def test_run_results_lock_rejects_second_acquire(tmp_path) -> None:
    results_path = tmp_path / "results.jsonl"
    lock_one, error_one, warning_one = run_eval._RunResultsLock.acquire(
        results_path=results_path,
        run_id="lock-run",
    )
    assert error_one is None
    if warning_one is not None:
        assert lock_one is None
        assert "continuing without cross-process lock" in warning_one
        return
    assert lock_one is not None

    lock_two, error_two, warning_two = run_eval._RunResultsLock.acquire(
        results_path=results_path,
        run_id="lock-run",
    )
    assert lock_two is None
    assert isinstance(error_two, str)
    assert "already held" in error_two
    assert warning_two is None

    lock_one.release()
    lock_three, error_three, warning_three = run_eval._RunResultsLock.acquire(
        results_path=results_path,
        run_id="lock-run",
    )
    assert error_three is None
    assert warning_three is None
    assert lock_three is not None
    lock_three.release()


def test_run_eval_continues_when_lock_is_unavailable(tmp_path, monkeypatch, capsys) -> None:
    repo_root = tmp_path / "repo"
    repo_root.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(run_eval, "_REPO_ROOT", repo_root)

    case = _make_case(repo_root, "lock_fallback_case")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run(*, case, condition, run_id, run_metadata, **_kwargs):
        return [
            _result_record(
                run_id=run_id,
                case=case,
                condition=condition,
                run_metadata=run_metadata,
                success=False,
            )
        ]

    monkeypatch.setattr(run_eval, "run_one_condition", _run)

    original_acquire = run_eval._RunResultsLock.acquire

    def _acquire_without_lock(*, results_path, run_id):
        del results_path, run_id
        return None, None, "simulated-missing-fcntl"

    monkeypatch.setattr(run_eval._RunResultsLock, "acquire", _acquire_without_lock)
    try:
        exit_code = _run_eval_main(
            [
                "--run-id",
                "lock-fallback-run",
                "--patcher",
                "null",
                "--conditions",
                "traceback_only",
            ]
        )
    finally:
        monkeypatch.setattr(run_eval._RunResultsLock, "acquire", original_acquire)
    assert exit_code == 0
    out = capsys.readouterr().out
    assert "Run lock warning: simulated-missing-fcntl" in out
