from __future__ import annotations

import json
from dataclasses import dataclass

import pytest

from evals import run_eval
from evals.patchers.base import PatchResult


@dataclass(frozen=True)
class _OpenAIDiffPatcher:
    name: str = "openai"
    base_url: str = "http://127.0.0.1:1234"
    model: str = "dummy-model"
    temperature: float = 0.0
    max_tokens: int = 128
    response_mode: str = "edits"
    seed: int | None = None

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        diff = (
            "diff --git a/buggy.py b/buggy.py\n"
            "--- a/buggy.py\n"
            "+++ b/buggy.py\n"
            "@@ -1,2 +1,2 @@\n"
            " def f() -> int:\n"
            "-    return 1\n"
            "+    return 2\n"
        )
        return PatchResult(
            diff=diff,
            meta={
                "base_url": "http://127.0.0.1:1234/v1",
                "model": "dummy-model",
                "temperature": 0.0,
                "max_tokens": 128,
                "response_mode": "edits",
                "api_compat_seed_requested": None,
                "api_compat_seed_acknowledged": False,
                "api_compat_seed_response": None,
            },
        )


@dataclass(frozen=True)
class _OpenAINoAckPatcher:
    name: str = "openai"
    base_url: str = "http://127.0.0.1:1234"
    model: str = "dummy-model"
    temperature: float = 0.0
    max_tokens: int = 128
    response_mode: str = "edits"
    seed: int | None = 123

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        return PatchResult(
            diff=None,
            meta={
                "base_url": "http://127.0.0.1:1234/v1",
                "model": "dummy-model",
                "temperature": 0.0,
                "max_tokens": 128,
                "response_mode": "edits",
                "api_compat_seed_requested": 123,
                "api_compat_seed_acknowledged": False,
                "api_compat_seed_response": None,
            },
        )


def _make_case(tmp_path) -> run_eval.CaseSpec:
    case_dir = tmp_path / "case_src"
    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="repro_case",
        path=case_dir,
        category="traceback_controls",
        difficulty="easy",
        snapshot_dependency="none",
        expected_exception="AssertionError",
        tags=["repro"],
        python_args=["-m", "pytest", "-q"],
        timeout_sec=30,
    )


def test_run_eval_main_deterministic_requires_seed() -> None:
    exit_code = run_eval.main(["--deterministic"])
    assert exit_code == 2


def test_run_one_condition_emits_schema_v2_metadata(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir"
    context_dir = tmp_path / "context"
    workdir.mkdir(parents=True, exist_ok=True)

    call_state = {"count": 0}

    def _fake_run_pytest(cwd, python_args, condition, timeout_sec, output_format):
        call_state["count"] += 1
        if call_state["count"] == 1:
            return run_eval.RunResult(
                returncode=1,
                duration_sec=0.01,
                stdout="E   AssertionError: expected 2 got 1\n",
                stderr="",
            )
        return run_eval.RunResult(
            returncode=0,
            duration_sec=0.01,
            stdout="",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(run_eval, "apply_diff", lambda cwd, diff: (True, ""))

    run_metadata = {
        "run_id": "run-meta",
        "started_at_utc": "2026-02-10T00:00:00Z",
        "deterministic": False,
        "seed": None,
        "python_version": "3.12.0",
        "platform": "test",
        "llmdebug_version": "2.1.0",
        "patcher_name": "openai",
        "patcher_config_fingerprint": "fp",
    }

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.TRACEBACK_ONLY,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=_OpenAIDiffPatcher(),
        k=1,
        output_format="json_compact",
        run_id="run-meta",
        include_files_context=False,
        force_patch_attempt=True,
        run_metadata=run_metadata,
        record_prompt="hash",
        record_evidence="hash",
        deterministic=False,
        seed=None,
    )

    assert len(records) == 1
    record = records[0]
    assert record["schema_version"] == 2
    assert record["note"] == "success"
    assert "run_metadata" in record
    assert "attempt_metadata" in record
    assert "model_metadata" in record
    assert record["run_metadata"]["patcher_config_fingerprint"] == "fp"
    assert record["attempt_metadata"]["attempt"] == 1
    assert isinstance(record["attempt_metadata"]["prompt_sha256"], str)
    assert isinstance(record["attempt_metadata"]["evidence_sha256"], str)
    assert record["model_metadata"]["backend"] == "openai"
    assert record["model_metadata"]["response_mode"] == "edits"
    assert json.loads(json.dumps(record))["schema_version"] == 2


def test_run_one_condition_deterministic_requires_seed_ack(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_noack"
    context_dir = tmp_path / "context_noack"
    workdir.mkdir(parents=True, exist_ok=True)

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda cwd, python_args, condition, timeout_sec, output_format: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="E   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )

    with pytest.raises(RuntimeError, match="deterministic mode failed"):
        run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.TRACEBACK_ONLY,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=_OpenAINoAckPatcher(),
            k=1,
            output_format="json_compact",
            run_id="run-noack",
            include_files_context=False,
            force_patch_attempt=False,
            deterministic=True,
            seed=123,
        )
