"""Tests for the adapter base types and registry."""

from types import SimpleNamespace
from unittest.mock import patch

import pytest
from pydantic import ValidationError

import dotpromptz.adapters as adapters_module
from dotpromptz.adapters import GeneratedImage, GenerateResponse, Usage, get_adapter, list_adapters
from dotpromptz.adapters._base import resolve_config_params, resolve_rendered_config_and_model
from dotpromptz.typing import PromptOutputConfig


class TestUsage:
    """Tests for the Usage model."""

    def test_defaults(self) -> None:
        usage = Usage()
        assert usage.prompt_tokens == 0
        assert usage.completion_tokens == 0
        assert usage.total_tokens == 0

    def test_values(self) -> None:
        usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
        assert usage.total_tokens == 30


class TestGenerateResponse:
    """Tests for the GenerateResponse model."""

    def test_text_only(self) -> None:
        resp = GenerateResponse(text='Hello!')
        assert resp.text == 'Hello!'
        assert resp.usage is None

    def test_images_payload(self) -> None:
        resp = GenerateResponse(images=[GeneratedImage(data=b'abc', mime_type='image/png')])
        assert resp.images is not None
        assert resp.images[0].mime_type == 'image/png'


class TestRegistry:
    """Tests for the adapter registry."""

    def test_list_adapters(self) -> None:
        names = list_adapters()
        assert 'openai' in names
        assert 'anthropic' in names
        assert 'google' in names
        assert len(names) == 3

    def test_get_unknown_adapter(self) -> None:
        with pytest.raises(ValueError, match='Unknown adapter'):
            get_adapter('nonexistent')

    def test_list_adapters_does_not_import_provider_modules(self) -> None:
        with patch('dotpromptz.adapters.import_module') as mock_import_module:
            names = list_adapters()

        mock_import_module.assert_not_called()
        assert names == ['anthropic', 'google', 'openai']

    def test_get_adapter_imports_only_requested_provider(self) -> None:
        class _FakeAdapter:
            def __init__(self, **kwargs: object) -> None:
                self.kwargs = kwargs

        fake_module = SimpleNamespace(OpenAIAdapter=_FakeAdapter)
        with patch.dict(adapters_module._ADAPTERS, {}, clear=True):
            with patch('dotpromptz.adapters.import_module', return_value=fake_module) as mock_import_module:
                adapter = get_adapter('openai', credential='test-credential')

        mock_import_module.assert_called_once_with('dotpromptz.adapters.openai')
        assert isinstance(adapter, _FakeAdapter)
        assert adapter.kwargs == {'credential': 'test-credential'}


class TestPromptOutputConfig:
    """Tests for PromptOutputConfig validation."""

    def test_json_format_ok(self) -> None:
        cfg = PromptOutputConfig(format='json', file_name='output')
        assert cfg.format == 'json'

    def test_yaml_format_ok(self) -> None:
        cfg = PromptOutputConfig(format='yaml', file_name='output')
        assert cfg.format == 'yaml'

    def test_txt_format_ok(self) -> None:
        cfg = PromptOutputConfig(format='txt', file_name='output')
        assert cfg.format == 'txt'

    def test_schema_only_for_json_or_yaml(self) -> None:
        """schema is only valid when format is 'json' or 'yaml'."""
        cfg = PromptOutputConfig(format='json', schema={'type': 'object'}, file_name='output')
        assert cfg.schema_value == {'type': 'object'}
        cfg2 = PromptOutputConfig(format='yaml', schema={'type': 'object'}, file_name='output')
        assert cfg2.schema_value == {'type': 'object'}

    def test_example_infers_schema(self) -> None:
        cfg = PromptOutputConfig(
            format='json',
            example='name: Alice # username\nage: 30 # age',
            file_name='output',
        )
        assert cfg.schema_value is not None
        assert cfg.schema_value['required'] == ['name', 'age']
        assert cfg.schema_value['properties']['name']['description'] == 'username'
        assert cfg.schema_value['properties']['age']['description'] == 'age'

    def test_schema_and_example_conflict(self) -> None:
        with pytest.raises(ValidationError, match='mutually exclusive'):
            PromptOutputConfig(
                format='json',
                schema={'type': 'object'},
                example='name: Alice',
                file_name='output',
            )

    def test_schema_legacy_alias_rejected(self) -> None:
        with pytest.raises(ValidationError, match='Extra inputs are not permitted'):
            PromptOutputConfig(format='json', schema_={'type': 'object'}, file_name='output')

    def test_schema_with_txt_raises(self) -> None:
        with pytest.raises(ValidationError, match='schema'):
            PromptOutputConfig(format='txt', schema={'type': 'object'}, file_name='output')

    def test_example_with_txt_raises(self) -> None:
        with pytest.raises(ValidationError, match='schema/output.example'):
            PromptOutputConfig(format='txt', example='name: Alice', file_name='output')

    def test_image_format_ok(self) -> None:
        cfg = PromptOutputConfig(format='image', file_name='output')
        assert cfg.format == 'image'

    def test_file_name_and_output_dir(self) -> None:
        cfg = PromptOutputConfig(format='json', output_dir='./out', file_name='result')
        assert cfg.output_dir == './out'
        assert cfg.file_name == 'result'


class TestResolveConfigParams:
    """Tests for config parameter normalization."""

    def test_thinking_defaults_to_high(self) -> None:
        params = resolve_config_params({'temperature': 1})
        assert params['thinking'] == 'high'

    def test_thinking_preserves_valid_level(self) -> None:
        params = resolve_config_params({'thinking': 'low'})
        assert params['thinking'] == 'low'

    def test_thinking_preserves_adaptive_level(self) -> None:
        params = resolve_config_params({'thinking': 'adaptive'})
        assert params['thinking'] == 'adaptive'

    def test_thinking_invalid_value_falls_back(self) -> None:
        params = resolve_config_params({'thinking': 'ultra'})
        assert params['thinking'] == 'high'

    def test_zero_values_are_preserved(self) -> None:
        params = resolve_config_params({
            'temperature': 1,
            'top_p': 0.0,
            'max_tokens': 0,
            'frequency_penalty': 0.0,
            'presence_penalty': 0.0,
            'seed': 0,
        })
        assert params['temperature'] == 1
        assert params['top_p'] == 0.0
        assert params['max_tokens'] == 0
        assert params['frequency_penalty'] == 0.0
        assert params['presence_penalty'] == 0.0
        assert params['seed'] == 0
        assert params['thinking'] == 'high'


class TestResolveRenderedConfigAndModel:
    """Tests for rendered prompt config/model extraction helper."""

    def test_returns_config_and_model(self) -> None:
        from dotpromptz.typing import Message, RenderedPrompt, Role, TextPart

        rendered = RenderedPrompt(
            config={'model': 'gpt-4o', 'temperature': 1},
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        config, model = resolve_rendered_config_and_model(rendered)
        assert model == 'gpt-4o'
        assert config['temperature'] == 1

    def test_non_dict_config_raises(self) -> None:
        from dotpromptz.typing import Message, RenderedPrompt, Role, TextPart

        rendered = RenderedPrompt(
            config=None,
            messages=[Message(role=Role.USER, content=[TextPart(text='hi')])],
        )
        with pytest.raises(ValueError, match='No model specified'):
            resolve_rendered_config_and_model(rendered)


class TestParseDataUri:
    """Tests for the parse_data_uri helper."""

    def test_png_data_uri(self) -> None:
        from dotpromptz.adapters._base import parse_data_uri

        result = parse_data_uri('data:image/png;base64,abc123')
        assert result == ('image/png', 'abc123')

    def test_jpeg_data_uri(self) -> None:
        from dotpromptz.adapters._base import parse_data_uri

        result = parse_data_uri('data:image/jpeg;base64,xyz')
        assert result == ('image/jpeg', 'xyz')

    def test_non_data_uri_returns_none(self) -> None:
        from dotpromptz.adapters._base import parse_data_uri

        assert parse_data_uri('https://example.com/img.png') is None

    def test_empty_data(self) -> None:
        from dotpromptz.adapters._base import parse_data_uri

        result = parse_data_uri('data:image/png;base64,')
        assert result == ('image/png', '')

    def test_data_with_commas(self) -> None:
        from dotpromptz.adapters._base import parse_data_uri

        result = parse_data_uri('data:text/plain;base64,a,b,c')
        assert result == ('text/plain', 'a,b,c')

    def test_plain_text_not_data_uri(self) -> None:
        from dotpromptz.adapters._base import parse_data_uri

        assert parse_data_uri('not-a-data-uri') is None

    def test_empty_string(self) -> None:
        from dotpromptz.adapters._base import parse_data_uri

        assert parse_data_uri('') is None
