"""Unit tests for BillingService."""

from datetime import UTC, datetime

import pytest

from augint_billing_lib.models import Link
from augint_billing_lib.service import BillingService


@pytest.mark.unit
class TestHandleStripeEvent:
    """Test stripe event handling."""

    def test_payment_method_attached_with_pm(self, billing_service, mock_stripe):
        """Test payment method attached event with default PM."""
        mock_stripe.has_pm = True
        event = {"type": "payment_method.attached", "data": {"object": {"customer": "cus_test456"}}}

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_test456"
        assert result["subscription_item_id"] == "si_test999"
        assert result["reason"] == "payment_method.attached"
        assert "cus_test456" in mock_stripe.subscriptions

    def test_payment_method_attached_without_pm(self, billing_service, mock_stripe):
        """Test payment method attached event without default PM."""
        mock_stripe.has_pm = False
        event = {"type": "payment_method.attached", "data": {"object": {"customer": "cus_test456"}}}

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] == "free"
        assert result["stripe_customer_id"] == "cus_test456"
        assert result["subscription_item_id"] is None
        assert result["reason"] == "no_default_pm"
        assert "cus_test456" not in mock_stripe.subscriptions

    def test_setup_intent_succeeded(self, billing_service, mock_stripe):
        """Test setup intent succeeded event."""
        mock_stripe.has_pm = True
        event = {
            "type": "setup_intent.succeeded",
            "data": {"object": {"customer": "cus_test456", "payment_method": "pm_test123"}},
        }

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_test456"
        assert result["subscription_item_id"] == "si_test999"

    def test_customer_subscription_created(self, billing_service, mock_stripe):
        """Test customer subscription created event."""
        mock_stripe.has_pm = True
        event = {
            "type": "customer.subscription.created",
            "data": {"object": {"customer": "cus_test456", "status": "active"}},
        }

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_test456"

    def test_customer_subscription_updated(self, billing_service, mock_stripe):
        """Test customer subscription updated event."""
        mock_stripe.has_pm = True
        event = {
            "type": "customer.subscription.updated",
            "data": {"object": {"customer": "cus_test456", "status": "active"}},
        }

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_test456"

    def test_customer_subscription_created_no_payment_method(self, billing_service, mock_stripe):
        """Test subscription created without default payment method promotes to METERED."""
        mock_stripe.has_pm = False  # No default payment method
        event = {
            "type": "customer.subscription.created",
            "data": {"object": {"customer": "cus_no_pm", "status": "active"}},
        }

        result = billing_service.handle_stripe_event(event)

        # Should still promote to metered even without default payment method
        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_no_pm"
        assert result["subscription_item_id"] == "si_test999"

    def test_customer_subscription_updated_no_payment_method(self, billing_service, mock_stripe):
        """Test subscription updated without default payment method promotes to METERED."""
        mock_stripe.has_pm = False  # No default payment method
        event = {
            "type": "customer.subscription.updated",
            "data": {"object": {"customer": "cus_no_pm", "status": "active"}},
        }

        result = billing_service.handle_stripe_event(event)

        # Should still promote to metered even without default payment method
        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_no_pm"
        assert result["subscription_item_id"] == "si_test999"

    def test_invoice_payment_failed(self, billing_service):
        """Test invoice payment failed event."""
        event = {
            "type": "invoice.payment_failed",
            "data": {"object": {"customer": "cus_test456", "amount_due": 1000}},
        }

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] == "free"
        assert result["stripe_customer_id"] == "cus_test456"
        assert result["subscription_item_id"] is None
        assert result["reason"] == "invoice.payment_failed"

    def test_event_missing_customer(self, billing_service):
        """Test event with missing customer ID."""
        event = {
            "type": "payment_method.attached",
            "data": {
                "object": {
                    "id": "pm_test123"
                    # Missing customer field
                }
            },
        }

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] is None
        assert result["reason"] == "no_customer_in_event"

    def test_unknown_event_type(self, billing_service):
        """Test unknown event type."""
        event = {"type": "unknown.event.type", "data": {"object": {"customer": "cus_test456"}}}

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] is None
        assert result["reason"] == "ignored:unknown.event.type"

    def test_event_with_detail_wrapper(self, billing_service, mock_stripe):
        """Test event wrapped in detail field (EventBridge format)."""
        mock_stripe.has_pm = True
        event = {
            "detail": {
                "type": "payment_method.attached",
                "data": {"object": {"customer": "cus_test456"}},
            }
        }

        result = billing_service.handle_stripe_event(event)

        assert result["target_plan"] == "metered"
        assert result["stripe_customer_id"] == "cus_test456"


@pytest.mark.unit
class TestUsageReporting:
    """Test usage reporting functionality."""

    def test_reconcile_usage_window_basic(self, billing_service, populated_repo, mock_usage):
        """Test basic usage reconciliation."""
        mock_usage.set_usage("METERED", "key_metered789", 150)
        since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 1, 1, 0, 0, tzinfo=UTC)

        reports = billing_service.reconcile_usage_window(since, until)

        assert len(reports) == 1
        assert reports[0]["api_key_id"] == "key_metered789"
        assert reports[0]["units"] == 150
        assert reports[0]["customer_id"] == "cus_metered012"

        # Check usage was reported to Stripe
        assert len(billing_service.stripe.usage_reports) == 1
        report = billing_service.stripe.usage_reports[0]
        assert report["subscription_item_id"] == "si_test999"
        assert report["units"] == 150

    def test_reconcile_usage_window_no_usage(self, billing_service, populated_repo, mock_usage):
        """Test reconciliation with no usage."""
        mock_usage.set_usage("METERED", "key_metered789", None)
        since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 1, 1, 0, 0, tzinfo=UTC)

        reports = billing_service.reconcile_usage_window(since, until)

        assert len(reports) == 0
        assert len(billing_service.stripe.usage_reports) == 0

    def test_reconcile_usage_window_zero_usage(self, billing_service, populated_repo, mock_usage):
        """Test reconciliation with zero usage."""
        mock_usage.set_usage("METERED", "key_metered789", 0)
        since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 1, 1, 0, 0, tzinfo=UTC)

        reports = billing_service.reconcile_usage_window(since, until)

        assert len(reports) == 0
        assert len(billing_service.stripe.usage_reports) == 0

    def test_reconcile_usage_window_missing_subscription(
        self, billing_service, populated_repo, mock_usage
    ):
        """Test reconciliation for customer without subscription item."""
        # Add customer without subscription item
        link = Link(
            api_key_id="key_no_sub",
            stripe_customer_id="cus_nosub",
            plan="metered",
            usage_plan_id="METERED",
            metered_subscription_item_id=None,
        )
        populated_repo.save(link)

        mock_usage.set_usage("METERED", "key_no_sub", 100)
        since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 1, 1, 0, 0, tzinfo=UTC)

        reports = billing_service.reconcile_usage_window(since, until)

        # Should not report usage for customer without subscription
        assert len(reports) == 1  # Only the existing metered customer
        assert reports[0]["api_key_id"] == "key_metered789"

    def test_reconcile_usage_window_multiple_customers(
        self, billing_service, mock_repo, mock_usage
    ):
        """Test reconciliation for multiple customers."""
        # Set up multiple metered customers
        link1 = Link(
            api_key_id="key1",
            stripe_customer_id="cus1",
            plan="metered",
            usage_plan_id="METERED",
            metered_subscription_item_id="si_1",
        )
        link2 = Link(
            api_key_id="key2",
            stripe_customer_id="cus2",
            plan="metered",
            usage_plan_id="METERED",
            metered_subscription_item_id="si_2",
        )
        mock_repo.save(link1)
        mock_repo.save(link2)

        mock_usage.set_usage("METERED", "key1", 100)
        mock_usage.set_usage("METERED", "key2", 200)

        since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 1, 1, 0, 0, tzinfo=UTC)

        reports = billing_service.reconcile_usage_window(since, until)

        assert len(reports) == 2
        assert {r["api_key_id"] for r in reports} == {"key1", "key2"}
        assert {r["units"] for r in reports} == {100, 200}

    def test_reconcile_requires_usage_port(self, mock_repo, mock_stripe):
        """Test that reconcile requires usage port."""
        service = BillingService(repo=mock_repo, stripe=mock_stripe, usage=None)

        since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 1, 1, 0, 0, tzinfo=UTC)

        with pytest.raises(AssertionError, match="UsageSourcePort required"):
            service.reconcile_usage_window(since, until)


@pytest.mark.unit
class TestApplyPlanMove:
    """Test plan move application."""

    def test_apply_plan_move_to_metered(self, billing_service, mock_repo):
        """Test moving customer to metered plan."""
        # Set up customer with multiple API keys
        link1 = Link(
            api_key_id="key1", stripe_customer_id="cus_test", plan="free", usage_plan_id="FREE_10K"
        )
        link2 = Link(
            api_key_id="key2", stripe_customer_id="cus_test", plan="free", usage_plan_id="FREE_10K"
        )
        mock_repo.save(link1)
        mock_repo.save(link2)

        moved = billing_service.apply_plan_move_for_customer(
            "cus_test", "metered", "si_new", "FREE_10K", "METERED"
        )

        assert moved == 2

        # Check both links were updated
        updated_link1 = mock_repo.get_by_api_key("key1")
        assert updated_link1.plan == "metered"
        assert updated_link1.usage_plan_id == "METERED"
        assert updated_link1.metered_subscription_item_id == "si_new"

        updated_link2 = mock_repo.get_by_api_key("key2")
        assert updated_link2.plan == "metered"
        assert updated_link2.usage_plan_id == "METERED"
        assert updated_link2.metered_subscription_item_id == "si_new"

        # Check plan admin was called
        assert billing_service.plan_admin.key_plans["key1"] == "METERED"
        assert billing_service.plan_admin.key_plans["key2"] == "METERED"

    def test_apply_plan_move_to_free(self, billing_service, mock_repo):
        """Test moving customer to free plan."""
        link = Link(
            api_key_id="key1",
            stripe_customer_id="cus_test",
            plan="metered",
            usage_plan_id="METERED",
            metered_subscription_item_id="si_old",
        )
        mock_repo.save(link)

        moved = billing_service.apply_plan_move_for_customer(
            "cus_test", "free", None, "FREE_10K", "METERED"
        )

        assert moved == 1

        updated_link = mock_repo.get_by_api_key("key1")
        assert updated_link.plan == "free"
        assert updated_link.usage_plan_id == "FREE_10K"

        assert billing_service.plan_admin.key_plans["key1"] == "FREE_10K"

    def test_apply_plan_move_no_admin(self, mock_repo, mock_stripe, mock_usage):
        """Test plan move without plan admin."""
        service = BillingService(
            repo=mock_repo,
            stripe=mock_stripe,
            usage=mock_usage,
            plan_admin=None,  # No plan admin
        )

        moved = service.apply_plan_move_for_customer(
            "cus_test", "metered", "si_new", "FREE_10K", "METERED"
        )

        assert moved == 0

    def test_apply_plan_move_no_matching_customer(self, billing_service):
        """Test plan move for non-existent customer."""
        moved = billing_service.apply_plan_move_for_customer(
            "cus_nonexistent", "metered", "si_new", "FREE_10K", "METERED"
        )

        assert moved == 0
