"""Unit tests for CLI main module."""

from __future__ import annotations

from pathlib import Path

import pytest
import typer
from tests.conftest import CliRunner

from teams_phone.cli.context import CLIContext, get_context
from teams_phone.cli.main import app
from teams_phone.infrastructure import ConfigManager, OutputFormatter


runner = CliRunner()


class TestAppInitialization:
    """Tests for Typer app initialization."""

    @pytest.mark.unit
    def test_app_name(self) -> None:
        """App has correct name."""
        assert app.info.name == "teams-phone"

    @pytest.mark.unit
    def test_app_no_args_shows_help(self) -> None:
        """App shows help when no arguments provided."""
        result = runner.invoke(app, [])
        # Typer returns exit code 2 for no_args_is_help
        assert result.exit_code == 2
        assert "Usage:" in result.stdout

    @pytest.mark.unit
    def test_help_flag(self) -> None:
        """App responds to --help flag."""
        result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "Manage Microsoft Teams Phone configurations" in result.stdout


class TestGlobalOptions:
    """Tests for global options in callback."""

    @pytest.mark.unit
    def test_json_option_short(self) -> None:
        """--json / -j option is available."""
        result = runner.invoke(app, ["--help"])
        assert "--json" in result.plain_stdout
        assert "-j" in result.plain_stdout

    @pytest.mark.unit
    def test_verbose_option_short(self) -> None:
        """--verbose / -v option is available."""
        result = runner.invoke(app, ["--help"])
        assert "--verbose" in result.plain_stdout
        assert "-v" in result.plain_stdout

    @pytest.mark.unit
    def test_tenant_option_short(self) -> None:
        """--tenant / -t option is available."""
        result = runner.invoke(app, ["--help"])
        assert "--tenant" in result.plain_stdout
        assert "-t" in result.plain_stdout

    @pytest.mark.unit
    def test_no_color_option(self) -> None:
        """--no-color option is available."""
        result = runner.invoke(app, ["--help"])
        assert "--no-color" in result.plain_stdout

    @pytest.mark.unit
    def test_debug_log_option(self) -> None:
        """--debug-log option is available."""
        result = runner.invoke(app, ["--help"])
        assert "--debug-log" in result.plain_stdout


class TestContextPopulation:
    """Tests for context object population.

    Uses a temporary test command to verify context values are set correctly.
    """

    @pytest.fixture
    def test_app_with_command(self) -> tuple[typer.Typer, dict[str, object]]:
        """Create a copy of the app with a test command to inspect context."""
        from teams_phone.cli.main import main

        # Create a fresh app and register the callback
        test_app = typer.Typer(
            name="teams-phone",
            help="Manage Microsoft Teams Phone configurations via Microsoft Graph API.",
            no_args_is_help=True,
        )
        test_app.callback()(main)

        # Storage for captured context values
        captured: dict[str, object] = {}

        @test_app.command()
        def test_cmd(ctx: typer.Context) -> None:
            """Test command to capture context."""
            cli_ctx = get_context(ctx)
            captured["json"] = cli_ctx.json_output
            captured["verbose"] = cli_ctx.verbose
            captured["tenant"] = cli_ctx.tenant
            captured["no_color"] = cli_ctx.no_color
            captured["debug_log_path"] = cli_ctx.debug_log_path
            captured["output_formatter"] = cli_ctx.output_formatter
            captured["config_manager"] = cli_ctx.config_manager
            captured["debug_logger"] = cli_ctx.debug_logger

        return test_app, captured

    @pytest.mark.unit
    def test_default_context_values(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """Default context values are set correctly."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["test-cmd"])
        assert result.exit_code == 0
        assert captured["json"] is False
        assert captured["verbose"] is False
        assert captured["tenant"] is None
        assert captured["no_color"] is False
        assert captured["debug_log_path"] is None
        assert captured["debug_logger"] is None

    @pytest.mark.unit
    def test_json_flag_in_context(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """--json flag is captured in context."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["--json", "test-cmd"])
        assert result.exit_code == 0
        assert captured["json"] is True

    @pytest.mark.unit
    def test_verbose_flag_in_context(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """--verbose flag is captured in context."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["--verbose", "test-cmd"])
        assert result.exit_code == 0
        assert captured["verbose"] is True

    @pytest.mark.unit
    def test_tenant_option_in_context(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """--tenant option is captured in context."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["--tenant", "my-tenant", "test-cmd"])
        assert result.exit_code == 0
        assert captured["tenant"] == "my-tenant"

    @pytest.mark.unit
    def test_no_color_flag_in_context(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """--no-color flag is captured in context."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["--no-color", "test-cmd"])
        assert result.exit_code == 0
        assert captured["no_color"] is True

    @pytest.mark.unit
    def test_debug_log_option_in_context(
        self,
        test_app_with_command: tuple[typer.Typer, dict[str, object]],
        tmp_path: Path,
    ) -> None:
        """--debug-log option is captured in context and creates DebugLogger."""
        from pathlib import Path as PathLib

        from teams_phone.infrastructure.debug_logger import DebugLogger

        test_app, captured = test_app_with_command
        log_path = tmp_path / "debug.log"
        result = runner.invoke(test_app, ["--debug-log", str(log_path), "test-cmd"])
        assert result.exit_code == 0
        assert captured["debug_log_path"] == PathLib(str(log_path))
        assert isinstance(captured["debug_logger"], DebugLogger)

    @pytest.mark.unit
    def test_multiple_options_combined(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """Multiple global options work together."""
        test_app, captured = test_app_with_command
        result = runner.invoke(
            test_app,
            [
                "--json",
                "--verbose",
                "--tenant",
                "test-tenant",
                "--no-color",
                "test-cmd",
            ],
        )
        assert result.exit_code == 0
        assert captured["json"] is True
        assert captured["verbose"] is True
        assert captured["tenant"] == "test-tenant"
        assert captured["no_color"] is True

    @pytest.mark.unit
    def test_short_options(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """Short option flags work correctly."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["-j", "-v", "-t", "short-tenant", "test-cmd"])
        assert result.exit_code == 0
        assert captured["json"] is True
        assert captured["verbose"] is True
        assert captured["tenant"] == "short-tenant"


class TestCommandGroupRegistration:
    """Tests for command group registration."""

    @pytest.mark.unit
    def test_command_groups_in_help(self) -> None:
        """All command groups appear in help output."""
        result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "users" in result.stdout
        assert "numbers" in result.stdout
        assert "policies" in result.stdout
        assert "locations" in result.stdout
        assert "tenants" in result.stdout
        assert "auth" in result.stdout

    @pytest.mark.unit
    def test_users_subcommand_help(self) -> None:
        """users --help shows users subcommand help."""
        result = runner.invoke(app, ["users", "--help"])
        assert result.exit_code == 0
        assert "Manage Teams users" in result.stdout

    @pytest.mark.unit
    def test_numbers_subcommand_help(self) -> None:
        """numbers --help shows numbers subcommand help."""
        result = runner.invoke(app, ["numbers", "--help"])
        assert result.exit_code == 0
        assert "Manage phone numbers" in result.stdout

    @pytest.mark.unit
    def test_policies_subcommand_help(self) -> None:
        """policies --help shows policies subcommand help."""
        result = runner.invoke(app, ["policies", "--help"])
        assert result.exit_code == 0
        assert "Manage voice policies" in result.stdout

    @pytest.mark.unit
    def test_locations_subcommand_help(self) -> None:
        """locations --help shows locations subcommand help."""
        result = runner.invoke(app, ["locations", "--help"])
        assert result.exit_code == 0
        assert "Manage emergency locations" in result.stdout

    @pytest.mark.unit
    def test_tenants_subcommand_help(self) -> None:
        """tenants --help shows tenants subcommand help."""
        result = runner.invoke(app, ["tenants", "--help"])
        assert result.exit_code == 0
        assert "Manage tenant profiles" in result.stdout

    @pytest.mark.unit
    def test_auth_subcommand_help(self) -> None:
        """auth --help shows auth subcommand help."""
        result = runner.invoke(app, ["auth", "--help"])
        assert result.exit_code == 0
        assert "Authentication commands" in result.stdout

    @pytest.mark.unit
    def test_command_group_no_args_shows_help(self) -> None:
        """Command groups with no args show help (exit code 2)."""
        for group in ["users", "numbers", "policies", "locations", "tenants", "auth"]:
            result = runner.invoke(app, [group])
            # no_args_is_help returns exit code 2
            assert result.exit_code == 2, f"{group} should return exit code 2"
            assert "Usage:" in result.stdout, f"{group} should show usage"


class TestCLIExport:
    """Tests for CLI module exports."""

    @pytest.mark.unit
    def test_app_exported_from_cli_package(self) -> None:
        """App is exported from teams_phone.cli package."""
        from teams_phone.cli import app as exported_app

        assert exported_app is app

    @pytest.mark.unit
    def test_cli_context_exported_from_cli_package(self) -> None:
        """CLIContext is exported from teams_phone.cli package."""
        from teams_phone.cli import CLIContext as exported_ctx

        assert exported_ctx is CLIContext

    @pytest.mark.unit
    def test_get_context_exported_from_cli_package(self) -> None:
        """get_context is exported from teams_phone.cli package."""
        from teams_phone.cli import get_context as exported_get_ctx

        assert exported_get_ctx is get_context


class TestCLIContext:
    """Tests for CLIContext dataclass."""

    @pytest.mark.unit
    def test_default_values(self) -> None:
        """CLIContext has correct default values."""
        ctx = CLIContext()
        assert ctx.json_output is False
        assert ctx.verbose is False
        assert ctx.tenant is None
        assert ctx.no_color is False
        assert ctx.debug_log_path is None
        assert ctx.config_manager is None
        assert ctx.debug_logger is None
        assert ctx.output_formatter is None

    @pytest.mark.unit
    def test_custom_values(self) -> None:
        """CLIContext accepts custom values."""
        ctx = CLIContext(
            json_output=True,
            verbose=True,
            tenant="my-tenant",
            no_color=True,
        )
        assert ctx.json_output is True
        assert ctx.verbose is True
        assert ctx.tenant == "my-tenant"
        assert ctx.no_color is True

    @pytest.mark.unit
    def test_get_output_formatter_creates_instance(self) -> None:
        """get_output_formatter creates OutputFormatter on first access."""
        ctx = CLIContext(json_output=True, no_color=True)
        assert ctx.output_formatter is None

        formatter = ctx.get_output_formatter()

        assert formatter is not None
        assert isinstance(formatter, OutputFormatter)
        assert formatter.json_mode is True
        assert formatter.no_color is True

    @pytest.mark.unit
    def test_get_output_formatter_returns_same_instance(self) -> None:
        """get_output_formatter returns the same instance on subsequent calls."""
        ctx = CLIContext()
        formatter1 = ctx.get_output_formatter()
        formatter2 = ctx.get_output_formatter()

        assert formatter1 is formatter2

    @pytest.mark.unit
    def test_get_output_formatter_uses_preset_instance(self) -> None:
        """get_output_formatter returns preset instance if already set."""
        preset_formatter = OutputFormatter()
        ctx = CLIContext(output_formatter=preset_formatter)

        formatter = ctx.get_output_formatter()

        assert formatter is preset_formatter

    @pytest.mark.unit
    def test_get_config_manager_creates_instance(self) -> None:
        """get_config_manager creates ConfigManager on first access."""
        ctx = CLIContext()
        assert ctx.config_manager is None

        manager = ctx.get_config_manager()

        assert manager is not None
        assert isinstance(manager, ConfigManager)

    @pytest.mark.unit
    def test_get_config_manager_returns_same_instance(self) -> None:
        """get_config_manager returns the same instance on subsequent calls."""
        ctx = CLIContext()
        manager1 = ctx.get_config_manager()
        manager2 = ctx.get_config_manager()

        assert manager1 is manager2

    @pytest.mark.unit
    def test_get_config_manager_uses_preset_instance(self) -> None:
        """get_config_manager returns preset instance if already set."""
        preset_manager = ConfigManager()
        ctx = CLIContext(config_manager=preset_manager)

        manager = ctx.get_config_manager()

        assert manager is preset_manager


class TestGetContext:
    """Tests for get_context helper function."""

    @pytest.mark.unit
    def test_get_context_returns_cli_context(self) -> None:
        """get_context returns CLIContext from ctx.obj."""
        from teams_phone.cli.main import main

        test_app = typer.Typer()
        test_app.callback()(main)

        captured: dict[str, object] = {}

        @test_app.command()
        def test_cmd(ctx: typer.Context) -> None:
            captured["context"] = get_context(ctx)

        result = runner.invoke(test_app, ["test-cmd"])
        assert result.exit_code == 0
        assert isinstance(captured["context"], CLIContext)

    @pytest.mark.unit
    def test_get_context_raises_on_missing_context(self) -> None:
        """get_context raises RuntimeError if ctx.obj is not CLIContext."""
        test_app = typer.Typer()

        @test_app.callback()
        def callback(ctx: typer.Context) -> None:
            ctx.obj = {"not": "a CLIContext"}  # Wrong type

        captured_error: dict[str, Exception | None] = {"error": None}

        @test_app.command()
        def test_cmd(ctx: typer.Context) -> None:
            try:
                get_context(ctx)
            except RuntimeError as e:
                captured_error["error"] = e
                raise

        result = runner.invoke(test_app, ["test-cmd"])
        assert result.exit_code != 0
        assert captured_error["error"] is not None
        assert "CLIContext not found" in str(captured_error["error"])

    @pytest.mark.unit
    def test_get_context_raises_on_none_context(self) -> None:
        """get_context raises RuntimeError if ctx.obj is None."""
        test_app = typer.Typer()

        @test_app.callback()
        def callback(ctx: typer.Context) -> None:
            ctx.obj = None  # Not set

        captured_error: dict[str, Exception | None] = {"error": None}

        @test_app.command()
        def test_cmd(ctx: typer.Context) -> None:
            try:
                get_context(ctx)
            except RuntimeError as e:
                captured_error["error"] = e
                raise

        result = runner.invoke(test_app, ["test-cmd"])
        assert result.exit_code != 0
        assert captured_error["error"] is not None
        assert "CLIContext not found" in str(captured_error["error"])


class TestContextServiceInitialization:
    """Tests for service initialization via context in callback."""

    @pytest.fixture
    def test_app_with_command(self) -> tuple[typer.Typer, dict[str, object]]:
        """Create app with test command to inspect service initialization."""
        from teams_phone.cli.main import main

        test_app = typer.Typer(no_args_is_help=True)
        test_app.callback()(main)

        captured: dict[str, object] = {}

        @test_app.command()
        def test_cmd(ctx: typer.Context) -> None:
            cli_ctx = get_context(ctx)
            captured["output_formatter"] = cli_ctx.output_formatter
            captured["config_manager"] = cli_ctx.config_manager
            captured["json_output"] = cli_ctx.json_output
            captured["no_color"] = cli_ctx.no_color

        return test_app, captured

    @pytest.mark.unit
    def test_output_formatter_initialized_in_callback(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """OutputFormatter is initialized in callback."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["test-cmd"])

        assert result.exit_code == 0
        assert captured["output_formatter"] is not None
        assert isinstance(captured["output_formatter"], OutputFormatter)

    @pytest.mark.unit
    def test_output_formatter_respects_json_flag(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """OutputFormatter uses json_mode from --json flag."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["--json", "test-cmd"])

        assert result.exit_code == 0
        formatter = captured["output_formatter"]
        assert isinstance(formatter, OutputFormatter)
        assert formatter.json_mode is True

    @pytest.mark.unit
    def test_output_formatter_respects_no_color_flag(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """OutputFormatter uses no_color from --no-color flag."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["--no-color", "test-cmd"])

        assert result.exit_code == 0
        formatter = captured["output_formatter"]
        assert isinstance(formatter, OutputFormatter)
        assert formatter.no_color is True

    @pytest.mark.unit
    def test_config_manager_lazy_initialized(
        self, test_app_with_command: tuple[typer.Typer, dict[str, object]]
    ) -> None:
        """ConfigManager is NOT initialized in callback (lazy initialization)."""
        test_app, captured = test_app_with_command
        result = runner.invoke(test_app, ["test-cmd"])

        assert result.exit_code == 0
        # ConfigManager should be None until explicitly accessed
        assert captured["config_manager"] is None
