"""Tests for the dotprompt CLI."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock

import pytest

from dotpromptz.cli import (
    _infer_adapter_from_model,
    _load_env_file,
    _part_to_dict,
    _resolve_adapter,
    cli,
)

SIMPLE_PROMPT = """\
---
config:
  model: gpt-4o
input:
  schema:
    topic: string
---
Tell me about {{topic}}.
"""

PROMPT_WITH_INLINE_DATA = """\
---
config:
  model: gpt-4o
input:
  data:
    topic: AI
---
Tell me about {{topic}}.
"""


class TestCLIArguments:
    def test_no_arguments_shows_error(self, capsys: pytest.CaptureFixture[str]) -> None:
        with pytest.raises(SystemExit) as exc_info:
            cli([])
        assert exc_info.value.code == 2

    def test_nonexistent_file_shows_error(self, capsys: pytest.CaptureFixture[str]) -> None:
        with pytest.raises(SystemExit) as exc_info:
            cli(['/nonexistent/file.prompt'])
        assert exc_info.value.code == 2
        captured = capsys.readouterr()
        assert 'does not exist' in captured.err.lower() or 'exist' in captured.err.lower()

    def test_help_flag_works(self, capsys: pytest.CaptureFixture[str]) -> None:
        with pytest.raises(SystemExit) as exc_info:
            cli(['--help'])
        assert exc_info.value.code == 0
        captured = capsys.readouterr()
        assert 'PROMPT_FILE' in captured.out
        assert 'Run a .prompt file' in captured.out


class TestInferAdapter:
    @pytest.mark.parametrize(
        'model, expected',
        [
            ('gpt-4o', 'openai'),
            ('gpt-4o-mini', 'openai'),
            ('o1-preview', 'openai'),
            ('o3-mini', 'openai'),
            ('o4-mini', 'openai'),
            ('chatgpt-4o-latest', 'openai'),
            ('claude-sonnet-4-20250514', 'anthropic'),
            ('claude-3-haiku-20240307', 'anthropic'),
            ('gemini-2.5-flash', 'google'),
            ('gemini-2.0-pro', 'google'),
            ('deepseek-chat', None),
            ('some-unknown-model', None),
            (None, None),
        ],
    )
    def test_infer(self, model: str | None, expected: str | None) -> None:
        assert _infer_adapter_from_model(model) == expected


class TestLoadEnvFile:
    def test_loads_existing_file(self, tmp_path: Path) -> None:
        env_file = tmp_path / '.env'
        env_file.write_text('MY_TEST_VAR_LOAD=hello\n', encoding='utf-8')
        import os

        os.chdir(tmp_path)
        _load_env_file(str(env_file))
        assert os.environ.get('MY_TEST_VAR_LOAD') == 'hello'
        os.environ.pop('MY_TEST_VAR_LOAD', None)

    def test_missing_file_no_error(self) -> None:
        _load_env_file('/nonexistent/path/.env')

    def test_none_tries_cwd_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.chdir(tmp_path)
        _load_env_file(None)


class TestPartToDict:
    def test_text_part(self) -> None:
        from dotpromptz.typing import TextPart

        result = _part_to_dict(TextPart(text='hello'))
        assert result == {'text': 'hello'}

    def test_media_part(self) -> None:
        from dotpromptz.typing import MediaContent, MediaPart

        part = MediaPart(media=MediaContent(url='https://example.com/img.png', content_type='image/png'))
        result = _part_to_dict(part)
        assert 'media' in result
        assert result['media']['url'] == 'https://example.com/img.png'

    def test_data_part(self) -> None:
        from dotpromptz.typing import DataPart

        result = _part_to_dict(DataPart(data={'key': 'value'}))
        assert result == {'data': {'key': 'value'}}

    def test_tool_request_part(self) -> None:
        from dotpromptz.typing import ToolRequestContent, ToolRequestPart

        part = ToolRequestPart(tool_request=ToolRequestContent(name='search', input={'q': 'test'}))
        result = _part_to_dict(part)
        assert 'tool_request' in result
        assert result['tool_request']['name'] == 'search'

    def test_tool_response_part(self) -> None:
        from dotpromptz.typing import ToolResponseContent, ToolResponsePart

        part = ToolResponsePart(tool_response=ToolResponseContent(name='search', output={'result': 1}))
        result = _part_to_dict(part)
        assert 'tool_response' in result
        assert result['tool_response']['name'] == 'search'

    def test_unknown_part_type(self) -> None:
        class FakePart:
            pass

        result = _part_to_dict(FakePart())
        assert result == {'type': 'FakePart'}


class TestResolveAdapter:
    def test_adapter_config_object(self) -> None:
        from unittest.mock import patch

        from dotpromptz.typing import AdapterConfig

        mock_rendered = MagicMock()
        mock_rendered.adapter = AdapterConfig(name='openai', base_url='https://custom.api')
        mock_rendered.config = {}

        with patch('dotpromptz.cli.get_adapter') as mock_get_adapter:
            _resolve_adapter(mock_rendered)
            mock_get_adapter.assert_called_once_with('openai', base_url='https://custom.api')

    def test_string_adapter(self) -> None:
        from unittest.mock import patch

        mock_rendered = MagicMock()
        mock_rendered.adapter = 'anthropic'
        mock_rendered.config = {}

        with patch('dotpromptz.cli.get_adapter') as mock_get_adapter:
            _resolve_adapter(mock_rendered)
            mock_get_adapter.assert_called_once_with('anthropic')

    def test_none_adapter_infers_from_model(self) -> None:
        from unittest.mock import patch

        mock_rendered = MagicMock()
        mock_rendered.adapter = None
        mock_rendered.config = {'model': 'gpt-4o'}

        with patch('dotpromptz.cli.get_adapter') as mock_get_adapter:
            _resolve_adapter(mock_rendered)
            mock_get_adapter.assert_called_once_with('openai')

    def test_no_adapter_no_model_exits(self) -> None:
        mock_rendered = MagicMock()
        mock_rendered.adapter = None
        mock_rendered.config = {}

        with pytest.raises(SystemExit) as exc_info:
            _resolve_adapter(mock_rendered)
        assert exc_info.value.code == 2
