"""Tests for promptkit.cli module using Click's CliRunner."""

import json
import os
from pathlib import Path
from typing import Optional

import pytest
from click.testing import CliRunner

from promptkit.cli import main
from promptkit.config import Config
from promptkit.store import PromptStore


@pytest.fixture
def runner() -> CliRunner:
    """Return a Click CliRunner."""
    return CliRunner(mix_stderr=False)


@pytest.fixture
def cli_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    """Initialize a promptkit project and chdir into it."""
    Config.init_project(tmp_path)
    monkeypatch.chdir(tmp_path)
    return tmp_path


@pytest.fixture
def cli_project_with_prompt(cli_project: Path) -> Path:
    """Initialized project with a sample prompt committed."""
    config = Config(project_dir=cli_project)
    store = PromptStore(config)
    store.commit("greeting", "Hello {{name}}, welcome!", message="init")
    return cli_project


class TestCliInit:
    """Tests for 'promptkit init' command."""

    def test_init_creates_project(self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.chdir(tmp_path)
        result = runner.invoke(main, ["init"])
        assert result.exit_code == 0
        assert (tmp_path / ".promptkit").is_dir()

    def test_init_already_initialized(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["init"])
        assert result.exit_code == 0
        assert "Already initialized" in result.output or "force" in result.output.lower()

    def test_init_force(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["init", "--force"])
        assert result.exit_code == 0
        assert (cli_project / ".promptkit").is_dir()


class TestCliSave:
    """Tests for 'promptkit save' command."""

    def test_save_inline(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["save", "my-prompt", "-c", "Hello world"])
        assert result.exit_code == 0

    def test_save_from_file(self, runner: CliRunner, cli_project: Path) -> None:
        prompt_file = cli_project / "prompt.md"
        prompt_file.write_text("Content from file")
        result = runner.invoke(main, ["save", "my-prompt", "-f", str(prompt_file)])
        assert result.exit_code == 0

    def test_save_with_message(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["save", "my-prompt", "-c", "content", "-m", "Initial commit"])
        assert result.exit_code == 0

    def test_save_with_description(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, [
            "save", "my-prompt", "-c", "content", "-d", "A greeting prompt"
        ])
        assert result.exit_code == 0

    def test_save_duplicate_warns(self, runner: CliRunner, cli_project: Path) -> None:
        runner.invoke(main, ["save", "dup", "-c", "same"])
        result = runner.invoke(main, ["save", "dup", "-c", "same"])
        assert result.exit_code == 0  # warns but doesn't crash
        assert "unchanged" in result.output.lower() or "Content unchanged" in result.output

    def test_save_no_content_from_empty_stdin(self, runner: CliRunner, cli_project: Path) -> None:
        # When stdin is provided (empty string), CLI reads it and commits empty content
        # which may succeed or warn depending on implementation
        result = runner.invoke(main, ["save", "my-prompt"], input="")
        # Just verify no unhandled exception
        assert result.exception is None or isinstance(result.exception, SystemExit)


class TestCliGet:
    """Tests for 'promptkit get' command."""

    def test_get_existing(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["get", "greeting"])
        assert result.exit_code == 0

    def test_get_raw(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["get", "greeting", "--raw"])
        assert result.exit_code == 0
        assert "{{name}}" in result.output

    def test_get_with_var(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["get", "greeting", "--raw", "--var", "name=Alice"])
        assert result.exit_code == 0
        assert "Alice" in result.output

    def test_get_nonexistent(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["get", "missing"])
        assert result.exit_code != 0


class TestCliList:
    """Tests for 'promptkit list' command."""

    def test_list_empty(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["list"])
        assert result.exit_code == 0

    def test_list_with_prompts(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["list"])
        assert result.exit_code == 0

    def test_list_search(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["list", "-s", "greeting"])
        assert result.exit_code == 0


class TestCliHistory:
    """Tests for 'promptkit history' command."""

    def test_history_existing(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["history", "greeting"])
        assert result.exit_code == 0

    def test_history_nonexistent(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["history", "missing"])
        assert result.exit_code != 0


class TestCliDiff:
    """Tests for 'promptkit diff' command."""

    def test_diff_two_versions(self, runner: CliRunner, cli_project: Path) -> None:
        runner.invoke(main, ["save", "dp", "-c", "Version one"])
        runner.invoke(main, ["save", "dp", "-c", "Version two"])
        result = runner.invoke(main, ["diff", "dp", "1", "2"])
        assert result.exit_code == 0

    def test_diff_nonexistent(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["diff", "missing", "1", "2"])
        assert result.exit_code != 0


class TestCliTag:
    """Tests for 'promptkit tag' and 'untag' commands."""

    def test_tag_prompt(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["tag", "greeting", "prod"])
        assert result.exit_code == 0

    def test_tag_with_version(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["tag", "greeting", "stable", "-v", "1"])
        assert result.exit_code == 0

    def test_tag_nonexistent(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["tag", "missing", "prod"])
        assert result.exit_code != 0

    def test_untag(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        runner.invoke(main, ["tag", "greeting", "staging"])
        result = runner.invoke(main, ["untag", "greeting", "staging"])
        assert result.exit_code == 0

    def test_untag_latest_fails(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["untag", "greeting", "latest"])
        assert result.exit_code != 0


class TestCliDelete:
    """Tests for 'promptkit delete' command."""

    def test_delete_with_yes(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["delete", "greeting", "-y"])
        assert result.exit_code == 0

    def test_delete_with_confirm(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["delete", "greeting"], input="y\n")
        assert result.exit_code == 0

    def test_delete_nonexistent(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["delete", "missing", "-y"])
        assert result.exit_code != 0


class TestCliImportExport:
    """Tests for 'promptkit import' and 'export' commands."""

    def test_import_file(self, runner: CliRunner, cli_project: Path) -> None:
        src = cli_project / "source.md"
        src.write_text("Imported prompt content")
        result = runner.invoke(main, ["import", "imported", str(src)])
        assert result.exit_code == 0

    def test_export_file(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        out = cli_project_with_prompt / "exported.md"
        result = runner.invoke(main, ["export", "greeting", str(out)])
        assert result.exit_code == 0
        assert out.exists()

    def test_export_nonexistent(self, runner: CliRunner, cli_project: Path) -> None:
        out = cli_project / "out.md"
        result = runner.invoke(main, ["export", "missing", str(out)])
        assert result.exit_code != 0


class TestCliCost:
    """Tests for 'promptkit cost' command."""

    def test_cost_default_model(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["cost", "greeting"])
        assert result.exit_code == 0

    def test_cost_specific_model(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["cost", "greeting", "-m", "gpt-4o-mini"])
        assert result.exit_code == 0

    def test_cost_unknown_model(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["cost", "greeting", "-m", "nonexistent"])
        assert result.exit_code != 0

    def test_cost_nonexistent_prompt(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["cost", "missing"])
        assert result.exit_code != 0


class TestCliRender:
    """Tests for 'promptkit render' command."""

    def test_render_with_var(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["render", "greeting", "--var", "name=Alice"])
        assert result.exit_code == 0

    def test_render_no_var_errors(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["render", "greeting"])
        assert result.exit_code != 0


class TestCliStats:
    """Tests for 'promptkit stats' command."""

    def test_stats_empty(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["stats"])
        assert result.exit_code == 0

    def test_stats_with_data(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["stats"])
        assert result.exit_code == 0


class TestCliLint:
    """Tests for 'promptkit lint' command."""

    def test_lint_prompt(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["lint", "greeting"])
        # exit code 0 if passed, 1 if failed — either is valid
        assert result.exit_code in (0, 1)

    def test_lint_json_output(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        result = runner.invoke(main, ["lint", "greeting", "--json-output"])
        assert result.exit_code == 0
        data = json.loads(result.output)
        assert "score" in data

    def test_lint_nonexistent(self, runner: CliRunner, cli_project: Path) -> None:
        result = runner.invoke(main, ["lint", "missing"])
        assert result.exit_code != 0


class TestCliTest:
    """Tests for 'promptkit test' command."""

    def test_test_with_json(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        test_file = cli_project_with_prompt / "tests.json"
        test_file.write_text(json.dumps({
            "tests": [
                {
                    "name": "basic",
                    "variables": {"name": "Bob"},
                    "assertions": [{"type": "contains", "value": "Bob"}],
                }
            ]
        }))
        result = runner.invoke(main, ["test", "greeting", str(test_file)])
        assert result.exit_code in (0, 1)

    def test_test_json_output(self, runner: CliRunner, cli_project_with_prompt: Path) -> None:
        test_file = cli_project_with_prompt / "tests.json"
        test_file.write_text(json.dumps({
            "tests": [
                {
                    "name": "basic",
                    "variables": {"name": "Bob"},
                    "assertions": [{"type": "contains", "value": "Bob"}],
                }
            ]
        }))
        result = runner.invoke(main, ["test", "greeting", str(test_file), "--json-output"])
        assert result.exit_code == 0
        data = json.loads(result.output)
        assert "summary" in data

    def test_test_nonexistent_prompt(self, runner: CliRunner, cli_project: Path) -> None:
        test_file = cli_project / "tests.json"
        test_file.write_text(json.dumps({"tests": []}))
        result = runner.invoke(main, ["test", "missing", str(test_file)])
        assert result.exit_code != 0


class TestCliVersionFlag:
    """Tests for --version flag."""

    def test_version(self, runner: CliRunner) -> None:
        result = runner.invoke(main, ["--version"])
        assert result.exit_code == 0
        assert "promptkit" in result.output.lower() or "0.1.0" in result.output
