"""Tests for OpenAI adapter conversion functions."""

from __future__ import annotations

import json

import pytest

from dotpromptz.adapters.openai import (
    OpenAIAdapter,
    to_openai_messages,
    to_openai_request,
    to_openai_tools,
)
from dotpromptz.typing import (
    MediaContent,
    MediaPart,
    Message,
    PromptMetadata,
    PromptOutputConfig,
    RenderedPrompt,
    Role,
    TextPart,
    ToolDefinition,
    ToolRequestContent,
    ToolRequestPart,
    ToolResponseContent,
    ToolResponsePart,
)


class TestRoleMapping:
    """Test role conversion from dotprompt to OpenAI."""

    def test_user_role(self) -> None:
        msgs = to_openai_messages([Message(role=Role.USER, content=[TextPart(text='hi')])])
        assert msgs[0].role == 'user'

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

    def test_system_role(self) -> None:
        msgs = to_openai_messages([Message(role=Role.SYSTEM, content=[TextPart(text='you are helpful')])])
        assert msgs[0].role == 'system'

    def test_tool_role(self) -> None:
        msgs = to_openai_messages([
            Message(
                role=Role.TOOL,
                content=[
                    ToolResponsePart(
                        tool_response=ToolResponseContent(name='search', output={'result': 'ok'}, ref='call_1')
                    )
                ],
            )
        ])
        assert msgs[0].role == 'tool'
        assert msgs[0].tool_call_id == 'call_1'


class TestContentConversion:
    """Test Part → OpenAI content conversion."""

    def test_text_only_becomes_string(self) -> None:
        msgs = to_openai_messages([Message(role=Role.USER, content=[TextPart(text='hello'), TextPart(text=' world')])])
        assert msgs[0].content == 'hello world'

    def test_media_part_becomes_image_url(self) -> None:
        msgs = to_openai_messages([
            Message(
                role=Role.USER,
                content=[
                    TextPart(text='Look at this:'),
                    MediaPart(media=MediaContent(url='https://example.com/img.png', content_type='image/png')),
                ],
            )
        ])
        content = msgs[0].content
        assert isinstance(content, list)
        assert len(content) == 2
        assert content[0].type.value == 'text'
        assert content[1].type.value == 'image_url'
        assert content[1].image_url.url == 'https://example.com/img.png'

    def test_tool_request_becomes_tool_calls(self) -> None:
        msgs = to_openai_messages([
            Message(
                role=Role.MODEL,
                content=[
                    ToolRequestPart(tool_request=ToolRequestContent(name='search', input={'q': 'test'}, ref='call_1')),
                ],
            )
        ])
        assert msgs[0].tool_calls is not None
        assert len(msgs[0].tool_calls) == 1
        assert msgs[0].tool_calls[0].function.name == 'search'
        assert json.loads(msgs[0].tool_calls[0].function.arguments) == {'q': 'test'}


class TestToolDefinitionConversion:
    """Test ToolDefinition → OpenAI tool format."""

    def test_basic_tool(self) -> None:
        tools = to_openai_tools([
            ToolDefinition(
                name='search',
                description='Search the web',
                input_schema={'type': 'object', 'properties': {'q': {'type': 'string'}}},
            )
        ])
        assert tools is not None
        assert len(tools) == 1
        assert tools[0].function.name == 'search'
        assert tools[0].function.description == 'Search the web'

    def test_none_returns_none(self) -> None:
        assert to_openai_tools(None) is None
        assert to_openai_tools([]) is None


class TestToOpenAIRequest:
    """Test full RenderedPrompt → OpenAIRequest conversion."""

    def test_basic_request(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hello')])],
        )
        req = to_openai_request(rendered)
        assert req.model == 'gpt-4o'
        assert len(req.messages) == 1
        assert req.messages[0].content == 'hello'

    def test_config_model_overrides_legacy(self) -> None:
        rendered = RenderedPrompt(
            model='gpt-4o',
            config={'model': 'gpt-4o-mini'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_openai_request(rendered)
        assert req.model == 'gpt-4o-mini'

    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_openai_request(rendered)

    def test_config_params(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o', 'temperature': 0.7, 'maxTokens': 100, 'topP': 0.9},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_openai_request(rendered)
        assert req.temperature == 0.7
        assert req.max_tokens == 100
        assert req.top_p == 0.9

    def test_json_output_format(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='json'),
        )
        req = to_openai_request(rendered)
        assert req.response_format is not None
        assert req.response_format.type.value == 'json_object'
        assert req.response_format.json_schema is None

    def test_json_output_format_with_schema(self) -> None:
        schema = {
            'type': 'object',
            'properties': {
                'name': {'type': 'string'},
                'age': {'type': 'integer'},
            },
            'required': ['name', 'age'],
            'additionalProperties': False,
        }
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='json', schema=schema),
        )
        req = to_openai_request(rendered)
        assert req.response_format is not None
        assert req.response_format.type.value == 'json_schema'
        assert req.response_format.json_schema is not None
        assert req.response_format.json_schema.name == 'response'
        assert req.response_format.json_schema.strict is True
        assert req.response_format.json_schema.schema_ == schema

    def test_json_output_format_with_schema_serialization(self) -> None:
        """Ensure the schema is serialized correctly under the 'schema' key, not 'schema_'."""
        schema = {
            'type': 'object',
            'properties': {'x': {'type': 'string'}},
            'required': ['x'],
            'additionalProperties': False,
        }
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='json', schema=schema),
        )
        req = to_openai_request(rendered)
        dumped = req.model_dump(exclude_none=True)
        rf = dumped['response_format']
        assert rf['type'] == 'json_schema'
        assert 'json_schema' in rf
        assert rf['json_schema']['name'] == 'response'
        assert rf['json_schema']['strict'] is True
        assert rf['json_schema']['schema'] == schema
        # Must not have 'schema_' key in serialized output
        assert 'schema_' not in rf['json_schema']

    def test_text_output_format_no_response_format(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='text'),
        )
        req = to_openai_request(rendered)
        assert req.response_format is None

    def test_with_tools(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o'},
            messages=[Message(role=Role.USER, content=[TextPart(text='find')])],
            tool_defs=[ToolDefinition(name='search', description='Search', input_schema={'type': 'object'})],
        )
        req = to_openai_request(rendered)
        assert req.tools is not None
        assert len(req.tools) == 1


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

    def test_convert_returns_dict(self) -> None:
        adapter = OpenAIAdapter(api_key='test-key')
        rendered = RenderedPrompt(
            config={'model': 'gpt-4o'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hello')])],
        )
        result = adapter.convert(rendered)
        assert isinstance(result, dict)
        assert result['model'] == 'gpt-4o'
        assert 'messages' in result

    def test_convert_uses_default_model(self) -> None:
        adapter = OpenAIAdapter(api_key='test-key', default_model='gpt-4o-mini')
        rendered = RenderedPrompt(
            messages=[Message(role=Role.USER, content=[TextPart(text='hello')])],
        )
        result = adapter.convert(rendered)
        assert result['model'] == 'gpt-4o-mini'


class TestOpenAIAdapterBaseUrl:
    """Test base_url support for third-party compatible endpoints."""

    def test_base_url_param(self) -> None:
        adapter = OpenAIAdapter(api_key='test-key', base_url='https://api.deepseek.com')
        assert adapter._base_url == 'https://api.deepseek.com'

    def test_base_url_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv('OPENAI_BASE_URL', 'https://env-proxy.example.com')
        adapter = OpenAIAdapter(api_key='test-key')
        assert adapter._base_url == 'https://env-proxy.example.com'

    def test_base_url_param_overrides_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv('OPENAI_BASE_URL', 'https://env-proxy.example.com')
        adapter = OpenAIAdapter(api_key='test-key', base_url='https://param-proxy.example.com')
        assert adapter._base_url == 'https://param-proxy.example.com'

    def test_base_url_none_by_default(self, monkeypatch) -> None:
        monkeypatch.delenv('OPENAI_BASE_URL', raising=False)
        adapter = OpenAIAdapter(api_key='test-key')
        assert adapter._base_url is None
