"""Tests for the Anthropic wrapper."""

from unittest.mock import MagicMock, patch

from llm_observatory.client import Anthropic


@patch("llm_observatory.client._anthropic", create=True)
def test_anthropic_wraps_client(mock_mod):
    """Anthropic should instantiate anthropic.Anthropic under the hood."""
    with patch("llm_observatory.client.__import__", create=True):
        with patch.dict("sys.modules", {"anthropic": MagicMock()}):
            import sys

            mock_anthropic_mod = sys.modules["anthropic"]
            mock_instance = MagicMock()
            mock_anthropic_mod.Anthropic.return_value = mock_instance

            client = Anthropic(api_key="sk-ant-test")

            mock_anthropic_mod.Anthropic.assert_called_once_with(api_key="sk-ant-test")
            assert client._client is mock_instance


def _make_anthropic_client():
    """Helper: create Anthropic wrapper with mocked underlying client."""
    with patch.dict("sys.modules", {"anthropic": MagicMock()}):
        import sys

        mock_mod = sys.modules["anthropic"]
        mock_instance = MagicMock()
        mock_mod.Anthropic.return_value = mock_instance
        client = Anthropic(api_key="sk-ant-test")
        return client, mock_instance


def test_anthropic_has_messages_create():
    """Anthropic should expose messages.create."""
    client, _ = _make_anthropic_client()
    assert hasattr(client, "messages")
    assert callable(client.messages.create)


def test_anthropic_set_metadata():
    """set_metadata should persist metadata on the client."""
    client, _ = _make_anthropic_client()
    client.set_metadata(user_id="u1", tags=["prod"])
    assert client._metadata == {"user_id": "u1", "tags": ["prod"]}


def test_anthropic_getattr_proxies():
    """Attributes not on the wrapper should proxy to the underlying client."""
    client, mock_instance = _make_anthropic_client()
    mock_instance.count_tokens = "token-counter"
    assert client.count_tokens == "token-counter"


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_create_captures_event(mock_get_config, mock_get_transport):
    """messages.create should capture an event via transport."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    # Build a mock Anthropic response
    mock_text_block = MagicMock()
    mock_text_block.type = "text"
    mock_text_block.text = "Hello from Claude!"

    mock_response = MagicMock()
    mock_response.id = "msg_abc123"
    mock_response.model = "claude-sonnet-4-20250514"
    mock_response.content = [mock_text_block]
    mock_response.stop_reason = "end_turn"
    mock_response.usage = MagicMock(input_tokens=25, output_tokens=10)

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.return_value = mock_response

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    result = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        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"] == "claude-sonnet-4-20250514"
    assert event["response"]["id"] == "msg_abc123"
    assert event["response"]["choices"][0]["message"]["content"] == "Hello from Claude!"
    assert event["usage"]["prompt_tokens"] == 25
    assert event["usage"]["completion_tokens"] == 10
    assert event["usage"]["total_tokens"] == 35


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

    mock_response = MagicMock()
    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.return_value = mock_response

    result = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hi"}],
    )

    assert result is mock_response


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_error_captures_event(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}

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.side_effect = Exception("API rate limit exceeded")

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    try:
        client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=[{"role": "user", "content": "Hi"}],
        )
    except Exception:
        pass

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


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_rate_limit_error_message_captured(mock_get_config, mock_get_transport):
    """On a rate-limit-style error, the message should be recorded in the event."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.side_effect = Exception("Rate limit reached for model claude-sonnet-4-20250514")

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    try:
        client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=[{"role": "user", "content": "Hi"}],
        )
    except Exception:
        pass

    mock_transport.enqueue.assert_called_once()
    event = mock_transport.enqueue.call_args[0][0]
    assert "rate limit" in event["error"]["message"].lower()


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_auth_error_captures_event(mock_get_config, mock_get_transport):
    """On an authentication error, the event should be captured with error info."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.side_effect = Exception("Invalid API key: authentication failed")

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    try:
        client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=[{"role": "user", "content": "Hi"}],
        )
    except Exception:
        pass

    mock_transport.enqueue.assert_called_once()
    event = mock_transport.enqueue.call_args[0][0]
    assert event["error"]["type"] == "Exception"
    assert "authentication" in event["error"]["message"].lower()


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_error_re_raises(mock_get_config, mock_get_transport):
    """The original exception must propagate to the caller after event capture."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.side_effect = Exception("connection timeout")

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    raised = None
    try:
        client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=[{"role": "user", "content": "Hi"}],
        )
    except Exception as exc:
        raised = exc

    assert raised is not None, "Exception should have been re-raised"
    assert "connection timeout" in str(raised)
    mock_transport.enqueue.assert_called_once()


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_system_prompt_normalized(mock_get_config, mock_get_transport):
    """System prompt (Anthropic top-level param) should be normalized into messages."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    mock_text_block = MagicMock()
    mock_text_block.type = "text"
    mock_text_block.text = "Response"

    mock_response = MagicMock()
    mock_response.id = "msg_xyz"
    mock_response.model = "claude-sonnet-4-20250514"
    mock_response.content = [mock_text_block]
    mock_response.stop_reason = "end_turn"
    mock_response.usage = MagicMock(input_tokens=30, output_tokens=5)

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.return_value = mock_response

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        system="You are a helpful assistant.",
        messages=[{"role": "user", "content": "Hi"}],
    )

    event = mock_transport.enqueue.call_args[0][0]
    # System prompt should appear as first message
    assert event["request"]["messages"][0] == {"role": "system", "content": "You are a helpful assistant."}
    assert event["request"]["messages"][1] == {"role": "user", "content": "Hi"}


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_redact_messages(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_text_block = MagicMock()
    mock_text_block.type = "text"
    mock_text_block.text = "Response"

    mock_response = MagicMock()
    mock_response.id = "msg_red"
    mock_response.model = "claude-sonnet-4-20250514"
    mock_response.content = [mock_text_block]
    mock_response.stop_reason = "end_turn"
    mock_response.usage = MagicMock(input_tokens=10, output_tokens=5)

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.return_value = mock_response

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        system="Secret system prompt",
        messages=[{"role": "user", "content": "Secret message"}],
    )

    event = mock_transport.enqueue.call_args[0][0]
    # System prompt should be redacted
    assert event["request"]["messages"][0]["content"] == "[REDACTED]"
    # User message should be redacted
    assert event["request"]["messages"][1]["content"] == "[REDACTED]"


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


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_per_request_metadata(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_text_block = MagicMock()
    mock_text_block.type = "text"
    mock_text_block.text = "Hello"

    mock_response = MagicMock()
    mock_response.id = "msg_meta"
    mock_response.model = "claude-sonnet-4-20250514"
    mock_response.content = [mock_text_block]
    mock_response.stop_reason = "end_turn"
    mock_response.usage = MagicMock(input_tokens=10, output_tokens=5)

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.return_value = mock_response

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client.set_metadata(user_id="user-global", session_id="sess-global")
    client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        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")
def test_anthropic_per_request_metadata_not_forwarded(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_text_block = MagicMock()
    mock_text_block.type = "text"
    mock_text_block.text = "Hello"

    mock_response = MagicMock()
    mock_response.id = "msg_fwd"
    mock_response.model = "claude-sonnet-4-20250514"
    mock_response.content = [mock_text_block]
    mock_response.stop_reason = "end_turn"
    mock_response.usage = MagicMock(input_tokens=10, output_tokens=5)

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.return_value = mock_response

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hi"}],
        metadata={"user_id": "u1"},
    )

    call_kwargs = mock_instance.messages.create.call_args[1]
    assert "metadata" not in call_kwargs


# ── Anthropic streaming tests ────────────────────────────────────


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_streaming_returns_wrapper(mock_get_config, mock_get_transport):
    """When stream=True, messages.create should return an AnthropicStreamWrapper."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    from llm_observatory.streaming import AnthropicStreamWrapper

    mock_stream = MagicMock()
    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.return_value = mock_stream

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    result = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hi"}],
        stream=True,
    )

    assert isinstance(result, AnthropicStreamWrapper)


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


@patch("llm_observatory.client.get_transport")
@patch("llm_observatory.client.get_config")
def test_anthropic_error_sanitizes_api_key(mock_get_config, mock_get_transport):
    """Error messages containing API keys should be sanitized."""
    mock_get_config.return_value = {"enabled": True, "redact_messages": False}

    client, mock_instance = _make_anthropic_client()
    mock_instance.messages.create.side_effect = Exception(
        "Failed with Bearer sk-ant-api03-verylongkeyhere123456 in request"
    )

    mock_transport = MagicMock()
    mock_get_transport.return_value = mock_transport

    try:
        client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=[{"role": "user", "content": "Hi"}],
        )
    except Exception:
        pass

    event = mock_transport.enqueue.call_args[0][0]
    assert "sk-ant-api03-verylongkeyhere123456" not in event["error"]["message"]
