"""Public commit-boundary behavior for canonical execution failures."""

from collections.abc import AsyncIterator
from typing import Any
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient

from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.anthropic.streaming import format_sse_event
from free_claude_code.core.failures import ExecutionFailure, FailureKind
from tests.api.support import create_test_app

_PARTIAL_CONTENT = "PARTIAL_ASSISTANT_CONTENT"


class CanonicalFailureProvider:
    """Provider double that raises one request-correlated canonical failure."""

    def __init__(
        self,
        chunks: list[str],
        *,
        kind: FailureKind,
        status_code: int,
        message: str,
        retryable: bool,
        grouped: bool = False,
    ) -> None:
        self._chunks = chunks
        self._kind = kind
        self._status_code = status_code
        self._message = message
        self._retryable = retryable
        self._grouped = grouped
        self.preflight_stream = MagicMock()
        self.stream_kwargs: list[dict[str, Any]] = []

    async def stream_response(
        self,
        _request: object,
        **kwargs: Any,
    ) -> AsyncIterator[str]:
        self.stream_kwargs.append(kwargs)
        for chunk in self._chunks:
            yield chunk
        request_id = kwargs["request_id"]
        failure = ExecutionFailure(
            kind=self._kind,
            status_code=self._status_code,
            message=f"{self._message}\n\nRequest ID: {request_id}",
            retryable=self._retryable,
        )
        if self._grouped:
            raise ExceptionGroup(
                "provider stream and cleanup failed",
                [
                    RuntimeError("cleanup failed"),
                    ExceptionGroup("provider request failed", [failure]),
                ],
            )
        raise failure


def _messages_payload(*, stream: bool) -> dict[str, object]:
    return {
        "model": "nvidia_nim/test-model",
        "messages": [{"role": "user", "content": "Hello"}],
        "max_tokens": 32,
        "stream": stream,
    }


def _responses_payload() -> dict[str, object]:
    return {
        "model": "nvidia_nim/test-model",
        "input": "Hello",
        "max_output_tokens": 32,
    }


def _partial_anthropic_stream(*, close_block: bool) -> list[str]:
    chunks = [
        format_sse_event("message_start", {"type": "message_start", "message": {}}),
        format_sse_event(
            "content_block_start",
            {
                "type": "content_block_start",
                "index": 0,
                "content_block": {"type": "text", "text": ""},
            },
        ),
        format_sse_event(
            "content_block_delta",
            {
                "type": "content_block_delta",
                "index": 0,
                "delta": {"type": "text_delta", "text": _PARTIAL_CONTENT},
            },
        ),
    ]
    if close_block:
        chunks.append(
            format_sse_event(
                "content_block_stop",
                {"type": "content_block_stop", "index": 0},
            )
        )
    return chunks


def _client_for(provider: CanonicalFailureProvider):
    app = create_test_app()
    return (
        patch("free_claude_code.api.routes.resolve_provider", return_value=provider),
        TestClient(app),
    )


def _terminal_trace(trace_mock: MagicMock) -> dict[str, Any]:
    return dict(
        next(
            call.kwargs
            for call in trace_mock.call_args_list
            if call.kwargs.get("event")
            == "free_claude_code.api.response.terminal_execution_error"
        )
    )


def _grouped_rate_limit_provider(chunks: list[str]) -> CanonicalFailureProvider:
    return CanonicalFailureProvider(
        chunks,
        kind=FailureKind.RATE_LIMIT,
        status_code=429,
        message="upstream is busy",
        retryable=True,
        grouped=True,
    )


@pytest.mark.parametrize(
    ("path", "payload", "expected_type"),
    [
        ("/v1/messages", _messages_payload(stream=True), "rate_limit_error"),
        ("/v1/responses", _responses_payload(), "rate_limit_error"),
    ],
)
def test_grouped_pre_start_execution_failure_keeps_canonical_wire_error(
    path: str,
    payload: dict[str, object],
    expected_type: str,
) -> None:
    provider = _grouped_rate_limit_provider([])
    resolver_patch, client = _client_for(provider)

    with (
        resolver_patch,
        patch("free_claude_code.api.response_streams.trace_event") as trace_mock,
        client,
    ):
        response = client.post(path, json=payload)

    request_id = response.headers["request-id"]
    assert response.status_code == 429
    assert response.headers["x-should-retry"] == "false"
    error = response.json()["error"]
    assert error["type"] == expected_type
    assert error["message"] == f"upstream is busy\n\nRequest ID: {request_id}"
    trace = _terminal_trace(trace_mock)
    assert trace["status_code"] == 429
    assert trace["error_type"] == "rate_limit_error"
    assert trace["exc_type"] == "ExecutionFailure"
    assert trace["failure_kind"] == "rate_limit"


@pytest.mark.parametrize("path", ["/v1/messages", "/v1/responses"])
def test_grouped_post_start_execution_failure_keeps_canonical_terminal_event(
    path: str,
) -> None:
    provider = _grouped_rate_limit_provider(_partial_anthropic_stream(close_block=True))
    payload = (
        _messages_payload(stream=True)
        if path == "/v1/messages"
        else _responses_payload()
    )
    resolver_patch, client = _client_for(provider)

    with (
        resolver_patch,
        patch("free_claude_code.api.response_streams.trace_event") as trace_mock,
        client,
    ):
        response = client.post(path, json=payload)

    request_id = response.headers["request-id"]
    events = parse_sse_text(response.text)
    if path == "/v1/messages":
        assert events[-1].event == "error"
        error = events[-1].data["error"]
    else:
        assert events[-1].event == "response.failed"
        error = events[-1].data["response"]["error"]
    assert response.status_code == 200
    assert error["type"] == "rate_limit_error"
    assert error["message"] == f"upstream is busy\n\nRequest ID: {request_id}"
    assert _terminal_trace(trace_mock)["failure_kind"] == "rate_limit"


def test_grouped_stream_false_execution_failure_discards_partial_content() -> None:
    provider = _grouped_rate_limit_provider(
        _partial_anthropic_stream(close_block=False)
    )
    resolver_patch, client = _client_for(provider)

    with (
        resolver_patch,
        patch("free_claude_code.api.response_streams.trace_event") as trace_mock,
        client,
    ):
        response = client.post("/v1/messages", json=_messages_payload(stream=False))

    request_id = response.headers["request-id"]
    assert response.status_code == 429
    assert response.headers["x-should-retry"] == "false"
    assert response.json()["error"] == {
        "type": "rate_limit_error",
        "message": f"upstream is busy\n\nRequest ID: {request_id}",
    }
    assert _PARTIAL_CONTENT not in response.text
    trace = _terminal_trace(trace_mock)
    assert trace["status_code"] == 429
    assert trace["error_type"] == "rate_limit_error"
    assert trace["exc_type"] == "ExecutionFailure"
    assert trace["failure_kind"] == "rate_limit"


def test_messages_pre_start_execution_failure_is_correlated_terminal_json() -> None:
    provider = CanonicalFailureProvider(
        [],
        kind=FailureKind.RATE_LIMIT,
        status_code=429,
        message="upstream is busy",
        retryable=True,
    )
    resolver_patch, client = _client_for(provider)

    with resolver_patch, client:
        response = client.post("/v1/messages", json=_messages_payload(stream=True))

    request_id = response.headers["request-id"]
    assert response.status_code == 429
    assert response.headers["content-type"].startswith("application/json")
    assert response.headers["x-should-retry"] == "false"
    assert "x-request-id" not in response.headers
    assert response.json() == {
        "type": "error",
        "error": {
            "type": "rate_limit_error",
            "message": f"upstream is busy\n\nRequest ID: {request_id}",
        },
        "request_id": request_id,
    }
    assert provider.stream_kwargs[0]["request_id"] == request_id


def test_responses_pre_start_execution_failure_is_correlated_terminal_json() -> None:
    provider = CanonicalFailureProvider(
        [],
        kind=FailureKind.OVERLOADED,
        status_code=529,
        message="provider overloaded",
        retryable=True,
    )
    resolver_patch, client = _client_for(provider)

    with resolver_patch, client:
        response = client.post("/v1/responses", json=_responses_payload())

    request_id = response.headers["request-id"]
    assert response.status_code == 529
    assert response.headers["content-type"].startswith("application/json")
    assert response.headers["x-should-retry"] == "false"
    assert response.headers["x-request-id"] == request_id
    assert response.json() == {
        "error": {
            "message": f"provider overloaded\n\nRequest ID: {request_id}",
            "type": "overloaded_error",
            "param": None,
            "code": None,
        }
    }
    assert provider.stream_kwargs[0]["request_id"] == request_id


def test_messages_post_start_execution_failure_follows_closed_block() -> None:
    provider = CanonicalFailureProvider(
        _partial_anthropic_stream(close_block=True),
        kind=FailureKind.OVERLOADED,
        status_code=529,
        message="provider overloaded",
        retryable=True,
    )
    resolver_patch, client = _client_for(provider)

    with (
        resolver_patch,
        patch("free_claude_code.api.response_streams.trace_event") as trace_mock,
        client,
    ):
        response = client.post("/v1/messages", json=_messages_payload(stream=True))

    request_id = response.headers["request-id"]
    events = parse_sse_text(response.text)
    assert response.status_code == 200
    assert "x-should-retry" not in response.headers
    assert [event.event for event in events] == [
        "message_start",
        "content_block_start",
        "content_block_delta",
        "content_block_stop",
        "error",
    ]
    assert events[-1].data["error"] == {
        "type": "overloaded_error",
        "message": f"provider overloaded\n\nRequest ID: {request_id}",
    }
    assert "message_stop" not in response.text
    assert _terminal_trace(trace_mock) == {
        "stage": "egress",
        "event": "free_claude_code.api.response.terminal_execution_error",
        "source": "api",
        "wire_api": "messages",
        "request_id": request_id,
        "status_code": 529,
        "error_type": "overloaded_error",
        "client_should_retry": False,
        "exc_type": "ExecutionFailure",
        "failure_kind": "overloaded",
        "provider_retryable": True,
    }


def test_responses_post_start_execution_failure_retains_id_after_block_close() -> None:
    provider = CanonicalFailureProvider(
        _partial_anthropic_stream(close_block=True),
        kind=FailureKind.RATE_LIMIT,
        status_code=429,
        message="upstream is busy",
        retryable=True,
    )
    resolver_patch, client = _client_for(provider)

    with (
        resolver_patch,
        patch("free_claude_code.api.response_streams.trace_event") as trace_mock,
        client,
    ):
        response = client.post("/v1/responses", json=_responses_payload())

    request_id = response.headers["request-id"]
    events = parse_sse_text(response.text)
    event_names = [event.event for event in events]
    created = events[0].data["response"]
    failed = events[-1].data["response"]
    assert response.status_code == 200
    assert response.headers["x-request-id"] == request_id
    assert "x-should-retry" not in response.headers
    assert event_names[0] == "response.created"
    assert "response.output_item.done" in event_names
    assert event_names.index("response.output_item.done") < event_names.index(
        "response.failed"
    )
    assert event_names[-1] == "response.failed"
    assert failed["id"] == created["id"]
    assert failed["status"] == "failed"
    assert failed["error"] == {
        "message": f"upstream is busy\n\nRequest ID: {request_id}",
        "type": "rate_limit_error",
        "param": None,
        "code": None,
    }
    assert _terminal_trace(trace_mock) == {
        "stage": "egress",
        "event": "free_claude_code.api.response.terminal_execution_error",
        "source": "api",
        "wire_api": "responses",
        "request_id": request_id,
        "status_code": 429,
        "error_type": "rate_limit_error",
        "client_should_retry": False,
        "exc_type": "ExecutionFailure",
        "failure_kind": "rate_limit",
        "provider_retryable": True,
    }


def test_messages_stream_false_discards_partial_content_on_execution_failure() -> None:
    provider = CanonicalFailureProvider(
        _partial_anthropic_stream(close_block=False),
        kind=FailureKind.RATE_LIMIT,
        status_code=429,
        message="upstream is busy",
        retryable=True,
    )
    resolver_patch, client = _client_for(provider)

    with resolver_patch, client:
        response = client.post("/v1/messages", json=_messages_payload(stream=False))

    request_id = response.headers["request-id"]
    assert response.status_code == 429
    assert response.headers["content-type"].startswith("application/json")
    assert response.headers["x-should-retry"] == "false"
    assert response.json()["request_id"] == request_id
    assert response.json()["error"] == {
        "type": "rate_limit_error",
        "message": f"upstream is busy\n\nRequest ID: {request_id}",
    }
    assert _PARTIAL_CONTENT not in response.text
