"""Tests for BaseCommand subcommand support."""

import sys
from unittest.mock import patch

import pytest

from vidhi import BaseCommand, field, frozen_dataclass, parse_cli_args

# ---------------------------------------------------------------------------
# Test fixtures — a small CLI tool with subcommands
# ---------------------------------------------------------------------------


@frozen_dataclass
class ToolCmd(BaseCommand):
    """Test Tool"""

    verbose: bool = field(False, help="Verbose output")


@frozen_dataclass
class Run(ToolCmd, name="run", alias="r", default=True):
    """Run something"""

    target: str = field(".", help="Target path")


@frozen_dataclass
class List(ToolCmd, name="list", alias="l"):
    """List items"""

    all: bool = field(False, help="Show all")


@frozen_dataclass
class Deploy(ToolCmd, name="deploy"):
    """Deploy to server"""

    host: str = field("localhost", help="Target host")
    port: int = field(8080, help="Target port")


# ---------------------------------------------------------------------------
# Registration tests
# ---------------------------------------------------------------------------


class TestRegistration:
    def test_subcommands_registered(self):
        assert "run" in ToolCmd._subcommands
        assert "list" in ToolCmd._subcommands
        assert "deploy" in ToolCmd._subcommands

    def test_aliases_registered(self):
        assert ToolCmd._aliases["r"] == "run"
        assert ToolCmd._aliases["l"] == "list"

    def test_default_command(self):
        assert ToolCmd._default_command == "run"

    def test_resolve_by_name(self):
        assert ToolCmd.resolve_subcommand("run") is Run
        assert ToolCmd.resolve_subcommand("list") is List
        assert ToolCmd.resolve_subcommand("deploy") is Deploy

    def test_resolve_by_alias(self):
        assert ToolCmd.resolve_subcommand("r") is Run
        assert ToolCmd.resolve_subcommand("l") is List

    def test_resolve_unknown_returns_none(self):
        assert ToolCmd.resolve_subcommand("unknown") is None

    def test_base_is_marked(self):
        assert ToolCmd._is_base_command is True
        assert Run._is_base_command is False

    def test_command_name_stored(self):
        assert Run._command_name == "run"
        assert Run._command_alias == "r"
        assert Deploy._command_name == "deploy"
        assert Deploy._command_alias is None


# ---------------------------------------------------------------------------
# Registration error tests
# ---------------------------------------------------------------------------


class TestRegistrationErrors:
    def test_duplicate_name_raises(self):
        with pytest.raises(ValueError, match="already registered"):

            @frozen_dataclass
            class Base(BaseCommand):
                pass

            @frozen_dataclass
            class A(Base, name="foo"):
                pass

            @frozen_dataclass
            class B(Base, name="foo"):
                pass

    def test_duplicate_alias_raises(self):
        with pytest.raises(ValueError, match="already registered"):

            @frozen_dataclass
            class Base(BaseCommand):
                pass

            @frozen_dataclass
            class A(Base, name="foo", alias="f"):
                pass

            @frozen_dataclass
            class B(Base, name="bar", alias="f"):
                pass

    def test_duplicate_default_raises(self):
        with pytest.raises(ValueError, match="Default command already set"):

            @frozen_dataclass
            class Base(BaseCommand):
                pass

            @frozen_dataclass
            class A(Base, name="foo", default=True):
                pass

            @frozen_dataclass
            class B(Base, name="bar", default=True):
                pass

    def test_alias_conflicts_with_name_raises(self):
        with pytest.raises(ValueError, match="conflicts with subcommand name"):

            @frozen_dataclass
            class Base(BaseCommand):
                pass

            @frozen_dataclass
            class A(Base, name="foo"):
                pass

            @frozen_dataclass
            class B(Base, name="bar", alias="foo"):
                pass


# ---------------------------------------------------------------------------
# Parsing tests
# ---------------------------------------------------------------------------


class TestParsing:
    def test_explicit_command(self):
        with patch.object(sys, "argv", ["tool", "run", "--target", "/tmp"]):
            result = parse_cli_args(ToolCmd)
        assert isinstance(result, Run)
        assert result.target == "/tmp"

    def test_command_with_alias(self):
        with patch.object(sys, "argv", ["tool", "r", "--target", "/tmp"]):
            result = parse_cli_args(ToolCmd)
        assert isinstance(result, Run)
        assert result.target == "/tmp"

    def test_default_command_no_args(self):
        """No args at all should use default command with its defaults."""
        with patch.object(sys, "argv", ["tool"]):
            result = parse_cli_args(ToolCmd)
        assert isinstance(result, Run)
        assert result.target == "."

    def test_default_command_with_flags(self):
        """Flags without a command name should use default command."""
        with patch.object(sys, "argv", ["tool", "--target", "/home"]):
            result = parse_cli_args(ToolCmd)
        assert isinstance(result, Run)
        assert result.target == "/home"

    def test_base_field_inherited(self):
        with patch.object(sys, "argv", ["tool", "run", "--verbose", "true"]):
            result = parse_cli_args(ToolCmd)
        assert isinstance(result, Run)
        assert result.verbose is True

    def test_base_field_on_different_command(self):
        with patch.object(sys, "argv", ["tool", "list", "--verbose", "true"]):
            result = parse_cli_args(ToolCmd)
        assert isinstance(result, List)
        assert result.verbose is True

    def test_different_command(self):
        with patch.object(
            sys, "argv", ["tool", "deploy", "--host", "prod", "--port", "443"]
        ):
            result = parse_cli_args(ToolCmd)
        assert isinstance(result, Deploy)
        assert result.host == "prod"
        assert result.port == 443

    def test_command_specific_fields_isolated(self):
        """Deploy's --host flag shouldn't exist on Run."""
        with patch.object(sys, "argv", ["tool", "run", "--host", "prod"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)

    def test_unknown_command_exits(self):
        with patch.object(sys, "argv", ["tool", "unknown_cmd"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)


# ---------------------------------------------------------------------------
# Positional args in subcommands
# ---------------------------------------------------------------------------


class TestPositionalInSubcommands:
    def test_positional_arg(self):
        @frozen_dataclass
        class Cmd(BaseCommand):
            pass

        @frozen_dataclass
        class Greet(Cmd, name="greet", default=True):
            """Say hello"""

            name: str = field("world", positional=True)

        with patch.object(sys, "argv", ["cmd", "greet", "Alice"]):
            result = parse_cli_args(Cmd)
        assert isinstance(result, Greet)
        assert result.name == "Alice"

    def test_positional_with_default_command(self):
        @frozen_dataclass
        class Cmd(BaseCommand):
            pass

        @frozen_dataclass
        class Echo(Cmd, name="echo", default=True):
            """Echo text"""

            text: str = field(None, positional=True)

        # "echo hello" — explicit command
        with patch.object(sys, "argv", ["cmd", "echo", "hello"]):
            result = parse_cli_args(Cmd)
        assert isinstance(result, Echo)
        assert result.text == "hello"


# ---------------------------------------------------------------------------
# Help output tests
# ---------------------------------------------------------------------------


class TestHelp:
    def test_root_help_exits(self):
        with patch.object(sys, "argv", ["tool", "--help"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)

    def test_root_help_lists_commands(self, capsys):
        with patch.object(sys, "argv", ["tool", "--help"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)
        output = capsys.readouterr().out
        assert "run" in output
        assert "list" in output
        assert "deploy" in output
        assert "default" in output.lower()

    def test_root_help_shows_aliases(self, capsys):
        with patch.object(sys, "argv", ["tool", "--help"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)
        output = capsys.readouterr().out
        assert ", r" in output
        assert ", l" in output

    def test_root_help_shows_descriptions(self, capsys):
        with patch.object(sys, "argv", ["tool", "--help"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)
        output = capsys.readouterr().out
        assert "Run something" in output
        assert "List items" in output
        assert "Deploy to server" in output

    def test_subcommand_help_shows_own_flags(self, capsys):
        with patch.object(sys, "argv", ["tool", "deploy", "--help"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)
        output = capsys.readouterr().out
        assert "--host" in output
        assert "--port" in output
        # Base field should also appear
        assert "--verbose" in output

    def test_subcommand_help_no_other_flags(self, capsys):
        with patch.object(sys, "argv", ["tool", "list", "--help"]):
            with pytest.raises(SystemExit):
                parse_cli_args(ToolCmd)
        output = capsys.readouterr().out
        # List shouldn't show deploy's flags
        assert "--host" not in output
        assert "--port" not in output
        assert "--target" not in output


# ---------------------------------------------------------------------------
# Isolated registries
# ---------------------------------------------------------------------------


class TestIsolation:
    def test_separate_bases_dont_share_registry(self):
        """Two different base commands should have separate registries."""

        @frozen_dataclass
        class AppA(BaseCommand):
            pass

        @frozen_dataclass
        class A1(AppA, name="cmd1"):
            pass

        @frozen_dataclass
        class AppB(BaseCommand):
            pass

        @frozen_dataclass
        class B1(AppB, name="cmd1"):  # Same name, different base — OK
            pass

        assert "cmd1" in AppA._subcommands
        assert "cmd1" in AppB._subcommands
        assert AppA._subcommands["cmd1"] is A1
        assert AppB._subcommands["cmd1"] is B1
