from __future__ import annotations

import shutil
from dataclasses import dataclass, field
from pathlib import Path

from evals import run_eval
from evals.patchers.base import ContextRequestResult, PatchResult
from evals.patchers.openai_compat_patcher import OpenAICompatPatcher


def _make_case(tmp_path: Path) -> run_eval.CaseSpec:
    case_dir = tmp_path / "case_src"
    case_dir.mkdir(parents=True, exist_ok=True)
    (case_dir / "buggy.py").write_text(
        "import helper\n\n\ndef f() -> int:\n    return helper.value()\n",
        encoding="utf-8",
    )
    (case_dir / "helper.py").write_text("def value() -> 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="adaptive_case",
        path=case_dir,
        category="traceback_controls",
        difficulty="easy",
        snapshot_dependency="helpful",
        expected_exception="AssertionError",
        tags=[],
        python_args=["-m", "pytest", "-q"],
        timeout_sec=30,
    )


def _snapshot_payload() -> dict:
    return {
        "exception": {"type": "AssertionError", "message": "expected 2 got 1"},
        "frames": [
            {
                "file_rel": "buggy.py",
                "line": 4,
                "function": "f",
                "code": "return helper.value()",
                "locals": {"x": 1},
            }
        ],
        "pytest": {"nodeid": "test_buggy.py::test_f", "repro": ["python", "-m", "pytest", "-q"]},
    }


@dataclass
class _AdaptiveGatedCapturePatcher:
    name: str = "adaptive_gated_capture"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need code context",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionDeclinePatcher:
    name: str = "adaptive_discretion_decline"
    request_calls: int = 0

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": False,
                "requests": [],
                "sufficiency_rationale": "enough evidence",
            },
            raw_response='{"need_more_context":false,"requests":[]}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionInvalidPatcher:
    name: str = "adaptive_discretion_invalid"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(request_json=None, raw_response="{invalid-json", meta={})

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


def test_adaptive_gated_invokes_helper_after_repeated_signature(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_gated"
    context_dir = tmp_path / "context_adaptive_gated"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_GATED,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=2,
        output_format="json_compact",
        run_id="run-adaptive-gated",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_gate_policy="balanced",
        adaptive_stagnation_window=1,
    )

    assert len(records) == 2
    assert patcher.request_calls == 1
    first_attempt_meta = records[0]["attempt_metadata"]
    second_attempt_meta = records[1]["attempt_metadata"]
    assert first_attempt_meta["adaptive_helper_invoked"] is False
    assert second_attempt_meta["adaptive_helper_invoked"] is True
    assert first_attempt_meta["context_protocol"] == "monolithic"
    assert second_attempt_meta["context_protocol"] == "staged"
    assert records[1]["patch_meta"]["adaptive_helper_decision_reason"] in {
        "gate_balanced_repeated_signature",
        "gate_requested_helper",
    }


def test_adaptive_llm_discretion_decline_keeps_monolithic_attempt(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_decline"
    context_dir = tmp_path / "context_adaptive_discretion_decline"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionDeclinePatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-discretion-decline",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    assert patcher.request_calls == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_invoked"] is False
    assert attempt_meta["adaptive_helper_decision_reason"] == "llm_declined_helper"
    assert attempt_meta["context_protocol"] == "monolithic"
    assert records[0]["patch_meta"]["stage_a_request_success"] is True


def test_adaptive_openai_helper_response_mode_override(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_openai_override"
    context_dir = tmp_path / "context_adaptive_openai_override"
    shutil.copytree(case.path, workdir)

    captured_response_modes: list[str] = []

    def _fake_request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del self, prompt, context_dir, cwd
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need source",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def _fake_propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        captured_response_modes.append(str(self.response_mode))
        return PatchResult(
            diff=None, meta={"reason": "no_diff", "response_mode": self.response_mode}
        )

    monkeypatch.setattr(OpenAICompatPatcher, "request_context", _fake_request_context)
    monkeypatch.setattr(OpenAICompatPatcher, "propose_patch", _fake_propose_patch)
    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="edits",
        force_patch_attempt=False,
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-openai-override",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_openai_helper_response_mode="diff",
    )

    assert len(records) == 1
    assert captured_response_modes == ["diff"]
    assert records[0]["patch_meta"]["adaptive_openai_response_mode_override"] == "diff"
    assert records[0]["model_metadata"]["response_mode"] == "diff"


def test_adaptive_llm_discretion_invalid_response_does_not_count_as_invocation(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_invalid"
    context_dir = tmp_path / "context_adaptive_discretion_invalid"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionInvalidPatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-discretion-invalid",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    assert patcher.request_calls == 1
    assert len(patcher.prompts) == 1
    assert "Stage-A requested context:" not in patcher.prompts[0]
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_invoked"] is False
    assert attempt_meta["adaptive_helper_decision_reason"] == "llm_invalid_response_no_helper"
    assert attempt_meta["context_protocol"] == "monolithic"
    assert records[0]["patch_meta"]["stage_a_request_success"] is False
