"""Tests for Anthropic adapter conversion functions."""

import base64
from pathlib import Path

import pytest

from dotpromptz.adapters.anthropic import (
    _ANTHROPIC_DEFAULT_MAX_TOKENS,
    AnthropicAdapter,
    to_anthropic_messages,
    to_anthropic_request,
)
from dotpromptz.credentials import Credential
from dotpromptz.typing import (
    MediaContent,
    MediaPart,
    Message,
    PromptOutputConfig,
    RenderedPrompt,
    Role,
    TextPart,
)


class TestSystemExtraction:
    """Test that system messages are extracted to top-level param."""

    def test_system_extracted(self) -> None:
        system_text, msgs = to_anthropic_messages(
            [
                Message(role=Role.SYSTEM, content=[TextPart(text='You are helpful.')]),
                Message(role=Role.USER, content=[TextPart(text='hi')]),
            ]
        )
        assert system_text == 'You are helpful.'
        assert len(msgs) == 1
        assert msgs[0]['role'] == 'user'

    def test_no_system(self) -> None:
        system_text, msgs = to_anthropic_messages(
            [
                Message(role=Role.USER, content=[TextPart(text='hi')]),
            ]
        )
        assert system_text is None
        assert len(msgs) == 1

    def test_multiple_system(self) -> None:
        system_text, _ = to_anthropic_messages(
            [
                Message(role=Role.SYSTEM, content=[TextPart(text='Rule 1.')]),
                Message(role=Role.SYSTEM, content=[TextPart(text='Rule 2.')]),
                Message(role=Role.USER, content=[TextPart(text='hi')]),
            ]
        )
        assert system_text == 'Rule 1.\nRule 2.'


class TestRoleMapping:
    """Test role conversion for Anthropic."""

    def test_model_to_assistant(self) -> None:
        _, msgs = to_anthropic_messages(
            [
                Message(role=Role.MODEL, content=[TextPart(text='hello')]),
            ]
        )
        assert msgs[0]['role'] == 'assistant'


class TestContentBlocks:
    """Test Part → Anthropic content block conversion."""

    def test_single_text_simplified(self) -> None:
        _, msgs = to_anthropic_messages(
            [
                Message(role=Role.USER, content=[TextPart(text='hello')]),
            ]
        )
        # Single text block simplifies to string.
        assert msgs[0]['content'] == 'hello'

    def test_multi_text_keeps_blocks(self) -> None:
        _, msgs = to_anthropic_messages(
            [
                Message(role=Role.USER, content=[TextPart(text='a'), TextPart(text='b')]),
            ]
        )
        content = msgs[0]['content']
        assert isinstance(content, list)
        assert len(content) == 2

    def test_media_local_file_becomes_base64_block(self, tmp_path: Path) -> None:
        image_path = tmp_path / 'tiny.png'
        image_path.write_bytes(
            base64.b64decode(
                'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg=='
            )
        )
        _, msgs = to_anthropic_messages(
            [
                Message(
                    role=Role.USER,
                    content=[MediaPart(media=MediaContent(url=str(image_path), contentType='image/png'))],
                ),
            ]
        )
        content = msgs[0]['content']
        # Single media block is kept as list (not simplified to string).
        assert isinstance(content, list)
        block = content[0]
        assert block['type'] == 'image'
        assert block['source']['type'] == 'base64'
        assert base64.b64decode(block['source']['data']) == image_path.read_bytes()

    def test_media_remote_url_becomes_base64_block(self, monkeypatch: pytest.MonkeyPatch) -> None:
        remote_bytes = b'remote-image-bytes'

        def _fake_read_remote(url: str, *, timeout_seconds: float) -> bytes:
            assert url == 'https://example.com/remote.png'
            assert timeout_seconds > 0
            return remote_bytes

        monkeypatch.setattr('dotpromptz.image._read_remote_image_bytes', _fake_read_remote)
        _, msgs = to_anthropic_messages(
            [
                Message(
                    role=Role.USER,
                    content=[
                        MediaPart(media=MediaContent(url='https://example.com/remote.png', contentType='image/png'))
                    ],
                ),
            ]
        )
        content = msgs[0]['content']
        assert isinstance(content, list)
        block = content[0]
        assert block['type'] == 'image'
        assert block['source']['type'] == 'base64'
        assert block['source']['media_type'] == 'image/png'
        assert base64.b64decode(block['source']['data']) == remote_bytes

    def test_media_data_uri_rejected(self) -> None:
        with pytest.raises(ValueError, match='data: image URLs are not supported'):
            to_anthropic_messages(
                [
                    Message(
                        role=Role.USER,
                        content=[
                            MediaPart(media=MediaContent(url='data:image/png;base64,abc123', contentType='image/png'))
                        ],
                    ),
                ]
            )


class TestToAnthropicRequest:
    """Test full RenderedPrompt → Anthropic request conversion."""

    def test_basic(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[
                Message(role=Role.SYSTEM, content=[TextPart(text='Be helpful')]),
                Message(role=Role.USER, content=[TextPart(text='hi')]),
            ],
        )
        req = to_anthropic_request(rendered)
        assert req['model'] == 'claude-sonnet-4-20250514'
        assert req['system'] == 'Be helpful'
        assert len(req['messages']) == 1

    def test_no_model_raises(self) -> None:
        rendered = RenderedPrompt(
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        with pytest.raises(ValueError, match='No model specified'):
            to_anthropic_request(rendered)

    def test_max_tokens_default(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_anthropic_request(rendered)
        assert req['max_tokens'] == _ANTHROPIC_DEFAULT_MAX_TOKENS

    def test_max_tokens_uses_custom_default_when_config_missing(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_anthropic_request(rendered, default_max_tokens=4096)
        assert req['max_tokens'] == 4096

    def test_thinking_defaults_to_enabled_max_budget(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_anthropic_request(rendered)
        assert req['thinking']['type'] == 'enabled'
        assert req['thinking']['budget_tokens'] == _ANTHROPIC_DEFAULT_MAX_TOKENS - 1

    def test_thinking_low_maps_to_enabled_budget(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514', 'thinking': 'low'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_anthropic_request(rendered)
        assert req['thinking']['type'] == 'enabled'
        assert req['thinking']['budget_tokens'] == int((_ANTHROPIC_DEFAULT_MAX_TOKENS - 1) * 0.25)

    def test_opus_4_6_uses_adaptive_thinking(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-opus-4-6', 'thinking': 'high'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_anthropic_request(rendered)
        assert req['thinking']['type'] == 'adaptive'
        assert 'budget_tokens' not in req['thinking']

    def test_thinking_with_small_max_tokens_raises(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514', 'max_tokens': 100},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        with pytest.raises(ValueError, match='max_tokens'):
            to_anthropic_request(rendered)

    def test_thinking_coerces_temperature_to_one(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514', 'thinking': 'high', 'temperature': 1},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_anthropic_request(rendered)
        assert req['temperature'] == 1

    def test_thinking_preserves_temperature_one(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514', 'thinking': 'medium', 'temperature': 1},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_anthropic_request(rendered)
        assert req['temperature'] == 1

    def test_json_output_no_schema(self) -> None:
        """JSON format without schema should not set output_config."""
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='json', file_name='output'),
        )
        req = to_anthropic_request(rendered)
        assert 'output_config' not in req

    def test_json_output_with_schema(self) -> None:
        """JSON format with schema should set output_config with json_schema."""
        schema = {
            'type': 'object',
            'properties': {
                'name': {'type': 'string'},
                'age': {'type': 'integer'},
            },
            'required': ['name', 'age'],
            'additionalProperties': False,
        }
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='json', schema_value=schema, file_name='output'),
        )
        req = to_anthropic_request(rendered)
        assert 'output_config' in req
        output_config = req['output_config']
        assert output_config['format']['type'] == 'json_schema'
        assert output_config['format']['schema'] == schema

    def test_yaml_output_with_schema(self) -> None:
        """YAML format with schema should still request JSON schema output."""
        schema = {
            'type': 'object',
            'properties': {'name': {'type': 'string'}},
            'required': ['name'],
            'additionalProperties': False,
        }
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='yaml', schema_value=schema, file_name='output'),
        )
        req = to_anthropic_request(rendered)
        assert 'output_config' in req
        output_config = req['output_config']
        assert output_config['format']['type'] == 'json_schema'
        assert output_config['format']['schema'] == schema

    def test_txt_output_no_output_config(self) -> None:
        """txt format should not set output_config."""
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='txt', file_name='output'),
        )
        req = to_anthropic_request(rendered)
        assert 'output_config' not in req


class TestAnthropicAdapterConvert:
    """Test the adapter's convert method (no API call)."""

    def test_convert_returns_dict(self) -> None:
        cred = Credential(name='test', adapter='anthropic', api_key='test-key')
        adapter = AnthropicAdapter(credential=cred)
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hello')])],
        )
        result = adapter.convert(rendered)
        assert isinstance(result, dict)
        assert result['model'] == 'claude-sonnet-4-20250514'

    def test_convert_uses_adapter_default_max_tokens(self) -> None:
        cred = Credential(name='test', adapter='anthropic', api_key='test-key')
        adapter = AnthropicAdapter(credential=cred, max_tokens=4096)
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hello')])],
        )
        result = adapter.convert(rendered)
        assert result['max_tokens'] == 4096

    def test_convert_prefers_prompt_max_tokens_over_adapter_default(self) -> None:
        cred = Credential(name='test', adapter='anthropic', api_key='test-key')
        adapter = AnthropicAdapter(credential=cred, max_tokens=4096)
        rendered = RenderedPrompt(
            config={'model': 'claude-sonnet-4-20250514', 'max_tokens': 2048},
            messages=[Message(role=Role.USER, content=[TextPart(text='hello')])],
        )
        result = adapter.convert(rendered)
        assert result['max_tokens'] == 2048


class TestAnthropicAdapterBaseUrl:
    """Test base_url from Credential is used correctly."""

    def test_credential_base_url(self) -> None:
        cred = Credential(name='test', adapter='anthropic', api_key='test-key', base_url='https://my-proxy.example.com')
        adapter = AnthropicAdapter(credential=cred)
        assert adapter._credential.base_url == 'https://my-proxy.example.com'

    def test_credential_no_base_url(self) -> None:
        cred = Credential(name='test', adapter='anthropic', api_key='test-key')
        adapter = AnthropicAdapter(credential=cred)
        assert adapter._credential.base_url is None
