"""Tests for billing router endpoints and billing service."""

import uuid
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from httpx import ASGITransport, AsyncClient

from mixlift.deps import get_current_user, get_db
from mixlift.main import app
from mixlift.projects.models import User


# ---------------------------------------------------------------------------
# Checkout session tests
# ---------------------------------------------------------------------------


class TestCreateCheckoutSession:
    """POST /billing/create-checkout-session"""

    async def test_success_pro_plan(self, client, fake_user):
        """Plan=pro with valid Stripe config returns a checkout URL."""
        mock_session_obj = MagicMock()
        mock_session_obj.url = "https://checkout.stripe.com/pay/cs_test_123"

        mock_stripe = MagicMock()
        mock_stripe.checkout.Session.create.return_value = mock_session_obj

        with (
            patch("mixlift.billing.router.settings") as mock_settings,
            patch.dict("sys.modules", {"stripe": mock_stripe}),
        ):
            mock_settings.stripe_secret_key = "sk_test_abc"
            mock_settings.stripe_pro_price_id = "price_pro_456"
            mock_settings.frontend_url = "http://localhost:8501"

            resp = await client.post(
                "/billing/create-checkout-session?plan=pro"
            )

        assert resp.status_code == 200
        body = resp.json()
        assert "checkout_url" in body
        assert body["checkout_url"] == "https://checkout.stripe.com/pay/cs_test_123"

    async def test_invalid_plan_returns_400(self, client):
        """Non-pro plan returns 400."""
        with patch("mixlift.billing.router.settings") as mock_settings:
            mock_settings.stripe_secret_key = "sk_test_abc"

            resp = await client.post(
                "/billing/create-checkout-session?plan=invalid"
            )

        assert resp.status_code == 400
        assert resp.json()["detail"] == "Invalid plan"

    async def test_no_auth_returns_401(self, mock_session):
        """Request without authentication returns 401."""
        # Use a client without auth overrides
        async def override_get_db():
            yield mock_session

        app.dependency_overrides[get_db] = override_get_db
        # Explicitly do NOT override get_current_user so auth fails
        app.dependency_overrides.pop(get_current_user, None)

        try:
            async with AsyncClient(
                transport=ASGITransport(app=app), base_url="http://test"
            ) as ac:
                resp = await ac.post(
                    "/billing/create-checkout-session?plan=pro"
                )
            assert resp.status_code == 401
        finally:
            app.dependency_overrides.clear()

    async def test_stripe_not_configured_returns_503(self, client):
        """When stripe_secret_key is empty, return 503."""
        with patch("mixlift.billing.router.settings") as mock_settings:
            mock_settings.stripe_secret_key = ""

            resp = await client.post(
                "/billing/create-checkout-session?plan=pro"
            )

        assert resp.status_code == 503
        assert "not configured" in resp.json()["detail"].lower()


# ---------------------------------------------------------------------------
# Webhook tests
# ---------------------------------------------------------------------------


class TestStripeWebhook:
    """POST /billing/webhook"""

    async def test_checkout_completed_updates_tier(self, client):
        """checkout.session.completed event should call update_user_tier with active status."""
        user_id = "12345678-1234-5678-1234-567812345678"
        event = {
            "type": "checkout.session.completed",
            "data": {
                "object": {
                    "metadata": {"user_id": user_id, "plan": "pro"},
                    "customer": "cus_test_123",
                }
            },
        }

        mock_stripe = MagicMock()
        mock_stripe.Webhook.construct_event.return_value = event
        mock_stripe.error = MagicMock()
        mock_stripe.error.SignatureVerificationError = Exception

        with (
            patch("mixlift.billing.router.settings") as mock_settings,
            patch.dict("sys.modules", {"stripe": mock_stripe}),
            patch(
                "mixlift.billing.router.update_user_tier", new_callable=AsyncMock
            ) as mock_update,
            patch(
                "mixlift.billing.router._save_stripe_customer_id",
                new_callable=AsyncMock,
            ) as mock_save,
        ):
            mock_settings.stripe_secret_key = "sk_test_abc"
            mock_settings.stripe_webhook_secret = "whsec_test"

            resp = await client.post(
                "/billing/webhook",
                content=b'{"type": "checkout.session.completed"}',
                headers={"stripe-signature": "t=123,v1=sig"},
            )

        assert resp.status_code == 200
        assert resp.json() == {"status": "ok"}
        mock_update.assert_awaited_once_with(
            user_id=user_id, plan="pro", status="active"
        )
        mock_save.assert_awaited_once_with(user_id, "cus_test_123")

    async def test_subscription_deleted_downgrades(self, client):
        """customer.subscription.deleted event should downgrade user."""
        user_id = "12345678-1234-5678-1234-567812345678"
        event = {
            "type": "customer.subscription.deleted",
            "data": {
                "object": {
                    "customer": "cus_test_123",
                    "status": "canceled",
                    "items": {"data": [{"price": {"id": "price_pro_456"}}]},
                }
            },
        }

        mock_stripe = MagicMock()
        mock_stripe.Webhook.construct_event.return_value = event
        mock_stripe.error = MagicMock()
        mock_stripe.error.SignatureVerificationError = Exception

        with (
            patch("mixlift.billing.router.settings") as mock_settings,
            patch.dict("sys.modules", {"stripe": mock_stripe}),
            patch(
                "mixlift.billing.router.update_user_tier", new_callable=AsyncMock
            ) as mock_update,
            patch(
                "mixlift.billing.router._get_user_id_by_stripe_customer",
                new_callable=AsyncMock,
                return_value=user_id,
            ),
        ):
            mock_settings.stripe_secret_key = "sk_test_abc"
            mock_settings.stripe_webhook_secret = "whsec_test"
            mock_settings.stripe_pro_price_id = "price_pro_456"

            resp = await client.post(
                "/billing/webhook",
                content=b'{"type": "customer.subscription.deleted"}',
                headers={"stripe-signature": "t=123,v1=sig"},
            )

        assert resp.status_code == 200
        mock_update.assert_awaited_once_with(
            user_id=user_id, plan="pro", status="canceled"
        )

    async def test_invalid_signature_returns_400(self, client):
        """Invalid webhook signature returns 400."""
        mock_stripe = MagicMock()

        class FakeSignatureError(Exception):
            pass

        mock_stripe.error.SignatureVerificationError = FakeSignatureError
        mock_stripe.Webhook.construct_event.side_effect = FakeSignatureError(
            "bad sig"
        )

        with (
            patch("mixlift.billing.router.settings") as mock_settings,
            patch.dict("sys.modules", {"stripe": mock_stripe}),
        ):
            mock_settings.stripe_secret_key = "sk_test_abc"
            mock_settings.stripe_webhook_secret = "whsec_test"

            resp = await client.post(
                "/billing/webhook",
                content=b"{}",
                headers={"stripe-signature": "invalid"},
            )

        assert resp.status_code == 400
        assert "invalid webhook" in resp.json()["detail"].lower()


# ---------------------------------------------------------------------------
# Service tests: update_user_tier
# ---------------------------------------------------------------------------


class TestUpdateUserTier:
    """Tests for mixlift.billing.service.update_user_tier."""

    async def _run_update(self, user, status_value, plan="pro"):
        """Helper: call update_user_tier with a mocked DB session."""
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = user

        mock_session = AsyncMock()
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.commit = AsyncMock()

        # async_session is used as `async with async_session() as session:`
        mock_session_ctx = AsyncMock()
        mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session_ctx.__aexit__ = AsyncMock(return_value=False)

        mock_session_factory = MagicMock(return_value=mock_session_ctx)

        with patch(
            "mixlift.db.async_session", mock_session_factory
        ):
            from mixlift.billing.service import update_user_tier

            await update_user_tier(
                user_id=str(user.id), plan=plan, status=status_value
            )

        return user, mock_session

    async def test_active_status_sets_tier_to_plan(self):
        """Status 'active' should set tier to the given plan."""
        user = User(
            id=uuid.UUID("12345678-1234-5678-1234-567812345678"),
            email="test@example.com",
            password_hash="hashed",
            name="Test",
            tier="free",
        )
        user, session = await self._run_update(user, "active", plan="pro")
        assert user.tier == "pro"
        session.commit.assert_awaited_once()

    async def test_trialing_status_sets_tier_to_plan(self):
        """Status 'trialing' should also set tier to the plan."""
        user = User(
            id=uuid.UUID("12345678-1234-5678-1234-567812345678"),
            email="test@example.com",
            password_hash="hashed",
            name="Test",
            tier="free",
        )
        user, _ = await self._run_update(user, "trialing", plan="pro")
        assert user.tier == "pro"

    async def test_canceled_status_sets_tier_to_free(self):
        """Status 'canceled' should downgrade tier to free."""
        user = User(
            id=uuid.UUID("12345678-1234-5678-1234-567812345678"),
            email="test@example.com",
            password_hash="hashed",
            name="Test",
            tier="pro",
        )
        user, _ = await self._run_update(user, "canceled")
        assert user.tier == "free"

    async def test_past_due_status_sets_tier_to_free(self):
        """Status 'past_due' should downgrade tier to free."""
        user = User(
            id=uuid.UUID("12345678-1234-5678-1234-567812345678"),
            email="test@example.com",
            password_hash="hashed",
            name="Test",
            tier="pro",
        )
        user, _ = await self._run_update(user, "past_due")
        assert user.tier == "free"

    async def test_unpaid_status_sets_tier_to_free(self):
        """Status 'unpaid' should downgrade tier to free."""
        user = User(
            id=uuid.UUID("12345678-1234-5678-1234-567812345678"),
            email="test@example.com",
            password_hash="hashed",
            name="Test",
            tier="pro",
        )
        user, _ = await self._run_update(user, "unpaid")
        assert user.tier == "free"

    async def test_incomplete_status_does_not_change_tier(self):
        """Status 'incomplete' should leave tier unchanged."""
        user = User(
            id=uuid.UUID("12345678-1234-5678-1234-567812345678"),
            email="test@example.com",
            password_hash="hashed",
            name="Test",
            tier="pro",
        )
        user, _ = await self._run_update(user, "incomplete")
        assert user.tier == "pro"

    async def test_user_not_found_does_not_raise(self):
        """If user is not found, update_user_tier should return silently."""
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None

        mock_session = AsyncMock()
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.commit = AsyncMock()

        mock_session_ctx = AsyncMock()
        mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session_ctx.__aexit__ = AsyncMock(return_value=False)

        mock_session_factory = MagicMock(return_value=mock_session_ctx)

        with patch(
            "mixlift.db.async_session", mock_session_factory
        ):
            from mixlift.billing.service import update_user_tier

            # Should not raise
            await update_user_tier(
                user_id="nonexistent-id", plan="pro", status="active"
            )

        # commit is still called (no early return before commit in current impl)
        # but the important thing is no exception
