"""AUTO-GENERATED by scripts/generate_sync.py - DO NOT EDIT.

Source: test_stack_manager.py

This module provides synchronous versions of the async classes.
Changes should be made to the source file, then regenerated.
"""

from unittest.mock import MagicMock, patch

import pytest
from botocore.exceptions import ClientError

from zae_limiter import StackOptions
from zae_limiter.exceptions import StackAlreadyExistsError, StackCreationError
from zae_limiter.infra.sync_stack_manager import SyncStackManager


class TestStackManager:
    """Test SyncStackManager functionality."""

    def test_stack_name_normalization(self) -> None:
        """Test stack name validation (no prefix added)."""
        manager = SyncStackManager(stack_name="my-table", region="us-east-1")
        assert manager.stack_name == "my-table"
        assert manager.table_name == "my-table"

    def test_stack_name_already_prefixed(self) -> None:
        """Test stack name when already prefixed (returned as-is)."""
        manager = SyncStackManager(stack_name="ZAEL-my-table", region="us-east-1")
        assert manager.stack_name == "ZAEL-my-table"
        assert manager.table_name == "ZAEL-my-table"

    def test_format_parameters_defaults(self) -> None:
        """Test parameter formatting with defaults."""
        manager = SyncStackManager(stack_name="rate-limits", region="us-east-1")
        params = manager._format_parameters(None)
        assert len(params) == 1
        param_dict = {p["ParameterKey"]: p["ParameterValue"] for p in params}
        assert "SchemaVersion" in param_dict
        assert "TableName" not in param_dict

    def test_format_parameters_with_values(self) -> None:
        """Test parameter formatting with custom values."""
        manager = SyncStackManager(stack_name="rate-limits", region="us-east-1")
        params = manager._format_parameters(
            {
                "snapshot_windows": "hourly,daily,monthly",
                "usage_retention_days": "180",
                "enable_aggregator": "false",
            }
        )
        assert len(params) == 4
        param_dict = {p["ParameterKey"]: p["ParameterValue"] for p in params}
        assert "TableName" not in param_dict
        assert "SchemaVersion" in param_dict
        assert param_dict["SnapshotWindows"] == "hourly,daily,monthly"
        assert param_dict["SnapshotRetentionDays"] == "180"
        assert param_dict["EnableAggregator"] == "false"

    def test_format_parameters_with_pitr_recovery_days(self) -> None:
        """Test parameter formatting with PITR recovery period."""
        manager = SyncStackManager(stack_name="rate-limits", region="us-east-1")
        params = manager._format_parameters({"pitr_recovery_days": "7"})
        assert len(params) == 2
        param_dict = {p["ParameterKey"]: p["ParameterValue"] for p in params}
        assert "TableName" not in param_dict
        assert "SchemaVersion" in param_dict
        assert param_dict["PITRRecoveryPeriodDays"] == "7"

    def test_format_parameters_pitr_edge_cases(self) -> None:
        """Test PITR parameter with edge case values."""
        manager = SyncStackManager(stack_name="rate-limits", region="us-east-1")
        params_min = manager._format_parameters({"pitr_recovery_days": "1"})
        param_dict_min = {p["ParameterKey"]: p["ParameterValue"] for p in params_min}
        assert param_dict_min["PITRRecoveryPeriodDays"] == "1"
        params_max = manager._format_parameters({"pitr_recovery_days": "35"})
        param_dict_max = {p["ParameterKey"]: p["ParameterValue"] for p in params_max}
        assert param_dict_max["PITRRecoveryPeriodDays"] == "35"

    def test_format_parameters_with_log_retention_days(self) -> None:
        """Test parameter formatting with log retention days."""
        manager = SyncStackManager(stack_name="rate-limits", region="us-east-1")
        params = manager._format_parameters({"log_retention_days": "90"})
        assert len(params) == 2
        param_dict = {p["ParameterKey"]: p["ParameterValue"] for p in params}
        assert "TableName" not in param_dict
        assert "SchemaVersion" in param_dict
        assert param_dict["LogRetentionDays"] == "90"

    def test_format_parameters_with_deletion_protection(self) -> None:
        """Test parameter formatting with deletion protection enabled."""
        manager = SyncStackManager(stack_name="rate-limits", region="us-east-1")
        params = manager._format_parameters({"enable_deletion_protection": "true"})
        param_dict = {p["ParameterKey"]: p["ParameterValue"] for p in params}
        assert param_dict["EnableDeletionProtection"] == "true"

    def test_load_template(self) -> None:
        """Test loading CloudFormation template."""
        manager = SyncStackManager(stack_name="test", region="us-east-1")
        template = manager._load_template()
        assert template is not None
        assert "AWSTemplateFormatVersion" in template
        assert "AWS::DynamoDB::Table" in template
        assert "RateLimitsTable" in template

    def test_create_stack_with_parameters(self) -> None:
        """Test create_stack parameter handling with CloudFormation."""
        from unittest.mock import MagicMock

        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DescribeStacks",
                )
            )
            mock_client.create_stack = MagicMock(return_value={"StackId": "test-stack-id"})
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock()
            mock_client.get_waiter.return_value = mock_waiter
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(
                stack_name="test", region="us-east-1", endpoint_url="http://localhost:4566"
            )
            result = manager.create_stack(
                stack_options=StackOptions(snapshot_windows="hourly", usage_retention_days=30)
            )
            assert result["status"] == "CREATE_COMPLETE"
            assert result["stack_id"] == "test-stack-id"
            mock_client.create_stack.assert_called_once()


class TestStackExists:
    """Tests for stack_exists method with mocks."""

    def test_stack_exists_returns_true(self) -> None:
        """stack_exists returns True when stack exists."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={"Stacks": [{"StackStatus": "CREATE_COMPLETE"}]}
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.stack_exists("test-stack")
            assert result is True

    def test_stack_exists_returns_false_for_delete_complete(self) -> None:
        """stack_exists returns False when stack is DELETE_COMPLETE."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={"Stacks": [{"StackStatus": "DELETE_COMPLETE"}]}
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.stack_exists("test-stack")
            assert result is False

    def test_stack_exists_returns_false_on_validation_error(self) -> None:
        """stack_exists returns False when stack doesn't exist."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DescribeStacks",
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.stack_exists("non-existent")
            assert result is False

    def test_stack_exists_returns_false_for_empty_stacks(self) -> None:
        """stack_exists returns False when Stacks list is empty."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": []})
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.stack_exists("test-stack")
            assert result is False


class TestGetStackStatus:
    """Tests for get_stack_status method with mocks."""

    def test_returns_status_when_exists(self) -> None:
        """get_stack_status returns status string when stack exists."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={"Stacks": [{"StackStatus": "UPDATE_COMPLETE"}]}
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.get_stack_status("test-stack")
            assert result == "UPDATE_COMPLETE"

    def test_returns_none_on_validation_error(self) -> None:
        """get_stack_status returns None when stack doesn't exist."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DescribeStacks",
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.get_stack_status("non-existent")
            assert result is None

    def test_returns_none_for_empty_stacks(self) -> None:
        """get_stack_status returns None when Stacks list is empty."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": []})
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.get_stack_status("test-stack")
            assert result is None


class TestCreateStackErrors:
    """Tests for create_stack error handling."""

    def test_waits_for_in_progress_stack(self) -> None:
        """create_stack waits when stack is CREATE_IN_PROGRESS."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={"Stacks": [{"StackStatus": "CREATE_IN_PROGRESS"}]}
            )
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock()
            mock_client.get_waiter.return_value = mock_waiter
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.create_stack(wait=True)
            assert result["status"] == "already_exists_and_ready"
            mock_waiter.wait.assert_called_once()

    def test_returns_existing_stack_status(self) -> None:
        """create_stack returns existing stack info without waiting."""
        with (
            patch.object(
                SyncStackManager, "_get_client", new_callable=MagicMock
            ) as mock_get_client,
            patch.object(SyncStackManager, "ensure_tags", new_callable=MagicMock),
        ):
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={"Stacks": [{"StackStatus": "CREATE_COMPLETE"}]}
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.create_stack(wait=True)
            assert result["status"] == "CREATE_COMPLETE"
            assert "already exists" in result.get("message", "")

    def test_calls_ensure_tags_when_stack_exists(self) -> None:
        """create_stack calls ensure_tags on existing stacks."""
        with (
            patch.object(
                SyncStackManager, "_get_client", new_callable=MagicMock
            ) as mock_get_client,
            patch.object(
                SyncStackManager, "ensure_tags", new_callable=MagicMock
            ) as mock_ensure_tags,
        ):
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={"Stacks": [{"StackStatus": "CREATE_COMPLETE"}]}
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            opts = StackOptions(tags={"env": "prod"})
            manager.create_stack(stack_options=opts, wait=True)
            mock_ensure_tags.assert_called_once_with({"env": "prod"})

    def test_calls_ensure_tags_without_user_tags(self) -> None:
        """create_stack calls ensure_tags with None when no stack_options."""
        with (
            patch.object(
                SyncStackManager, "_get_client", new_callable=MagicMock
            ) as mock_get_client,
            patch.object(
                SyncStackManager, "ensure_tags", new_callable=MagicMock
            ) as mock_ensure_tags,
        ):
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={"Stacks": [{"StackStatus": "CREATE_COMPLETE"}]}
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            manager.create_stack(wait=True)
            mock_ensure_tags.assert_called_once_with(None)

    def test_raises_already_exists_on_race(self) -> None:
        """create_stack raises StackAlreadyExistsError on race condition."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DescribeStacks",
                )
            )
            mock_client.create_stack = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "AlreadyExistsException", "Message": "Stack exists"}},
                    "CreateStack",
                )
            )
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock()
            mock_client.get_waiter.return_value = mock_waiter
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackAlreadyExistsError):
                manager.create_stack()

    def test_already_exists_waiter_failure_still_raises(self) -> None:
        """create_stack still raises StackAlreadyExistsError when waiter fails."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DescribeStacks",
                )
            )
            mock_client.create_stack = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "AlreadyExistsException", "Message": "Stack exists"}},
                    "CreateStack",
                )
            )
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock(side_effect=Exception("Waiter timed out"))
            mock_client.get_waiter.return_value = mock_waiter
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackAlreadyExistsError):
                manager.create_stack()

    def test_raises_creation_error_on_client_error(self) -> None:
        """create_stack raises StackCreationError on other ClientErrors."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DescribeStacks",
                )
            )
            mock_client.create_stack = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "InsufficientCapabilities", "Message": "Need IAM"}},
                    "CreateStack",
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError) as exc_info:
                manager.create_stack()
            assert "Need IAM" in str(exc_info.value)

    def test_raises_on_waiter_failure(self) -> None:
        """create_stack raises StackCreationError when waiter fails."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DescribeStacks",
                )
            )
            mock_client.create_stack = MagicMock(return_value={"StackId": "test-id"})
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock(side_effect=Exception("Waiter timeout"))
            mock_client.get_waiter.return_value = mock_waiter
            mock_client.describe_stack_events = MagicMock(return_value={"StackEvents": []})
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError) as exc_info:
                manager.create_stack(wait=True)
            assert "Waiter timeout" in str(exc_info.value)


class TestDeleteStack:
    """Tests for delete_stack method."""

    def test_deletes_existing_stack(self) -> None:
        """delete_stack successfully deletes an existing stack."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.delete_stack = MagicMock()
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock()
            mock_client.get_waiter.return_value = mock_waiter
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            manager.delete_stack("test-stack", wait=True)
            mock_client.delete_stack.assert_called_once_with(StackName="test-stack")
            mock_waiter.wait.assert_called_once()

    def test_ignores_nonexistent_stack(self) -> None:
        """delete_stack ignores ValidationError for non-existent stack."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.delete_stack = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "Stack does not exist"}},
                    "DeleteStack",
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            manager.delete_stack("non-existent")

    def test_raises_on_other_errors(self) -> None:
        """delete_stack raises StackCreationError on other errors."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.delete_stack = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "AccessDenied", "Message": "Not authorized"}}, "DeleteStack"
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError) as exc_info:
                manager.delete_stack("test-stack")
            assert "Not authorized" in str(exc_info.value)


class TestDeployLambdaCode:
    """Tests for deploy_lambda_code method."""

    def test_successful_deployment(self) -> None:
        """deploy_lambda_code successfully deploys Lambda."""
        with (
            patch(
                "zae_limiter.infra.sync_stack_manager.build_lambda_package",
                return_value=b"fake-zip",
            ),
            patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class,
        ):
            mock_lambda = MagicMock()
            mock_lambda.update_function_code = MagicMock(
                return_value={
                    "FunctionArn": "arn:aws:lambda:us-east-1:123:function:test",
                    "CodeSha256": "abc123",
                }
            )
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock()
            mock_lambda.get_waiter.return_value = mock_waiter
            mock_lambda.tag_resource = MagicMock()
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.deploy_lambda_code()
            assert result["status"] == "deployed"
            assert result["function_arn"] == "arn:aws:lambda:us-east-1:123:function:test"
            mock_lambda.update_function_code.assert_called_once()

    def test_raises_on_build_failure(self) -> None:
        """deploy_lambda_code raises on package build failure."""
        with patch(
            "zae_limiter.infra.sync_stack_manager.build_lambda_package",
            side_effect=Exception("Build failed"),
        ):
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError) as exc_info:
                manager.deploy_lambda_code()
            assert "Build failed" in str(exc_info.value)


class TestDeployProvisionerCode:
    """Tests for deploy_provisioner_code method."""

    def test_successful_deployment(self) -> None:
        """deploy_provisioner_code successfully deploys Lambda."""
        with (
            patch(
                "zae_limiter.infra.sync_stack_manager.build_provisioner_package",
                return_value=b"fake-zip",
            ),
            patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class,
        ):
            mock_lambda = MagicMock()
            mock_lambda.update_function_code = MagicMock(
                return_value={
                    "FunctionArn": "arn:aws:lambda:us-east-1:123:function:test-limits-provisioner",
                    "CodeSha256": "abc123",
                }
            )
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock()
            mock_lambda.get_waiter.return_value = mock_waiter
            mock_lambda.tag_resource = MagicMock()
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.deploy_provisioner_code()
            assert result["status"] == "deployed"
            assert "limits-provisioner" in result["function_arn"]
            mock_lambda.update_function_code.assert_called_once_with(
                FunctionName="test-limits-provisioner", ZipFile=b"fake-zip"
            )

    def test_raises_on_build_failure(self) -> None:
        """deploy_provisioner_code raises on package build failure."""
        with patch(
            "zae_limiter.infra.sync_stack_manager.build_provisioner_package",
            side_effect=Exception("Build failed"),
        ):
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError) as exc_info:
                manager.deploy_provisioner_code()
            assert "Build failed" in str(exc_info.value)

    def test_endpoint_url_passed_to_client(self) -> None:
        """deploy_provisioner_code passes endpoint_url to Lambda client."""
        with (
            patch(
                "zae_limiter.infra.sync_stack_manager.build_provisioner_package",
                return_value=b"fake-zip",
            ),
            patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class,
        ):
            mock_lambda = MagicMock()
            mock_lambda.update_function_code = MagicMock(
                return_value={
                    "FunctionArn": "arn:aws:lambda:us-east-1:123:function:test-limits-provisioner",
                    "CodeSha256": "abc123",
                }
            )
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock()
            mock_lambda.get_waiter.return_value = mock_waiter
            mock_lambda.tag_resource = MagicMock()
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(
                stack_name="test", region="us-east-1", endpoint_url="http://localhost:4566"
            )
            manager.deploy_provisioner_code()
            mock_session.client.assert_called_once_with(
                "lambda", region_name="us-east-1", endpoint_url="http://localhost:4566"
            )

    def test_raises_on_waiter_failure(self) -> None:
        """deploy_provisioner_code raises when function_updated waiter fails."""
        with (
            patch(
                "zae_limiter.infra.sync_stack_manager.build_provisioner_package",
                return_value=b"fake-zip",
            ),
            patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class,
        ):
            mock_lambda = MagicMock()
            mock_lambda.update_function_code = MagicMock(
                return_value={
                    "FunctionArn": "arn:aws:lambda:us-east-1:123:function:test",
                    "CodeSha256": "abc123",
                }
            )
            mock_waiter = MagicMock()
            mock_waiter.wait = MagicMock(side_effect=Exception("Waiter timeout"))
            mock_lambda.get_waiter.return_value = mock_waiter
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError, match="provisioner update failed"):
                manager.deploy_provisioner_code()

    def test_raises_on_active_waiter_failure(self) -> None:
        """deploy_provisioner_code raises when function_active waiter fails."""
        with (
            patch(
                "zae_limiter.infra.sync_stack_manager.build_provisioner_package",
                return_value=b"fake-zip",
            ),
            patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class,
        ):
            mock_lambda = MagicMock()
            mock_lambda.update_function_code = MagicMock(
                return_value={
                    "FunctionArn": "arn:aws:lambda:us-east-1:123:function:test",
                    "CodeSha256": "abc123",
                }
            )
            updated_waiter = MagicMock()
            updated_waiter.wait = MagicMock()
            active_waiter = MagicMock()
            active_waiter.wait = MagicMock(side_effect=Exception("Not active"))
            mock_lambda.get_waiter.side_effect = [updated_waiter, active_waiter]
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError, match="provisioner to be active failed"):
                manager.deploy_provisioner_code()

    def test_raises_on_client_error(self) -> None:
        """deploy_provisioner_code raises on ClientError."""
        with (
            patch(
                "zae_limiter.infra.sync_stack_manager.build_provisioner_package",
                return_value=b"fake-zip",
            ),
            patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class,
        ):
            mock_lambda = MagicMock()
            mock_lambda.update_function_code = MagicMock(
                side_effect=ClientError(
                    {
                        "Error": {
                            "Code": "ResourceNotFoundException",
                            "Message": "Function not found",
                        }
                    },
                    "UpdateFunctionCode",
                )
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            with pytest.raises(StackCreationError, match="ResourceNotFoundException"):
                manager.deploy_provisioner_code()


class TestContextManager:
    """Tests for context manager functionality."""

    def test_aenter_returns_self(self) -> None:
        """__aenter__ returns the manager instance."""
        manager = SyncStackManager(stack_name="test", region="us-east-1")
        result = manager.__enter__()
        assert result is manager

    def test_aexit_calls_close(self) -> None:
        """__aexit__ calls close."""
        manager = SyncStackManager(stack_name="test", region="us-east-1")
        with patch.object(manager, "close", new_callable=MagicMock) as mock_close:
            manager.__exit__(None, None, None)
            mock_close.assert_called_once()

    def test_context_manager_usage(self) -> None:
        """Context manager works correctly."""
        with patch.object(SyncStackManager, "close", new_callable=MagicMock) as mock_close:
            with SyncStackManager(stack_name="test", region="us-east-1") as manager:
                assert manager is not None
            mock_close.assert_called_once()


class TestClose:
    """Tests for close method."""

    def test_close_handles_none_client(self) -> None:
        """close handles None client gracefully."""
        manager = SyncStackManager(stack_name="test", region="us-east-1")
        manager._client = None
        manager._session = None
        manager.close()
        assert manager._client is None
        assert manager._session is None

    def test_close_handles_client_exit_error(self) -> None:
        """close handles errors from client __aexit__."""
        manager = SyncStackManager(stack_name="test", region="us-east-1")
        mock_client = MagicMock()
        mock_client.__exit__ = MagicMock(side_effect=Exception("Cleanup error"))
        manager._client = mock_client
        manager._session = MagicMock()
        manager.close()
        assert manager._client is None
        assert manager._session is None


class TestGetStackEvents:
    """Tests for _get_stack_events method."""

    def test_returns_formatted_events(self) -> None:
        """_get_stack_events returns formatted event list."""
        manager = SyncStackManager(stack_name="test", region="us-east-1")
        mock_client = MagicMock()
        mock_client.describe_stack_events = MagicMock(
            return_value={
                "StackEvents": [
                    {
                        "Timestamp": "2024-01-01T00:00:00Z",
                        "ResourceType": "AWS::DynamoDB::Table",
                        "LogicalResourceId": "Table",
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceStatusReason": None,
                    }
                ]
            }
        )
        events = manager._get_stack_events(mock_client, "test-stack")
        assert len(events) == 1
        assert events[0]["resource_type"] == "AWS::DynamoDB::Table"
        assert events[0]["status"] == "CREATE_COMPLETE"

    def test_returns_empty_on_error(self) -> None:
        """_get_stack_events returns empty list on error."""
        manager = SyncStackManager(stack_name="test", region="us-east-1")
        mock_client = MagicMock()
        mock_client.describe_stack_events = MagicMock(side_effect=Exception("API error"))
        events = manager._get_stack_events(mock_client, "test-stack")
        assert events == []


class TestGetAllTags:
    """Test _get_all_tags method for building complete tag lists."""

    def test_includes_discovery_tags(self) -> None:
        """_get_all_tags includes ManagedBy and name discovery tags."""
        manager = SyncStackManager(stack_name="my-app", region="us-east-1")
        tags = manager._get_all_tags()
        tag_dict = {t["Key"]: t["Value"] for t in tags}
        assert tag_dict["ManagedBy"] == "zae-limiter"
        assert tag_dict["zae-limiter:name"] == "my-app"

    def test_includes_version_tags(self) -> None:
        """_get_all_tags includes version, schema, and lambda version tags."""
        manager = SyncStackManager(stack_name="my-app", region="us-east-1")
        tags = manager._get_all_tags()
        tag_dict = {t["Key"]: t["Value"] for t in tags}
        assert "zae-limiter:version" in tag_dict
        assert "zae-limiter:schema-version" in tag_dict
        assert "zae-limiter:lambda-version" in tag_dict

    def test_includes_user_tags(self) -> None:
        """_get_all_tags includes user-defined tags."""
        manager = SyncStackManager(stack_name="my-app", region="us-east-1")
        user_tags = {"env": "prod", "team": "platform"}
        tags = manager._get_all_tags(user_tags)
        tag_dict = {t["Key"]: t["Value"] for t in tags}
        assert tag_dict["env"] == "prod"
        assert tag_dict["team"] == "platform"

    def test_managed_tags_override_user_tags(self) -> None:
        """Managed tags take precedence over user tags with same key."""
        manager = SyncStackManager(stack_name="my-app", region="us-east-1")
        user_tags = {"ManagedBy": "someone-else"}
        tags = manager._get_all_tags(user_tags)
        tag_dict = {t["Key"]: t["Value"] for t in tags}
        assert tag_dict["ManagedBy"] == "zae-limiter"

    def test_no_user_tags(self) -> None:
        """_get_all_tags works without user tags."""
        manager = SyncStackManager(stack_name="my-app", region="us-east-1")
        tags = manager._get_all_tags(None)
        tag_dict = {t["Key"]: t["Value"] for t in tags}
        assert tag_dict["ManagedBy"] == "zae-limiter"
        assert tag_dict["zae-limiter:name"] == "my-app"

    def test_user_name_from_plain_name(self) -> None:
        """zae-limiter:name tag matches stack name when no prefix."""
        manager = SyncStackManager(stack_name="my-app", region="us-east-1")
        tags = manager._get_all_tags()
        tag_dict = {t["Key"]: t["Value"] for t in tags}
        assert tag_dict["zae-limiter:name"] == "my-app"

    def test_user_name_extracted_from_legacy_prefixed_name(self) -> None:
        """zae-limiter:name tag strips ZAEL- prefix for legacy stacks."""
        manager = SyncStackManager(stack_name="ZAEL-my-app", region="us-east-1")
        tags = manager._get_all_tags()
        tag_dict = {t["Key"]: t["Value"] for t in tags}
        assert tag_dict["zae-limiter:name"] == "my-app"


class TestEnsureTags:
    """Test ensure_tags method for auto-tagging existing stacks."""

    def test_returns_false_when_tags_already_present(self) -> None:
        """ensure_tags returns False if discovery tags already exist."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "Tags": [
                                {"Key": "ManagedBy", "Value": "zae-limiter"},
                                {"Key": "zae-limiter:name", "Value": "my-app"},
                            ]
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="my-app", region="us-east-1")
            result = manager.ensure_tags()
            assert result is False
            mock_client.update_stack.assert_not_called()

    def test_returns_true_when_tags_missing(self) -> None:
        """ensure_tags updates stack and returns True if tags are missing."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [{"Tags": [{"Key": "zae-limiter:version", "Value": "0.6.0"}]}]
                }
            )
            mock_client.update_stack = MagicMock()
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="my-app", region="us-east-1")
            result = manager.ensure_tags()
            assert result is True
            mock_client.update_stack.assert_called_once()
            call_kwargs = mock_client.update_stack.call_args[1]
            assert call_kwargs["UsePreviousTemplate"] is True
            tag_dict = {t["Key"]: t["Value"] for t in call_kwargs["Tags"]}
            assert tag_dict["ManagedBy"] == "zae-limiter"

    def test_returns_false_on_describe_error(self) -> None:
        """ensure_tags returns False if describe_stacks fails."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "ValidationError", "Message": "not found"}}, "DescribeStacks"
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="my-app", region="us-east-1")
            result = manager.ensure_tags()
            assert result is False

    def test_returns_false_when_no_stacks(self) -> None:
        """ensure_tags returns False if describe_stacks returns empty list."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": []})
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="my-app", region="us-east-1")
            result = manager.ensure_tags()
            assert result is False

    def test_strips_legacy_prefix_for_tag_comparison(self) -> None:
        """ensure_tags strips ZAEL- prefix when comparing name tag value."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "Tags": [
                                {"Key": "ManagedBy", "Value": "zae-limiter"},
                                {"Key": "zae-limiter:name", "Value": "my-app"},
                            ]
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="ZAEL-my-app", region="us-east-1")
            result = manager.ensure_tags()
            assert result is False

    def test_returns_false_on_no_updates_error(self) -> None:
        """ensure_tags returns False when update_stack says no updates needed."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": [{"Tags": []}]})
            mock_client.update_stack = MagicMock(
                side_effect=ClientError(
                    {
                        "Error": {
                            "Code": "ValidationError",
                            "Message": "No updates are to be performed",
                        }
                    },
                    "UpdateStack",
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="my-app", region="us-east-1")
            result = manager.ensure_tags()
            assert result is False

    def test_raises_on_update_error(self) -> None:
        """ensure_tags re-raises ClientError that is not 'No updates'."""
        with patch.object(
            SyncStackManager, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": [{"Tags": []}]})
            mock_client.update_stack = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "AccessDenied", "Message": "Not authorized"}}, "UpdateStack"
                )
            )
            mock_get_client.return_value = mock_client
            manager = SyncStackManager(stack_name="my-app", region="us-east-1")
            with pytest.raises(ClientError) as exc_info:
                manager.ensure_tags()
            assert "Not authorized" in str(exc_info.value)


class TestWaitForEsmReady:
    """Tests for wait_for_esm_ready method."""

    def test_returns_true_when_esm_enabled_and_stabilized(self) -> None:
        """wait_for_esm_ready returns True when ESM enabled with OK result."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(
                return_value={
                    "EventSourceMappings": [{"State": "Enabled", "LastProcessingResult": "OK"}]
                }
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready(
                "test-aggregator", max_seconds=10, min_stabilization=0.0
            )
            assert result is True

    def test_returns_true_when_esm_enabled_no_records_processed(self) -> None:
        """wait_for_esm_ready returns True when ESM has polled but found no records."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(
                return_value={
                    "EventSourceMappings": [
                        {"State": "Enabled", "LastProcessingResult": "No records processed"}
                    ]
                }
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready(
                "test-aggregator", max_seconds=10, min_stabilization=0.0
            )
            assert result is True

    def test_returns_false_when_esm_disabled(self) -> None:
        """wait_for_esm_ready returns False when ESM is Disabled."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(
                return_value={
                    "EventSourceMappings": [{"State": "Disabled", "LastProcessingResult": None}]
                }
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready("test-aggregator", max_seconds=1)
            assert result is False

    def test_returns_false_when_esm_disabling(self) -> None:
        """wait_for_esm_ready returns False when ESM is Disabling."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(
                return_value={
                    "EventSourceMappings": [{"State": "Disabling", "LastProcessingResult": None}]
                }
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready("test-aggregator", max_seconds=1)
            assert result is False

    def test_returns_false_on_timeout(self) -> None:
        """wait_for_esm_ready returns False when ESM never becomes ready."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(
                return_value={
                    "EventSourceMappings": [{"State": "Creating", "LastProcessingResult": None}]
                }
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready("test-aggregator", max_seconds=1)
            assert result is False

    def test_returns_false_when_no_esm_found(self) -> None:
        """wait_for_esm_ready returns False when no ESM mappings exist."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(
                return_value={"EventSourceMappings": []}
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready("test-aggregator", max_seconds=1)
            assert result is False

    def test_handles_api_exception(self) -> None:
        """wait_for_esm_ready handles API exceptions gracefully."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(side_effect=Exception("API error"))
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready("test-aggregator", max_seconds=1)
            assert result is False

    def test_waits_when_last_processing_result_is_none(self) -> None:
        """wait_for_esm_ready keeps waiting when LastProcessingResult is None."""
        call_count = 0

        def mock_list_esm(*_args, **_kwargs):
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                return {"EventSourceMappings": [{"State": "Enabled", "LastProcessingResult": None}]}
            else:
                return {"EventSourceMappings": [{"State": "Enabled", "LastProcessingResult": "OK"}]}

        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(side_effect=mock_list_esm)
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready(
                "test-aggregator", max_seconds=60, min_stabilization=0.0
            )
            assert result is True
            assert call_count >= 3

    def test_uses_endpoint_url_when_provided(self) -> None:
        """wait_for_esm_ready passes endpoint_url to Lambda client."""
        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(
                return_value={
                    "EventSourceMappings": [{"State": "Enabled", "LastProcessingResult": "OK"}]
                }
            )
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(
                stack_name="test", region="us-east-1", endpoint_url="http://localhost:4566"
            )
            manager.wait_for_esm_ready("test-aggregator", max_seconds=10, min_stabilization=0.0)
            mock_session.client.assert_called_with(
                "lambda", region_name="us-east-1", endpoint_url="http://localhost:4566"
            )

    def test_waits_for_enabling_state(self) -> None:
        """wait_for_esm_ready keeps waiting when ESM is in Enabling state."""
        call_count = 0

        def mock_list_esm(*_args, **_kwargs):
            nonlocal call_count
            call_count += 1
            if call_count < 2:
                return {
                    "EventSourceMappings": [{"State": "Enabling", "LastProcessingResult": None}]
                }
            else:
                return {"EventSourceMappings": [{"State": "Enabled", "LastProcessingResult": "OK"}]}

        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(side_effect=mock_list_esm)
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready(
                "test-aggregator", max_seconds=60, min_stabilization=0.0
            )
            assert result is True

    def test_waits_on_error_processing_result(self) -> None:
        """wait_for_esm_ready keeps waiting when LastProcessingResult is an error."""
        call_count = 0

        def mock_list_esm(*_args, **_kwargs):
            nonlocal call_count
            call_count += 1
            if call_count < 2:
                return {
                    "EventSourceMappings": [
                        {"State": "Enabled", "LastProcessingResult": "FunctionError"}
                    ]
                }
            else:
                return {"EventSourceMappings": [{"State": "Enabled", "LastProcessingResult": "OK"}]}

        with patch("zae_limiter.infra.sync_stack_manager.boto3.Session") as mock_session_class:
            mock_lambda = MagicMock()
            mock_lambda.list_event_source_mappings = MagicMock(side_effect=mock_list_esm)
            mock_session = MagicMock()
            mock_session.client.return_value = mock_lambda
            mock_session_class.return_value = mock_session
            manager = SyncStackManager(stack_name="test", region="us-east-1")
            result = manager.wait_for_esm_ready(
                "test-aggregator", max_seconds=60, min_stabilization=0.0
            )
            assert result is True
