"""Tests for CLI commands."""

from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch

import pytest
from click.testing import CliRunner

from zae_limiter.cli import cli


@pytest.fixture
def runner() -> CliRunner:
    """Create a Click CLI runner."""
    return CliRunner()


class TestCLI:
    """Test CLI commands."""

    def test_cli_help(self, runner: CliRunner) -> None:
        """Test CLI help message."""
        result = runner.invoke(cli, ["--help"])
        assert result.exit_code == 0
        assert "zae-limiter infrastructure management CLI" in result.output

    def test_deploy_help(self, runner: CliRunner) -> None:
        """Test deploy command help."""
        result = runner.invoke(cli, ["deploy", "--help"])
        assert result.exit_code == 0
        assert "Deploy CloudFormation stack" in result.output
        assert "--name" in result.output
        assert "--region" in result.output
        assert "--endpoint-url" in result.output
        # New options
        assert "--lambda-timeout" in result.output
        assert "--lambda-memory" in result.output
        assert "--enable-alarms" in result.output
        assert "--no-alarms" in result.output
        assert "--alarm-sns-topic" in result.output
        assert "--lambda-duration-threshold-pct" in result.output

    def test_delete_help(self, runner: CliRunner) -> None:
        """Test delete command help."""
        result = runner.invoke(cli, ["delete", "--help"])
        assert result.exit_code == 0
        assert "Delete CloudFormation stack" in result.output
        assert "--name" in result.output

    def test_status_help(self, runner: CliRunner) -> None:
        """Test status command help."""
        result = runner.invoke(cli, ["status", "--help"])
        assert result.exit_code == 0
        assert "Get comprehensive status" in result.output

    def test_cfn_template_help(self, runner: CliRunner) -> None:
        """Test cfn-template command help."""
        result = runner.invoke(cli, ["cfn-template", "--help"])
        assert result.exit_code == 0
        assert "Export CloudFormation template" in result.output

    def test_cfn_template_to_stdout(self, runner: CliRunner) -> None:
        """Test exporting template to stdout."""
        result = runner.invoke(cli, ["cfn-template"])
        assert result.exit_code == 0
        assert "AWSTemplateFormatVersion" in result.output
        assert "AWS::DynamoDB::Table" in result.output
        assert "RateLimitsTable" in result.output

    def test_cfn_template_to_file(self, runner: CliRunner, tmp_path: Path) -> None:
        """Test exporting template to file."""
        output_file = tmp_path / "template.yaml"
        result = runner.invoke(cli, ["cfn-template", "--output", str(output_file)])

        assert result.exit_code == 0
        assert "Template exported to:" in result.output
        assert output_file.exists()

        content = output_file.read_text()
        assert "AWSTemplateFormatVersion" in content
        assert "AWS::DynamoDB::Table" in content

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_default_parameters(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with default parameters."""
        # Mock stack manager
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
                "stack_name": "ZAEL-rate-limits",
            }
        )
        mock_instance.deploy_lambda_code = AsyncMock(
            return_value={
                "status": "deployed",
                "function_arn": "arn:aws:lambda:us-east-1:123456789:function:test",
                "code_sha256": "abc123def456",
                "size_bytes": 30000,
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(cli, ["deploy"])

        assert result.exit_code == 0
        assert "Deploying stack: ZAEL-rate-limits" in result.output
        assert "✓" in result.output
        assert "Version record initialized" in result.output

        # Verify default values for new parameters via StackOptions
        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        # Default values: lambda_timeout=60, lambda_memory=256, alarms enabled, 80% threshold
        assert stack_options.lambda_timeout == 60
        assert stack_options.lambda_memory == 256
        assert stack_options.enable_alarms is True
        assert stack_options.lambda_duration_threshold_pct == 80

        # Verify version record was created
        mock_repo_instance.set_version_record.assert_called_once()
        version_call_args = mock_repo_instance.set_version_record.call_args
        assert version_call_args[1]["schema_version"] == "1.0.0"
        assert version_call_args[1]["client_min_version"] == "0.0.0"

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_custom_parameters(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with custom parameters."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-my-custom-stack"
        mock_instance.table_name = "ZAEL-my-custom-stack"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--name",
                "my-custom-stack",
                "--region",
                "us-west-2",
                "--snapshot-windows",
                "hourly",
                "--retention-days",
                "30",
                "--no-aggregator",
            ],
        )

        assert result.exit_code == 0
        assert "ZAEL-my-custom-stack" in result.output

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_with_pitr_recovery_days(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with --pitr-recovery-days parameter."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--pitr-recovery-days",
                "7",
                "--no-aggregator",
            ],
        )

        assert result.exit_code == 0
        # Verify create_stack was called with pitr_recovery_days parameter
        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        assert stack_options.pitr_recovery_days == 7

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_with_log_retention_days(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with --log-retention-days parameter."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--log-retention-days",
                "90",
                "--no-aggregator",
            ],
        )

        assert result.exit_code == 0
        # Verify create_stack was called with log_retention_days parameter
        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        assert stack_options.log_retention_days == 90

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_with_lambda_timeout_and_memory(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with --lambda-timeout and --lambda-memory parameters."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.deploy_lambda_code = AsyncMock(
            return_value={
                "status": "deployed",
                "function_arn": "arn:aws:lambda:us-east-1:123456789:function:test",
                "code_sha256": "abc123def456",
                "size_bytes": 30000,
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-timeout",
                "120",
                "--lambda-memory",
                "512",
            ],
        )

        assert result.exit_code == 0
        assert "Lambda timeout: 120s" in result.output
        assert "Lambda memory: 512MB" in result.output

        # Verify create_stack was called with correct parameters
        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        assert stack_options.lambda_timeout == 120
        assert stack_options.lambda_memory == 512

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_with_alarms_disabled(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with --no-alarms parameter."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--no-alarms",
                "--no-aggregator",
            ],
        )

        assert result.exit_code == 0
        assert "Alarms: disabled" in result.output

        # Verify create_stack was called with enable_alarms=false
        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        assert stack_options.enable_alarms is False

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_with_alarm_sns_topic(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with --alarm-sns-topic parameter."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        sns_topic = "arn:aws:sns:us-east-1:123456789012:my-topic"
        result = runner.invoke(
            cli,
            [
                "deploy",
                "--alarm-sns-topic",
                sns_topic,
                "--no-aggregator",
            ],
        )

        assert result.exit_code == 0
        assert f"Alarm SNS topic: {sns_topic}" in result.output

        # Verify create_stack was called with alarm_sns_topic
        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        assert stack_options.alarm_sns_topic == sns_topic

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_with_lambda_duration_threshold_pct(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with --lambda-duration-threshold-pct parameter."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        # Lambda timeout 60s with 50% threshold
        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-timeout",
                "60",
                "--lambda-duration-threshold-pct",
                "50",
                "--no-aggregator",
            ],
        )

        assert result.exit_code == 0

        # Verify create_stack was called with StackOptions having correct values
        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        assert stack_options.lambda_timeout == 60
        assert stack_options.lambda_duration_threshold_pct == 50

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_duration_threshold_calculation(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test that duration threshold is correctly calculated from timeout and percentage."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        # Lambda timeout 120s with 80% threshold
        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-timeout",
                "120",
                "--lambda-duration-threshold-pct",
                "80",
                "--no-aggregator",
            ],
        )

        assert result.exit_code == 0

        call_args = mock_instance.create_stack.call_args
        assert call_args is not None
        stack_options = call_args[1]["stack_options"]
        assert stack_options.lambda_timeout == 120
        assert stack_options.lambda_duration_threshold_pct == 80
        # Verify to_parameters computes the ms value correctly (120s * 1000 * 0.8 = 96000ms)
        params = stack_options.to_parameters()
        assert params["lambda_duration_threshold"] == "96000"

    def test_deploy_lambda_timeout_invalid_range(self, runner: CliRunner) -> None:
        """Test that --lambda-timeout rejects values outside 1-900."""
        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-timeout",
                "0",
            ],
        )
        assert result.exit_code != 0
        # Click's IntRange provides error message with the invalid value
        assert "0" in result.output or "range" in result.output.lower()

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-timeout",
                "901",
            ],
        )
        assert result.exit_code != 0

    def test_deploy_lambda_memory_invalid_range(self, runner: CliRunner) -> None:
        """Test that --lambda-memory rejects values outside 128-3008."""
        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-memory",
                "64",
            ],
        )
        assert result.exit_code != 0

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-memory",
                "3009",
            ],
        )
        assert result.exit_code != 0

    def test_deploy_duration_threshold_pct_invalid_range(self, runner: CliRunner) -> None:
        """Test that --lambda-duration-threshold-pct rejects values outside 1-100."""
        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-duration-threshold-pct",
                "0",
            ],
        )
        assert result.exit_code != 0

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--lambda-duration-threshold-pct",
                "101",
            ],
        )
        assert result.exit_code != 0

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_lambda_skipped_local(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy shows correct message when Lambda deployment is skipped for local."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-rate-limits"
        mock_instance.table_name = "ZAEL-rate-limits"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        # Lambda deployment returns skipped_local status
        mock_instance.deploy_lambda_code = AsyncMock(
            return_value={
                "status": "skipped_local",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(cli, ["deploy"])

        assert result.exit_code == 0
        assert "Lambda deployment skipped (local environment)" in result.output

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_deploy_with_endpoint_url(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test deploy command with --endpoint-url for LocalStack."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-test"
        mock_instance.table_name = "ZAEL-test"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "CREATE_COMPLETE",
                "stack_id": "test-stack-id",
            }
        )
        mock_instance.deploy_lambda_code = AsyncMock(
            return_value={
                "status": "deployed",
                "function_arn": "arn:aws:lambda:us-east-1:000000000000:function:test",
                "code_sha256": "abc123",
                "size_bytes": 40000,
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        # Mock repository for version record
        mock_repo_instance = Mock()
        mock_repo_instance.set_version_record = AsyncMock()
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(
            cli,
            [
                "deploy",
                "--name",
                "test-table",
                "--endpoint-url",
                "http://localhost:4566",
                "--region",
                "us-east-1",
            ],
        )

        assert result.exit_code == 0
        # Verify StackManager was called with endpoint_url
        mock_stack_manager.assert_called_once_with(
            "test-table", "us-east-1", "http://localhost:4566"
        )

    @patch("zae_limiter.cli.StackManager")
    def test_deploy_skip_local(self, mock_stack_manager: Mock, runner: CliRunner) -> None:
        """Test deploy skips CloudFormation for local DynamoDB."""
        mock_instance = Mock()
        mock_instance.stack_name = "ZAEL-test-stack"
        mock_instance.table_name = "ZAEL-test-stack"
        mock_instance.create_stack = AsyncMock(
            return_value={
                "status": "skipped_local",
                "stack_id": None,
                "message": "CloudFormation skipped for local DynamoDB",
            }
        )
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        result = runner.invoke(cli, ["deploy"])

        assert result.exit_code == 0
        assert "skipped" in result.output.lower()

    @patch("zae_limiter.cli.StackManager")
    def test_delete_with_confirmation(self, mock_stack_manager: Mock, runner: CliRunner) -> None:
        """Test delete command requires confirmation."""
        mock_instance = Mock()
        mock_stack_manager.return_value = mock_instance

        # No confirmation - should abort
        result = runner.invoke(cli, ["delete", "--name", "test-stack"], input="n\n")
        assert result.exit_code == 1
        assert "Aborted" in result.output

    @patch("zae_limiter.cli.StackManager")
    def test_delete_with_yes_flag(self, mock_stack_manager: Mock, runner: CliRunner) -> None:
        """Test delete command with -y flag."""
        mock_instance = Mock()
        mock_instance.delete_stack = AsyncMock(return_value=None)
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        result = runner.invoke(cli, ["delete", "--name", "test-stack", "--yes", "--wait"])

        assert result.exit_code == 0
        assert "✓" in result.output
        assert "deleted successfully" in result.output

    @patch("zae_limiter.cli.StackManager")
    def test_delete_no_wait(self, mock_stack_manager: Mock, runner: CliRunner) -> None:
        """Test delete command without waiting."""
        mock_instance = Mock()
        mock_instance.delete_stack = AsyncMock(return_value=None)
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        result = runner.invoke(cli, ["delete", "--name", "test-stack", "--yes", "--no-wait"])

        assert result.exit_code == 0
        assert "initiated" in result.output

    @patch("zae_limiter.cli.StackManager")
    def test_delete_with_endpoint_url(self, mock_stack_manager: Mock, runner: CliRunner) -> None:
        """Test delete command with --endpoint-url for LocalStack."""
        mock_instance = Mock()
        mock_instance.delete_stack = AsyncMock(return_value=None)
        mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
        mock_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_instance

        result = runner.invoke(
            cli,
            [
                "delete",
                "--name",
                "test-stack",
                "--endpoint-url",
                "http://localhost:4566",
                "--region",
                "us-east-1",
                "--yes",
            ],
        )

        assert result.exit_code == 0
        # Verify StackManager was called with endpoint_url
        mock_stack_manager.assert_called_once_with(
            "test-stack", "us-east-1", "http://localhost:4566"
        )

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_status_exists(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test status command for existing stack."""
        # Mock StackManager
        mock_manager_instance = Mock()
        mock_manager_instance.get_stack_status = AsyncMock(return_value="CREATE_COMPLETE")
        mock_manager_instance.__aenter__ = AsyncMock(return_value=mock_manager_instance)
        mock_manager_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_manager_instance

        # Mock Repository
        mock_repo_instance = Mock()
        mock_repo_instance._get_client = AsyncMock(
            return_value=Mock(
                describe_table=AsyncMock(
                    return_value={
                        "Table": {
                            "TableStatus": "ACTIVE",
                            "ItemCount": 100,
                            "TableSizeInBytes": 1024,
                            "StreamSpecification": {"StreamEnabled": True},
                        }
                    }
                )
            )
        )
        mock_repo_instance.get_version_record = AsyncMock(
            return_value={"schema_version": "1.0.0", "lambda_version": "0.1.0"}
        )
        mock_repo_instance.close = AsyncMock(return_value=None)
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(cli, ["status", "--name", "test-stack"])

        assert result.exit_code == 0
        assert "CREATE_COMPLETE" in result.output
        assert "✓ Infrastructure is ready" in result.output
        assert "Available:     ✓ Yes" in result.output

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_status_not_found(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test status command for non-existent stack."""
        # Mock StackManager - stack doesn't exist
        mock_manager_instance = Mock()
        mock_manager_instance.get_stack_status = AsyncMock(return_value=None)
        mock_manager_instance.__aenter__ = AsyncMock(return_value=mock_manager_instance)
        mock_manager_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_manager_instance

        # Mock Repository - table doesn't exist
        mock_repo_instance = Mock()
        mock_repo_instance._get_client = AsyncMock(
            return_value=Mock(describe_table=AsyncMock(side_effect=Exception("Table not found")))
        )
        mock_repo_instance.close = AsyncMock(return_value=None)
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(cli, ["status", "--name", "nonexistent"])

        assert result.exit_code == 1
        assert "Not found" in result.output
        assert "✗ Infrastructure is not available" in result.output

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_status_in_progress(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test status command for stack in progress."""
        # Mock StackManager
        mock_manager_instance = Mock()
        mock_manager_instance.get_stack_status = AsyncMock(return_value="CREATE_IN_PROGRESS")
        mock_manager_instance.__aenter__ = AsyncMock(return_value=mock_manager_instance)
        mock_manager_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_manager_instance

        # Mock Repository
        mock_repo_instance = Mock()
        mock_repo_instance._get_client = AsyncMock(
            return_value=Mock(
                describe_table=AsyncMock(
                    return_value={
                        "Table": {
                            "TableStatus": "ACTIVE",
                            "ItemCount": 0,
                            "TableSizeInBytes": 0,
                            "StreamSpecification": {"StreamEnabled": True},
                        }
                    }
                )
            )
        )
        mock_repo_instance.get_version_record = AsyncMock(return_value=None)
        mock_repo_instance.close = AsyncMock(return_value=None)
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(cli, ["status", "--name", "test-stack"])

        assert result.exit_code == 0
        assert "CREATE_IN_PROGRESS" in result.output
        assert "⏳" in result.output

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.cli.StackManager")
    def test_status_failed(
        self, mock_stack_manager: Mock, mock_repository: Mock, runner: CliRunner
    ) -> None:
        """Test status command for failed stack."""
        # Mock StackManager
        mock_manager_instance = Mock()
        mock_manager_instance.get_stack_status = AsyncMock(return_value="CREATE_FAILED")
        mock_manager_instance.__aenter__ = AsyncMock(return_value=mock_manager_instance)
        mock_manager_instance.__aexit__ = AsyncMock(return_value=None)
        mock_stack_manager.return_value = mock_manager_instance

        # Mock Repository - table might not exist after failed create
        mock_repo_instance = Mock()
        mock_repo_instance._get_client = AsyncMock(
            return_value=Mock(
                describe_table=AsyncMock(
                    return_value={
                        "Table": {
                            "TableStatus": "ACTIVE",
                            "ItemCount": 0,
                            "TableSizeInBytes": 0,
                        }
                    }
                )
            )
        )
        mock_repo_instance.get_version_record = AsyncMock(return_value=None)
        mock_repo_instance.close = AsyncMock(return_value=None)
        mock_repository.return_value = mock_repo_instance

        result = runner.invoke(cli, ["status", "--name", "test-stack"])

        assert result.exit_code == 1
        assert "CREATE_FAILED" in result.output
        assert "✗" in result.output

    def test_version_help(self, runner: CliRunner) -> None:
        """Test version command help."""
        result = runner.invoke(cli, ["version", "--help"])
        assert result.exit_code == 0
        assert "Show infrastructure version information" in result.output
        assert "--name" in result.output

    def test_upgrade_help(self, runner: CliRunner) -> None:
        """Test upgrade command help."""
        result = runner.invoke(cli, ["upgrade", "--help"])
        assert result.exit_code == 0
        assert "Upgrade infrastructure to match client version" in result.output
        assert "--name" in result.output
        assert "--lambda-only" in result.output
        assert "--force" in result.output

    def test_check_help(self, runner: CliRunner) -> None:
        """Test check command help."""
        result = runner.invoke(cli, ["check", "--help"])
        assert result.exit_code == 0
        assert "Check infrastructure compatibility" in result.output
        assert "--name" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_version_not_initialized(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test version command when infrastructure not initialized."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(return_value=None)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["version", "--name", "test-table"])

        assert result.exit_code == 0
        assert "Not initialized" in result.output
        assert "zae-limiter deploy" in result.output

    @patch("zae_limiter.__version__", "1.0.0")
    @patch("zae_limiter.repository.Repository")
    def test_version_compatible(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test version command when versions are compatible."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(
            return_value={
                "schema_version": "1.0.0",
                "lambda_version": "1.0.0",
                "client_min_version": "0.0.0",
            }
        )
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["version", "--name", "test-table"])

        assert result.exit_code == 0
        assert "Infrastructure Version" in result.output
        assert "Schema Version:" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_version_with_endpoint_url(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test version command with --endpoint-url for LocalStack."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(return_value=None)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(
            cli,
            [
                "version",
                "--name",
                "test-table",
                "--endpoint-url",
                "http://localhost:4566",
                "--region",
                "us-east-1",
            ],
        )

        assert result.exit_code == 0
        # Verify Repository was called with endpoint_url
        mock_repo_class.assert_called_once_with("test-table", "us-east-1", "http://localhost:4566")

    @patch("zae_limiter.repository.Repository")
    def test_check_not_initialized(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test check command when infrastructure not initialized."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(return_value=None)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["check", "--name", "test-table"])

        assert result.exit_code == 1
        assert "NOT INITIALIZED" in result.output

    @patch("zae_limiter.__version__", "1.0.0")
    @patch("zae_limiter.repository.Repository")
    def test_check_compatible(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test check command when compatible."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(
            return_value={
                "schema_version": "1.0.0",
                "lambda_version": "1.0.0",
                "client_min_version": "0.0.0",
            }
        )
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["check", "--name", "test-table"])

        assert result.exit_code == 0
        assert "Compatibility Check" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_check_with_endpoint_url(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test check command with --endpoint-url for LocalStack."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(return_value=None)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(
            cli,
            [
                "check",
                "--name",
                "test-table",
                "--endpoint-url",
                "http://localhost:4566",
                "--region",
                "us-east-1",
            ],
        )

        assert result.exit_code == 1  # Not initialized
        # Verify Repository was called with endpoint_url
        mock_repo_class.assert_called_once_with("test-table", "us-east-1", "http://localhost:4566")

    @patch("zae_limiter.repository.Repository")
    def test_upgrade_not_initialized(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test upgrade command when infrastructure not initialized."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(return_value=None)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["upgrade", "--name", "test-table"])

        assert result.exit_code == 1
        assert "not initialized" in result.output.lower()
        assert "zae-limiter deploy" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_upgrade_with_endpoint_url(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test upgrade command with --endpoint-url for LocalStack."""
        mock_repo = Mock()
        mock_repo.get_version_record = AsyncMock(return_value=None)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(
            cli,
            [
                "upgrade",
                "--name",
                "test-table",
                "--endpoint-url",
                "http://localhost:4566",
                "--region",
                "us-east-1",
            ],
        )

        assert result.exit_code == 1  # Not initialized
        # Verify Repository was called with endpoint_url
        mock_repo_class.assert_called_once_with("test-table", "us-east-1", "http://localhost:4566")


class TestLambdaExport:
    """Test lambda-export command."""

    def test_lambda_export_help(self, runner: CliRunner) -> None:
        """Test lambda-export command help."""
        result = runner.invoke(cli, ["lambda-export", "--help"])
        assert result.exit_code == 0
        assert "Export Lambda deployment package" in result.output
        assert "--output" in result.output
        assert "--info" in result.output
        assert "--force" in result.output

    def test_lambda_export_info(self, runner: CliRunner) -> None:
        """Test lambda-export --info flag."""
        result = runner.invoke(cli, ["lambda-export", "--info"])
        assert result.exit_code == 0
        assert "Lambda Package Information" in result.output
        assert "Package path:" in result.output
        assert "Python files:" in result.output
        assert "Handler:" in result.output
        assert "zae_limiter.aggregator.handler.handler" in result.output

    def test_lambda_export_to_file(self, runner: CliRunner, tmp_path: Path) -> None:
        """Test exporting Lambda package to file."""
        import zipfile

        output_file = tmp_path / "test-lambda.zip"
        result = runner.invoke(cli, ["lambda-export", "--output", str(output_file)])

        assert result.exit_code == 0
        assert "Exported Lambda package to:" in result.output
        assert "KB" in result.output
        assert output_file.exists()

        # Verify it's a valid zip file
        with zipfile.ZipFile(output_file, "r") as zf:
            names = zf.namelist()
            assert any("zae_limiter" in name for name in names)
            assert any("handler.py" in name for name in names)

    def test_lambda_export_default_filename(self, runner: CliRunner) -> None:
        """Test lambda-export uses default filename."""
        # Use isolated filesystem to avoid polluting the project directory
        with runner.isolated_filesystem():
            result = runner.invoke(cli, ["lambda-export"])

            assert result.exit_code == 0
            assert "lambda.zip" in result.output
            assert Path("lambda.zip").exists()

    def test_lambda_export_refuses_overwrite(self, runner: CliRunner, tmp_path: Path) -> None:
        """Test lambda-export refuses to overwrite existing file."""
        output_file = tmp_path / "existing.zip"
        output_file.write_bytes(b"existing content")

        result = runner.invoke(cli, ["lambda-export", "--output", str(output_file)])

        assert result.exit_code == 1
        assert "File already exists" in result.output
        assert "--force" in result.output
        # Original file should be unchanged
        assert output_file.read_bytes() == b"existing content"

    def test_lambda_export_force_overwrite(self, runner: CliRunner, tmp_path: Path) -> None:
        """Test lambda-export with --force overwrites existing file."""
        output_file = tmp_path / "existing.zip"
        output_file.write_bytes(b"existing content")

        result = runner.invoke(cli, ["lambda-export", "--output", str(output_file), "--force"])

        assert result.exit_code == 0
        assert "Exported Lambda package to:" in result.output
        # File should be overwritten (different content)
        assert output_file.read_bytes() != b"existing content"

    def test_lambda_export_creates_parent_directory(
        self, runner: CliRunner, tmp_path: Path
    ) -> None:
        """Test lambda-export creates parent directories if needed."""
        output_file = tmp_path / "nested" / "dirs" / "lambda.zip"
        result = runner.invoke(cli, ["lambda-export", "--output", str(output_file)])

        assert result.exit_code == 0
        assert output_file.exists()

    def test_lambda_export_short_flags(self, runner: CliRunner, tmp_path: Path) -> None:
        """Test lambda-export short flags -o and -f work."""
        output_file = tmp_path / "short-flag.zip"
        output_file.write_bytes(b"existing")

        result = runner.invoke(cli, ["lambda-export", "-o", str(output_file), "-f"])

        assert result.exit_code == 0
        assert output_file.exists()


class TestCLIValidationErrors:
    """Test CLI commands reject invalid names with helpful error messages."""

    def test_deploy_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test deploy rejects names with underscores."""
        result = runner.invoke(cli, ["deploy", "--name", "rate_limits"])
        assert result.exit_code == 1
        assert "underscore" in result.output.lower()

    def test_deploy_invalid_name_starts_with_number(self, runner: CliRunner) -> None:
        """Test deploy rejects names starting with numbers."""
        result = runner.invoke(cli, ["deploy", "--name", "123app"])
        assert result.exit_code == 1
        assert "error" in result.output.lower()

    def test_delete_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test delete rejects names with underscores."""
        result = runner.invoke(cli, ["delete", "--name", "rate_limits", "--yes"])
        assert result.exit_code == 1
        assert "underscore" in result.output.lower()

    def test_status_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test status rejects names with underscores."""
        result = runner.invoke(cli, ["status", "--name", "rate_limits"])
        assert result.exit_code == 1
        assert "underscore" in result.output.lower()

    def test_version_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test version rejects names with underscores."""
        result = runner.invoke(cli, ["version", "--name", "rate_limits"])
        assert result.exit_code == 1
        assert "underscore" in result.output.lower()

    def test_check_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test check rejects names with underscores."""
        result = runner.invoke(cli, ["check", "--name", "rate_limits"])
        assert result.exit_code == 1
        assert "underscore" in result.output.lower()

    def test_upgrade_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test upgrade rejects names with underscores."""
        result = runner.invoke(cli, ["upgrade", "--name", "rate_limits"])
        assert result.exit_code == 1
        assert "underscore" in result.output.lower()

    def test_deploy_invalid_name_with_period(self, runner: CliRunner) -> None:
        """Test deploy rejects names with periods."""
        result = runner.invoke(cli, ["deploy", "--name", "my.app"])
        assert result.exit_code == 1
        assert "period" in result.output.lower()

    def test_deploy_invalid_name_with_space(self, runner: CliRunner) -> None:
        """Test deploy rejects names with spaces."""
        result = runner.invoke(cli, ["deploy", "--name", "my app"])
        assert result.exit_code == 1
        assert "space" in result.output.lower()

    def test_audit_list_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test audit list rejects names with underscores."""
        result = runner.invoke(cli, ["audit", "list", "--name", "rate_limits", "-e", "test"])
        assert result.exit_code == 1
        assert "underscore" in result.output.lower()


class TestAuditCommands:
    """Test audit CLI commands."""

    def test_audit_help(self, runner: CliRunner) -> None:
        """Test audit command group help."""
        result = runner.invoke(cli, ["audit", "--help"])
        assert result.exit_code == 0
        assert "Audit log commands" in result.output

    def test_audit_list_help(self, runner: CliRunner) -> None:
        """Test audit list command help."""
        result = runner.invoke(cli, ["audit", "list", "--help"])
        assert result.exit_code == 0
        assert "List audit events for an entity" in result.output
        assert "--entity-id" in result.output
        assert "--limit" in result.output
        assert "--start-event-id" in result.output

    def test_audit_list_requires_entity_id(self, runner: CliRunner) -> None:
        """Test audit list requires --entity-id."""
        result = runner.invoke(cli, ["audit", "list"])
        assert result.exit_code != 0
        assert "entity-id" in result.output.lower()

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_no_events(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test audit list when no events are found."""
        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=[])
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["audit", "list", "-e", "test-entity"])

        assert result.exit_code == 0
        assert "No audit events found for entity: test-entity" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_with_events(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test audit list displays events in table format."""
        from zae_limiter.models import AuditEvent

        mock_events = [
            AuditEvent(
                event_id="01ABCDEF",
                timestamp="2025-01-16T12:00:00Z",
                action="entity_created",
                entity_id="test-entity",
                principal="admin@example.com",
                resource=None,
            ),
            AuditEvent(
                event_id="01ABCDEG",
                timestamp="2025-01-16T12:01:00Z",
                action="limits_set",
                entity_id="test-entity",
                principal="admin@example.com",
                resource="api-calls",
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=mock_events)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["audit", "list", "-e", "test-entity"])

        assert result.exit_code == 0
        assert "Audit Events for: test-entity" in result.output
        assert "Timestamp" in result.output
        assert "Action" in result.output
        assert "Principal" in result.output
        assert "Resource" in result.output
        assert "entity_created" in result.output
        assert "limits_set" in result.output
        assert "admin@example.com" in result.output
        assert "api-calls" in result.output
        assert "Total: 2 events" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_shows_long_principal_in_full(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test audit list shows long principal in full (auto-sized columns)."""
        from zae_limiter.models import AuditEvent

        long_principal = "arn:aws:iam::123456789012:user/very-long-username-that-exceeds-limit"
        mock_events = [
            AuditEvent(
                event_id="01ABCDEF",
                timestamp="2025-01-16T12:00:00Z",
                action="entity_created",
                entity_id="test-entity",
                principal=long_principal,
                resource=None,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=mock_events)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["audit", "list", "-e", "test-entity"])

        assert result.exit_code == 0
        # Full principal shown in auto-sized table
        assert long_principal in result.output
        # Box-drawing table format
        assert "+-" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_shows_pagination_hint(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test audit list shows pagination hint when limit is reached."""
        from zae_limiter.models import AuditEvent

        # Create exactly 5 events (matches limit)
        mock_events = [
            AuditEvent(
                event_id=f"01ABCDE{i}",
                timestamp="2025-01-16T12:00:00Z",
                action="entity_created",
                entity_id="test-entity",
                principal="admin@example.com",
                resource=None,
            )
            for i in range(5)
        ]

        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=mock_events)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["audit", "list", "-e", "test-entity", "-l", "5"])

        assert result.exit_code == 0
        assert "More events may exist" in result.output
        assert "--start-event-id" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_with_custom_limit(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test audit list with custom limit."""
        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=[])
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["audit", "list", "-e", "test-entity", "--limit", "50"])

        assert result.exit_code == 0
        mock_repo.get_audit_events.assert_called_once_with(
            entity_id="test-entity",
            limit=50,
            start_event_id=None,
        )

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_with_start_event_id(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test audit list with pagination via start-event-id."""
        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=[])
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(
            cli, ["audit", "list", "-e", "test-entity", "--start-event-id", "01ABCDEF"]
        )

        assert result.exit_code == 0
        mock_repo.get_audit_events.assert_called_once_with(
            entity_id="test-entity",
            limit=100,
            start_event_id="01ABCDEF",
        )

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_with_endpoint_url(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test audit list with --endpoint-url for LocalStack."""
        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=[])
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(
            cli,
            [
                "audit",
                "list",
                "-e",
                "test-entity",
                "--endpoint-url",
                "http://localhost:4566",
                "--region",
                "us-east-1",
            ],
        )

        assert result.exit_code == 0
        mock_repo_class.assert_called_once_with("limiter", "us-east-1", "http://localhost:4566")

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_handles_exception(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test audit list handles exceptions gracefully."""
        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(side_effect=Exception("DynamoDB error"))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["audit", "list", "-e", "test-entity"])

        assert result.exit_code == 1
        assert "Error" in result.output
        assert "Failed to list audit events" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_audit_list_handles_none_resource(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test audit list handles None resource field."""
        from zae_limiter.models import AuditEvent

        mock_events = [
            AuditEvent(
                event_id="01ABCDEF",
                timestamp="2025-01-16T12:00:00Z",
                action="entity_deleted",
                entity_id="test-entity",
                principal=None,
                resource=None,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_audit_events = AsyncMock(return_value=mock_events)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["audit", "list", "-e", "test-entity"])

        assert result.exit_code == 0
        # None values should display as "-"
        assert "-" in result.output
        assert "entity_deleted" in result.output


class TestUsageCommands:
    """Test usage CLI commands."""

    def test_usage_help(self, runner: CliRunner) -> None:
        """Test usage command group help."""
        result = runner.invoke(cli, ["usage", "--help"])
        assert result.exit_code == 0
        assert "Usage snapshot commands" in result.output

    def test_usage_list_help(self, runner: CliRunner) -> None:
        """Test usage list command help."""
        result = runner.invoke(cli, ["usage", "list", "--help"])
        assert result.exit_code == 0
        assert "List usage snapshots" in result.output
        assert "--entity-id" in result.output
        assert "--resource" in result.output
        assert "--window" in result.output
        assert "--start" in result.output
        assert "--end" in result.output
        assert "--limit" in result.output

    def test_usage_list_requires_entity_or_resource(self, runner: CliRunner) -> None:
        """Test usage list requires --entity-id or --resource."""
        result = runner.invoke(cli, ["usage", "list"])
        assert result.exit_code != 0
        assert "entity-id" in result.output.lower() or "resource" in result.output.lower()

    def test_usage_summary_help(self, runner: CliRunner) -> None:
        """Test usage summary command help."""
        result = runner.invoke(cli, ["usage", "summary", "--help"])
        assert result.exit_code == 0
        assert "Show aggregated usage summary" in result.output

    def test_usage_summary_requires_entity_or_resource(self, runner: CliRunner) -> None:
        """Test usage summary requires --entity-id or --resource."""
        result = runner.invoke(cli, ["usage", "summary"])
        assert result.exit_code != 0
        assert "entity-id" in result.output.lower() or "resource" in result.output.lower()

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_no_snapshots(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage list when no snapshots are found."""
        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=([], None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity"])

        assert result.exit_code == 0
        assert "No usage snapshots found" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_with_snapshots(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage list displays snapshots in table format."""
        from zae_limiter.models import UsageSnapshot

        mock_snapshots = [
            UsageSnapshot(
                entity_id="test-entity",
                resource="gpt-4",
                window_start="2024-01-15T10:00:00Z",
                window_end="2024-01-15T10:59:59Z",
                window_type="hourly",
                counters={"tpm": 1000, "rpm": 5},
                total_events=5,
            ),
            UsageSnapshot(
                entity_id="test-entity",
                resource="gpt-4",
                window_start="2024-01-15T11:00:00Z",
                window_end="2024-01-15T11:59:59Z",
                window_type="hourly",
                counters={"tpm": 2000, "rpm": 10},
                total_events=10,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=(mock_snapshots, None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity"])

        assert result.exit_code == 0
        assert "Usage Snapshots" in result.output
        assert "Window Start" in result.output
        assert "Resource" in result.output
        assert "gpt-4" in result.output
        assert "2024-01-15T10:00:00Z" in result.output
        assert "Total: 2 snapshots" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_by_resource(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage list by resource (GSI2 query)."""
        from zae_limiter.models import UsageSnapshot

        mock_snapshots = [
            UsageSnapshot(
                entity_id="entity-1",
                resource="gpt-4",
                window_start="2024-01-15T10:00:00Z",
                window_end="2024-01-15T10:59:59Z",
                window_type="hourly",
                counters={"tpm": 1000},
                total_events=5,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=(mock_snapshots, None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-r", "gpt-4"])

        assert result.exit_code == 0
        assert "gpt-4" in result.output
        mock_repo.get_usage_snapshots.assert_called_once()
        call_kwargs = mock_repo.get_usage_snapshots.call_args.kwargs
        assert call_kwargs["resource"] == "gpt-4"

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_with_filters(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage list with window type and time filters."""
        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=([], None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(
            cli,
            [
                "usage",
                "list",
                "-e",
                "test-entity",
                "-w",
                "hourly",
                "--start",
                "2024-01-15T00:00:00Z",
                "--end",
                "2024-01-15T23:59:59Z",
            ],
        )

        assert result.exit_code == 0
        mock_repo.get_usage_snapshots.assert_called_once()
        call_kwargs = mock_repo.get_usage_snapshots.call_args.kwargs
        assert call_kwargs["window_type"] == "hourly"
        assert call_kwargs["start_time"] == "2024-01-15T00:00:00Z"
        assert call_kwargs["end_time"] == "2024-01-15T23:59:59Z"

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_shows_pagination_hint(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test usage list shows pagination hint when more results available."""
        from zae_limiter.models import UsageSnapshot

        mock_snapshots = [
            UsageSnapshot(
                entity_id="test-entity",
                resource="gpt-4",
                window_start="2024-01-15T10:00:00Z",
                window_end="2024-01-15T10:59:59Z",
                window_type="hourly",
                counters={"tpm": 1000},
                total_events=5,
            ),
        ]
        # Return a next_key to indicate more results
        next_key = {"PK": {"S": "ENTITY#test"}, "SK": {"S": "#USAGE#..."}}

        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=(mock_snapshots, next_key))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity", "-l", "1"])

        assert result.exit_code == 0
        assert "more snapshots exist" in result.output.lower()

    @patch("zae_limiter.repository.Repository")
    def test_usage_summary_empty(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage summary with no data."""
        from zae_limiter.models import UsageSummary

        mock_summary = UsageSummary(
            snapshot_count=0,
            total={},
            average={},
            min_window_start=None,
            max_window_start=None,
        )

        mock_repo = Mock()
        mock_repo.get_usage_summary = AsyncMock(return_value=mock_summary)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "summary", "-e", "test-entity"])

        assert result.exit_code == 0
        assert "No usage data found" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_summary_with_data(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage summary displays aggregated data."""
        from zae_limiter.models import UsageSummary

        mock_summary = UsageSummary(
            snapshot_count=10,
            total={"tpm": 15000, "rpm": 75},
            average={"tpm": 1500.0, "rpm": 7.5},
            min_window_start="2024-01-15T10:00:00Z",
            max_window_start="2024-01-15T19:00:00Z",
        )

        mock_repo = Mock()
        mock_repo.get_usage_summary = AsyncMock(return_value=mock_summary)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "summary", "-e", "test-entity"])

        assert result.exit_code == 0
        assert "Usage Summary" in result.output
        assert "Snapshots:" in result.output
        assert "10" in result.output
        assert "Time Range:" in result.output
        assert "tpm" in result.output
        assert "rpm" in result.output
        assert "15000" in result.output or "15,000" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_summary_by_resource(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage summary by resource."""
        from zae_limiter.models import UsageSummary

        mock_summary = UsageSummary(
            snapshot_count=5,
            total={"tpm": 5000},
            average={"tpm": 1000.0},
            min_window_start="2024-01-15T10:00:00Z",
            max_window_start="2024-01-15T14:00:00Z",
        )

        mock_repo = Mock()
        mock_repo.get_usage_summary = AsyncMock(return_value=mock_summary)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "summary", "-r", "gpt-4"])

        assert result.exit_code == 0
        mock_repo.get_usage_summary.assert_called_once()
        call_kwargs = mock_repo.get_usage_summary.call_args.kwargs
        assert call_kwargs["resource"] == "gpt-4"

    def test_usage_list_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test usage list rejects names with underscores."""
        result = runner.invoke(cli, ["usage", "list", "--name", "rate_limits", "-e", "test"])
        assert result.exit_code != 0
        assert "underscore" in result.output.lower() or "hyphen" in result.output.lower()

    def test_usage_summary_invalid_name_with_underscore(self, runner: CliRunner) -> None:
        """Test usage summary rejects names with underscores."""
        result = runner.invoke(cli, ["usage", "summary", "--name", "rate_limits", "-e", "test"])
        assert result.exit_code != 0
        assert "underscore" in result.output.lower() or "hyphen" in result.output.lower()

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_shows_long_entity_id_in_full(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test usage list shows long entity IDs in full (auto-sized columns)."""
        from zae_limiter.models import UsageSnapshot

        long_entity_id = "very-long-entity-identifier-that-exceeds-display-width"
        mock_snapshots = [
            UsageSnapshot(
                entity_id=long_entity_id,
                resource="gpt-4",
                window_start="2024-01-15T10:00:00Z",
                window_end="2024-01-15T10:59:59Z",
                window_type="hourly",
                counters={"tpm": 1000},
                total_events=5,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=(mock_snapshots, None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "long-entity"])

        assert result.exit_code == 0
        # Full entity shown in auto-sized table (TableRenderer)
        assert long_entity_id in result.output
        # Box-drawing table format
        assert "+-" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_shows_long_resource_name_in_full(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test usage list shows long resource names in full (auto-sized columns)."""
        from zae_limiter.models import UsageSnapshot

        long_resource = "very-long-resource-name-that-exceeds-width"
        mock_snapshots = [
            UsageSnapshot(
                entity_id="user-123",
                resource=long_resource,
                window_start="2024-01-15T10:00:00Z",
                window_end="2024-01-15T10:59:59Z",
                window_type="hourly",
                counters={"tpm": 1000},
                total_events=5,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=(mock_snapshots, None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "user-123"])

        assert result.exit_code == 0
        # Full resource shown in auto-sized table (TableRenderer)
        assert long_resource in result.output
        # Box-drawing table format
        assert "+-" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_value_error(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage list handles ValueError from repository."""
        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(side_effect=ValueError("Invalid input"))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity"])

        assert result.exit_code != 0
        assert "Invalid input" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_generic_exception(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage list handles generic exceptions from repository."""
        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(side_effect=RuntimeError("Connection failed"))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity"])

        assert result.exit_code != 0
        assert "Failed to list usage snapshots" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_summary_with_window_filter(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test usage summary displays window filter info."""
        from zae_limiter.models import UsageSummary

        mock_summary = UsageSummary(
            snapshot_count=5,
            total={"tpm": 5000},
            average={"tpm": 1000.0},
            min_window_start="2024-01-15T10:00:00Z",
            max_window_start="2024-01-15T14:00:00Z",
        )

        mock_repo = Mock()
        mock_repo.get_usage_summary = AsyncMock(return_value=mock_summary)
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "summary", "-e", "test-entity", "-w", "hourly"])

        assert result.exit_code == 0
        assert "Window:" in result.output
        assert "hourly" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_summary_value_error(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage summary handles ValueError from repository."""
        mock_repo = Mock()
        mock_repo.get_usage_summary = AsyncMock(side_effect=ValueError("Bad date format"))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "summary", "-e", "test-entity"])

        assert result.exit_code != 0
        assert "Bad date format" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_summary_generic_exception(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test usage summary handles generic exceptions."""
        mock_repo = Mock()
        mock_repo.get_usage_summary = AsyncMock(side_effect=RuntimeError("Network timeout"))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "summary", "-e", "test-entity"])

        assert result.exit_code != 0
        assert "Failed to get usage summary" in result.output

    def test_usage_list_plot_help(self, runner: CliRunner) -> None:
        """Test usage list --plot option is in help."""
        result = runner.invoke(cli, ["usage", "list", "--help"])
        assert result.exit_code == 0
        assert "--plot" in result.output
        assert "ascii charts" in result.output.lower()

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_with_plot(self, mock_repo_class: Mock, runner: CliRunner) -> None:
        """Test usage list with --plot flag generates ASCII charts."""
        from zae_limiter.models import UsageSnapshot

        mock_snapshots = [
            UsageSnapshot(
                entity_id="test-entity",
                resource="gpt-4",
                window_start="2024-01-15T11:00:00Z",
                window_end="2024-01-15T11:59:59Z",
                window_type="hourly",
                counters={"tpm": 2000, "rpm": 10},
                total_events=10,
            ),
            UsageSnapshot(
                entity_id="test-entity",
                resource="gpt-4",
                window_start="2024-01-15T10:00:00Z",
                window_end="2024-01-15T10:59:59Z",
                window_type="hourly",
                counters={"tpm": 1000, "rpm": 5},
                total_events=5,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=(mock_snapshots, None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity", "--plot"])

        assert result.exit_code == 0
        # Plot output should have header with entity/resource context
        assert "Usage Plot: gpt-4 (hourly)" in result.output
        assert "Entity: test-entity" in result.output
        # Counter labels
        assert "TPM" in result.output
        assert "RPM" in result.output
        # Should have time range
        assert "Time range:" in result.output
        assert "Data points: 2" in result.output
        # Should still show total
        assert "Total: 2 snapshots" in result.output

    @patch("zae_limiter.repository.Repository")
    def test_usage_list_plot_shows_table_on_no_snapshots(
        self, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test usage list --plot with no snapshots shows empty message."""
        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=([], None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity", "--plot"])

        assert result.exit_code == 0
        assert "No usage snapshots found" in result.output

    @patch("zae_limiter.repository.Repository")
    @patch("zae_limiter.visualization.factory.PlotFormatter")
    def test_usage_list_plot_falls_back_to_table(
        self, mock_plot_formatter: Mock, mock_repo_class: Mock, runner: CliRunner
    ) -> None:
        """Test usage list --plot falls back to table if asciichartpy not installed."""
        from zae_limiter.models import UsageSnapshot

        mock_snapshots = [
            UsageSnapshot(
                entity_id="test-entity",
                resource="gpt-4",
                window_start="2024-01-15T10:00:00Z",
                window_end="2024-01-15T10:59:59Z",
                window_type="hourly",
                counters={"tpm": 1000},
                total_events=5,
            ),
        ]

        mock_repo = Mock()
        mock_repo.get_usage_snapshots = AsyncMock(return_value=(mock_snapshots, None))
        mock_repo.close = AsyncMock(return_value=None)
        mock_repo_class.return_value = mock_repo

        # Simulate asciichartpy not installed
        mock_plot_formatter.side_effect = ImportError(
            "asciichartpy is required for plot format. "
            "Install with: pip install 'zae-limiter[plot]'"
        )

        result = runner.invoke(cli, ["usage", "list", "-e", "test-entity", "--plot"])

        assert result.exit_code == 0
        # Should show warning and fallback message
        assert "Warning:" in result.output
        assert "Falling back to table format" in result.output
        # Should show table output
        assert "Usage Snapshots" in result.output
        assert "Window Start" in result.output


class TestListCommand:
    """Test list CLI command."""

    def test_list_help(self, runner: CliRunner) -> None:
        """Test list command help."""
        result = runner.invoke(cli, ["list", "--help"])
        assert result.exit_code == 0
        assert "List all deployed rate limiter instances" in result.output
        assert "--region" in result.output
        assert "--endpoint-url" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_empty_result(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command when no stacks exist."""
        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=[])
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "No rate limiter instances found in region" in result.output
        assert "zae-limiter deploy --name my-app" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_with_instances(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command displays instances in table format."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-my-app",
                user_name="my-app",
                region="us-east-1",
                stack_status="CREATE_COMPLETE",
                creation_time="2024-01-15T10:30:00Z",
                version="0.5.0",
                lambda_version="0.5.0",
                schema_version="1.0.0",
            ),
            LimiterInfo(
                stack_name="ZAEL-other-app",
                user_name="other-app",
                region="us-east-1",
                stack_status="UPDATE_COMPLETE",
                creation_time="2024-01-14T09:00:00Z",
                last_updated_time="2024-01-16T14:00:00Z",
                version="0.4.0",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "Rate Limiter Instances" in result.output
        assert "my-app" in result.output
        assert "other-app" in result.output
        # Full status shown in rich table format
        assert "CREATE_COMPLETE" in result.output
        assert "UPDATE_COMPLETE" in result.output
        assert "0.5.0" in result.output
        assert "Total: 2 instance(s)" in result.output
        # Box-drawing table borders
        assert "+-" in result.output
        assert "| Name" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_shows_healthy_status(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command shows healthy status in table."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-healthy",
                user_name="healthy",
                region="us-east-1",
                stack_status="CREATE_COMPLETE",
                creation_time="2024-01-15T10:30:00Z",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "CREATE_COMPLETE" in result.output
        # No problem summary for healthy stacks
        assert "failed" not in result.output
        assert "in progress" not in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_shows_in_progress_summary(
        self, mock_discovery_class: Mock, runner: CliRunner
    ) -> None:
        """Test list command shows in-progress summary."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-updating",
                user_name="updating",
                region="us-east-1",
                stack_status="UPDATE_IN_PROGRESS",
                creation_time="2024-01-15T10:30:00Z",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "UPDATE_IN_PROGRESS" in result.output
        assert "1 in progress" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_shows_failed_summary(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command shows failed summary."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-failed",
                user_name="failed",
                region="us-east-1",
                stack_status="CREATE_FAILED",
                creation_time="2024-01-15T10:30:00Z",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "CREATE_FAILED" in result.output
        assert "1 failed" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_shows_full_names(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command shows full names for copy/paste usability."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-very-long-name-exceeding-limit",
                user_name="very-long-name-exceeding-limit",
                region="us-east-1",
                stack_status="UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
                creation_time="2024-01-15T10:30:00Z",
                version="1.2.3-beta.4567890",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        # Full name shown in rich table format
        assert "very-long-name-exceeding-limit" in result.output
        # Full status shown
        assert "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_with_region(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command with --region option."""
        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=[])
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list", "--region", "eu-west-1"])

        assert result.exit_code == 0
        mock_discovery_class.assert_called_once_with(region="eu-west-1", endpoint_url=None)

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_with_endpoint_url(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command with --endpoint-url for LocalStack."""
        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=[])
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(
            cli,
            ["list", "--endpoint-url", "http://localhost:4566", "--region", "us-east-1"],
        )

        assert result.exit_code == 0
        mock_discovery_class.assert_called_once_with(
            region="us-east-1", endpoint_url="http://localhost:4566"
        )

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_handles_exception(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command handles exceptions gracefully."""
        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(side_effect=Exception("CloudFormation API error"))
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 1
        assert "Failed to list limiters" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_shows_na_for_missing_versions(
        self, mock_discovery_class: Mock, runner: CliRunner
    ) -> None:
        """Test list command shows N/A for missing version info."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-no-tags",
                user_name="no-tags",
                region="us-east-1",
                stack_status="CREATE_COMPLETE",
                creation_time="2024-01-15T10:30:00Z",
                version=None,
                lambda_version=None,
                schema_version=None,
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        # Missing versions shown as "-" for compact display
        assert "no-tags" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_formats_creation_date(
        self, mock_discovery_class: Mock, runner: CliRunner
    ) -> None:
        """Test list command formats creation date correctly."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-test",
                user_name="test",
                region="us-east-1",
                stack_status="CREATE_COMPLETE",
                creation_time="2024-01-15T10:30:00+00:00",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "2024-01-15" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_handles_invalid_creation_time(
        self, mock_discovery_class: Mock, runner: CliRunner
    ) -> None:
        """Test list command handles invalid creation time format."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-test",
                user_name="test",
                region="us-east-1",
                stack_status="CREATE_COMPLETE",
                creation_time="invalid-date-format",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "unknown" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_shows_region_in_header(
        self, mock_discovery_class: Mock, runner: CliRunner
    ) -> None:
        """Test list command shows region in header."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-test",
                user_name="test",
                region="ap-northeast-1",
                stack_status="CREATE_COMPLETE",
                creation_time="2024-01-15T10:30:00Z",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list", "--region", "ap-northeast-1"])

        assert result.exit_code == 0
        assert "ap-northeast-1" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_shows_default_region_when_not_specified(
        self, mock_discovery_class: Mock, runner: CliRunner
    ) -> None:
        """Test list command shows 'default' when region not specified."""
        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=[])
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "default" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_counts_problems(self, mock_discovery_class: Mock, runner: CliRunner) -> None:
        """Test list command counts failed and in-progress stacks."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-healthy",
                user_name="healthy",
                region="us-east-1",
                stack_status="CREATE_COMPLETE",
                creation_time="2024-01-15T10:30:00Z",
            ),
            LimiterInfo(
                stack_name="ZAEL-failed",
                user_name="failed",
                region="us-east-1",
                stack_status="CREATE_FAILED",
                creation_time="2024-01-15T10:30:00Z",
            ),
            LimiterInfo(
                stack_name="ZAEL-updating",
                user_name="updating",
                region="us-east-1",
                stack_status="UPDATE_IN_PROGRESS",
                creation_time="2024-01-15T10:30:00Z",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "Total: 3 instance(s)" in result.output
        # New format shows separate counts
        assert "1 failed" in result.output
        assert "1 in progress" in result.output

    @patch("zae_limiter.infra.discovery.InfrastructureDiscovery")
    def test_list_handles_unknown_status(
        self, mock_discovery_class: Mock, runner: CliRunner
    ) -> None:
        """Test list command handles unknown status gracefully."""
        from zae_limiter.models import LimiterInfo

        mock_limiters = [
            LimiterInfo(
                stack_name="ZAEL-unknown",
                user_name="unknown",
                region="us-east-1",
                stack_status="IMPORT_COMPLETE",  # Not healthy, not in_progress, not failed
                creation_time="2024-01-15T10:30:00Z",
            ),
        ]

        mock_discovery = Mock()
        mock_discovery.list_limiters = AsyncMock(return_value=mock_limiters)
        mock_discovery.__aenter__ = AsyncMock(return_value=mock_discovery)
        mock_discovery.__aexit__ = AsyncMock()
        mock_discovery_class.return_value = mock_discovery

        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        # Should not show problem count for IMPORT_COMPLETE
        assert "instance(s) need attention" not in result.output
