"""Tests for the OpenAI wrapper — the primary SDK client."""

from unittest.mock import MagicMock, patch

import openai as _openai

from llm_observatory.client import OpenAI, _BaseClient, _sanitize_error_message


def test_openai_client_inherits_base():
    """OpenAI should be a subclass of _BaseClient."""
    assert issubclass(OpenAI, _BaseClient)


@patch("llm_observatory.client.openai.OpenAI")
def test_openai_client_wraps_openai(mock_openai_cls):
    """OpenAI should instantiate openai.OpenAI under the hood."""
    mock_instance = MagicMock()
    mock_openai_cls.return_value = mock_instance

    client = OpenAI(api_key="sk-test-key")

    mock_openai_cls.assert_called_once_with(api_key="sk-test-key")
    assert client._client is mock_instance


@patch("llm_observatory.client.openai.OpenAI")
def test_openai_client_has_chat_completions(mock_openai_cls):
    """OpenAI should expose chat.completions.create."""
    mock_openai_cls.return_value = MagicMock()
    client = OpenAI(api_key="sk-test")

    assert hasattr(client, "chat")
    assert hasattr(client.chat, "completions")
    assert callable(client.chat.completions.create)


@patch("llm_observatory.client.openai.OpenAI")
def test_openai_set_metadata(mock_openai_cls):
    """set_metadata should persist metadata on the client."""
    mock_openai_cls.return_value = MagicMock()
    client = OpenAI(api_key="sk-test")

    client.set_metadata(user_id="u1", session_id="s1", tags=["prod", "search"])
    assert client._metadata == {"user_id": "u1", "session_id": "s1", "tags": ["prod", "search"]}


@patch("llm_observatory.client.openai.OpenAI")
def test_openai_getattr_proxies(mock_openai_cls):
    """Attributes not on the wrapper should proxy to the underlying client."""
    mock_instance = MagicMock()
    mock_instance.models = "models-namespace"
    mock_instance.embeddings = "embeddings-namespace"
    mock_openai_cls.return_value = mock_instance

    client = OpenAI(api_key="sk-test")
    assert client.models == "models-namespace"
    assert client.embeddings == "embeddings-namespace"


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_create_captures_event(mock_openai_cls, mock_get_config, mock_get_transport):
    """chat.completions.create should capture an event via transport."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    mock_response = MagicMock()
    mock_response.id = "chatcmpl-abc123"
    mock_response.model = "gpt-4o-2025-01-01"
    mock_response.choices = [
        MagicMock(index=0, finish_reason="stop", message=MagicMock(role="assistant", content="Hello!", tool_calls=None))
    ]
    mock_response.usage = MagicMock(prompt_tokens=100, completion_tokens=50, total_tokens=150)

    mock_underlying = MagicMock()
    mock_underlying.chat.completions.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    result = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hi"}],
    )

    assert result is mock_response
    mock_transport.enqueue.assert_called_once()

    event = mock_transport.enqueue.call_args[0][0]
    assert event["request"]["model"] == "gpt-4o"
    assert event["response"]["id"] == "chatcmpl-abc123"
    assert event["response"]["model"] == "gpt-4o-2025-01-01"
    assert event["usage"]["prompt_tokens"] == 100
    assert event["usage"]["completion_tokens"] == 50
    assert event["usage"]["total_tokens"] == 150
    assert event["metrics"]["latency_ms"] >= 0


@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_disabled_passthrough(mock_openai_cls, mock_get_config):
    """When disabled, create should pass through without capturing."""
    mock_get_config.return_value = {"enabled": False}

    mock_response = MagicMock()
    mock_underlying = MagicMock()
    mock_underlying.chat.completions.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    client = OpenAI(api_key="sk-test")
    result = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hi"}],
    )

    assert result is mock_response


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_error_captures_event(mock_openai_cls, mock_get_config, mock_get_transport):
    """On API error, the event should still be captured with error info."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    mock_underlying = MagicMock()
    mock_request = MagicMock()
    api_error = _openai.APIError("Rate limit exceeded", request=mock_request, body=None)
    mock_underlying.chat.completions.create.side_effect = api_error
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    try:
        client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
        )
    except _openai.APIError:
        pass

    mock_transport.enqueue.assert_called_once()
    event = mock_transport.enqueue.call_args[0][0]
    assert event["error"]["type"] == "APIError"
    assert "Rate limit" in event["error"]["message"]


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_redact_messages(mock_openai_cls, mock_get_config, mock_get_transport):
    """When redact_messages is True, message content should be redacted."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": True}

    mock_response = MagicMock()
    mock_response.id = "chatcmpl-xyz"
    mock_response.model = "gpt-4o"
    mock_response.choices = [
        MagicMock(index=0, finish_reason="stop", message=MagicMock(role="assistant", content="Secret response", tool_calls=None))
    ]
    mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=5, total_tokens=15)

    mock_underlying = MagicMock()
    mock_underlying.chat.completions.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are helpful."},
            {"role": "user", "content": "My secret data"},
        ],
    )

    event = mock_transport.enqueue.call_args[0][0]
    for msg in event["request"]["messages"]:
        assert msg["content"] == "[REDACTED]"


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_metadata_attached_to_event(mock_openai_cls, mock_get_config, mock_get_transport):
    """set_metadata values should be included in captured events."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    mock_response = MagicMock()
    mock_response.id = "chatcmpl-meta"
    mock_response.model = "gpt-4o"
    mock_response.choices = [
        MagicMock(index=0, finish_reason="stop", message=MagicMock(role="assistant", content="Ok", tool_calls=None))
    ]
    mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=5, total_tokens=15)

    mock_underlying = MagicMock()
    mock_underlying.chat.completions.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    client.set_metadata(user_id="user-42", session_id="sess-7", tags=["production"])
    client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hi"}],
    )

    event = mock_transport.enqueue.call_args[0][0]
    assert event["metadata"]["user_id"] == "user-42"
    assert event["metadata"]["session_id"] == "sess-7"
    assert event["metadata"]["tags"] == ["production"]


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_flush_calls_transport(mock_openai_cls, mock_get_config, mock_get_transport):
    """client.flush() should delegate to the transport."""
    mock_openai_cls.return_value = MagicMock()

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    client.flush()

    mock_transport.flush.assert_called_once()


# ── Error sanitization tests ─────────────────────────────────────


def test_sanitize_error_strips_api_key_in_url():
    msg = "Error calling https://api.openai.com/v1/chat?api_key=sk-abc123secret remaining text"
    sanitized = _sanitize_error_message(msg)
    assert "sk-abc123secret" not in sanitized
    assert "[REDACTED]" in sanitized


def test_sanitize_error_strips_bearer_token():
    msg = "Request failed with Bearer sk-ant-api03-longtoken123 in header"
    sanitized = _sanitize_error_message(msg)
    assert "sk-ant-api03-longtoken123" not in sanitized
    assert "[REDACTED]" in sanitized


def test_sanitize_error_strips_sk_key():
    msg = "Invalid key: sk-abcdefghijklmnopqrstuvwxyz"
    sanitized = _sanitize_error_message(msg)
    assert "sk-abcdefghijklmnopqrstuvwxyz" not in sanitized


def test_sanitize_error_truncates_long_messages():
    msg = "A" * 1000
    sanitized = _sanitize_error_message(msg)
    assert len(sanitized) <= 504  # 500 + "..."
    assert sanitized.endswith("...")


def test_sanitize_error_preserves_short_messages():
    msg = "Rate limit exceeded for model gpt-4o"
    sanitized = _sanitize_error_message(msg)
    assert sanitized == msg


# ── Per-request metadata tests ───────────────────────────────────


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_per_request_metadata(mock_openai_cls, mock_get_config, mock_get_transport):
    """Per-request metadata should override client-level metadata."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    mock_response = MagicMock()
    mock_response.id = "chatcmpl-meta2"
    mock_response.model = "gpt-4o"
    mock_response.choices = [
        MagicMock(index=0, finish_reason="stop", message=MagicMock(role="assistant", content="Ok", tool_calls=None))
    ]
    mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=5, total_tokens=15)

    mock_underlying = MagicMock()
    mock_underlying.chat.completions.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    client.set_metadata(user_id="user-global", session_id="sess-global")

    # Per-request metadata should override user_id but keep session_id
    client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hi"}],
        metadata={"user_id": "user-request"},
    )

    event = mock_transport.enqueue.call_args[0][0]
    assert event["metadata"]["user_id"] == "user-request"
    assert event["metadata"]["session_id"] == "sess-global"


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_per_request_metadata_does_not_mutate_client(mock_openai_cls, mock_get_config, mock_get_transport):
    """Per-request metadata should not mutate the client's metadata."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    mock_response = MagicMock()
    mock_response.id = "chatcmpl-meta3"
    mock_response.model = "gpt-4o"
    mock_response.choices = [
        MagicMock(index=0, finish_reason="stop", message=MagicMock(role="assistant", content="Ok", tool_calls=None))
    ]
    mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=5, total_tokens=15)

    mock_underlying = MagicMock()
    mock_underlying.chat.completions.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    client.set_metadata(user_id="user-global")

    client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hi"}],
        metadata={"user_id": "user-override", "tags": ["temp"]},
    )

    # Client metadata should be unchanged
    assert client._metadata == {"user_id": "user-global"}
    assert "tags" not in client._metadata


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_metadata_kwarg_not_forwarded(mock_openai_cls, mock_get_config, mock_get_transport):
    """The metadata kwarg should be popped and not forwarded to the underlying client."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    mock_response = MagicMock()
    mock_response.id = "chatcmpl-fwd"
    mock_response.model = "gpt-4o"
    mock_response.choices = [
        MagicMock(index=0, finish_reason="stop", message=MagicMock(role="assistant", content="Ok", tool_calls=None))
    ]
    mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=5, total_tokens=15)

    mock_underlying = MagicMock()
    mock_underlying.chat.completions.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hi"}],
        metadata={"user_id": "u1"},
    )

    # metadata should NOT appear in the call to the underlying client
    call_kwargs = mock_underlying.chat.completions.create.call_args[1]
    assert "metadata" not in call_kwargs


# ── Responses API tests ──────────────────────────────────────────


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_responses_create(mock_openai_cls, mock_get_config, mock_get_transport):
    """client.responses.create() should capture an event."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    # Build mock responses API response
    mock_text_block = MagicMock()
    mock_text_block.type = "output_text"
    mock_text_block.text = "Hello from responses!"

    mock_msg_item = MagicMock()
    mock_msg_item.type = "message"
    mock_msg_item.content = [mock_text_block]

    mock_usage = MagicMock()
    mock_usage.input_tokens = 20
    mock_usage.output_tokens = 10

    mock_response = MagicMock()
    mock_response.id = "resp_abc123"
    mock_response.model = "gpt-4o"
    mock_response.output = [mock_msg_item]
    mock_response.usage = mock_usage

    mock_underlying = MagicMock()
    mock_underlying.responses.create.return_value = mock_response
    mock_openai_cls.return_value = mock_underlying

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client = OpenAI(api_key="sk-test")
    result = client.responses.create(
        model="gpt-4o",
        input="Hello",
    )

    assert result is mock_response
    mock_transport.enqueue.assert_called_once()

    event = mock_transport.enqueue.call_args[0][0]
    assert event["request"]["model"] == "gpt-4o"
    assert event["usage"]["prompt_tokens"] == 20
    assert event["usage"]["completion_tokens"] == 10


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
@patch("llm_observatory.client.openai.OpenAI")
def test_openai_has_responses_namespace(mock_openai_cls, mock_get_config, mock_get_transport):
    """OpenAI client should have a responses namespace."""
    mock_openai_cls.return_value = MagicMock()
    client = OpenAI(api_key="sk-test")
    assert hasattr(client, "responses")
    assert callable(client.responses.create)
