"""
Module: test_add_command
Description: Tests for the pack add-command CLI

Implements:
    - docs/cli/reference.md
"""

import os
import textwrap
from pathlib import Path

import pytest
from typer.testing import CliRunner

from huitzo_cli.commands.add_command import (
    add_entry_point_to_pyproject,
    add_command_to_huitzo_yaml,
    command_name_to_class,
    command_name_to_file,
    command_name_to_function,
    detect_pack_context,
    update_commands_init,
    validate_command_name,
)
from huitzo_cli.main import app


# ---------------------------------------------------------------------------
# Unit tests: helpers
# ---------------------------------------------------------------------------


class TestValidateCommandName:
    def test_valid_simple(self) -> None:
        assert validate_command_name("hello")

    def test_valid_hyphenated(self) -> None:
        assert validate_command_name("analyze-data")

    def test_valid_with_digits(self) -> None:
        assert validate_command_name("run2")

    def test_valid_multi_hyphen(self) -> None:
        assert validate_command_name("do-the-thing")

    def test_invalid_uppercase(self) -> None:
        assert not validate_command_name("Hello")

    def test_invalid_underscore(self) -> None:
        assert not validate_command_name("analyze_data")

    def test_invalid_starts_with_hyphen(self) -> None:
        assert not validate_command_name("-hello")

    def test_invalid_ends_with_hyphen(self) -> None:
        assert not validate_command_name("hello-")

    def test_invalid_double_hyphen(self) -> None:
        assert not validate_command_name("hello--world")

    def test_invalid_empty(self) -> None:
        assert not validate_command_name("")

    def test_invalid_starts_with_digit(self) -> None:
        assert not validate_command_name("1hello")


class TestCommandNameConversions:
    def test_to_function(self) -> None:
        assert command_name_to_function("analyze-data") == "analyze_data"

    def test_to_function_simple(self) -> None:
        assert command_name_to_function("hello") == "hello"

    def test_to_file(self) -> None:
        assert command_name_to_file("analyze-data") == "analyze_data"

    def test_to_class(self) -> None:
        assert command_name_to_class("analyze-data") == "AnalyzeDataArgs"

    def test_to_class_single(self) -> None:
        assert command_name_to_class("hello") == "HelloArgs"

    def test_to_class_triple(self) -> None:
        assert command_name_to_class("do-the-thing") == "DoTheThingArgs"


# ---------------------------------------------------------------------------
# Unit tests: detect_pack_context
# ---------------------------------------------------------------------------


def _scaffold_pack(root: Path) -> None:
    """Create a minimal pack structure for testing."""
    (root / "src" / "my_pack" / "commands").mkdir(parents=True, exist_ok=True)
    (root / "src" / "my_pack" / "commands" / "__init__.py").write_text(
        '"""Commands."""\n\nfrom .hello import hello_world\n\n__all__ = ["hello_world"]\n'
    )
    (root / "src" / "my_pack" / "commands" / "hello.py").write_text(
        'async def hello_world(args, ctx): return {"message": "Hello"}\n'
    )
    (root / "tests").mkdir(parents=True, exist_ok=True)
    (root / "huitzo.yaml").write_text(
        textwrap.dedent("""\
        pack:
          name: my-pack
          namespace: my-ns
          version: 0.0.0
          description: Test pack
          author: Dev

        commands:
          - name: hello
            description: A hello command
            enabled: true
        """)
    )
    (root / "pyproject.toml").write_text(
        textwrap.dedent("""\
        [project]
        name = "my-pack"
        version = "0.0.0"
        description = "Test pack"
        requires-python = ">=3.11"
        dependencies = ["huitzo-sdk>=0.0.1"]

        [project.entry-points."huitzo.commands"]
        "@my-ns/my-pack/hello" = "my_pack.commands.hello:hello_world"

        [build-system]
        requires = ["hatchling"]
        build-backend = "hatchling.build"
        """)
    )


class TestDetectPackContext:
    def test_valid_pack(self, temp_dir: Path) -> None:
        _scaffold_pack(temp_dir)
        ctx = detect_pack_context(temp_dir)
        assert ctx.pack_name == "my-pack"
        assert ctx.namespace == "my-ns"
        assert ctx.module_name == "my_pack"
        assert "hello" in ctx.existing_commands

    def test_no_huitzo_yaml(self, temp_dir: Path) -> None:
        from click.exceptions import Exit

        (temp_dir / "pyproject.toml").write_text("[project]\nname = 'x'\n")
        with pytest.raises(Exit):
            detect_pack_context(temp_dir)

    def test_no_pyproject(self, temp_dir: Path) -> None:
        from click.exceptions import Exit

        (temp_dir / "huitzo.yaml").write_text("pack:\n  name: x\n  namespace: y\n")
        with pytest.raises(Exit):
            detect_pack_context(temp_dir)


# ---------------------------------------------------------------------------
# Unit tests: file modification helpers
# ---------------------------------------------------------------------------


class TestAddEntryPointToPyproject:
    def test_appends_entry_point(self, temp_dir: Path) -> None:
        _scaffold_pack(temp_dir)
        pyproject = temp_dir / "pyproject.toml"

        add_entry_point_to_pyproject(
            pyproject,
            "my-ns",
            "my-pack",
            "analyze-data",
            "my_pack",
            "analyze_data",
            "analyze_data",
        )

        content = pyproject.read_text()
        assert (
            '"@my-ns/my-pack/analyze-data" = "my_pack.commands.analyze_data:analyze_data"'
            in content
        )
        # Original entry still present
        assert '"@my-ns/my-pack/hello"' in content

    def test_creates_section_if_missing(self, temp_dir: Path) -> None:
        pyproject = temp_dir / "pyproject.toml"
        pyproject.write_text(
            textwrap.dedent("""\
            [project]
            name = "my-pack"

            [build-system]
            requires = ["hatchling"]
            """)
        )

        add_entry_point_to_pyproject(
            pyproject,
            "ns",
            "pk",
            "cmd",
            "pk_mod",
            "cmd_file",
            "cmd_func",
        )

        content = pyproject.read_text()
        assert '[project.entry-points."huitzo.commands"]' in content
        assert '"@ns/pk/cmd"' in content
        # build-system should still be present
        assert "[build-system]" in content


class TestAddCommandToHuitzoYaml:
    def test_appends_command(self, temp_dir: Path) -> None:
        _scaffold_pack(temp_dir)
        yaml_path = temp_dir / "huitzo.yaml"

        add_command_to_huitzo_yaml(
            yaml_path,
            "analyze-data",
            "Analyze data",
            timeout=30,
            queue="fast",
            retries=2,
            permissions=["llm:complete", "storage:write"],
        )

        content = yaml_path.read_text()
        assert "  - name: analyze-data" in content
        assert '    description: "Analyze data"' in content
        assert "    timeout: 30" in content
        assert "    queue: fast" in content
        assert "    retries: 2" in content
        assert "      - llm:complete" in content
        assert "      - storage:write" in content

    def test_defaults_omitted(self, temp_dir: Path) -> None:
        _scaffold_pack(temp_dir)
        yaml_path = temp_dir / "huitzo.yaml"

        add_command_to_huitzo_yaml(yaml_path, "simple", "Simple cmd")

        content = yaml_path.read_text()
        assert "  - name: simple" in content
        # Extract only the "simple" command section
        simple_section = content.split("- name: simple")[1]
        if "- name:" in simple_section:
            simple_section = simple_section.split("- name:")[0]
        assert "timeout" not in simple_section
        assert "queue" not in simple_section
        assert "retries" not in simple_section

    def test_creates_commands_section(self, temp_dir: Path) -> None:
        yaml_path = temp_dir / "huitzo.yaml"
        yaml_path.write_text("pack:\n  name: test\n  namespace: ns\n")

        add_command_to_huitzo_yaml(yaml_path, "new-cmd", "A new command")

        content = yaml_path.read_text()
        assert "commands:" in content
        assert "  - name: new-cmd" in content


class TestUpdateCommandsInit:
    def test_adds_import_and_all(self, temp_dir: Path) -> None:
        _scaffold_pack(temp_dir)
        init_path = temp_dir / "src" / "my_pack" / "commands" / "__init__.py"

        update_commands_init(init_path, "analyze_data", "analyze_data")

        content = init_path.read_text()
        assert "from .analyze_data import analyze_data" in content
        assert '"analyze_data"' in content
        # Original import still present
        assert "from .hello import hello_world" in content

    def test_creates_init_if_missing(self, temp_dir: Path) -> None:
        init_path = temp_dir / "__init__.py"
        update_commands_init(init_path, "my_cmd", "my_cmd")

        content = init_path.read_text()
        assert "from .my_cmd import my_cmd" in content
        assert '__all__ = ["my_cmd"]' in content

    def test_no_duplicate_import(self, temp_dir: Path) -> None:
        _scaffold_pack(temp_dir)
        init_path = temp_dir / "src" / "my_pack" / "commands" / "__init__.py"

        # Add same command twice
        update_commands_init(init_path, "hello", "hello_world")

        content = init_path.read_text()
        # Should only appear once (already existed)
        assert content.count("from .hello import hello_world") == 1


# ---------------------------------------------------------------------------
# CLI integration tests
# ---------------------------------------------------------------------------


class TestAddCommandCLI:
    def _run_add_command(
        self,
        temp_dir: Path,
        extra_args: list[str] | None = None,
        input_text: str | None = None,
    ) -> object:
        """Helper to run pack add-command in a scaffolded pack."""
        _scaffold_pack(temp_dir)
        runner = CliRunner()
        old_cwd = os.getcwd()
        try:
            os.chdir(temp_dir)
            args = ["pack", "add-command"]
            if extra_args:
                args.extend(extra_args)
            return runner.invoke(app, args, input=input_text)
        finally:
            os.chdir(old_cwd)

    def test_non_interactive_basic(self, temp_dir: Path) -> None:
        result = self._run_add_command(
            temp_dir,
            [
                "analyze-data",
                "-d",
                "Analyze data",
            ],
            input_text="\n",
        )

        assert result.exit_code == 0, result.stdout
        assert "Done!" in result.stdout
        assert "@my-ns/my-pack/analyze-data" in result.stdout

        # Verify files
        assert (temp_dir / "src" / "my_pack" / "commands" / "analyze_data.py").exists()
        assert (temp_dir / "tests" / "test_analyze_data.py").exists()

        # Verify pyproject.toml updated
        pyproject = (temp_dir / "pyproject.toml").read_text()
        assert "analyze-data" in pyproject

        # Verify huitzo.yaml updated
        yaml_content = (temp_dir / "huitzo.yaml").read_text()
        assert "analyze-data" in yaml_content

        # Verify commands/__init__.py updated
        init_content = (temp_dir / "src" / "my_pack" / "commands" / "__init__.py").read_text()
        assert "analyze_data" in init_content

    def test_with_all_options(self, temp_dir: Path) -> None:
        result = self._run_add_command(
            temp_dir,
            [
                "process-claim",
                "-d",
                "Process insurance claim",
                "-p",
                "llm:complete,storage:write",
                "-t",
                "120",
                "-q",
                "long",
                "-r",
                "5",
            ],
        )

        assert result.exit_code == 0, result.stdout

        # Check command file has pydantic args by default
        cmd_content = (temp_dir / "src" / "my_pack" / "commands" / "process_claim.py").read_text()
        assert "ProcessClaimArgs" in cmd_content
        assert "from pydantic import" in cmd_content
        assert 'namespace="my-ns"' in cmd_content
        assert "timeout=120" in cmd_content

        # Check huitzo.yaml has permissions
        yaml_content = (temp_dir / "huitzo.yaml").read_text()
        assert "queue: long" in yaml_content
        assert "retries: 5" in yaml_content
        assert "- llm:complete" in yaml_content

    def test_no_pydantic(self, temp_dir: Path) -> None:
        result = self._run_add_command(
            temp_dir,
            [
                "simple-cmd",
                "-d",
                "Simple",
                "--no-pydantic",
            ],
            input_text="\n",
        )

        assert result.exit_code == 0, result.stdout

        cmd_content = (temp_dir / "src" / "my_pack" / "commands" / "simple_cmd.py").read_text()
        assert "pydantic" not in cmd_content.lower()
        assert "args: dict" in cmd_content

    def test_invalid_name(self, temp_dir: Path) -> None:
        result = self._run_add_command(temp_dir, ["InvalidName", "-d", "Bad"])
        assert result.exit_code == 1
        assert "Invalid command name" in result.stdout

    def test_duplicate_command(self, temp_dir: Path) -> None:
        result = self._run_add_command(temp_dir, ["hello", "-d", "Dupe"])
        assert result.exit_code == 1
        assert "already exists" in result.stdout

    def test_invalid_queue(self, temp_dir: Path) -> None:
        result = self._run_add_command(
            temp_dir,
            [
                "test-cmd",
                "-d",
                "Test",
                "-q",
                "invalid",
            ],
        )
        assert result.exit_code == 1
        assert "Invalid queue" in result.stdout

    def test_not_a_pack_directory(self, temp_dir: Path) -> None:
        """Running outside a pack directory should error."""
        runner = CliRunner()
        old_cwd = os.getcwd()
        try:
            os.chdir(temp_dir)
            result = runner.invoke(app, ["pack", "add-command", "test", "-d", "x"])
            assert result.exit_code == 1
            assert "huitzo.yaml" in result.stdout
        finally:
            os.chdir(old_cwd)

    def test_python_keyword_name(self, temp_dir: Path) -> None:
        """Command name that maps to a Python keyword should be rejected."""
        result = self._run_add_command(temp_dir, ["import", "-d", "Bad name"])
        assert result.exit_code == 1
        assert "invalid Python identifier" in result.stdout

    def test_flat_commands_py_rejected(self, temp_dir: Path) -> None:
        """Flat commands.py layout should be rejected."""
        _scaffold_pack(temp_dir)
        # Replace commands/ with commands.py
        import shutil

        commands_dir = temp_dir / "src" / "my_pack" / "commands"
        shutil.rmtree(commands_dir)
        (temp_dir / "src" / "my_pack" / "commands.py").write_text("# flat\n")

        runner = CliRunner()
        old_cwd = os.getcwd()
        try:
            os.chdir(temp_dir)
            result = runner.invoke(
                app,
                [
                    "pack",
                    "add-command",
                    "new-cmd",
                    "-d",
                    "test",
                ],
                input="\n",
            )
            assert result.exit_code == 1
            assert "flat commands.py" in result.stdout
        finally:
            os.chdir(old_cwd)

    def test_multiple_commands_added(self, temp_dir: Path) -> None:
        """Adding two commands sequentially should work."""
        _scaffold_pack(temp_dir)
        runner = CliRunner()
        old_cwd = os.getcwd()
        try:
            os.chdir(temp_dir)

            # First command
            r1 = runner.invoke(
                app,
                [
                    "pack",
                    "add-command",
                    "cmd-one",
                    "-d",
                    "First",
                ],
                input="\n",
            )
            assert r1.exit_code == 0, r1.stdout

            # Second command
            r2 = runner.invoke(
                app,
                [
                    "pack",
                    "add-command",
                    "cmd-two",
                    "-d",
                    "Second",
                ],
                input="\n",
            )
            assert r2.exit_code == 0, r2.stdout

            # Both files exist
            assert (temp_dir / "src" / "my_pack" / "commands" / "cmd_one.py").exists()
            assert (temp_dir / "src" / "my_pack" / "commands" / "cmd_two.py").exists()

            # Both in __init__.py
            init_content = (temp_dir / "src" / "my_pack" / "commands" / "__init__.py").read_text()
            assert "from .cmd_one import cmd_one" in init_content
            assert "from .cmd_two import cmd_two" in init_content

            # Both in pyproject.toml
            pyproject = (temp_dir / "pyproject.toml").read_text()
            assert "cmd-one" in pyproject
            assert "cmd-two" in pyproject

            # Both in huitzo.yaml
            yaml_content = (temp_dir / "huitzo.yaml").read_text()
            assert "cmd-one" in yaml_content
            assert "cmd-two" in yaml_content

        finally:
            os.chdir(old_cwd)

    def test_path_option(self, temp_dir: Path) -> None:
        """The --path option should work from any directory."""
        _scaffold_pack(temp_dir)
        runner = CliRunner()
        # Run from a different directory, pointing --path to the pack
        result = runner.invoke(
            app,
            [
                "pack",
                "add-command",
                "remote-cmd",
                "-d",
                "Remote command",
                "--path",
                str(temp_dir),
                "-p",
                "",
            ],
        )
        assert result.exit_code == 0, result.stdout
        assert (temp_dir / "src" / "my_pack" / "commands" / "remote_cmd.py").exists()

    def test_generated_files_are_valid_python(self, temp_dir: Path) -> None:
        """Generated command and test files should be syntactically valid Python."""
        import ast

        result = self._run_add_command(
            temp_dir,
            [
                "valid-syntax",
                "-d",
                "Syntax test",
                "-p",
                "",
            ],
        )
        assert result.exit_code == 0, result.stdout

        cmd_source = (temp_dir / "src" / "my_pack" / "commands" / "valid_syntax.py").read_text()
        ast.parse(cmd_source)  # Raises SyntaxError if invalid

        test_source = (temp_dir / "tests" / "test_valid_syntax.py").read_text()
        ast.parse(test_source)  # Raises SyntaxError if invalid

    def test_description_with_special_chars(self, temp_dir: Path) -> None:
        """Description with YAML-special characters should be safely quoted."""
        import yaml as pyyaml

        _scaffold_pack(temp_dir)
        runner = CliRunner()
        old_cwd = os.getcwd()
        try:
            os.chdir(temp_dir)
            result = runner.invoke(
                app,
                [
                    "pack",
                    "add-command",
                    "special-desc",
                    "-d",
                    "Analyze: data & more # stuff",
                    "-p",
                    "",
                ],
            )
            assert result.exit_code == 0, result.stdout

            # Verify huitzo.yaml is still valid YAML
            yaml_content = (temp_dir / "huitzo.yaml").read_text()
            parsed = pyyaml.safe_load(yaml_content)
            commands = parsed.get("commands", [])
            special_cmd = [c for c in commands if c["name"] == "special-desc"]
            assert len(special_cmd) == 1
            assert "Analyze: data & more # stuff" in special_cmd[0]["description"]
        finally:
            os.chdir(old_cwd)


class TestDetectPackContextEdgeCases:
    def test_invalid_yaml(self, temp_dir: Path) -> None:
        """Invalid YAML in huitzo.yaml should produce a clear error."""
        from click.exceptions import Exit

        (temp_dir / "pyproject.toml").write_text("[project]\nname = 'x'\n")
        (temp_dir / "huitzo.yaml").write_text("invalid: yaml: {{{broken")
        with pytest.raises(Exit):
            detect_pack_context(temp_dir)

    def test_empty_yaml(self, temp_dir: Path) -> None:
        """Empty huitzo.yaml should produce a clear error."""
        from click.exceptions import Exit

        (temp_dir / "pyproject.toml").write_text("[project]\nname = 'x'\n")
        (temp_dir / "huitzo.yaml").write_text("")
        with pytest.raises(Exit):
            detect_pack_context(temp_dir)
