"""Integration tests for complete billing flows with real-world scenarios."""

import contextlib
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime, timedelta
from unittest.mock import Mock, patch

import pytest
from botocore.exceptions import ClientError

from augint_billing_lib.adapters.apigw_usage import ApiGwUsage
from augint_billing_lib.adapters.ddb_repo import DdbRepo
from augint_billing_lib.adapters.stripe import StripeAdapter
from augint_billing_lib.bootstrap import process_event_and_apply_plan_moves
from augint_billing_lib.service import BillingService


@pytest.mark.integration
class TestCompleteFlows:
    """Test complete billing flows from entry point to completion."""

    @pytest.fixture
    def mock_env(self, monkeypatch):
        """Set up environment variables."""
        monkeypatch.setenv("STACK_NAME", "test-stack")
        monkeypatch.setenv("AWS_REGION", "us-east-1")
        monkeypatch.setenv("STRIPE_SECRET_KEY", "sk_test_123")
        monkeypatch.setenv("TABLE_NAME", "test-customer-links")
        monkeypatch.setenv("FREE_USAGE_PLAN_ID", "FREE_10K")
        monkeypatch.setenv("METERED_USAGE_PLAN_ID", "METERED")
        monkeypatch.setenv("API_USAGE_PRODUCT_ID", "prod_test")
        monkeypatch.setenv("STRIPE_PRICE_ID_METERED", "price_metered_test")

        # Mock stripe module api_key setter
        with patch("stripe.api_key", "sk_test_123"):
            yield

    @pytest.fixture
    def mock_aws_resources(self):
        """Mock AWS resources."""
        with patch("boto3.client") as mock_client, patch("boto3.resource") as mock_resource:
            mock_apigw = Mock()
            mock_cfn = Mock()
            mock_dynamodb_resource = Mock()
            mock_table = Mock()

            def client_factory(service_name, **kwargs):
                if service_name == "apigateway":
                    return mock_apigw
                if service_name == "cloudformation":
                    return mock_cfn
                return Mock()

            def resource_factory(service_name, **kwargs):
                if service_name == "dynamodb":
                    return mock_dynamodb_resource
                return Mock()

            mock_client.side_effect = client_factory
            mock_resource.side_effect = resource_factory
            mock_dynamodb_resource.Table.return_value = mock_table

            # Mock CloudFormation to return empty outputs (stack doesn't exist scenario)
            error_response = {
                "Error": {
                    "Code": "ValidationError",
                    "Message": "Stack with id test-stack does not exist",
                }
            }
            mock_cfn.describe_stacks.side_effect = ClientError(error_response, "DescribeStacks")

            yield mock_apigw, mock_table

    @pytest.fixture
    def mock_stripe_client(self):
        """Mock Stripe client."""
        with (
            patch("stripe.Customer") as mock_customer,
            patch("stripe.Subscription") as mock_sub,
            patch("stripe.SubscriptionItem") as mock_item,
            patch("stripe.Price") as mock_price,
            patch("augint_billing_lib.adapters.stripe.stripe") as mock_stripe_module,
        ):
            # Mock price discovery for metered price
            mock_price.list.return_value = Mock(
                auto_paging_iter=lambda: [
                    {
                        "id": "price_metered_test",
                        "recurring": {"usage_type": "metered"},
                    }
                ]
            )

            # Mock for meter event reporting
            mock_stripe_module.SubscriptionItem.retrieve.return_value = Mock(
                subscription="sub_test"
            )
            mock_stripe_module.Subscription.retrieve.return_value = Mock(customer="cus_test")
            mock_stripe_module.billing.MeterEvent.create.return_value = {
                "id": "evt_test",
                "object": "billing.meter_event",
            }

            yield {
                "Customer": mock_customer,
                "Subscription": mock_sub,
                "SubscriptionItem": mock_item,
                "Price": mock_price,
                "stripe_module": mock_stripe_module,
            }

    def test_lambda_handler_stripe_event_flow(
        self, mock_env, mock_aws_resources, mock_stripe_client
    ):
        """Test the complete flow from Lambda handler receiving Stripe event."""
        mock_apigw, mock_table = mock_aws_resources

        # Prepare Stripe event
        stripe_event = {
            "id": "evt_test123",
            "type": "setup_intent.succeeded",
            "data": {
                "object": {
                    "customer": "cus_lambda_test",
                    "payment_method": "pm_test",
                    "status": "succeeded",
                }
            },
        }

        # Mock customer has payment method
        mock_stripe_client["Customer"].retrieve.return_value = Mock(
            invoice_settings=Mock(default_payment_method="pm_test")
        )

        # Mock subscription creation
        mock_stripe_client["Subscription"].list.return_value = Mock(auto_paging_iter=list)
        mock_stripe_client["Subscription"].create.return_value = {
            "id": "sub_new",
            "items": {
                "data": [{"id": "si_lambda", "price": {"recurring": {"usage_type": "metered"}}}]
            },
        }

        # Mock DynamoDB operations
        mock_table.query.return_value = {
            "Items": [
                {
                    "api_key_id": "key_lambda",
                    "stripe_customer_id": "cus_lambda_test",
                    "plan": "free",
                    "usage_plan_id": "FREE_10K",
                }
            ]
        }

        # Mock API Gateway operations
        mock_paginator = Mock()
        mock_apigw.get_paginator.return_value = mock_paginator
        mock_paginator.paginate.return_value = [
            {"items": [{"id": "key_id_lambda", "value": "key_lambda"}]}
        ]

        # Mock usage plan discovery for bootstrap
        mock_apigw.get_usage_plans.return_value = {
            "items": [
                {"id": "FREE_10K", "name": "FREE_10K"},
                {"id": "METERED", "name": "METERED"},
            ]
        }

        # Call the bootstrap function
        result = process_event_and_apply_plan_moves(stripe_event)

        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_lambda_test"
        assert result.get("moved_count", 0) >= 0

    def test_usage_sync_lambda_flow(self, mock_env, mock_aws_resources, mock_stripe_client):
        """Test the complete usage sync flow from Lambda handler."""
        mock_apigw, mock_table = mock_aws_resources

        # Create service with adapters
        repo = DdbRepo(mock_table)
        stripe_adapter = StripeAdapter("sk_test", "price_metered")
        usage_adapter = ApiGwUsage(mock_apigw)

        service = BillingService(repo=repo, stripe=stripe_adapter, usage=usage_adapter)

        # Mock metered customers
        mock_table.scan.return_value = {
            "Items": [
                {
                    "api_key_id": "key_sync_1",
                    "stripe_customer_id": "cus_sync_1",
                    "plan": "metered",
                    "usage_plan_id": "METERED",
                    "metered_subscription_item_id": "si_sync_1",
                },
                {
                    "api_key_id": "key_sync_2",
                    "stripe_customer_id": "cus_sync_2",
                    "plan": "metered",
                    "usage_plan_id": "METERED",
                    "metered_subscription_item_id": "si_sync_2",
                },
            ]
        }

        # Mock usage data
        def get_usage_side_effect(plan_id, api_key, since, until):
            if "sync_1" in api_key:
                return 1500
            if "sync_2" in api_key:
                return 2500
            return None

        mock_apigw.get_usage_plan_key.return_value = {"id": "key_id", "value": "key_value"}
        mock_apigw.get_usage.return_value = {"items": {"key_id": [[2000, 1500]]}}

        # Mock Stripe usage reporting via meter events
        mock_stripe_module = mock_stripe_client["stripe_module"]

        # Mock subscription lookups to return different customers
        def sub_item_side_effect(sub_item_id):
            mock = Mock()
            if "sync_1" in sub_item_id:
                mock.subscription = "sub_sync_1"
            else:
                mock.subscription = "sub_sync_2"
            return mock

        def sub_side_effect(sub_id):
            mock = Mock()
            if "sync_1" in sub_id:
                mock.customer = "cus_sync_1"
            else:
                mock.customer = "cus_sync_2"
            return mock

        mock_stripe_module.SubscriptionItem.retrieve.side_effect = sub_item_side_effect
        mock_stripe_module.Subscription.retrieve.side_effect = sub_side_effect

        # Call the service reconcile method
        from datetime import timedelta

        now = datetime.now(UTC)
        hour_ago = now - timedelta(hours=1)

        with patch.object(usage_adapter, "get_usage", side_effect=get_usage_side_effect):
            reports = service.reconcile_usage_window(hour_ago, now)

        assert len(reports) == 2
        assert reports[0]["units"] == 1500
        assert reports[1]["units"] == 2500

    def test_concurrent_event_processing(self, mock_env, mock_aws_resources, mock_stripe_client):
        """Test handling multiple concurrent Stripe events for same customer."""
        mock_apigw, mock_table = mock_aws_resources

        customer_id = "cus_concurrent"

        # Mock usage plan discovery for bootstrap
        mock_apigw.get_usage_plans.return_value = {
            "items": [
                {"id": "FREE_10K", "name": "FREE_10K"},
                {"id": "METERED", "name": "METERED"},
            ]
        }

        # Create multiple events for same customer
        events = [
            {"type": "payment_method.attached", "data": {"object": {"customer": customer_id}}},
            {"type": "setup_intent.succeeded", "data": {"object": {"customer": customer_id}}},
            {
                "type": "customer.subscription.updated",
                "data": {"object": {"customer": customer_id}},
            },
        ]

        # Mock responses
        mock_stripe_client["Customer"].retrieve.return_value = Mock(
            invoice_settings=Mock(default_payment_method="pm_concurrent")
        )

        mock_stripe_client["Subscription"].list.return_value = Mock(auto_paging_iter=list)
        mock_stripe_client["Subscription"].create.return_value = {
            "id": "sub_concurrent",
            "items": {
                "data": [{"id": "si_concurrent", "price": {"recurring": {"usage_type": "metered"}}}]
            },
        }

        mock_table.query.return_value = {
            "Items": [
                {
                    "api_key_id": "key_concurrent",
                    "stripe_customer_id": customer_id,
                    "plan": "free",
                    "usage_plan_id": "FREE_10K",
                }
            ]
        }

        # Mock API Gateway paginator for move_key_to_plan
        mock_paginator = Mock()
        mock_apigw.get_paginator.return_value = mock_paginator
        mock_paginator.paginate.return_value = [
            {"items": [{"id": "key_id_concurrent", "value": "key_concurrent"}]}
        ]

        # Process events concurrently
        results = []
        with ThreadPoolExecutor(max_workers=3) as executor:
            futures = [
                executor.submit(process_event_and_apply_plan_moves, event) for event in events
            ]
            results = [f.result() for f in futures]

        # All should result in metered plan
        for result in results:
            assert result["target_plan"] == "metered"
            assert result["stripe_customer_id"] == customer_id

    def test_race_condition_protection(self, mock_env, mock_aws_resources, mock_stripe_client):
        """Test protection against race conditions in plan updates."""
        mock_apigw, mock_table = mock_aws_resources

        customer_id = "cus_race"

        # Mock usage plan discovery for bootstrap
        mock_apigw.get_usage_plans.return_value = {
            "items": [
                {"id": "FREE_10K", "name": "FREE_10K"},
                {"id": "METERED", "name": "METERED"},
            ]
        }
        api_key = "key_race"

        # Simulate concurrent reads returning different states
        call_count = [0]

        def query_side_effect(**kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                # First call: free plan
                return {
                    "Items": [
                        {
                            "api_key_id": api_key,
                            "stripe_customer_id": customer_id,
                            "plan": "free",
                            "usage_plan_id": "FREE_10K",
                        }
                    ]
                }
            # Subsequent calls: already metered
            return {
                "Items": [
                    {
                        "api_key_id": api_key,
                        "stripe_customer_id": customer_id,
                        "plan": "metered",
                        "usage_plan_id": "METERED",
                        "metered_subscription_item_id": "si_race",
                    }
                ]
            }

        mock_table.query.side_effect = query_side_effect

        # Mock API Gateway paginator for move_key_to_plan
        mock_paginator = Mock()
        mock_apigw.get_paginator.return_value = mock_paginator
        mock_paginator.paginate.return_value = [
            {"items": [{"id": "key_id_race", "value": "key_race"}]}
        ]

        # Mock Stripe operations
        mock_stripe_client["Customer"].retrieve.return_value = Mock(
            invoice_settings=Mock(default_payment_method="pm_race")
        )

        # Existing subscription should be found on second check
        mock_stripe_client["Subscription"].list.return_value = Mock(
            auto_paging_iter=lambda: [
                {
                    "id": "sub_existing",
                    "items": {
                        "data": [
                            {"id": "si_race", "price": {"recurring": {"usage_type": "metered"}}}
                        ]
                    },
                }
            ]
        )

        # Process payment event
        event = {"type": "payment_method.attached", "data": {"object": {"customer": customer_id}}}

        result = process_event_and_apply_plan_moves(event)

        # Should handle gracefully
        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == customer_id

    def test_partial_failure_recovery(self, mock_env, mock_aws_resources, mock_stripe_client):
        """Test recovery from partial failures in multi-step operations."""
        mock_apigw, mock_table = mock_aws_resources

        customer_id = "cus_partial"

        # Mock usage plan discovery for bootstrap
        mock_apigw.get_usage_plans.return_value = {
            "items": [
                {"id": "FREE_10K", "name": "FREE_10K"},
                {"id": "METERED", "name": "METERED"},
            ]
        }
        api_keys = ["key_partial_1", "key_partial_2", "key_partial_3"]

        # Mock customer with multiple keys
        mock_table.query.return_value = {
            "Items": [
                {
                    "api_key_id": key,
                    "stripe_customer_id": customer_id,
                    "plan": "free",
                    "usage_plan_id": "FREE_10K",
                }
                for key in api_keys
            ]
        }

        # Mock API Gateway operations - fail on second key
        mock_paginator = Mock()
        mock_apigw.get_paginator.return_value = mock_paginator

        call_count = [0]

        def paginate_side_effect(**kwargs):
            call_count[0] += 1
            if call_count[0] == 2:
                # Fail on second key
                raise ClientError(
                    {"Error": {"Code": "TooManyRequestsException"}}, "GetUsagePlanKeys"
                )
            # Return items for plan searches - wrap around if needed
            key_index = (call_count[0] - 1) % len(api_keys)
            return [{"items": [{"id": f"id_{call_count[0]}", "value": api_keys[key_index]}]}]

        mock_paginator.paginate.side_effect = paginate_side_effect

        # Mock Stripe operations
        mock_stripe_client["Customer"].retrieve.return_value = Mock(
            invoice_settings=Mock(default_payment_method="pm_partial")
        )
        mock_stripe_client["Subscription"].list.return_value = Mock(auto_paging_iter=list)
        mock_stripe_client["Subscription"].create.return_value = {
            "id": "sub_partial",
            "items": {
                "data": [{"id": "si_partial", "price": {"recurring": {"usage_type": "metered"}}}]
            },
        }

        # Process event
        event = {"type": "payment_method.attached", "data": {"object": {"customer": customer_id}}}

        # Should handle partial failure
        with (
            patch(
                "augint_billing_lib.adapters.apigw_admin.retry",
                side_effect=lambda *args, **kwargs: lambda f: f,
            ),
            contextlib.suppress(ClientError),
        ):
            process_event_and_apply_plan_moves(event)
            # May complete partially or fail - Expected for some keys

    def test_webhook_replay_safety(self, mock_env, mock_aws_resources, mock_stripe_client):
        """Test that webhook replays don't cause issues."""
        mock_apigw, mock_table = mock_aws_resources

        customer_id = "cus_replay"

        # Mock usage plan discovery for bootstrap
        mock_apigw.get_usage_plans.return_value = {
            "items": [
                {"id": "FREE_10K", "name": "FREE_10K"},
                {"id": "METERED", "name": "METERED"},
            ]
        }

        # Same event replayed multiple times
        event = {
            "id": "evt_replay_123",
            "type": "payment_method.attached",
            "data": {"object": {"customer": customer_id}},
        }

        # Customer already on metered plan
        mock_table.query.return_value = {
            "Items": [
                {
                    "api_key_id": "key_replay",
                    "stripe_customer_id": customer_id,
                    "plan": "metered",
                    "usage_plan_id": "METERED",
                    "metered_subscription_item_id": "si_existing",
                }
            ]
        }

        # Mock API Gateway paginator for move_key_to_plan
        mock_paginator = Mock()
        mock_apigw.get_paginator.return_value = mock_paginator
        mock_paginator.paginate.return_value = [
            {"items": [{"id": "key_id_replay", "value": "key_replay"}]}
        ]

        # Mock Stripe - already has subscription
        mock_stripe_client["Customer"].retrieve.return_value = Mock(
            invoice_settings=Mock(default_payment_method="pm_replay")
        )
        mock_stripe_client["Subscription"].list.return_value = Mock(
            auto_paging_iter=lambda: [
                {
                    "id": "sub_existing",
                    "items": {
                        "data": [
                            {"id": "si_existing", "price": {"recurring": {"usage_type": "metered"}}}
                        ]
                    },
                }
            ]
        )

        # Process same event 3 times
        for _ in range(3):
            result = process_event_and_apply_plan_moves(event)
            assert result["target_plan"] == "metered"
            # When already on metered plan, subscription_item_id may not be in the result
            # This is correct behavior for idempotent webhook replay

        # Should not create new subscriptions
        mock_stripe_client["Subscription"].create.assert_not_called()

    def test_usage_window_overlap_handling(self, mock_env, mock_aws_resources, mock_stripe_client):
        """Test handling of overlapping usage windows."""
        mock_apigw, mock_table = mock_aws_resources

        # Mock metered customer
        mock_table.scan.return_value = {
            "Items": [
                {
                    "api_key_id": "key_overlap",
                    "stripe_customer_id": "cus_overlap",
                    "plan": "metered",
                    "usage_plan_id": "METERED",
                    "metered_subscription_item_id": "si_overlap",
                    "last_reported_usage_ts": "2024-01-15T10:00:00+00:00",
                    "current_month_reported_units": 1000,
                }
            ]
        }

        # Mock usage data
        mock_apigw.get_usage_plan_key.return_value = {"id": "key_id", "value": "key_overlap"}
        mock_apigw.get_usage.return_value = {
            "items": {"key_overlap": [[2000, 1500]]}  # Total 1500
        }

        # Create service
        repo = DdbRepo(mock_table)
        stripe_adapter = StripeAdapter("sk_test", "price_metered")
        usage_adapter = ApiGwUsage(mock_apigw)
        service = BillingService(repo=repo, stripe=stripe_adapter, usage=usage_adapter)

        # First sync
        now = datetime.now(UTC)
        hour_ago = now - timedelta(hours=1)

        with (
            patch.object(usage_adapter, "get_usage", return_value=1500),
            patch("augint_billing_lib.adapters.stripe.stripe") as mock_stripe_module,
        ):
            # Mock subscription lookups
            mock_stripe_module.SubscriptionItem.retrieve.return_value = Mock(
                subscription="sub_overlap"
            )
            mock_stripe_module.Subscription.retrieve.return_value = Mock(customer="cus_overlap")
            mock_stripe_module.billing.MeterEvent.create.return_value = {
                "id": "evt_overlap",
                "object": "billing.meter_event",
            }

            reports1 = service.reconcile_usage_window(hour_ago, now)
        assert reports1[0]["units"] == 1500

        # Update last reported timestamp
        mock_table.scan.return_value["Items"][0]["last_reported_usage_ts"] = datetime.now(
            UTC
        ).isoformat()
        mock_table.scan.return_value["Items"][0]["current_month_reported_units"] = 1500

        # Second sync with overlapping window - should report delta
        mock_apigw.get_usage.return_value = {
            "items": {"key_overlap": [[2000, 2000]]}  # Total 2000
        }

        with (
            patch.object(usage_adapter, "get_usage", return_value=2000),
            patch("augint_billing_lib.adapters.stripe.stripe") as mock_stripe_module2,
        ):
            # Mock subscription lookups for second call
            mock_stripe_module2.SubscriptionItem.retrieve.return_value = Mock(
                subscription="sub_overlap"
            )
            mock_stripe_module2.Subscription.retrieve.return_value = Mock(customer="cus_overlap")
            mock_stripe_module2.billing.MeterEvent.create.return_value = {
                "id": "evt_overlap2",
                "object": "billing.meter_event",
            }

            reports2 = service.reconcile_usage_window(hour_ago, now)
        # Should report delta: 2000 - 1500 = 500
        # But our simple MVP assumes non-overlapping, so it reports full amount
        assert reports2[0]["units"] == 2000  # MVP behavior
