from __future__ import annotations

from evals import run_eval
from evals.patchers.base import PatchResult
from evals.patchers.openai_compat_patcher import (
    OpenAICompatPatcher,
    _base_url_from_chat_url,
    _build_diff_from_changes,
)


def test_openai_force_patch_attempt_builds_fallback_diff(tmp_path, monkeypatch) -> None:
    module_path = "evals.patchers.openai_compat_patcher._propose_patch_via_diff"
    monkeypatch.setattr(
        module_path,
        lambda **kwargs: PatchResult(diff=None, meta={"error": "no_diff_found"}),
    )

    (tmp_path / "buggy.py").write_text("def f():\n    return 1\n", encoding="utf-8")
    (tmp_path / "test_buggy.py").write_text("def test_f():\n    assert True\n", encoding="utf-8")

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="diff",
        force_patch_attempt=True,
    )
    prompt = (
        "Available files (use exact relative paths):\n"
        "test_buggy.py\n"
        "buggy.py\n\n"
        "Pytest output (stdout):\nfailed\n"
    )
    result = patcher.propose_patch(prompt=prompt, context_dir=tmp_path, cwd=tmp_path)

    assert result.diff is not None
    assert "diff --git a/buggy.py b/buggy.py" in result.diff
    assert "llmdebug-force-attempt" in result.diff
    assert result.meta is not None
    assert result.meta["error"] == "no_diff_found"
    assert result.meta["forced_patch_attempt"] is True
    assert result.meta["forced_patch_target"] == "buggy.py"

    applied, _ = run_eval.apply_diff(tmp_path, result.diff)
    assert applied


def test_openai_force_patch_attempt_disabled_preserves_none(tmp_path, monkeypatch) -> None:
    module_path = "evals.patchers.openai_compat_patcher._propose_patch_via_diff"
    monkeypatch.setattr(
        module_path,
        lambda **kwargs: PatchResult(diff=None, meta={"error": "no_diff_found"}),
    )

    (tmp_path / "buggy.py").write_text("def f():\n    return 1\n", encoding="utf-8")

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="diff",
        force_patch_attempt=False,
    )
    prompt = (
        "Available files (use exact relative paths):\nbuggy.py\n\nPytest output (stdout):\nfailed\n"
    )
    result = patcher.propose_patch(prompt=prompt, context_dir=tmp_path, cwd=tmp_path)

    assert result.diff is None
    assert result.meta is not None
    assert result.meta["error"] == "no_diff_found"
    assert result.meta["response_mode"] == "diff"
    assert result.meta["api_compat_seed_requested"] is None


def test_build_diff_from_changes_is_patch_apply_compatible(tmp_path) -> None:
    target = tmp_path / "buggy.py"
    before = "def f():\n\n    return 1\n"
    after = "def f():\n\n    return 2\n"
    target.write_text(before, encoding="utf-8")

    diff = _build_diff_from_changes([("buggy.py", before, after)])
    assert "diff --git a/buggy.py b/buggy.py" in diff

    applied, message = run_eval.apply_diff(tmp_path, diff)
    assert applied, message
    assert target.read_text(encoding="utf-8") == after


def test_base_url_from_chat_url_supports_non_v1_paths() -> None:
    assert _base_url_from_chat_url("https://api.z.ai/api/coding/paas/v4/chat/completions") == (
        "https://api.z.ai/api/coding/paas/v4"
    )
    assert _base_url_from_chat_url("http://127.0.0.1:1234/v1/chat/completions") == (
        "http://127.0.0.1:1234/v1"
    )


def test_openai_patcher_prefers_z_api_env_for_missing_key(tmp_path, monkeypatch) -> None:
    captured: dict[str, str] = {}

    def _fake_diff(**kwargs) -> PatchResult:
        captured["api_key"] = str(kwargs.get("api_key") or "")
        return PatchResult(diff=None, meta={"error": "no_diff_found"})

    monkeypatch.setattr(
        "evals.patchers.openai_compat_patcher._propose_patch_via_diff",
        _fake_diff,
    )
    monkeypatch.setenv("Z_API", "z-key")
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    monkeypatch.delenv("LLMDEBUG_EVAL_OPENAI_API_KEY", raising=False)

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="diff",
        force_patch_attempt=False,
        api_key=None,
    )
    _ = patcher.propose_patch(
        prompt="Pytest output (stdout):\nfailed\n", context_dir=tmp_path, cwd=tmp_path
    )
    assert captured["api_key"] == "z-key"


def test_openai_edits_fallback_aggregates_call_and_token_usage(tmp_path, monkeypatch) -> None:
    def _fake_chat_completion(**kwargs):
        del kwargs
        return (
            {"choices": [{"message": {"content": "not json"}}]},
            {
                "llm_call_count": 1,
                "usage": {"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7},
            },
        )

    def _fake_diff(**kwargs) -> PatchResult:
        del kwargs
        return PatchResult(
            diff=(
                "diff --git a/buggy.py b/buggy.py\n"
                "--- a/buggy.py\n"
                "+++ b/buggy.py\n"
                "@@ -1 +1 @@\n"
                "-old\n"
                "+new\n"
            ),
            meta={
                "llm_call_count": 2,
                "usage": {"prompt_tokens": 11, "completion_tokens": 4, "total_tokens": 15},
            },
        )

    monkeypatch.setattr(
        "evals.patchers.openai_compat_patcher._chat_completion",
        _fake_chat_completion,
    )
    monkeypatch.setattr(
        "evals.patchers.openai_compat_patcher._propose_patch_via_diff",
        _fake_diff,
    )

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="edits",
        force_patch_attempt=False,
    )
    result = patcher.propose_patch(
        prompt="Pytest output (stdout):\nfailed\n", context_dir=tmp_path, cwd=tmp_path
    )

    assert result.diff is not None
    assert result.meta is not None
    assert int(result.meta["llm_call_count"]) == 3
    usage = result.meta.get("usage")
    assert isinstance(usage, dict)
    assert int(usage.get("prompt_tokens") or 0) == 16
    assert int(usage.get("completion_tokens") or 0) == 6
    assert int(usage.get("total_tokens") or 0) == 22


def test_request_context_retry_aggregates_usage_when_response_format_is_rejected(
    tmp_path, monkeypatch
) -> None:
    calls = {"count": 0}

    def _fake_chat_completion(**kwargs):
        del kwargs
        calls["count"] += 1
        if calls["count"] == 1:
            return (
                None,
                {
                    "error": "http_error",
                    "body": "response_format not supported",
                    "llm_call_count": 1,
                    "usage": {"prompt_tokens": 3, "completion_tokens": 0, "total_tokens": 3},
                },
            )
        return (
            {
                "choices": [
                    {
                        "message": {
                            "content": (
                                '{"need_more_context":false,"requests":[],'
                                + '"sufficiency_rationale":"ok"}'
                            )
                        }
                    }
                ]
            },
            {
                "llm_call_count": 1,
                "usage": {"prompt_tokens": 2, "completion_tokens": 1, "total_tokens": 3},
            },
        )

    monkeypatch.setattr(
        "evals.patchers.openai_compat_patcher._chat_completion",
        _fake_chat_completion,
    )

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="edits",
    )
    result = patcher.request_context(prompt="x", context_dir=tmp_path, cwd=tmp_path)
    assert result is not None
    assert result.meta is not None
    assert int(result.meta["llm_call_count"]) == 2
    usage = result.meta.get("usage")
    assert isinstance(usage, dict)
    assert int(usage.get("prompt_tokens") or 0) == 5
    assert int(usage.get("completion_tokens") or 0) == 1
    assert int(usage.get("total_tokens") or 0) == 6
