import re
from pathlib import Path
from typing import Annotated, Literal

import pytest

from cyclopts import App, Parameter
from cyclopts.completion.zsh import generate_completion_script

from .apps import (
    app_basic,
    app_disabled_negative,
    app_enum,
    app_list_annotated_path,
    app_list_path,
    app_markup,
    app_negative,
    app_nested,
    app_path,
    app_rst,
)


def test_basic_option_completion(zsh_tester):
    """Test basic option name completion."""
    tester = zsh_tester(app_basic, "basic")

    assert "--verbose" in tester.completion_script
    assert "--count" in tester.completion_script
    assert "--help" in tester.completion_script


def test_command_completion(zsh_tester):
    """Test command completion."""
    tester = zsh_tester(app_basic, "basic")

    assert "deploy" in tester.completion_script


def test_literal_value_completion(zsh_tester):
    """Test Literal type value completion."""
    tester = zsh_tester(app_basic, "basic")

    assert "dev" in tester.completion_script
    assert "staging" in tester.completion_script
    assert "prod" in tester.completion_script


def test_enum_value_completion(zsh_tester):
    """Test Enum type value completion."""
    tester = zsh_tester(app_enum, "enumapp")

    assert "fast" in tester.completion_script
    assert "slow" in tester.completion_script


def test_nested_subcommand_completion(zsh_tester):
    """Test nested subcommand completion."""
    tester = zsh_tester(app_nested, "nested")

    assert "config" in tester.completion_script
    assert "get" in tester.completion_script
    assert "set" in tester.completion_script


def test_negative_flag_completion(zsh_tester):
    """Test negative flag completion."""
    tester = zsh_tester(app_negative, "negapp")

    assert "--no-verbose" in tester.completion_script
    assert "--no-colors" in tester.completion_script


def test_disabled_negative_flag_completion(zsh_tester):
    """Test that negative flags are not generated when disabled via App default_parameter."""
    tester = zsh_tester(app_disabled_negative, "disabledneg")

    assert "--param" in tester.completion_script
    assert "--empty-param" not in tester.completion_script


def test_help_descriptions(zsh_tester):
    """Test that help descriptions appear in completions."""
    tester = zsh_tester(app_basic, "basic")

    assert "Enable verbose output" in tester.completion_script
    assert "Deploy to environment" in tester.completion_script


def test_script_syntax_valid(zsh_tester):
    """Test that generated script has valid zsh syntax."""
    tester = zsh_tester(app_basic, "basic")

    assert tester.validate_script_syntax()


def test_compdef_header(zsh_tester):
    """Test that script has proper #compdef header."""
    tester = zsh_tester(app_basic, "basic")

    assert tester.completion_script.startswith("#compdef basic")


def test_path_completion(zsh_tester):
    """Test that Path types generate file completion."""
    tester = zsh_tester(app_path, "pathapp")

    assert "_files" in tester.completion_script


def test_optional_path_completion(zsh_tester):
    """Test that Optional[Path] and Path | None generate file completion.

    Value-bearing options carry a ``*`` prefix so the option can repeat
    (required for collection-typed options like ``list[Path]``). The
    option name has no ``=`` suffix by default — TAB-completion inserts
    ``--opt`` plus a space rather than ``--opt=``. ``Parameter(requires_equals=True)``
    flips this on and is exercised separately.
    """
    tester = zsh_tester(app_path, "pathapp")

    assert "'*--output[Output file]:output:_files'" in tester.completion_script


def test_nested_command_uses_correct_word_index(zsh_tester):
    """Test that nested commands use $words[1] for subcommand dispatch."""
    tester = zsh_tester(app_nested, "nested")

    script_lines = tester.completion_script.split("\n")
    words_checks = [line for line in script_lines if "case $words[1] in" in line]

    assert len(words_checks) >= 2, "Should have multiple case $words[1] checks for nested commands"


def test_invalid_prog_name():
    """Test that invalid prog names raise ValueError."""
    with pytest.raises(ValueError, match="Invalid prog_name"):
        generate_completion_script(app_basic, "foo bar")

    with pytest.raises(ValueError, match="Invalid prog_name"):
        generate_completion_script(app_basic, "test;rm -rf /")

    with pytest.raises(ValueError, match="Invalid prog_name"):
        generate_completion_script(app_basic, "")


def test_description_escaping(zsh_tester):
    """Test that descriptions with special chars are properly escaped.

    Note: Backticks are now treated as markdown code syntax and stripped,
    so they no longer appear in the completion script.
    """
    app = App(name="escape_test")

    @app.default
    def main(
        param1: Annotated[str, Parameter(help="Test 'single' quotes")] = "",
        param2: Annotated[str, Parameter(help='Test "double" quotes')] = "",
        param3: Annotated[str, Parameter(help="Test $variable and `backticks`")] = "",
        param4: Annotated[str, Parameter(help="Test [brackets] here")] = "",
    ):
        """Test app."""

    tester = zsh_tester(app, "escape_test")

    assert r"'\'' " in tester.completion_script or "'\\''" in tester.completion_script
    assert "\\$" in tester.completion_script
    # Backticks are now stripped as markdown code syntax
    assert "backticks" in tester.completion_script
    assert "\\`" not in tester.completion_script
    assert r"\[" in tester.completion_script
    assert r"\]" in tester.completion_script


def test_special_chars_in_literal_choices(zsh_tester):
    """Test that Literal choices with special characters are properly escaped.

    Choice-bearing specs are emitted with double-quoted outer strings (so a
    literal ``'`` can be embedded) which means each backslash from the inner
    choice-list-parser layer is doubled by the outer DQ layer. The runtime
    behavior is verified separately in ``test_choice_with_*`` below.
    """
    app = App(name="special_choices")

    @app.default
    def main(
        choice: Annotated[Literal["foo bar", "baz()", "test[1]", "back\\slash"], Parameter()] = "foo bar",
    ):
        """Test app with special chars in choices."""

    tester = zsh_tester(app, "special_choices")

    # After the two-layer escape ("" outer + choice-list parser): every
    # backslash from layer 1 is doubled.
    assert r"foo\\ bar" in tester.completion_script
    assert r"baz\\(\\)" in tester.completion_script
    assert r"test\\[1\\]" in tester.completion_script
    # User-supplied backslash gets layer-1 ``\\\\`` then DQ-doubled to ``\\\\\\\\``
    assert r"back\\\\slash" in tester.completion_script


def test_unicode_in_descriptions(zsh_tester):
    """Test that Unicode characters in descriptions are handled properly."""
    app = App(name="unicode_test")

    @app.default
    def main(
        emoji: Annotated[str, Parameter(help="Enable 🚀 rocket mode")] = "",
        chinese: Annotated[str, Parameter(help="中文描述")] = "",
        arabic: Annotated[str, Parameter(help="وصف بالعربية")] = "",
    ):
        """Test app with Unicode."""

    tester = zsh_tester(app, "unicode_test")

    assert "🚀" in tester.completion_script or "rocket mode" in tester.completion_script
    assert tester.validate_script_syntax()


def test_deeply_nested_commands(zsh_tester):
    """Test completion for deeply nested commands (3+ levels)."""
    root = App(name="root")
    level1 = App(name="level1")
    level2 = App(name="level2")
    level3 = App(name="level3")

    @level3.command
    def action(value: str):
        """Perform action at level 3."""
        pass

    level2.command(level3)
    level1.command(level2)
    root.command(level1)

    tester = zsh_tester(root, "root")

    assert "level1" in tester.completion_script
    assert "level2" in tester.completion_script
    assert "level3" in tester.completion_script
    assert "action" in tester.completion_script
    assert tester.validate_script_syntax()


def test_no_trailing_colons_in_specs(zsh_tester):
    """Test that argument specs don't have trailing colons when action is empty.

    Regression test for issue where specs like '1:description:' or '*:args:'
    would cause zsh eval errors. When there's no completion action, the spec
    should be '1:description' or '*:args' (no trailing colon).
    """
    app = App(name="notrail")

    @app.command
    def run(
        pos1: Annotated[str, Parameter(help="First positional")],
        pos2: Annotated[int, Parameter(help="Second positional")],
        *args: Annotated[str, Parameter(help="Variadic args")],
    ):
        """Command with positionals."""
        pass

    @app.default
    def main(
        flag: Annotated[str, Parameter(help="A non-path option")],
    ):
        """Main command."""
        pass

    tester = zsh_tester(app, "notrail")

    # Check for problematic trailing colons (but not in file/directory actions)
    for line in tester.completion_script.split("\n"):
        stripped = line.strip()
        # Skip comments and lines with valid actions
        if stripped.startswith("#") or ":_files" in line or ":_directories" in line:
            continue
        # Check for trailing :'  patterns (ignoring quote escaping)
        if line.rstrip().endswith(":'") or line.rstrip().endswith(":' \\"):
            if "'\\'':" not in line:  # Not part of quote escaping in descriptions
                raise AssertionError(f"Found trailing colon in spec: {line}")

    assert tester.validate_script_syntax()


def test_colon_escaping_in_descriptions(zsh_tester):
    """Test that colons in descriptions are escaped to prevent field separator issues.

    Regression test for issue where colons in positional argument descriptions
    like ':app_object' would be treated as field separators in specs like
    '1:message:action', causing unmatched quote errors.
    """
    app = App(name="colontest")

    @app.command
    def run(
        script: Annotated[str, Parameter(help="Path with ':app' notation")],
    ):
        """Command with colon in description."""
        pass

    tester = zsh_tester(app, "colontest")

    assert r"\:" in tester.completion_script
    assert tester.validate_script_syntax()


def test_run_command_only_special_for_cyclopts(zsh_tester):
    """Test that 'run' command only gets dynamic completion for cyclopts CLI, not user apps.

    Regression test for issue where any app with a 'run' command would get
    dynamic completion instead of normal static completion.
    """
    app = App(name="myapp")

    @app.command
    def run(
        script: Annotated[str, Parameter(help="Script to execute")],
        verbose: Annotated[bool, Parameter(help="Verbose mode")] = False,
    ):
        """Run a script."""
        pass

    tester = zsh_tester(app, "myapp")

    # Should generate normal static completion, not dynamic completion
    # Dynamic completion has "local script_path", "local -a completions", etc.
    assert "local script_path" not in tester.completion_script
    assert "_complete run" not in tester.completion_script

    # Should have normal argument specs for the run command
    assert "--verbose" in tester.completion_script or "verbose" in tester.completion_script.lower()
    assert tester.validate_script_syntax()


def test_cyclopts_run_command_has_dynamic_completion(zsh_tester):
    """Test that cyclopts CLI's 'run' command gets dynamic completion."""
    from cyclopts.cli import app as cyclopts_app

    tester = zsh_tester(cyclopts_app, "cyclopts")

    # Should have dynamic completion for the run command
    assert "local script_path" in tester.completion_script
    assert "_complete run" in tester.completion_script
    assert tester.validate_script_syntax()


def test_empty_iterable_flag_completion(zsh_tester):
    """Test that --empty-* flags for list parameters are treated as flags.

    Regression test for issue where --empty-items on list[str] parameters
    would expect a value instead of being treated as a flag.
    """
    app = App(name="listapp")

    @app.command
    def process(
        items: Annotated[list[str], Parameter(help="Items to process")],
        tags: Annotated[list[str] | None, Parameter(help="Optional tags")] = None,
        count: Annotated[int, Parameter(help="Count")] = 1,
    ):
        """Process items."""
        pass

    tester = zsh_tester(app, "listapp")

    # Both positive and negative flags should be present
    assert "--items" in tester.completion_script
    assert "--empty-items" in tester.completion_script
    assert "--tags" in tester.completion_script
    assert "--empty-tags" in tester.completion_script
    assert "--count" in tester.completion_script

    # Negative flags should be formatted as flags (no trailing :action)
    # They should have the format '--empty-items[description]' not '--empty-items[description]:empty-items'
    assert "'--empty-items[Items to process]'" in tester.completion_script
    assert "'--empty-tags[Optional tags]'" in tester.completion_script

    # Positive names should expect values (have :action or :name suffix).
    # Match both the plain and ``*``-prefixed (repeatable) spec forms.
    lines_with_items = [
        line for line in tester.completion_script.split("\n") if "--items[" in line and "empty" not in line
    ]
    assert any(":items:" in line or ":items'" in line for line in lines_with_items), (
        "Positive --items flag should expect a value"
    )

    assert tester.validate_script_syntax()


def test_positional_or_keyword_literal_completion(zsh_tester):
    """Test that POSITIONAL_OR_KEYWORD Literal arguments generate positional completion.

    Regression test for issue where 'cyclopts-demo deploy d<TAB>' should complete
    to 'dev' but no completions were shown. The environment parameter is
    POSITIONAL_OR_KEYWORD (not positional-only) and has Literal choices.
    """
    from typing import Literal

    app = App(name="testapp")

    @app.command
    def deploy(
        environment: Literal["dev", "staging", "production"],
        region: Literal["us-east-1", "us-west-2"] = "us-east-1",
    ):
        """Deploy to environment.

        Parameters
        ----------
        environment : Literal["dev", "staging", "production"]
            Target environment.
        region : Literal["us-east-1", "us-west-2"]
            AWS region.
        """
        pass

    tester = zsh_tester(app, "testapp")

    # In nested context, positionals are now included as specs in _arguments
    # Positions are '1:' and '2:' (not '2:' and '3:') because after *::arg:->args,
    # $words[1] is the subcommand and positionals start at position 1.
    # Choice-bearing specs use double-quoted outer to allow embedding ``'``.
    assert '"1:Target environment.:(dev staging production)"' in tester.completion_script
    assert '"2:AWS region.:(us-east-1 us-west-2)"' in tester.completion_script

    # Should have choices for both positionals
    assert "dev" in tester.completion_script
    assert "staging" in tester.completion_script
    assert "production" in tester.completion_script
    assert "us-east-1" in tester.completion_script
    assert "us-west-2" in tester.completion_script

    # Should also have keyword spec for --environment
    assert "--environment" in tester.completion_script

    assert tester.validate_script_syntax()


def test_help_version_flags_in_subcommands(zsh_tester):
    """Test that help and version flags appear in subcommand completions.

    Regression test for issue where --help and --version were only available
    in the root command but not in subcommands.
    """
    tester = zsh_tester(app_basic, "basic")

    script_lines = tester.completion_script.split("\n")

    # Walk from the deploy case label to its closing ``;;``. Track
    # nested ``case ... in / esac`` so the eq-form pre-pass's inner
    # ``;;`` markers don't terminate the scan early.
    in_deploy = False
    case_depth = 0
    deploy_section = []
    for line in script_lines:
        if "deploy)" in line and not in_deploy:
            in_deploy = True
        if in_deploy:
            deploy_section.append(line)
            if re.search(r"\bcase\b.*\bin\b", line):
                case_depth += 1
            elif re.search(r"\besac\b", line):
                case_depth -= 1
            elif ";;" in line and case_depth == 0:
                break

    deploy_text = "\n".join(deploy_section)

    assert "--help[Display this message and exit.]" in deploy_text
    assert "-h[Display this message and exit.]" in deploy_text
    assert "--version[Display application version.]" in deploy_text


def test_nested_command_disambiguation(zsh_tester):
    """Test that nested commands are properly disambiguated.

    This test verifies that the helper function correctly distinguishes between
    commands with overlapping names (e.g., 'config get' vs 'admin get').
    """
    root = App(name="myapp")

    config = App(name="config")
    admin = App(name="admin")

    @config.command(name="get")
    def config_get(key: Annotated[str, Parameter(help="Config key")] = ""):
        """Get config value."""
        pass

    @config.command(name="set")
    def config_set(key: str = "", value: str = ""):
        """Set config value."""
        pass

    @admin.command(name="get")
    def admin_get(user: Annotated[str, Parameter(help="Username")] = ""):
        """Get admin user."""
        pass

    root.command(config)
    root.command(admin)

    tester = zsh_tester(root, "myapp")

    script = tester.completion_script

    config_lines = [line for line in script.split("\n") if "config" in line.lower()]
    admin_lines = [line for line in script.split("\n") if "admin" in line.lower()]

    assert any("config" in line for line in config_lines), "Should have config-specific completions"
    assert any("admin" in line for line in admin_lines), "Should have admin-specific completions"

    assert tester.validate_script_syntax()


def test_helper_function_generation(zsh_tester):
    """Test that command path detection logic is generated when needed."""
    root_only = App(name="rootonly")

    @root_only.default
    def main(verbose: bool = False):
        """Root only app."""
        pass

    tester_root = zsh_tester(root_only, "rootonly")
    script_root = tester_root.completion_script

    nested = App(name="nested")
    sub = App(name="sub")

    @sub.default
    def action():
        """Sub action."""
        pass

    nested.command(sub)

    tester_nested = zsh_tester(nested, "nested")
    script_nested = tester_nested.completion_script

    assert len(script_nested) > len(script_root), "Nested app should have more complex completion logic"


def test_no_file_completion_for_strings(zsh_tester):
    """Test that string options don't default to file completion."""
    app = App(name="strtest")

    @app.default
    def main(
        name: Annotated[str, Parameter(help="Name")] = "default",
    ):
        """Test app."""

    tester = zsh_tester(app, "strtest")
    assert tester.validate_script_syntax()


def test_helper_function_skips_option_values(zsh_tester):
    """Test that helper function correctly identifies and skips option values.

    Critical bug fix test: ensures that when building command path,
    the helper function skips values for options that take arguments.
    Without this, 'myapp --config file.yaml subcommand' would incorrectly
    extract [file.yaml, subcommand] instead of [subcommand].
    """
    app = App(name="myapp")
    sub = App(name="sub")

    @sub.default
    def action():
        """Subcommand action."""
        pass

    @app.default
    def main(
        config: Annotated[str, Parameter(help="Config file path")],
        verbose: bool = False,
    ):
        """Main app."""
        pass

    app.command(sub)

    tester = zsh_tester(app, "myapp")
    script = tester.completion_script

    assert "--config" in script or "config" in script
    assert tester.validate_script_syntax()


def test_markdown_markup_stripped_from_descriptions(zsh_tester):
    """Test that markdown markup is stripped from help descriptions.

    Ensures that **bold**, *italic*, `code`, and other markdown syntax
    is properly removed from completion descriptions.
    """
    tester = zsh_tester(app_markup, "markupapp")
    script = tester.completion_script

    assert "Enable verbose output with extra details" in script, "Should contain plain text version"
    assert "**verbose**" not in script, "Should not contain markdown bold syntax"
    assert "`extra`" not in script, "Should not contain markdown code syntax"

    # Note: zsh escapes colons with backslashes in descriptions
    assert "Choose execution mode" in script and "fast or slow" in script, "Should contain plain text version"
    assert "*execution*" not in script, "Should not contain markdown italic syntax"
    assert "**fast**" not in script, "Should not contain markdown bold in mode description"
    assert "**slow**" not in script, "Should not contain markdown bold in mode description"

    assert "Target environment like dev or prod" in script, "Should contain plain text version"
    assert "`environment`" not in script, "Should not contain markdown code in env description"
    assert "**dev**" not in script, "Should not contain markdown bold in env description"
    assert "**prod**" not in script, "Should not contain markdown bold in env description"

    assert "Deploy to environment" in script, "Should contain plain text command description"

    assert tester.validate_script_syntax()


def test_rst_markup_stripped_from_descriptions(zsh_tester):
    """Test that RST markup is stripped from help descriptions.

    Ensures that **bold**, ``code``, and other RST syntax
    is properly removed from completion descriptions.
    """
    tester = zsh_tester(app_rst, "rstapp")
    script = tester.completion_script

    assert "Enable verbose output with code samples" in script, "Should contain plain text version"
    assert "**verbose**" not in script, "Should not contain RST bold syntax"
    assert "``code``" not in script, "Should not contain RST code syntax (double backticks)"

    assert "Choose execution mode" in script and "fast or slow" in script, "Should contain plain text version"
    assert "*execution*" not in script, "Should not contain RST italic syntax"
    assert "**fast**" not in script, "Should not contain RST bold in mode description"
    assert "**slow**" not in script, "Should not contain RST bold in mode description"

    assert tester.validate_script_syntax()


def test_no_direct_function_call(zsh_tester):
    """Test that completion script doesn't call the completion function directly.

    Regression test for issue where the script ended with '_progname "$@"' which
    caused "_arguments:comparguments:327: can only be called from completion function"
    error when the completion file was sourced during shell startup.

    The #compdef directive is sufficient to register the completion function;
    a direct call is unnecessary and causes errors since _arguments can only be
    called within a completion context.
    """
    tester = zsh_tester(app_basic, "basic")
    script = tester.completion_script

    # Should not have a direct call to the completion function
    assert '_basic "$@"' not in script, "Should not directly call the completion function"

    # Script should end with the closing brace (no compdef needed - #compdef directive suffices)
    lines = script.rstrip().split("\n")
    assert lines[-1] == "}", "Script should end with closing brace"

    # Should have the #compdef directive for autoload compatibility
    assert script.startswith("#compdef basic"), "Should have #compdef directive"

    assert tester.validate_script_syntax()


def test_literal_with_show_choices_false(zsh_tester):
    """Test that Literal with show_choices=False still provides completions.

    Regression test: When show_choices=False is set on a Literal parameter,
    the choices should still be available for shell completion, even though
    they are hidden from the help text.
    """
    app = App(name="deploy")

    @app.default
    def main(
        env: Annotated[
            Literal["dev", "staging", "prod"],
            Parameter(help="Environment to deploy to", show_choices=False),
        ],
    ):
        """Deploy to environment."""
        pass

    tester = zsh_tester(app, "deploy")
    script = tester.completion_script

    # Choices should be in completion script even with show_choices=False
    assert "dev" in script
    assert "staging" in script
    assert "prod" in script


def test_command_with_multiple_names_and_aliases(zsh_tester):
    """Test that commands registered with multiple names/aliases all appear in completions.

    Regression test for groups_from_app() deduplication - ensures all registered
    names are included in completion scripts.
    """
    app = App(name="myapp")
    sub = App()

    @sub.default
    def action(value: str = ""):
        """Perform an action."""
        pass

    app.command(sub, name="foo", alias=["bar", "baz"])

    tester = zsh_tester(app, "myapp")
    script = tester.completion_script

    assert "foo" in script, "Primary name should be in completion script"
    assert "bar" in script, "First alias should be in completion script"
    assert "baz" in script, "Second alias should be in completion script"

    assert tester.validate_script_syntax()


def test_nested_variadic_positional_completion(zsh_tester):
    """Test that variadic positionals work in nested command contexts.

    Variadic positionals (*args) should accept completion at multiple positions.
    """
    app = App(name="testapp")

    @app.command
    def process(
        *files: Annotated[Path, Parameter(help="Files to process")],
    ):
        """Process multiple files.

        Parameters
        ----------
        files : Path
            Files to process.
        """
        pass

    tester = zsh_tester(app, "testapp")
    script = tester.completion_script

    # Should use _files completion for Path positionals
    assert "_files" in script, "Path positionals should use _files completion"

    # For variadic, should have '*:desc:_files' spec
    assert "'*:" in script, "Variadic positionals should use * spec"

    assert tester.validate_script_syntax()


def test_list_path_completion(zsh_tester):
    """Test that list[Path] arguments generate file completion.

    Regression test for issue #654: list[Path] arguments should use
    file completion (_files) just like Path arguments.
    """
    tester = zsh_tester(app_list_path, "listpath")
    script = tester.completion_script

    assert "_files" in script, "list[Path] should generate file completion"
    assert tester.validate_script_syntax()


def test_list_path_positional_variadic(zsh_tester):
    """Test that list[Path] positional generates variadic '*' spec, not just '1:'.

    A list parameter that can consume multiple positional values should offer
    file completion for ALL positions, not just the first.
    """
    tester = zsh_tester(app_list_path, "listpath")
    script = tester.completion_script

    assert "'*:" in script, "list[Path] positional should use variadic '*' spec for all positions"
    assert "_files" in script
    assert tester.validate_script_syntax()


def test_list_annotated_path_completion(zsh_tester):
    """Test that list[Annotated[Path, ...]] arguments generate file completion.

    When a type like ExistingFile (Annotated[Path, Parameter(...)]) is used
    inside a collection like list[], the Annotated wrapper must be unwrapped
    to detect the underlying Path type for completion.
    """
    tester = zsh_tester(app_list_annotated_path, "listannotatedpath")
    script = tester.completion_script

    assert "_files" in script, "list[ExistingFile] should generate file completion"
    assert tester.validate_script_syntax()


def test_colon_in_command_name(zsh_tester):
    """Test that colons in command names are properly escaped.

    Regression test for issue #715: Command names containing colons like
    'utility:ping' should have the colon escaped in zsh completion scripts
    so that the full name is displayed, not just the part before the colon.

    Colons need escaping both in _describe format and in case patterns because
    zsh's completion system treats them specially when populating the $words array.
    """
    app = App(name="myapp")

    sub = App()

    @sub.default
    def action(value: str = ""):
        """Perform an action."""
        pass

    # Register command with colon in name
    app.command(sub, name="utility:ping")

    tester = zsh_tester(app, "myapp")
    script = tester.completion_script

    # The colon should be escaped in the _describe format
    # 'utility\:ping:description' instead of 'utility:ping:description'
    assert r"utility\:ping" in script, "Colon in command name should be escaped"

    # The case pattern should also have the colon escaped for proper $words matching
    assert r"utility\:ping)" in script, "Colon should be escaped in case pattern for $words matching"

    assert tester.validate_script_syntax()


def test_special_chars_in_command_name(zsh_tester):
    """Test that special characters in command names are properly escaped.

    Tests escaping for various special characters that could appear in command names
    and cause issues in zsh completion scripts.
    """
    app = App(name="myapp")

    sub1 = App()

    @sub1.default
    def action1():
        """Action with brackets."""
        pass

    # Register command with brackets in name (unusual but possible)
    app.command(sub1, name="test[1]")

    tester = zsh_tester(app, "myapp")
    script = tester.completion_script

    # Brackets should be escaped in the _describe format
    assert r"test\[1\]" in script, "Brackets in command name should be escaped"

    # In case patterns, brackets should also be escaped
    assert r"test\[1\])" in script, "Brackets in case pattern should be escaped"

    assert tester.validate_script_syntax()


def test_positional_without_help_uses_name_fallback(zsh_tester):
    """Test that positional arguments without help text use parameter name as description.

    Regression test: zsh _arguments requires non-empty descriptions for positional
    specs to provide completions. When a parameter has no help text, the completion
    script should fall back to using the parameter name as the description.

    Without this fallback, '1::(choices)' is generated which silently breaks
    completion, while '1:param_name:(choices)' works correctly.
    """
    app = App(name="testapp")

    @app.command
    def action(
        choice: Literal["foo", "bar"],  # No Parameter() annotation, no help text
        /,
    ):
        """Do something."""
        pass

    tester = zsh_tester(app, "testapp")
    script = tester.completion_script

    # Should NOT have empty description '1::(foo bar)' or "1::(foo bar)"
    assert "'1::(foo bar)'" not in script, "Should not have empty description in positional spec"
    assert '"1::(foo bar)"' not in script, "Should not have empty description in positional spec"

    # Should have parameter name as fallback. Choice-bearing specs use
    # double-quoted outer; the format is `"1:description:(choices)"`.
    positional_spec = re.search(r'"1:([^:]+):\(foo bar\)"', script)
    assert positional_spec is not None, "Should have positional spec with choices"
    description = positional_spec.group(1)
    assert description, "Description should not be empty"
    assert len(description) > 0, "Description should have content"

    assert tester.validate_script_syntax()


# --- Choice values containing whitespace / shell metacharacters --------------
#
# End-to-end regression tests for tricky choice values surviving zsh's
# completion machinery. zsh's ``_arguments`` puts choices through *two*
# parsers: the outer shell quoting and the inner choice-list parser. The
# generator now uses double-quoted outer specs so a literal ``'`` can be
# embedded as ``\'``. Without that, ``Literal["a'b"]`` produced ``(eval):1:
# unmatched '`` at completion time.


def test_choice_with_whitespace(zsh_tester):
    """A choice value containing a space stays a single completion."""
    app = App(name="ws")

    @app.default
    def m(x: Literal["hello world", "normal"] = "normal", /):
        """Whitespace in choice."""

    tester = zsh_tester(app, "ws")
    assert tester.validate_script_syntax()
    completions = tester.get_completions("ws ")
    assert "hello world" in completions
    assert "hello" not in completions
    assert "world" not in completions


def test_choice_with_single_quote(zsh_tester):
    r"""A choice value containing a single quote does not break the script.

    Regression test: previously ``Literal["a'b"]`` produced
    ``'1:X:(... a'\\''b)'``. After the outer single-quote-end-restart
    trick, ``_arguments``' choice-list parser saw an unbalanced ``'`` and
    failed with ``(eval):1: unmatched '``.
    """
    app = App(name="sq")

    @app.default
    def m(x: Literal["a'b", "normal"] = "normal", /):
        """Single quote in choice."""

    tester = zsh_tester(app, "sq")
    assert tester.validate_script_syntax()
    completions = tester.get_completions("sq ")
    assert "a'b" in completions
    assert not any("unmatched" in c for c in completions)


def test_choice_with_backtick(zsh_tester):
    """A choice value containing a backtick does not break the script."""
    app = App(name="btk")

    @app.default
    def m(x: Literal["a`b", "normal"] = "normal", /):
        """Backtick in choice."""

    tester = zsh_tester(app, "btk")
    assert tester.validate_script_syntax()
    completions = tester.get_completions("btk ")
    assert "a`b" in completions


def test_choice_with_dollar(zsh_tester):
    """A choice value containing $ is not parameter-expanded."""
    app = App(name="dol")

    @app.default
    def m(x: Literal["$home", "normal"] = "normal", /):
        """Dollar in choice."""

    tester = zsh_tester(app, "dol")
    assert tester.validate_script_syntax()
    completions = tester.get_completions("dol ")
    assert "$home" in completions


def test_choice_value_after_keyword_option(zsh_tester):
    """Tricky choices also survive the ``--opt <choice>`` value-completion path."""
    app = App(name="optchoice")

    @app.default
    def m(env: Annotated[Literal["a b", "c'd", "e`f"], Parameter(help="env")] = "a b"):
        """Tricky choices behind a keyword option."""

    tester = zsh_tester(app, "optchoice")
    assert tester.validate_script_syntax()
    completions = tester.get_completions("optchoice --env ")
    assert {"a b", "c'd", "e`f"} <= set(completions)


# --- --opt=value form: gated on Parameter.requires_equals ------------------
#
# The ``=`` suffix on a zsh ``_arguments`` option spec is load-bearing in
# two ways at once: it enables ``--opt=value`` value-completion AND it
# changes the *name* TAB-insert from ``--opt `` (trailing space) to
# ``--opt=`` (no space). zsh has no native syntax that decouples these.
#
# Because forced ``=`` insertion surprises most users and the space form
# is the more common across CLIs, the default (``requires_equals=False``)
# emits the plain spec — at the cost of ``--opt=value<TAB>``
# value-completion silently doing nothing. ``requires_equals=True`` opts
# back into the eq spec so completion mirrors what the parser accepts.
#
# Bash is unaffected by this trade-off — its eq-form completion goes
# through ``_value_prev`` hopping over ``=`` and works regardless.


def test_default_no_eq_in_spec_name(zsh_tester):
    """Default: no ``=`` suffix on the option name in the spec.

    The ``--env`` option has Literal choices, so its spec uses a
    double-quoted outer string. The assertion checks for the ``=``-less
    form regardless of which quote style wraps the spec.
    """
    tester = zsh_tester(app_basic, "basic")
    script = tester.completion_script
    # Plain spec — TAB on the name will insert ``--env`` plus a space.
    assert '"*--env[' in script or "'*--env[" in script
    assert '"*--env=[' not in script
    assert "'*--env=[" not in script


def test_default_space_form_value_completion(zsh_tester):
    """The space form must offer value completion regardless of ``requires_equals``."""
    tester = zsh_tester(app_basic, "basic")
    completions = tester.get_completions("basic deploy --env ")
    assert {"dev", "staging", "prod"} <= set(completions)


def test_default_eq_form_value_completion_via_prepass(zsh_tester):
    """``--opt=value<TAB>`` works in the default case via the compset pre-pass.

    Even with the plain (no-``=``) ``_arguments`` spec, a user who
    explicitly types ``--env=`` should still see value completions: the
    pre-pass intercepts the pattern, strips the ``--env=`` prefix, and
    dispatches to the choice list.
    """
    tester = zsh_tester(app_basic, "basic")
    completions = tester.get_completions("basic deploy --env=")
    assert {"dev", "staging", "prod"} <= set(completions)


def test_default_eq_form_value_completion_partial(zsh_tester):
    """``--opt=d<TAB>`` narrows via the pre-pass."""
    tester = zsh_tester(app_basic, "basic")
    completions = tester.get_completions("basic deploy --env=d")
    assert "dev" in completions
    assert "staging" not in completions
    assert "prod" not in completions


def test_default_eq_form_path_completion_via_prepass(zsh_tester):
    """Path-typed options also dispatch to ``_files`` from the pre-pass."""
    import os
    import tempfile
    from pathlib import Path as _Path

    app = App(name="ekw")

    @app.default
    def m(input_file: Annotated[_Path, Parameter(help="In")] = _Path()):
        """Path keyword."""

    tester = zsh_tester(app, "ekw")
    with tempfile.TemporaryDirectory() as td:
        (_Path(td) / "sample.txt").write_text("x")
        cwd = _Path.cwd()
        try:
            os.chdir(td)
            completions = tester.get_completions("ekw --input-file=")
        finally:
            os.chdir(cwd)
    assert completions, f"expected file completion via pre-pass, got {completions!r}"


def test_eq_form_prepass_compadd_no_stray_backslashes(zsh_tester):
    r"""Eq-form prepass ``compadd`` args must be POSIX-safe single-quoted.

    Reproduces the Copilot review finding: choices are passed through
    ``_escape_completion_choice`` (which inserts ``\\`` escapes designed for
    ``_describe``'s inner parser) and then wrapped in *single quotes* for
    ``compadd``. Inside single quotes, backslashes are literal — so a choice
    like ``"foo bar"`` ends up as ``'foo\\ bar'`` and the user sees a
    completion containing a stray backslash. The fix is proper single-quote
    escaping (only ``'`` needs handling, via the ``'\\''`` end/restart trick).
    """
    app = App(name="eqp")

    @app.default
    def m(env: Annotated[Literal["foo bar", "baz qux"], Parameter(help="E")] = "foo bar"):
        """Eq-form choices with spaces."""

    tester = zsh_tester(app, "eqp")
    script = tester.completion_script

    # Locate the prepass case branch for ``--env`` and the inner ``compadd`` line.
    lines = script.splitlines()
    in_env_case = False
    compadd_line = None
    for line in lines:
        stripped = line.strip()
        if stripped == "--env=*)":
            in_env_case = True
            continue
        if in_env_case:
            if stripped.startswith("compadd "):
                compadd_line = stripped
                break
            if stripped == ";;":
                break

    assert compadd_line is not None, "no ``compadd`` line found in --env=*) prepass branch"

    # Single-quoted compadd args must not contain literal backslash-escape
    # sequences — those would render as visible backslashes in the user's
    # completion menu.
    assert "\\ " not in compadd_line, (
        f"single-quoted compadd args contain literal '\\ ' (backslash-space): {compadd_line!r}"
    )


def test_requires_equals_emits_eq_spec(zsh_tester):
    """``Parameter(requires_equals=True)`` opts back into the eq spec."""
    app = App(name="rqeq")

    @app.default
    def m(env: Annotated[Literal["dev", "prod"], Parameter(requires_equals=True)] = "dev"):
        """Eq-required."""

    tester = zsh_tester(app, "rqeq")
    script = tester.completion_script
    assert "*--env=[" in script
    # The pre-pass should NOT include this option — its eq-form is already
    # handled by the spec.
    assert "--env=*)" not in script


def test_requires_equals_eq_form_value_completion(zsh_tester):
    """``--opt=<TAB>`` value completion works when ``requires_equals=True``."""
    app = App(name="rqeq")

    @app.default
    def m(env: Annotated[Literal["dev", "prod"], Parameter(requires_equals=True)] = "dev"):
        """Eq-required."""

    tester = zsh_tester(app, "rqeq")
    completions = tester.get_completions("rqeq --env=")
    assert {"dev", "prod"} <= set(completions)


# --- Repeatable value-options ----------------------------------------------
#
# zsh's ``_arguments`` defaults each spec to single-use, so once an option
# has been consumed it disappears from suggestions and its value can't be
# completed again. Cyclopts now prefixes value-bearing specs with ``*`` so
# the option may repeat — required for collection-typed options
# (``list[X]``) and matches bash's behavior. Bool flags stay non-repeating.


def test_value_option_repeatable_space_form(zsh_tester):
    """``--env dev --env<TAB>`` should still offer choices."""
    tester = zsh_tester(app_basic, "basic")
    completions = tester.get_completions("basic deploy --env dev --env ")
    assert {"dev", "staging", "prod"} <= set(completions)


def test_collection_option_repeats(zsh_tester):
    """A ``list[Path]`` keyword option must accept repeated ``--file``.

    Without ``*`` prefix on the spec, zsh would refuse to complete a second
    ``--file`` value, breaking the natural ``--file a --file b --file c``
    usage of collection-typed parameters.
    """
    import os
    import tempfile
    from pathlib import Path as _Path

    app = App(name="files")

    @app.default
    def m(file: Annotated[list[_Path], Parameter(help="files")] = []):  # noqa: B006
        """Files."""

    tester = zsh_tester(app, "files")
    with tempfile.TemporaryDirectory() as td:
        (_Path(td) / "first.txt").write_text("x")
        cwd = _Path.cwd()
        try:
            os.chdir(td)
            completions = tester.get_completions("files --file first.txt --file ")
        finally:
            os.chdir(cwd)
    assert completions, f"expected file completion on second --file, got {completions!r}"


def test_multiple_iterable_positionals_emit_one_rest_spec(zsh_tester):
    """Functions with multiple ``list[X]`` positional-or-keyword params.

    Regression test for ``_arguments:comparguments:327: doubled rest argument
    definition``: zsh allows at most one ``*:`` rest spec per command, but
    each iterable-typed positional was emitting one. Cyclopts now collapses
    them to a single rest spec (the first iterable, or the ``*args``
    var-positional if present); the others remain available via their
    ``--name`` keyword forms.
    """
    app = App(name="multi_iter")

    @app.command
    def process(
        items: list[str],
        tags: list[str] | None = None,
        exclude: list[str] | None = None,
    ):
        """Process items.

        Parameters
        ----------
        items
            Items to process.
        tags
            Tags to apply.
        exclude
            Items to exclude.
        """

    tester = zsh_tester(app, "multi_iter")
    script = tester.completion_script

    assert tester.validate_script_syntax()

    # Exactly one rest-arg spec inside the process block.
    process_block = re.search(r"process\)(.*?);;", script, re.DOTALL)
    assert process_block is not None
    rest_specs = re.findall(r"'\*:[^']*'|\"\*:[^\"]*\"", process_block.group(1))
    assert len(rest_specs) == 1, f"expected one rest spec, found {rest_specs!r}"

    # The collapsed-out positionals are still available as keyword options.
    assert "--tags[" in script
    assert "--exclude[" in script

    # Real zsh accepts the script and offers ``--tags`` after ``--ta``.
    completions = tester.get_completions("multi_iter process --ta")
    assert "--tags" in completions


def test_bool_flag_not_repeated(zsh_tester):
    """Bool flags stay single-use in zsh (matches zsh convention)."""
    tester = zsh_tester(app_basic, "basic")
    script = tester.completion_script
    # No ``*`` prefix on the verbose spec.
    assert "'--verbose[" in script
    assert "'*--verbose[" not in script


# --- Naked-TAB at no-arg subcommand: known per-shell divergence -------------
#
# See the doc note at the top of test_behavior.py. zsh's ``_arguments``
# always lists ``--help`` / ``--version`` whether or not the user has
# typed a leading ``-``; bash gates them behind ``cur == -*``. The current
# behavior is intentional and locked in here.


def test_naked_tab_at_no_arg_subcommand_offers_help(zsh_tester):
    """Zsh always surfaces ``--help`` / ``--version`` via ``_arguments``."""
    app = App(name="probe")

    @app.command
    def noarg():
        """No-arg subcommand."""

    tester = zsh_tester(app, "probe")
    completions = tester.get_completions("probe noarg ")
    assert "--help" in completions
    assert "--version" in completions
