from __future__ import annotations

import json
import threading
import time
from pathlib import Path
from types import SimpleNamespace

from evals import run_eval, validate_cases


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=["parallel"],
        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_jobs_argument_validation() -> None:
    exit_code = run_eval.main(["--jobs", "0"])
    assert exit_code == 2


def test_run_eval_parallel_best_effort_continues_after_runtime_error(
    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_ok = _make_case(repo_root, "case_ok")
    case_err = _make_case(repo_root, "case_err")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case_ok, case_err])
    monkeypatch.setattr(run_eval, "make_patcher", lambda _args: SimpleNamespace(name="stub"))

    def _run(*, case, condition, run_id, run_metadata, **_kwargs):
        if case.case_id == "case_err":
            raise RuntimeError("simulated interruption")
        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)
    exit_code = run_eval.main(
        [
            "--run-id",
            "parallel-best-effort",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "1",
            "--jobs",
            "2",
            "--executor",
            "local",
        ]
    )

    assert exit_code == 2
    out = capsys.readouterr().out
    assert "ERROR case_err/traceback_only" in out
    assert "DONE case_ok/traceback_only" in out

    results_path = repo_root / "evals" / "results" / "parallel-best-effort.jsonl"
    rows = [json.loads(line) for line in results_path.read_text(encoding="utf-8").splitlines()]
    assert any(row["case_id"] == "case_ok" for row in rows)
    assert all(row["case_id"] != "case_err" for row in rows)


def test_run_eval_parallel_resume_retries_runtime_error_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)

    case_ok = _make_case(repo_root, "case_ok")
    case_err = _make_case(repo_root, "case_err")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case_ok, case_err])
    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_err":
            raise RuntimeError("simulated interruption")
        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_first)
    first_exit = run_eval.main(
        [
            "--run-id",
            "parallel-resume",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "1",
            "--jobs",
            "2",
            "--executor",
            "local",
        ]
    )
    assert first_exit == 2
    capsys.readouterr()

    calls: list[str] = []

    def _run_second(*, case, condition, run_id, run_metadata, **_kwargs):
        calls.append(case.case_id)
        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_second)
    second_exit = run_eval.main(
        [
            "--run-id",
            "parallel-resume",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "1",
            "--jobs",
            "2",
            "--executor",
            "local",
        ]
    )
    assert second_exit == 0
    assert calls == ["case_err"]
    assert "SKIP (resumed) case_ok/traceback_only" in capsys.readouterr().out


def test_run_eval_jobs1_handles_unit_setup_error_as_runtime_error(
    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_ok = _make_case(repo_root, "case_ok")
    case_err = _make_case(repo_root, "case_err")
    monkeypatch.setattr(run_eval, "load_cases", lambda _cases_dir: [case_ok, case_err])
    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_copytree = run_eval.shutil.copytree

    def _copytree(src, dst, *args, **kwargs):
        if Path(src).name == "case_err":
            raise OSError("copy fail")
        return original_copytree(src, dst, *args, **kwargs)

    monkeypatch.setattr(run_eval.shutil, "copytree", _copytree)
    exit_code = run_eval.main(
        [
            "--run-id",
            "jobs1-setup-error",
            "--patcher",
            "null",
            "--conditions",
            "traceback_only",
            "--k",
            "1",
            "--jobs",
            "1",
            "--executor",
            "local",
        ]
    )

    assert exit_code == 2
    out = capsys.readouterr().out
    assert "DONE case_ok/traceback_only" in out
    assert "ERROR case_err/traceback_only" in out

    results_path = repo_root / "evals" / "results" / "jobs1-setup-error.jsonl"
    rows = [json.loads(line) for line in results_path.read_text(encoding="utf-8").splitlines()]
    assert any(row["case_id"] == "case_ok" for row in rows)
    assert all(row["case_id"] != "case_err" for row in rows)


def test_validate_cases_jobs_argument_validation() -> None:
    exit_code = validate_cases.main(["--jobs", "0"])
    assert exit_code == 2


def test_validate_cases_parallel_uses_multiple_workers(tmp_path, monkeypatch) -> None:
    cases = [
        _make_case(tmp_path, "validate_case_a"),
        _make_case(tmp_path, "validate_case_b"),
        _make_case(tmp_path, "validate_case_c"),
    ]
    monkeypatch.setattr(validate_cases, "_REPO_ROOT", tmp_path)
    monkeypatch.setattr(validate_cases, "load_cases", lambda _cases_dir: cases)
    monkeypatch.setattr(validate_cases, "check_pytest_available", lambda: (True, "ok"))
    monkeypatch.setattr(validate_cases, "validate_case_contract", lambda _path: (True, "ok"))

    lock = threading.Lock()
    active = {"value": 0}
    max_seen = {"value": 0}

    def _validate(*_args, **_kwargs):
        with lock:
            active["value"] += 1
            max_seen["value"] = max(max_seen["value"], active["value"])
        try:
            time.sleep(0.05)
        finally:
            with lock:
                active["value"] -= 1
        return True, "baseline fails and oracle patch passes"

    monkeypatch.setattr(validate_cases, "validate_one_case", _validate)
    exit_code = validate_cases.main(
        ["--cases-dir", str(tmp_path), "--executor", "local", "--jobs", "2"]
    )

    assert exit_code == 0
    assert max_seen["value"] >= 2


def test_validate_cases_parallel_internal_error_is_reported(tmp_path, monkeypatch, capsys) -> None:
    cases = [_make_case(tmp_path, "validate_case_a"), _make_case(tmp_path, "validate_case_b")]
    monkeypatch.setattr(validate_cases, "_REPO_ROOT", tmp_path)
    monkeypatch.setattr(validate_cases, "load_cases", lambda _cases_dir: cases)
    monkeypatch.setattr(validate_cases, "check_pytest_available", lambda: (True, "ok"))
    monkeypatch.setattr(validate_cases, "validate_case_contract", lambda _path: (True, "ok"))
    monkeypatch.setattr(
        validate_cases,
        "validate_one_case",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")),
    )

    exit_code = validate_cases.main(
        ["--cases-dir", str(tmp_path), "--executor", "local", "--jobs", "2"]
    )

    assert exit_code == 1
    assert "internal error: RuntimeError: boom" in capsys.readouterr().out
