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

from __future__ import annotations

import json
import logging
import subprocess
import sys
from pathlib import Path

import pytest
from click.testing import CliRunner

from dotpromptz.cli import (
    _configure_logging,
    _infer_adapter_from_model,
    _load_env_file,
    _part_to_dict,
    _run_coroutine,
    cli,
)

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


def _run_cli(*args: str, cwd: str | Path | None = None) -> subprocess.CompletedProcess[str]:
    """Invoke the CLI as a subprocess to avoid asyncio/pytest capture conflicts."""
    return subprocess.run(
        [sys.executable, '-m', 'dotpromptz', *args],
        capture_output=True,
        text=True,
        cwd=cwd,
        timeout=30,
    )


class TestCLIDryRun:
    """Test --dry-run mode (no API call needed)."""

    def _write_prompt(self, tmp: Path) -> Path:
        p = tmp / 'test.prompt'
        p.write_text(SIMPLE_PROMPT, encoding='utf-8')
        return p

    def test_dry_run_outputs_json(self, tmp_path: Path) -> None:
        """Dry-run with dict input from JSON file outputs rendered messages."""
        prompt_path = self._write_prompt(tmp_path)
        input_file = tmp_path / 'input.json'
        input_file.write_text('{"topic": "AI"}', encoding='utf-8')
        result = _run_cli(str(prompt_path), str(input_file), '--dry-run')
        assert result.returncode == 0, result.stderr
        data = json.loads(result.stdout)
        assert data['model'] == 'gpt-4o'
        assert len(data['messages']) > 0
        # Check that the topic was rendered.
        text_content = data['messages'][0]['content'][0].get('text', '')
        assert 'AI' in text_content

    def test_dry_run_with_yaml_input(self, tmp_path: Path) -> None:
        """Dry-run with YAML input file."""
        prompt_path = self._write_prompt(tmp_path)
        input_file = tmp_path / 'input.yaml'
        input_file.write_text('topic: quantum', encoding='utf-8')
        result = _run_cli(str(prompt_path), str(input_file), '--dry-run')
        assert result.returncode == 0, result.stderr
        data = json.loads(result.stdout)
        text_content = data['messages'][0]['content'][0].get('text', '')
        assert 'quantum' in text_content

    def test_dry_run_no_input_fails(self, tmp_path: Path) -> None:
        """Missing INPUT_FILE argument should fail."""
        prompt_path = self._write_prompt(tmp_path)
        result = _run_cli(str(prompt_path), '--dry-run')
        assert result.returncode != 0


class TestCLIAdapterInference:
    """Test that adapter is auto-inferred from model name or frontmatter."""

    def test_no_adapter_no_model_no_dry_run(self, tmp_path: Path) -> None:
        """If model can't infer adapter and no frontmatter adapter, error."""
        prompt_no_model = """\
---
input:
  schema:
    topic: string
---
Tell me about {{topic}}.
"""
        p = tmp_path / 'test.prompt'
        p.write_text(prompt_no_model, encoding='utf-8')
        input_file = tmp_path / 'input.json'
        input_file.write_text('{"topic": "AI"}', encoding='utf-8')
        result = _run_cli(str(p), str(input_file))
        assert result.returncode != 0
        assert 'adapter' in result.stderr.lower()

    def test_adapter_field_in_dry_run(self, tmp_path: Path) -> None:
        """Dry-run still works when adapter is set (it's just ignored)."""
        prompt_with_adapter = """\
---
model: deepseek-chat
adapter:
  name: openai
  base_url: https://api.deepseek.com
input:
  schema:
    topic: string
---
Tell me about {{topic}}.
"""
        p = tmp_path / 'test.prompt'
        p.write_text(prompt_with_adapter, encoding='utf-8')
        input_file = tmp_path / 'input.json'
        input_file.write_text('{"topic": "AI"}', encoding='utf-8')
        result = _run_cli(str(p), str(input_file), '--dry-run')
        assert result.returncode == 0, result.stderr
        data = json.loads(result.stdout)
        assert data['model'] == 'deepseek-chat'


class TestCLIHelp:
    """Test help output (these use CliRunner since no async is involved)."""

    def test_main_help(self) -> None:
        runner = CliRunner()
        result = runner.invoke(cli, ['--help'])
        assert result.exit_code == 0
        assert 'dotprompt' in result.output.lower()
        assert 'PROMPT_FILE' in result.output
        assert 'INPUT_FILE' in result.output
        # Old flags/subcommands should NOT appear
        assert '--input' not in result.output
        assert '--input-file' not in result.output
        # Verify no 'run' as standalone subcommand (would be in Usage line)
        assert 'run [OPTIONS]' 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 TestConfigureLogging:
    """Test _configure_logging helper."""

    def test_verbose_sets_debug(self) -> None:
        _configure_logging(verbose=True)
        assert logging.root.level == logging.DEBUG
        assert len(logging.root.handlers) == 1
        assert isinstance(logging.root.handlers[0], logging.StreamHandler)

    def test_non_verbose_sets_warning(self) -> None:
        _configure_logging(verbose=False)
        assert logging.root.level == logging.WARNING

    def test_handler_writes_to_stderr(self) -> None:
        _configure_logging(verbose=False)
        handler = logging.root.handlers[0]
        assert handler.stream is sys.stderr


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 TestInputLoading:
    """Test input file loading with various formats and edge cases."""

    def test_json_dict(self, tmp_path: Path) -> None:
        """Load JSON file containing a dict."""
        from dotpromptz.cli import _load_input_file

        f = tmp_path / 'input.json'
        f.write_text('{"key": "value"}', encoding='utf-8')
        result = _load_input_file(str(f))
        assert result == {'key': 'value'}

    def test_json_list(self, tmp_path: Path) -> None:
        """Load JSON file containing a list of dicts."""
        from dotpromptz.cli import _load_input_file

        f = tmp_path / 'input.json'
        f.write_text('[{"a": 1}, {"a": 2}]', encoding='utf-8')
        result = _load_input_file(str(f))
        assert result == [{'a': 1}, {'a': 2}]

    def test_yaml_dict(self, tmp_path: Path) -> None:
        """Load YAML file containing a dict."""
        from dotpromptz.cli import _load_input_file

        f = tmp_path / 'input.yaml'
        f.write_text('key: value', encoding='utf-8')
        result = _load_input_file(str(f))
        assert result == {'key': 'value'}

    def test_jsonl_format(self, tmp_path: Path) -> None:
        """Load JSONL file, skipping blank lines."""
        from dotpromptz.cli import _load_input_file

        f = tmp_path / 'input.jsonl'
        f.write_text('{"a": 1}\n\n{"a": 2}\n', encoding='utf-8')
        result = _load_input_file(str(f))
        assert result == [{'a': 1}, {'a': 2}]

    def test_unsupported_extension_errors(self, tmp_path: Path) -> None:
        """Unsupported file extension should raise error."""
        import click

        from dotpromptz.cli import _load_input_file

        f = tmp_path / 'input.xml'
        f.write_text('<data/>', encoding='utf-8')
        with pytest.raises(click.UsageError, match='Unsupported input file format'):
            _load_input_file(str(f))

    def test_invalid_json_errors(self, tmp_path: Path) -> None:
        """Malformed JSON should raise error."""
        import click

        from dotpromptz.cli import _load_input_file

        f = tmp_path / 'input.json'
        f.write_text('{bad json}', encoding='utf-8')
        with pytest.raises(click.UsageError, match='Invalid JSON'):
            _load_input_file(str(f))

    def test_list_of_non_dicts_errors(self, tmp_path: Path) -> None:
        """JSON array containing non-dict items should raise error."""
        import click

        from dotpromptz.cli import _load_input_file

        f = tmp_path / 'input.json'
        f.write_text('[1, 2, 3]', encoding='utf-8')
        with pytest.raises(click.UsageError, match='must contain only objects'):
            _load_input_file(str(f))


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 TestBatchMode:
    """Test batch execution mode with list inputs."""

    BATCH_PROMPT = """\
---
model: gpt-4o
runtime:
  max_workers: 2
input:
  schema:
    topic: string
---
Tell me about {{topic}}.
"""

    def test_batch_json_array_dry_run(self, tmp_path: Path) -> None:
        """Batch mode with JSON array input outputs JSONL."""
        p = tmp_path / 'test.prompt'
        p.write_text(self.BATCH_PROMPT, encoding='utf-8')
        input_file = tmp_path / 'batch.json'
        input_file.write_text('[{"topic": "AI"}, {"topic": "ML"}]', encoding='utf-8')
        result = _run_cli(str(p), str(input_file), '--dry-run')
        assert result.returncode == 0, result.stderr
        lines = [line for line in result.stdout.strip().split('\n') if line]
        assert len(lines) == 2
        # Each line should be valid JSON
        item1 = json.loads(lines[0])
        item2 = json.loads(lines[1])
        assert item1['status'] == 'success'
        assert item2['status'] == 'success'

    def test_batch_jsonl_dry_run(self, tmp_path: Path) -> None:
        """Batch mode auto-detected from .jsonl file."""
        p = tmp_path / 'test.prompt'
        p.write_text(self.BATCH_PROMPT, encoding='utf-8')
        input_file = tmp_path / 'batch.jsonl'
        input_file.write_text('{"topic": "AI"}\n{"topic": "ML"}', encoding='utf-8')
        result = _run_cli(str(p), str(input_file), '--dry-run')
        assert result.returncode == 0, result.stderr
        lines = [line for line in result.stdout.strip().split('\n') if line]
        assert len(lines) == 2

    def test_single_element_array_is_batch(self, tmp_path: Path) -> None:
        """Single-element array should trigger batch mode."""
        p = tmp_path / 'test.prompt'
        p.write_text(self.BATCH_PROMPT, encoding='utf-8')
        input_file = tmp_path / 'batch.json'
        input_file.write_text('[{"topic": "AI"}]', encoding='utf-8')
        result = _run_cli(str(p), str(input_file), '--dry-run')
        assert result.returncode == 0, result.stderr
        lines = [line for line in result.stdout.strip().split('\n') if line]
        assert len(lines) == 1
        item = json.loads(lines[0])
        assert item['index'] == 0

    def test_empty_array_exits_cleanly(self, tmp_path: Path) -> None:
        """Empty array should exit silently with code 0."""
        p = tmp_path / 'test.prompt'
        p.write_text(self.BATCH_PROMPT, encoding='utf-8')
        input_file = tmp_path / 'batch.json'
        input_file.write_text('[]', encoding='utf-8')
        result = _run_cli(str(p), str(input_file), '--dry-run')
        assert result.returncode == 0
        assert result.stdout == ''  # No output
        assert result.stderr == ''  # No error message


class TestCLIDryRunVerbose:
    """Test --verbose flag in dry-run mode."""

    def test_verbose_dry_run_shows_config(self, tmp_path: Path) -> None:
        p = tmp_path / 'test.prompt'
        p.write_text(SIMPLE_PROMPT, encoding='utf-8')
        input_file = tmp_path / 'input.json'
        input_file.write_text('{"topic": "AI"}', encoding='utf-8')
        result = _run_cli(str(p), str(input_file), '--dry-run', '--verbose')
        assert result.returncode == 0, result.stderr
        # Verbose still produces valid JSON on stdout
        data = json.loads(result.stdout)
        assert data['model'] == 'gpt-4o'
