"""Integration tests for the OpenAI-compatible patcher against a stub HTTP server."""

from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any

import pytest

from evals import run_eval
from evals.patchers.openai_compat_patcher import OpenAICompatPatcher
from llmdebug import _json as json

from ._openai_stub_server import OpenAIStubServer

pytestmark = pytest.mark.integration


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="openai_integration_case",
        path=case_dir,
        category="traceback_controls",
        difficulty="easy",
        snapshot_dependency="none",
        expected_exception="AssertionError",
        tags=[],
        python_args=["-m", "pytest", "-q"],
        timeout_sec=30,
    )


def _chat_json_response(content: str) -> dict[str, Any]:
    return {
        "choices": [{"message": {"content": content}}],
        "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
    }


def _diff_response_body() -> dict[str, Any]:
    diff = (
        "diff --git a/helper.py b/helper.py\n"
        "--- a/helper.py\n"
        "+++ b/helper.py\n"
        "@@ -1,2 +1,2 @@\n"
        " def value() -> int:\n"
        "-    return 1\n"
        "+    return 2\n"
    )
    return _chat_json_response(diff)


def test_openai_request_context_request_contract(tmp_path) -> None:
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response('{"need_more_context":false,"requests":[]}'),
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url,
            model="demo-model",
            api_key="local",
            temperature=0.2,
            max_tokens=321,
        )

        result = patcher.request_context(
            prompt="Need more context?", context_dir=tmp_path, cwd=tmp_path
        )

        assert result is not None
        assert result.request_json is not None
        assert result.request_json["need_more_context"] is False

        req = server.last_request(method="POST", path="/v1/chat/completions")
        assert req is not None
        assert req["headers"]["authorization"] == "Bearer local"
        payload = req["json"]
        assert isinstance(payload, dict)
        assert payload["model"] == "demo-model"
        assert payload["temperature"] == 0.2
        assert payload["max_tokens"] == 321
        assert payload["response_format"]["type"] == "json_object"
        messages = payload["messages"]
        assert isinstance(messages, list)
        assert messages[0]["role"] == "system"
        system_content = str(messages[0]["content"])
        assert '"diagnosis"' in system_content
        assert '"missing_fact"' in system_content
        assert '"need_snapshot"' in system_content
        assert '"snapshot_justification"' in system_content
        assert messages[1]["role"] == "user"


def test_openai_request_context_retries_invalid_payload_with_strict_json(tmp_path) -> None:
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response("not valid json"),
        )
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response('{"need_more_context":false,"requests":[]}'),
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url,
            model="demo-model",
            api_key="local",
            max_tokens=1024,
        )

        result = patcher.request_context(
            prompt="Need more context?", context_dir=tmp_path, cwd=tmp_path
        )

        assert result is not None
        assert result.request_json is not None
        assert result.request_json["need_more_context"] is False
        assert result.meta is not None
        assert result.meta["request_context_retry_count"] == 1
        assert result.meta["request_context_transport_retry_count"] == 0
        assert result.meta["request_context_strict_retry_count"] == 1
        assert result.meta["request_context_failure_kind"] == ""

        reqs = server.requests_for(method="POST", path="/v1/chat/completions")
        assert len(reqs) == 2
        first_payload = reqs[0]["json"]
        second_payload = reqs[1]["json"]
        assert isinstance(first_payload, dict)
        assert isinstance(second_payload, dict)
        assert first_payload["max_tokens"] == 1024
        assert second_payload["max_tokens"] == 384
        second_messages = second_payload["messages"]
        assert isinstance(second_messages, list)
        assert "If unsure, return a valid object" in str(second_messages[0]["content"])


def test_openai_compact_context_request_contract(tmp_path) -> None:
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response('{"compacted_context":"key traceback facts"}'),
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url,
            model="demo-model",
            api_key="local",
            temperature=0.0,
            max_tokens=222,
        )

        result = patcher.compact_context(
            prompt="compact evidence",
            context_text="stdout ...\nsnapshot ...",
            context_dir=tmp_path,
            cwd=tmp_path,
            target_chars=32,
        )

        assert result is not None
        assert result.compacted_text == "key traceback facts"

        req = server.last_request(method="POST", path="/v1/chat/completions")
        assert req is not None
        payload = req["json"]
        assert isinstance(payload, dict)
        assert payload["model"] == "demo-model"
        assert payload["temperature"] == 0.0
        assert payload["max_tokens"] == 222
        assert payload["response_format"]["type"] == "json_object"
        messages = payload["messages"]
        assert isinstance(messages, list)
        assert messages[0]["role"] == "system"
        assert "<= 32 characters" in str(messages[0]["content"])
        assert messages[1]["role"] == "user"
        assert "Context to compact:" in str(messages[1]["content"])


def test_openai_edits_mode_response_format_fallback_contract(tmp_path) -> None:
    (tmp_path / "helper.py").write_text("def value() -> int:\n    return 1\n", encoding="utf-8")
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            status=400,
            body={"error": {"message": "response_format not supported"}},
        )
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response(
                json.dumps(
                    {
                        "edits": [
                            {
                                "path": "helper.py",
                                "find": "return 1",
                                "replace": "return 2",
                                "count": 1,
                            }
                        ]
                    }
                )
            ),
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url,
            model="demo-model",
            api_key="local",
            response_mode="edits",
        )

        result = patcher.propose_patch(prompt="fix helper", context_dir=tmp_path, cwd=tmp_path)

        assert result.diff is not None
        assert "diff --git a/helper.py b/helper.py" in result.diff

        reqs = server.requests_for(method="POST", path="/v1/chat/completions")
        assert len(reqs) == 2
        first_payload = reqs[0]["json"]
        second_payload = reqs[1]["json"]
        assert isinstance(first_payload, dict)
        assert isinstance(second_payload, dict)
        assert first_payload["response_format"]["type"] == "json_schema"
        assert second_payload["response_format"]["type"] == "json_object"


def test_openai_diff_mode_request_has_no_response_format(tmp_path) -> None:
    with OpenAIStubServer() as server:
        server.enqueue_json(method="POST", path="/v1/chat/completions", body=_diff_response_body())
        patcher = OpenAICompatPatcher(
            base_url=server.base_url,
            model="demo-model",
            api_key="local",
            response_mode="diff",
        )

        result = patcher.propose_patch(
            prompt="return unified diff", context_dir=tmp_path, cwd=tmp_path
        )

        assert result.diff is not None
        req = server.last_request(method="POST", path="/v1/chat/completions")
        assert req is not None
        payload = req["json"]
        assert isinstance(payload, dict)
        assert "response_format" not in payload


def test_openai_probe_context_window_endpoint_order(tmp_path) -> None:
    del tmp_path
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="GET",
            path="/api/v1/models",
            status=404,
            body={"error": {"message": "not found"}},
        )
        server.enqueue_json(
            method="GET",
            path="/api/v0/models",
            body={"data": [{"id": "demo-model", "max_context_length": 16384}]},
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url_v1,
            model="demo-model",
            api_key="local",
        )

        tokens, meta = patcher.probe_context_window_tokens()

        assert tokens == 16384
        assert meta["context_window_probe_provider"] == "lmstudio_api_v0"
        assert meta["context_window_source"] == "max_context_length"
        request_paths = [item["path"] for item in server.requests if item["method"] == "GET"]
        assert request_paths == ["/api/v1/models", "/api/v0/models"]


def test_openai_probe_context_window_falls_back_to_openai_models_endpoint(tmp_path) -> None:
    del tmp_path
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="GET",
            path="/api/v1/models",
            status=404,
            body={"error": {"message": "not found"}},
        )
        server.enqueue_json(
            method="GET",
            path="/api/v0/models",
            status=404,
            body={"error": {"message": "not found"}},
        )
        server.enqueue_json(
            method="GET",
            path="/v1/models",
            body={"data": [{"id": "demo-model", "context_length": 8192}]},
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url_v1,
            model="demo-model",
            api_key="local",
        )

        tokens, meta = patcher.probe_context_window_tokens()

        assert tokens == 8192
        assert meta["context_window_probe_provider"] == "openai_models"
        assert meta["context_window_source"] == "context_length"
        request_paths = [item["path"] for item in server.requests if item["method"] == "GET"]
        assert request_paths == ["/api/v1/models", "/api/v0/models", "/v1/models"]


def test_openai_edits_rejects_path_traversal_without_mutating_parent_file(tmp_path) -> None:
    outside_file = tmp_path.parent / "outside_sensitive_file.py"
    outside_file.write_text("FLAG = 'original'\n", encoding="utf-8")
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response(
                json.dumps(
                    {
                        "edits": [
                            {
                                "path": "../outside_sensitive_file.py",
                                "find": "original",
                                "replace": "modified",
                                "count": 1,
                            }
                        ]
                    }
                )
            ),
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url_v1,
            model="demo-model",
            api_key="local",
            response_mode="edits",
        )

        result = patcher.propose_patch(prompt="fix", context_dir=tmp_path, cwd=tmp_path)

        assert result.diff is None
        assert result.meta is not None
        assert result.meta["error"] == "path_outside_case"
        assert outside_file.read_text(encoding="utf-8") == "FLAG = 'original'\n"


def test_openai_edits_rejects_directory_target(tmp_path) -> None:
    (tmp_path / "target_dir").mkdir(parents=True, exist_ok=True)
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response(
                json.dumps(
                    {
                        "edits": [
                            {
                                "path": "target_dir",
                                "find": "x",
                                "replace": "y",
                                "count": 1,
                            }
                        ]
                    }
                )
            ),
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url_v1,
            model="demo-model",
            api_key="local",
            response_mode="edits",
        )

        result = patcher.propose_patch(prompt="fix", context_dir=tmp_path, cwd=tmp_path)

        assert result.diff is None
        assert result.meta is not None
        assert result.meta["error"] == "path_not_file"
        assert result.meta["path"] == "target_dir"


def test_openai_edits_surfaces_file_read_error(tmp_path, monkeypatch) -> None:
    target_file = tmp_path / "helper.py"
    target_file.write_text("def value() -> int:\n    return 1\n", encoding="utf-8")
    original_read_text = Path.read_text

    def _raising_read_text(self: Path, *args, **kwargs) -> str:
        if self.resolve() == target_file.resolve():
            raise OSError("simulated read failure")
        return original_read_text(self, *args, **kwargs)

    monkeypatch.setattr(Path, "read_text", _raising_read_text)
    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response(
                json.dumps(
                    {
                        "edits": [
                            {
                                "path": "helper.py",
                                "find": "return 1",
                                "replace": "return 2",
                                "count": 1,
                            }
                        ]
                    }
                )
            ),
        )
        patcher = OpenAICompatPatcher(
            base_url=server.base_url_v1,
            model="demo-model",
            api_key="local",
            response_mode="edits",
        )

        result = patcher.propose_patch(prompt="fix", context_dir=tmp_path, cwd=tmp_path)

        assert result.diff is None
        assert result.meta is not None
        assert result.meta["error"] == "file_read_error"
        assert result.meta["path"] == "helper.py"
        assert "simulated read failure" in str(result.meta.get("exc"))


def test_stage_a_outside_file_request_uses_fallback_and_does_not_leak_secret(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    secret_marker = "TOP_SECRET_SENTINEL_4731"
    outside_secret_path = tmp_path / "secret.txt"
    outside_secret_path.write_text(secret_marker, encoding="utf-8")
    workdir = tmp_path / "workdir_openai_stage_a"
    context_dir = tmp_path / "context_openai_stage_a"
    shutil.copytree(case.path, workdir)

    with OpenAIStubServer() as server:
        server.enqueue_json(
            method="POST",
            path="/v1/chat/completions",
            body=_chat_json_response(
                json.dumps(
                    {
                        "need_more_context": True,
                        "requests": [{"type": "file", "path": "../secret.txt"}],
                        "sufficiency_rationale": "need more context",
                    }
                )
            ),
        )
        server.enqueue_json(method="POST", path="/v1/chat/completions", body=_diff_response_body())
        patcher = OpenAICompatPatcher(
            base_url=server.base_url_v1,
            model="demo-model",
            api_key="local",
            response_mode="diff",
            force_patch_attempt=False,
        )

        call_counter = {"count": 0}

        def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
            del args, kwargs
            call_counter["count"] += 1
            if call_counter["count"] == 1:
                return run_eval.RunResult(
                    returncode=1,
                    duration_sec=0.01,
                    stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
                    stderr="",
                )
            return run_eval.RunResult(
                returncode=0,
                duration_sec=0.01,
                stdout="1 passed\n",
                stderr="",
            )

        monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
        monkeypatch.setattr(
            run_eval,
            "read_latest_snapshot",
            lambda *_args, **_kwargs: (None, "missing snapshot"),
        )

        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.WITH_SNAPSHOT_STRUCTURED,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-openai-stage-a-fallback",
            include_files_context=False,
            force_patch_attempt=False,
            context_protocol="staged",
        )

        assert len(records) == 1
        assert records[0]["success"] is True
        assert call_counter["count"] == 2
        assert records[0]["patch_meta"]["stage_a_used_fallback"] is True
        assert records[0]["patch_meta"]["stage_a_reason"] == "no_valid_stage_a_requests"

        requests = server.requests_for(method="POST", path="/v1/chat/completions")
        assert len(requests) == 2
        stage_b_payload = requests[1]["json"]
        assert isinstance(stage_b_payload, dict)
        messages = stage_b_payload["messages"]
        assert isinstance(messages, list)
        stage_b_user_prompt = str(messages[1]["content"])
        assert secret_marker not in stage_b_user_prompt
        assert "../secret.txt" not in stage_b_user_prompt
