"""Tests for OpenAI SDK patching and record generation."""

import json
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import openai

from caliper._features import features
from caliper._openai import (
    _build_record_from_error,
    _build_record_from_response,
    _classify_error,
    _extract_tool_calls,
    _extract_tools_provided,
    _process_chunk,
)
from caliper._patch_helpers import set_capture_tools

# ---------------------------------------------------------------------------
# Fake response/chunk objects
# ---------------------------------------------------------------------------


def _make_usage(prompt_tokens=100, completion_tokens=50, cached_tokens=0):
    details = SimpleNamespace(cached_tokens=cached_tokens) if cached_tokens else None
    return SimpleNamespace(
        prompt_tokens=prompt_tokens,
        completion_tokens=completion_tokens,
        prompt_tokens_details=details,
    )


def _make_response(model="gpt-4o", prompt_tokens=100, completion_tokens=50, cached_tokens=0):
    return SimpleNamespace(
        id="chatcmpl-abc",
        model=model,
        usage=_make_usage(prompt_tokens, completion_tokens, cached_tokens),
        choices=[
            SimpleNamespace(
                message=SimpleNamespace(
                    role="assistant",
                    content="Hello!",
                    tool_calls=None,
                ),
                finish_reason="stop",
            )
        ],
        _request_id="req_xyz123",
    )


def _make_chunk(content=None, model=None, finish_reason=None, tool_calls=None, usage=None):
    delta = SimpleNamespace(content=content, tool_calls=tool_calls)
    has_choice = content is not None or finish_reason or tool_calls
    choices = [SimpleNamespace(delta=delta, finish_reason=finish_reason)] if has_choice else []
    ns = SimpleNamespace(
        choices=choices,
        model=model,
        usage=usage,
        _request_id="req_stream_123",
    )
    return ns


# ---------------------------------------------------------------------------
# _classify_error
# ---------------------------------------------------------------------------


def test_classify_rate_limit():
    exc = openai.RateLimitError(
        message="rate limited",
        response=MagicMock(status_code=429),
        body=None,
    )
    status, http_status, error_code = _classify_error(exc)
    assert status == "rate_limited"
    assert http_status == 429


def test_classify_timeout():
    exc = openai.APITimeoutError(request=MagicMock())
    status, http_status, _ = _classify_error(exc)
    assert status == "timeout"
    assert http_status is None


def test_classify_connection_error():
    exc = openai.APIConnectionError(request=MagicMock())
    status, http_status, _ = _classify_error(exc)
    assert status == "timeout"
    assert http_status is None


def test_classify_api_status_error():
    exc = openai.APIStatusError(
        message="server error",
        response=MagicMock(status_code=500),
        body=None,
    )
    status, http_status, _ = _classify_error(exc)
    assert status == "error"
    assert http_status == 500


def test_classify_generic_error():
    status, http_status, _ = _classify_error(RuntimeError("boom"))
    assert status == "error"
    assert http_status is None


# ---------------------------------------------------------------------------
# _build_record_from_response
# ---------------------------------------------------------------------------


def test_build_record_from_response():
    response = _make_response(prompt_tokens=100, completion_tokens=50)
    record = _build_record_from_response(
        response, {"model": "gpt-4o"}, duration_ms=150, request_meta={}, request_id="test-uuid"
    )

    assert record.provider == "openai"
    assert record.model == "gpt-4o"
    assert record.endpoint == "chat"
    assert record.tokens_input == 100
    assert record.tokens_output == 50
    assert record.tokens_total == 150
    assert record.status == "success"
    assert record.http_status == 200
    assert record.provider_request_id == "req_xyz123"
    assert record.cached is False


def test_build_record_from_response_cached():
    response = _make_response(cached_tokens=50)
    record = _build_record_from_response(response, {}, duration_ms=100, request_meta={}, request_id="test-uuid")
    assert record.cached is True


def test_build_record_from_response_request_id():
    response = _make_response()
    record = _build_record_from_response(response, {}, duration_ms=100, request_meta={}, request_id="my-req-id")
    assert record.request_id == "my-req-id"
    assert record.provider_request_id == "req_xyz123"


# ---------------------------------------------------------------------------
# _build_record_from_error
# ---------------------------------------------------------------------------


def test_build_record_from_error():
    exc = openai.RateLimitError(
        message="rate limited",
        response=MagicMock(status_code=429),
        body=None,
    )
    record = _build_record_from_error(exc, {"model": "gpt-4o"}, duration_ms=50, request_meta={}, request_id="test-uuid")

    assert record.status == "rate_limited"
    assert record.http_status == 429
    assert record.tokens_input == 0
    assert record.tokens_output == 0
    assert record.model == "gpt-4o"


# ---------------------------------------------------------------------------
# _process_chunk (streaming)
# ---------------------------------------------------------------------------


def test_process_chunk_text_content():
    state = {}
    chunk = _make_chunk(content="Hello")
    result = _process_chunk(chunk, state)

    assert result == "Hello"
    assert "first_token_time" in state


def test_process_chunk_model_extraction():
    state = {}
    chunk = _make_chunk(content="Hi", model="gpt-4o")
    _process_chunk(chunk, state)

    assert state["model"] == "gpt-4o"


def test_process_chunk_usage_from_final():
    state = {}
    usage = SimpleNamespace(prompt_tokens=80, completion_tokens=40)
    # Final chunk: no choices, just usage
    chunk = SimpleNamespace(choices=[], model=None, usage=usage, _request_id=None)
    _process_chunk(chunk, state)

    assert state["input_tokens"] == 80
    assert state["output_tokens"] == 40


def test_process_chunk_finish_reason():
    state = {}
    chunk = _make_chunk(content=None, finish_reason="stop")
    # Need at least one choice for finish_reason to be processed
    chunk.choices = [SimpleNamespace(delta=SimpleNamespace(content=None, tool_calls=None), finish_reason="stop")]
    _process_chunk(chunk, state)

    assert state["finish_reason"] == "stop"


# ---------------------------------------------------------------------------
# _extract_tools_provided
# ---------------------------------------------------------------------------


def test_extract_tools_provided_with_tools():
    kwargs = {
        "tools": [
            {"type": "function", "function": {"name": "get_weather", "description": "..."}},
            {"type": "function", "function": {"name": "search", "description": "..."}},
        ]
    }
    result = _extract_tools_provided(kwargs)
    assert result == ["get_weather", "search"]


def test_extract_tools_provided_no_tools():
    assert _extract_tools_provided({}) is None
    assert _extract_tools_provided({"tools": []}) is None
    assert _extract_tools_provided({"tools": None}) is None


# ---------------------------------------------------------------------------
# _extract_tool_calls
# ---------------------------------------------------------------------------


def test_extract_tool_calls_with_tool_calls():
    response = SimpleNamespace(
        choices=[
            SimpleNamespace(
                message=SimpleNamespace(
                    tool_calls=[
                        SimpleNamespace(
                            id="call_1",
                            function=SimpleNamespace(name="get_weather", arguments='{"city": "NYC"}'),
                        ),
                    ]
                ),
            )
        ]
    )
    result = _extract_tool_calls(response)
    assert result is not None
    parsed = json.loads(result)
    assert len(parsed) == 1
    assert parsed[0]["name"] == "get_weather"
    assert parsed[0]["id"] == "call_1"
    assert parsed[0]["arguments"] == '{"city": "NYC"}'


def test_extract_tool_calls_no_tool_calls():
    response = SimpleNamespace(
        choices=[
            SimpleNamespace(
                message=SimpleNamespace(tool_calls=None),
            )
        ]
    )
    assert _extract_tool_calls(response) is None


def test_extract_tool_calls_no_choices():
    response = SimpleNamespace(choices=None)
    assert _extract_tool_calls(response) is None


# ---------------------------------------------------------------------------
# Metadata and linking
# ---------------------------------------------------------------------------


def test_metadata_attached_to_record():
    response = _make_response()
    with features(feature="chat", user_id="42"):
        record = _build_record_from_response(
            response, {}, duration_ms=100, request_meta={"campaign": "q4"}, request_id="test-uuid"
        )
    assert record.metadata == {"feature": "chat", "user_id": "42", "campaign": "q4"}


def test_linked_request_id_from_features_context():
    response = _make_response()
    with features(previous_request="prev-req-id"):
        record = _build_record_from_response(response, {}, duration_ms=100, request_meta={}, request_id="test-uuid")
    assert record.linked_request_id == "prev-req-id"


def test_linked_request_id_none_without_features():
    response = _make_response()
    record = _build_record_from_response(response, {}, duration_ms=100, request_meta={}, request_id="test-uuid")
    assert record.linked_request_id is None


def test_tracking_failure_does_not_crash():
    """If push_record fails, the user's call should still succeed."""
    response = _make_response()

    with patch("caliper._patch_helpers.push_record", side_effect=RuntimeError("buffer broken")):
        record = _build_record_from_response(response, {}, duration_ms=100, request_meta={}, request_id="test-uuid")
        assert record is not None


# ---------------------------------------------------------------------------
# capture_tools integration with _build_record_from_response
# ---------------------------------------------------------------------------


def test_build_record_capture_tools_enabled():
    set_capture_tools(True)
    try:
        response = SimpleNamespace(
            model="gpt-4o",
            usage=_make_usage(),
            _request_id="req_1",
            choices=[
                SimpleNamespace(
                    message=SimpleNamespace(
                        tool_calls=[
                            SimpleNamespace(
                                id="call_1",
                                function=SimpleNamespace(name="get_weather", arguments='{"city": "NYC"}'),
                            ),
                        ]
                    ),
                    finish_reason="tool_calls",
                )
            ],
        )
        kwargs = {
            "model": "gpt-4o",
            "tools": [{"type": "function", "function": {"name": "get_weather", "description": "..."}}],
        }
        record = _build_record_from_response(response, kwargs, duration_ms=100, request_meta={}, request_id="test-uuid")
        assert record.tools_provided == ["get_weather"]
        assert record.tool_calls is not None
    finally:
        set_capture_tools(False)


def test_build_record_capture_tools_disabled():
    set_capture_tools(False)
    response = SimpleNamespace(
        model="gpt-4o",
        usage=_make_usage(),
        _request_id="req_1",
        choices=[
            SimpleNamespace(
                message=SimpleNamespace(
                    tool_calls=[
                        SimpleNamespace(
                            id="call_1",
                            function=SimpleNamespace(name="get_weather", arguments='{"city": "NYC"}'),
                        ),
                    ]
                ),
                finish_reason="tool_calls",
            )
        ],
    )
    kwargs = {
        "model": "gpt-4o",
        "tools": [{"type": "function", "function": {"name": "get_weather", "description": "..."}}],
    }
    record = _build_record_from_response(response, kwargs, duration_ms=100, request_meta={}, request_id="test-uuid")
    assert record.tools_provided is None
    assert record.tool_calls is None


# ---------------------------------------------------------------------------
# Streaming tool accumulation
# ---------------------------------------------------------------------------


def test_streaming_tool_call_accumulation():
    """Tool call chunks streamed by index should accumulate correctly."""
    set_capture_tools(True)
    try:
        state = {}

        # First chunk: tool call start (index 0)
        tc1 = SimpleNamespace(
            index=0,
            id="call_1",
            function=SimpleNamespace(name="get_weather", arguments='{"ci'),
        )
        chunk1 = _make_chunk(content=None)
        chunk1.choices = [SimpleNamespace(delta=SimpleNamespace(content=None, tool_calls=[tc1]), finish_reason=None)]
        _process_chunk(chunk1, state)

        # Second chunk: continuation (index 0)
        tc2 = SimpleNamespace(
            index=0,
            id=None,
            function=SimpleNamespace(name="", arguments='ty": "NYC"}'),
        )
        chunk2 = _make_chunk(content=None)
        chunk2.choices = [SimpleNamespace(delta=SimpleNamespace(content=None, tool_calls=[tc2]), finish_reason=None)]
        _process_chunk(chunk2, state)

        assert 0 in state["_tool_calls"]
        entry = state["_tool_calls"][0]
        assert entry["name"] == "get_weather"
        assert "".join(entry["_arg_parts"]) == '{"city": "NYC"}'
    finally:
        set_capture_tools(False)
