"""
Integration tests: full HTTP request → router → mocked LLM → response.

Every endpoint is tested through the FastAPI test client with auth deps
overridden and the LLM client mocked.  These verify serialization,
routing, status codes, and the glue logic between components.
"""

from __future__ import annotations

import json
import uuid
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock

import httpx
import pytest

from codeflash_api.llm._client import LLMOutputUnparseableError

from .conftest import _stub_llm_response, make_trace_id

if TYPE_CHECKING:
    from unittest.mock import MagicMock


# ── /healthcheck ───────────────────────────────────────────────


class TestHealthcheck:
    """Integration tests for GET /healthcheck."""

    @pytest.mark.asyncio
    async def test_returns_ok(self, client: httpx.AsyncClient) -> None:
        """
        Healthcheck returns 200 with status ok.
        """
        resp = await client.get("/healthcheck")

        assert 200 == resp.status_code
        assert {"status": "ok"} == resp.json()


# ── /ai/explain ────────────────────────────────────────────────


class TestExplainIntegration:
    """Integration tests for POST /ai/explain."""

    @pytest.mark.asyncio
    async def test_success(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Successful explain returns the extracted explanation.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(
                "<explain>The optimization uses a faster algorithm.</explain>"
            ),
        )
        resp = await client.post(
            "/ai/explain",
            json={
                "trace_id": make_trace_id(),
                "source_code": "def f(): pass",
                "optimized_code": "def f(): return 1",
            },
        )

        assert 200 == resp.status_code
        data = resp.json()
        assert "faster algorithm" in data["explanation"]

    @pytest.mark.asyncio
    async def test_invalid_trace_id(self, client: httpx.AsyncClient) -> None:
        """
        Invalid trace_id returns 400.
        """
        resp = await client.post(
            "/ai/explain",
            json={
                "trace_id": "not-a-uuid",
                "source_code": "def f(): pass",
                "optimized_code": "def f(): return 1",
            },
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_llm_failure(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        LLM call failure returns 422.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            side_effect=RuntimeError("provider down"),
        )
        resp = await client.post(
            "/ai/explain",
            json={
                "trace_id": make_trace_id(),
                "source_code": "def f(): pass",
                "optimized_code": "def f(): return 1",
            },
        )

        assert 422 == resp.status_code

    @pytest.mark.asyncio
    async def test_empty_llm_response(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Empty LLM output returns 422.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(""),
        )
        resp = await client.post(
            "/ai/explain",
            json={
                "trace_id": make_trace_id(),
                "source_code": "def f(): pass",
                "optimized_code": "def f(): return 1",
            },
        )

        assert 422 == resp.status_code


# ── /ai/rank ───────────────────────────────────────────────────


class TestRankIntegration:
    """Integration tests for POST /ai/rank."""

    @pytest.mark.asyncio
    async def test_success_json(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Successful ranking with JSON response returns 0-indexed ranking.
        """
        llm_content = json.dumps(
            {
                "scores": {
                    "1": {
                        "speedup_quality": 8,
                        "readability": 7,
                        "diff_precision": 9,
                        "correctness_confidence": 8,
                    },
                    "2": {
                        "speedup_quality": 6,
                        "readability": 9,
                        "diff_precision": 8,
                        "correctness_confidence": 7,
                    },
                },
                "ranking": [1, 2],
                "explanation": "Candidate 1 has better speedup.",
            }
        )
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(llm_content),
        )
        resp = await client.post(
            "/ai/rank",
            json={
                "trace_id": make_trace_id(),
                "speedups": [2.5, 1.8],
                "diffs": ["diff1", "diff2"],
                "optimization_ids": [str(uuid.uuid4()), str(uuid.uuid4())],
            },
        )

        assert 200 == resp.status_code
        data = resp.json()
        assert isinstance(data["ranking"], list)
        assert 2 == len(data["ranking"])
        assert all(r in (0, 1) for r in data["ranking"])

    @pytest.mark.asyncio
    async def test_invalid_trace_id(self, client: httpx.AsyncClient) -> None:
        """
        Invalid trace_id returns 400.
        """
        resp = await client.post(
            "/ai/rank",
            json={
                "trace_id": "bad",
                "speedups": [2.0],
                "diffs": ["diff"],
                "optimization_ids": [str(uuid.uuid4())],
            },
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_unparseable_response(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Completely unparseable LLM output returns 422.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response("I cannot rank these."),
        )
        resp = await client.post(
            "/ai/rank",
            json={
                "trace_id": make_trace_id(),
                "speedups": [2.0],
                "diffs": ["diff"],
                "optimization_ids": [str(uuid.uuid4())],
            },
        )

        assert 422 == resp.status_code


# ── /ai/workflow-gen ───────────────────────────────────────────


class TestWorkflowIntegration:
    """Integration tests for POST /ai/workflow-gen."""

    @pytest.mark.asyncio
    async def test_success(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Valid workflow generation returns YAML steps.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(
                "steps:\n"
                "- name: Checkout\n"
                "  uses: actions/checkout@v4\n"
                "- name: Setup Python\n"
                "  uses: actions/setup-python@v5\n"
            ),
        )
        resp = await client.post(
            "/ai/workflow-gen",
            json={
                "repo_files": {"setup.py": "from setuptools import setup"},
                "directory_structure": {"src": {"main.py": None}},
            },
        )

        assert 200 == resp.status_code
        data = resp.json()
        assert "steps:" in data["workflow_steps"]
        assert "Checkout" in data["workflow_steps"]

    @pytest.mark.asyncio
    async def test_empty_input(self, client: httpx.AsyncClient) -> None:
        """
        Empty repo_files and directory_structure returns 400.
        """
        resp = await client.post(
            "/ai/workflow-gen",
            json={
                "repo_files": {},
                "directory_structure": {},
            },
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_llm_failure(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        LLM call failure returns 500.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            side_effect=RuntimeError("timeout"),
        )
        resp = await client.post(
            "/ai/workflow-gen",
            json={
                "repo_files": {"setup.py": "pass"},
                "directory_structure": {"src": None},
            },
        )

        assert 500 == resp.status_code

    @pytest.mark.asyncio
    async def test_no_yaml_in_response(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        LLM response without YAML steps returns 500.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(
                "I'm not sure how to generate a workflow."
            ),
        )
        resp = await client.post(
            "/ai/workflow-gen",
            json={
                "repo_files": {"setup.py": "pass"},
                "directory_structure": {"src": None},
            },
        )

        assert 500 == resp.status_code


# ── /ai/log_features ──────────────────────────────────────────


class TestLogFeaturesIntegration:
    """Integration tests for POST /ai/log_features."""

    @pytest.mark.asyncio
    async def test_success(self, client: httpx.AsyncClient) -> None:
        """
        Valid log_features request returns 200 with null body.
        """
        resp = await client.post(
            "/ai/log_features",
            json={"trace_id": make_trace_id()},
        )

        assert 200 == resp.status_code

    @pytest.mark.asyncio
    async def test_invalid_trace_id(self, client: httpx.AsyncClient) -> None:
        """
        Invalid trace_id returns 400.
        """
        resp = await client.post(
            "/ai/log_features",
            json={"trace_id": "not-valid"},
        )

        assert 400 == resp.status_code


# ── /ai/optimization_review ───────────────────────────────────


class TestReviewIntegration:
    """Integration tests for POST /ai/optimization_review."""

    @pytest.mark.asyncio
    async def test_success(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Successful review returns a rating and explanation.
        """
        review_json = json.dumps(
            {
                "rating": "high",
                "performance_score": 9,
                "code_quality_score": 8,
                "hot_path_score": 7,
            }
        )
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(
                f"Great optimization.\n```json\n{review_json}\n```\n"
                "Overall excellent."
            ),
        )
        resp = await client.post(
            "/ai/optimization_review",
            json={
                "trace_id": make_trace_id(),
                "original_code": "def f(): pass",
                "optimized_code": "def f(): return 1",
                "original_runtime": "100ms",
                "optimized_runtime": "50ms",
                "speedup": "2x",
                "coverage_message": "100%",
                "loop_count": 1000,
                "explanation": "Optimized return",
                "calling_fn_details": "main()",
            },
        )

        assert 200 == resp.status_code
        data = resp.json()
        assert "high" == data["review"]
        assert data["review_explanation"]

    @pytest.mark.asyncio
    async def test_invalid_trace_id(self, client: httpx.AsyncClient) -> None:
        """
        Invalid trace_id returns 400.
        """
        resp = await client.post(
            "/ai/optimization_review",
            json={
                "trace_id": "bad-id",
                "original_code": "def f(): pass",
                "optimized_code": "def f(): return 1",
                "original_runtime": "100ms",
                "optimized_runtime": "50ms",
                "speedup": "2x",
                "coverage_message": "100%",
                "loop_count": 1000,
                "explanation": "faster",
                "calling_fn_details": "main()",
            },
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_unparseable_review(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        LLM response without valid JSON review block returns 422.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response("No valid review here."),
        )
        resp = await client.post(
            "/ai/optimization_review",
            json={
                "trace_id": make_trace_id(),
                "original_code": "def f(): pass",
                "optimized_code": "def f(): return 1",
                "original_runtime": "100ms",
                "optimized_runtime": "50ms",
                "speedup": "2x",
                "coverage_message": "100%",
                "loop_count": 1000,
                "explanation": "faster",
                "calling_fn_details": "main()",
            },
        )

        assert 422 == resp.status_code

    @pytest.mark.asyncio
    async def test_missing_code_and_diff(
        self,
        client: httpx.AsyncClient,
    ) -> None:
        """
        Request with neither code pair nor diff returns 400.
        """
        resp = await client.post(
            "/ai/optimization_review",
            json={
                "trace_id": make_trace_id(),
                "original_runtime": "100ms",
                "optimized_runtime": "50ms",
                "speedup": "2x",
                "coverage_message": "100%",
                "loop_count": 1000,
                "explanation": "faster",
                "calling_fn_details": "main()",
            },
        )

        assert 400 == resp.status_code


# ── /ai/code_repair ───────────────────────────────────────────


class TestRepairIntegration:
    """Integration tests for POST /ai/code_repair."""

    @pytest.mark.asyncio
    async def test_success(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Successful repair returns a new optimization with repaired code.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(
                "<replace_in_file>\n"
                "<path>file.py</path>\n"
                "<diff>\n"
                "<<<<<<< SEARCH\n"
                "    return 2\n"
                "=======\n"
                "    return 1\n"
                ">>>>>>> REPLACE\n"
                "</diff>\n"
                "</replace_in_file>"
            ),
        )
        resp = await client.post(
            "/ai/code_repair",
            json={
                "trace_id": make_trace_id(),
                "optimization_id": str(uuid.uuid4()),
                "original_source_code": "def f():\n    return 1\n",
                "modified_source_code": "def f():\n    return 2\n",
                "test_diffs": [
                    {
                        "scope": "return_value",
                        "original_pass": True,
                        "candidate_pass": False,
                        "original_value": "1",
                        "candidate_value": "2",
                    }
                ],
            },
        )

        assert 200 == resp.status_code
        data = resp.json()
        assert "return 1" in data["source_code"]
        assert data["optimization_id"]

    @pytest.mark.asyncio
    async def test_invalid_trace_id(self, client: httpx.AsyncClient) -> None:
        """
        Invalid trace_id returns 400.
        """
        resp = await client.post(
            "/ai/code_repair",
            json={
                "trace_id": "bad",
                "optimization_id": str(uuid.uuid4()),
                "original_source_code": "def f(): pass",
                "modified_source_code": "def f(): return 1",
                "test_diffs": [],
            },
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_all_attempts_fail(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        All repair attempts failing returns 422.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            side_effect=LLMOutputUnparseableError("parse failed"),
        )
        resp = await client.post(
            "/ai/code_repair",
            json={
                "trace_id": make_trace_id(),
                "optimization_id": str(uuid.uuid4()),
                "original_source_code": "def f(): pass",
                "modified_source_code": "def f(): return 1",
                "test_diffs": [],
            },
        )

        assert 422 == resp.status_code


# ── /ai/refinement ────────────────────────────────────────────


class TestRefinementIntegration:
    """Integration tests for POST /ai/refinement."""

    @pytest.mark.asyncio
    async def test_success(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        Successful refinement returns refined code items.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response(
                "<explanation>Better variable naming</explanation>\n"
                "<replace_in_file>\n"
                "<path>file</path>\n"
                "<diff>\n"
                "<<<<<<< SEARCH\n"
                "    return 2\n"
                "=======\n"
                "    result = 2\n"
                "    return result\n"
                ">>>>>>> REPLACE\n"
                "</diff>\n"
                "</replace_in_file>"
            ),
        )
        resp = await client.post(
            "/ai/refinement",
            json=[
                {
                    "trace_id": make_trace_id(),
                    "optimization_id": str(uuid.uuid4()),
                    "original_source_code": "def f():\n    return 1\n",
                    "optimized_source_code": "def f():\n    return 2\n",
                }
            ],
        )

        assert 200 == resp.status_code
        data = resp.json()
        assert len(data["refinements"]) >= 1
        assert data["refinements"][0]["source_code"]
        assert data["refinements"][0]["explanation"]

    @pytest.mark.asyncio
    async def test_empty_request(self, client: httpx.AsyncClient) -> None:
        """
        Empty request list returns 400.
        """
        resp = await client.post("/ai/refinement", json=[])

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_invalid_trace_id(self, client: httpx.AsyncClient) -> None:
        """
        Invalid trace_id returns 400.
        """
        resp = await client.post(
            "/ai/refinement",
            json=[
                {
                    "trace_id": "bad",
                    "optimization_id": str(uuid.uuid4()),
                    "original_source_code": "def f(): pass",
                    "optimized_source_code": "def f(): return 1",
                }
            ],
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_all_refinements_fail(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        All refinement attempts failing returns 422.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            return_value=_stub_llm_response("No useful patches here."),
        )
        resp = await client.post(
            "/ai/refinement",
            json=[
                {
                    "trace_id": make_trace_id(),
                    "optimization_id": str(uuid.uuid4()),
                    "original_source_code": "def f():\n    return 1\n",
                    "optimized_source_code": "def f():\n    return 2\n",
                }
            ],
        )

        assert 422 == resp.status_code


# ── /ai/adaptive_optimize ─────────────────────────────────────


class TestAdaptiveIntegration:
    """Integration tests for POST /ai/adaptive_optimize."""

    @pytest.mark.asyncio
    async def test_invalid_trace_id(self, client: httpx.AsyncClient) -> None:
        """
        Invalid trace_id returns 400.
        """
        resp = await client.post(
            "/ai/adaptive_optimize",
            json={
                "trace_id": "bad",
                "original_source_code": "def f(): pass",
                "candidates": [],
            },
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_empty_candidates(self, client: httpx.AsyncClient) -> None:
        """
        Empty candidates list returns 400.
        """
        resp = await client.post(
            "/ai/adaptive_optimize",
            json={
                "trace_id": make_trace_id(),
                "original_source_code": "def f(): pass",
                "candidates": [],
            },
        )

        assert 400 == resp.status_code

    @pytest.mark.asyncio
    async def test_llm_failure(
        self,
        client: httpx.AsyncClient,
        mock_llm_client: MagicMock,
    ) -> None:
        """
        LLM call failure returns 422.
        """
        mock_llm_client.call = AsyncMock(  # type: ignore[method-assign]
            side_effect=RuntimeError("provider error"),
        )
        resp = await client.post(
            "/ai/adaptive_optimize",
            json={
                "trace_id": make_trace_id(),
                "original_source_code": "def f():\n    return 1\n",
                "candidates": [
                    {
                        "optimization_id": str(uuid.uuid4()),
                        "source_code": "def f():\n    return 2\n",
                        "explanation": "faster",
                        "source": "OPTIMIZE",
                        "speedup": "2x",
                    }
                ],
            },
        )

        assert 422 == resp.status_code


# ── Unknown route ──────────────────────────────────────────────


class TestUnknownRoute:
    """Integration test for unknown routes."""

    @pytest.mark.asyncio
    async def test_404(self, client: httpx.AsyncClient) -> None:
        """
        Unknown routes return 404.
        """
        resp = await client.get("/ai/nonexistent")

        assert 404 == resp.status_code
