"""Tests for the dotprompt CLI - simplified to avoid CliRunner/asyncio conflicts."""
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock

import pytest
from click.testing import CliRunner

from dotpromptz.cli import (
    _infer_adapter_from_model,
    _load_env_file,
    _part_to_dict,
    _resolve_adapter,
    _run_coroutine,
    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:
    """Test CLI argument validation (no execution)."""

    def test_no_arguments_shows_error(self) -> None:
        """CLI with no arguments should show usage error."""
        runner = CliRunner()
        result = runner.invoke(cli, [])
        assert result.exit_code == 2  # Missing required argument
        assert 'PROMPT_FILE' in result.output or 'prompt_file' in result.output.lower()

    def test_nonexistent_file_shows_error(self) -> None:
        """CLI with nonexistent file shows error."""
        runner = CliRunner()
        result = runner.invoke(cli, ['/nonexistent/file.prompt'])
        assert result.exit_code == 2  # Click validation error
        assert 'exist' in result.output.lower()

    def test_help_flag_works(self) -> None:
        """--help flag shows usage information."""
        runner = CliRunner()
        result = runner.invoke(cli, ['--help'])
        assert result.exit_code == 0
        assert 'PROMPT_FILE' in result.output
        assert 'Run a .prompt file' in result.output
        # Verify old flags are gone
        assert '--dry-run' not in result.output
        assert '--verbose' not in result.output
        assert 'INPUT_FILE' not in result.output


class TestInferAdapter:
    """Test model-name-based adapter auto-inference."""

    @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 TestRunCoroutine:
    """Test _run_coroutine helper."""

    def test_returns_result(self) -> None:

        async def coro():
            return 42

        assert _run_coroutine(coro()) == 42

    def test_propagates_exception(self) -> None:

        async def boom():
            raise RuntimeError('kaboom')

        with pytest.raises(RuntimeError, match='kaboom'):
            _run_coroutine(boom())


class TestLoadEnvFile:
    """Test _load_env_file helper."""

    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))
        # dotenv load_dotenv should have loaded the var
        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:
        # Should not raise on missing file
        _load_env_file('/nonexistent/path/.env')

    def test_none_tries_cwd_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.chdir(tmp_path)
        # No .env in tmp_path, should not raise
        _load_env_file(None)


class TestPartToDict:
    """Test _part_to_dict helper."""

    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:
    """Test _resolve_adapter function."""

    def test_adapter_config_object(self) -> None:
        """AdapterConfig object should extract name and base_url."""
        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:
        """String adapter should be used directly."""
        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:
        """None adapter should infer from config.model."""
        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_raises(self) -> None:
        """No adapter and no inferrable model should raise UsageError."""
        import click
        
        mock_rendered = MagicMock()
        mock_rendered.adapter = None
        mock_rendered.config = {}
        
        with pytest.raises(click.UsageError, match='Cannot determine which adapter'):
            _resolve_adapter(mock_rendered)
