"""Tests for Google adapter conversion functions."""

import base64
from pathlib import Path
from unittest.mock import AsyncMock

import pytest

from dotpromptz.adapters.google import (
    GoogleAdapter,
    _extract_google_response_text,
    to_genai_contents,
    to_genai_image_request,
    to_genai_request,
)
from dotpromptz.credentials import Credential
from dotpromptz.typing import (
    DataPart,
    MediaContent,
    MediaPart,
    Message,
    PromptOutputConfig,
    RenderedPrompt,
    Role,
    TextPart,
)


class TestSystemExtraction:
    """Test system message extraction."""

    def test_system_extracted(self) -> None:
        system_text, contents = to_genai_contents([
            Message(role=Role.SYSTEM, content=[TextPart(text='Be concise.')]),
            Message(role=Role.USER, content=[TextPart(text='hi')]),
        ])
        assert system_text == 'Be concise.'
        assert len(contents) == 1
        assert contents[0]['role'] == 'user'

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


class TestContentConversion:
    """Test Part → Gemini parts conversion."""

    def test_text_part(self) -> None:
        _, contents = to_genai_contents([
            Message(role=Role.USER, content=[TextPart(text='hello')]),
        ])
        assert contents[0]['parts'] == [{'text': 'hello'}]


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

    def test_model_role(self) -> None:
        _, contents = to_genai_contents([
            Message(role=Role.MODEL, content=[TextPart(text='ok')]),
        ])
        assert contents[0]['role'] == 'model'

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


class TestToGenAIRequest:
    """Test full RenderedPrompt → Gemini request conversion."""

    def test_basic(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_genai_request(rendered)
        assert req['model'] == 'gemini-3-flash-preview'
        assert len(req['contents']) == 1

    def test_thinking_defaults_to_high_level(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_genai_request(rendered)
        assert req['config']['thinking_config']['thinking_budget'] == -1

    def test_thinking_low_level(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview', 'thinking': 'low'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_genai_request(rendered)
        assert req['config']['thinking_config']['thinking_budget'] == 1024

    def test_thinking_uses_levels_for_gemini_3(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview', 'thinking': 'low'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        req = to_genai_request(rendered)
        assert req['config']['thinking_config']['thinking_budget'] == 1024

    def test_system_in_config(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[
                Message(role=Role.SYSTEM, content=[TextPart(text='sys')]),
                Message(role=Role.USER, content=[TextPart(text='hi')]),
            ],
        )
        req = to_genai_request(rendered)
        assert req['config']['system_instruction'] == 'sys'

    def test_json_output(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='json', file_name='output'),
        )
        req = to_genai_request(rendered)
        assert req['config']['response_mime_type'] == 'application/json'
        assert 'response_json_schema' not in req.get('config', {})

    def test_json_output_with_schema(self) -> None:
        schema = {
            'type': 'object',
            'properties': {
                'name': {'type': 'string'},
                'age': {'type': 'integer'},
            },
            'required': ['name', 'age'],
            'additionalProperties': False,
        }
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='json', schema=schema, file_name='output'),
        )
        req = to_genai_request(rendered)
        assert req['config']['response_mime_type'] == 'application/json'
        assert req['config']['response_json_schema'] == schema

    def test_yaml_output(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='yaml', file_name='output'),
        )
        req = to_genai_request(rendered)
        assert req['config']['response_mime_type'] == 'application/json'
        assert 'response_json_schema' not in req.get('config', {})

    def test_yaml_output_with_schema(self) -> None:
        schema = {
            'type': 'object',
            'properties': {'name': {'type': 'string'}},
            'required': ['name'],
            'additionalProperties': False,
        }
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='yaml', schema=schema, file_name='output'),
        )
        req = to_genai_request(rendered)
        assert req['config']['response_mime_type'] == 'application/json'
        assert req['config']['response_json_schema'] == schema

    def test_txt_output_no_schema(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
            output=PromptOutputConfig(format='txt', file_name='output'),
        )
        req = to_genai_request(rendered)
        assert 'response_json_schema' not in req.get('config', {})
        assert 'response_mime_type' not in req.get('config', {})

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


class TestToGenAIImageRequest:
    """Test image request conversion for Gemini."""

    def test_sets_image_response_modality(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3.1-flash-image-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='draw a fox')])],
            output=PromptOutputConfig(format='image', file_name='img'),
        )
        request = to_genai_image_request(rendered)
        assert request['model'] == 'gemini-3.1-flash-image-preview'
        assert request['config']['response_modalities'] == ['IMAGE']

    def test_maps_resolution_and_aspect_ratio(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3.1-flash-image-preview', 'resolution': '2K', 'aspect_ratio': '16:9'},
            messages=[Message(role=Role.USER, content=[TextPart(text='draw a fox')])],
            output=PromptOutputConfig(format='image', file_name='img'),
        )
        request = to_genai_image_request(rendered)
        assert request['config']['image_config']['image_size'] == '2K'
        assert request['config']['image_config']['aspect_ratio'] == '16:9'

    def test_invalid_resolution_raises(self) -> None:
        rendered = RenderedPrompt(
            config={'model': 'gemini-3.1-flash-image-preview', 'resolution': '8K'},
            messages=[Message(role=Role.USER, content=[TextPart(text='draw a fox')])],
            output=PromptOutputConfig(format='image', file_name='img'),
        )
        with pytest.raises(ValueError, match='resolution'):
            to_genai_image_request(rendered)


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

    def test_convert_returns_dict(self) -> None:
        cred = Credential(name='test', adapter='google', api_key='test-key')
        adapter = GoogleAdapter(credential=cred)
        rendered = RenderedPrompt(
            config={'model': 'gemini-3-flash-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='hello')])],
        )
        result = adapter.convert(rendered)
        assert isinstance(result, dict)
        assert result['model'] == 'gemini-3-flash-preview'


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

    def test_credential_base_url(self) -> None:
        cred = Credential(name='test', adapter='google', api_key='test-key', base_url='https://my-proxy.example.com')
        adapter = GoogleAdapter(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='google', api_key='test-key')
        adapter = GoogleAdapter(credential=cred)
        assert adapter._credential.base_url is None


class TestMediaPartConversion:
    """Tests for _part_to_genai with different Part types."""

    def test_local_path_media_part(self, tmp_path: Path) -> None:
        from dotpromptz.adapters.google import _part_to_genai

        image_path = tmp_path / 'tiny.png'
        image_path.write_bytes(
            base64.b64decode(
                'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg=='
            )
        )
        part = MediaPart(media=MediaContent(url=str(image_path), content_type='image/png'))
        result = _part_to_genai(part)
        assert result is not None
        assert 'inline_data' in result
        assert result['inline_data']['mime_type'] == 'image/png'
        assert base64.b64decode(result['inline_data']['data']) == image_path.read_bytes()

    def test_http_url_media_part(self) -> None:
        from dotpromptz.adapters.google import _part_to_genai

        part = MediaPart(media=MediaContent(url='https://example.com/img.jpg', content_type='image/jpeg'))
        result = _part_to_genai(part)
        assert result is not None
        assert 'file_data' in result
        assert result['file_data']['file_uri'] == 'https://example.com/img.jpg'
        assert result['file_data']['mime_type'] == 'image/jpeg'

    def test_data_uri_media_part_rejected(self) -> None:
        from dotpromptz.adapters.google import _part_to_genai

        part = MediaPart(media=MediaContent(url='data:image/png;base64,abc123', content_type='image/png'))
        with pytest.raises(ValueError, match='data: image URLs are not supported'):
            _part_to_genai(part)

    def test_data_part_becomes_json_text(self) -> None:
        from dotpromptz.adapters.google import _part_to_genai

        part = DataPart(data={'key': 'value'})
        result = _part_to_genai(part)
        assert result is not None
        assert 'text' in result
        import json

        assert json.loads(result['text']) == {'key': 'value'}


class _FakePart:
    def __init__(self, text: str, *, thought: bool = False) -> None:
        self.text = text
        self.thought = thought


class _FakeContent:
    def __init__(self, parts: list[_FakePart]) -> None:
        self.parts = parts


class _FakeCandidate:
    def __init__(self, parts: list[_FakePart]) -> None:
        self.content = _FakeContent(parts)


class _FakeResponse:
    def __init__(self, *, text: str | None, parts: list[_FakePart]) -> None:
        self.text = text
        self.candidates = [_FakeCandidate(parts)]


class TestGoogleResponseTextExtraction:
    """Tests for response text extraction in Google adapter."""

    def test_prefers_sdk_text_property(self) -> None:
        response = _FakeResponse(
            text='{"x":1}',
            parts=[_FakePart('Thought text'), _FakePart('{"x":1}')],
        )
        assert _extract_google_response_text(response) == '{"x":1}'

    def test_falls_back_to_parts_when_text_missing(self) -> None:
        response = _FakeResponse(
            text=None,
            parts=[_FakePart('A'), _FakePart('B')],
        )
        assert _extract_google_response_text(response) == 'AB'

    def test_fallback_ignores_thought_parts(self) -> None:
        response = _FakeResponse(
            text=None,
            parts=[_FakePart('thinking', thought=True), _FakePart('{"ok":1}')],
        )
        assert _extract_google_response_text(response) == '{"ok":1}'


class _FakeInlineData:
    def __init__(self, *, mime_type: str, data: bytes) -> None:
        self.mime_type = mime_type
        self.data = data


class _FakeImagePart:
    def __init__(self, inline_data: _FakeInlineData) -> None:
        self.inline_data = inline_data


class _FakeImageContent:
    def __init__(self, parts: list[_FakeImagePart]) -> None:
        self.parts = parts


class _FakeImageCandidate:
    def __init__(self, parts: list[_FakeImagePart]) -> None:
        self.content = _FakeImageContent(parts)
        self.finish_reason = 'STOP'


class _FakeUsageMetadata:
    def __init__(self) -> None:
        self.prompt_token_count = 3
        self.candidates_token_count = 5


class _FakeImageResponse:
    def __init__(self, *, mime_type: str, data: bytes) -> None:
        self.candidates = [_FakeImageCandidate([_FakeImagePart(_FakeInlineData(mime_type=mime_type, data=data))])]
        self.usage_metadata = _FakeUsageMetadata()
        self.text = None


class _FakeModelsApi:
    def __init__(self, response: _FakeImageResponse) -> None:
        self.generate_content = AsyncMock(return_value=response)


class _FakeAio:
    def __init__(self, response: _FakeImageResponse) -> None:
        self.models = _FakeModelsApi(response)


class _FakeClientImage:
    def __init__(self, response: _FakeImageResponse) -> None:
        self.aio = _FakeAio(response)


class TestGoogleGenerateImagePath:
    """Tests for Gemini image output branch."""

    @pytest.mark.asyncio
    async def test_generate_image_success(self) -> None:
        cred = Credential(name='test', adapter='google', api_key='test-key')
        adapter = GoogleAdapter(credential=cred, default_model='gemini-3.1-flash-image-preview')
        fake_response = _FakeImageResponse(mime_type='image/png', data=b'\x89PNG')
        adapter._get_client = lambda: _FakeClientImage(fake_response)  # type: ignore[method-assign]
        rendered = RenderedPrompt(
            config={'model': 'gemini-3.1-flash-image-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='draw a fox')])],
            output=PromptOutputConfig(format='image', file_name='img'),
        )

        response = await adapter.generate(rendered)

        assert response.images is not None
        assert response.images[0].data == b'\x89PNG'
        assert response.images[0].mime_type == 'image/png'
        assert response.usage is not None
        assert response.usage.total_tokens == 8

    @pytest.mark.asyncio
    async def test_generate_image_rejects_non_png(self) -> None:
        cred = Credential(name='test', adapter='google', api_key='test-key')
        adapter = GoogleAdapter(credential=cred, default_model='gemini-3.1-flash-image-preview')
        fake_response = _FakeImageResponse(mime_type='image/jpeg', data=b'\xff\xd8')
        adapter._get_client = lambda: _FakeClientImage(fake_response)  # type: ignore[method-assign]
        rendered = RenderedPrompt(
            config={'model': 'gemini-3.1-flash-image-preview'},
            messages=[Message(role=Role.USER, content=[TextPart(text='draw a fox')])],
            output=PromptOutputConfig(format='image', file_name='img'),
        )

        with pytest.raises(ValueError, match='image/png'):
            await adapter.generate(rendered)
