"""Self-service plan changes must preserve billing identity and consumed allowance."""

from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from src.core.exceptions import ValidationError
from src.domain.billing.repository import WorkspaceUsageAllowanceRepository
from src.domain.billing.service import (
    BillingService,
    _sync_workspace_selected_plan_from_subscription,
)


def service(plan_slug="basic", existing=None):
    catalog = MagicMock()
    catalog.get_plan_by_slug = AsyncMock(
        return_value=SimpleNamespace(slug=plan_slug, limits={"included_ai_units": 100})
    )
    catalog.resolve_stripe_price_id_for_plan.return_value = "price_target"
    svc = BillingService(
        customer_repo=AsyncMock(),
        subscription_repo=AsyncMock(),
        invoice_repo=AsyncMock(),
        event_repo=AsyncMock(),
        usage_repo=AsyncMock(),
        purchase_repo=AsyncMock(),
        ledger_repo=AsyncMock(),
        catalog=catalog,
        stripe_client=AsyncMock(),
        allowance_repo=AsyncMock(),
    )
    svc._subscription_repo.get_latest_for_workspace.return_value = existing
    svc._customer_repo.get_by_workspace.return_value = SimpleNamespace(stripe_customer_id="cus_1")
    svc._stripe.create_subscription_update_portal.return_value = "https://billing.stripe.com/change"
    return svc


async def checkout(svc, slug, interval="month"):
    return await svc.start_subscription_checkout(
        uuid4(),
        uuid4(),
        "test@example.com",
        "Workspace",
        slug,
        interval,
        "https://app.test/success",
        "https://app.test/billing",
    )


@pytest.mark.asyncio
async def test_staff_cannot_be_selected_directly():
    svc = service("staff")
    with pytest.raises(ValidationError, match="Staff"):
        await checkout(svc, "staff")
    svc._stripe.create_subscription_checkout.assert_not_awaited()
    svc._subscription_repo.create_for_no_charge_plan.assert_not_awaited()


@pytest.mark.asyncio
@pytest.mark.parametrize("slug,interval", [("pro", "year"), ("basic", "month")])
async def test_existing_subscription_changes_use_confirmation(slug, interval):
    svc = service(slug, SimpleNamespace(stripe_subscription_id="sub_1", status="active"))
    result = await checkout(svc, slug, interval)
    assert result["checkout_url"] == "https://billing.stripe.com/change"
    svc._stripe.create_subscription_update_portal.assert_awaited_once_with(
        "cus_1", "sub_1", "price_target", "https://app.test/billing"
    )
    svc._stripe.create_subscription_checkout.assert_not_awaited()


@pytest.mark.asyncio
async def test_paid_subscription_cannot_be_shadowed_by_free():
    svc = service("free", SimpleNamespace(stripe_subscription_id="sub_1", status="active"))
    with pytest.raises(ValidationError, match="Cancel"):
        await checkout(svc, "free")
    svc._subscription_repo.create_for_no_charge_plan.assert_not_awaited()


@pytest.mark.asyncio
@pytest.mark.parametrize("consumed,remaining", [(40, 60), (150, 0)])
async def test_downgrade_preserves_consumption_and_is_idempotent(consumed, remaining):
    allowance = SimpleNamespace(
        included_units=900, consumed_units=consumed, remaining_units=900 - consumed
    )
    repo = WorkspaceUsageAllowanceRepository(AsyncMock())
    repo.get_current_for_workspace_for_update = AsyncMock(return_value=allowance)
    for _ in range(2):
        await repo.reconcile_plan_limit(uuid4(), 100)
        assert allowance.remaining_units == remaining
        assert allowance.consumed_units == consumed


@pytest.mark.asyncio
async def test_release_after_downgrade_does_not_restore_excess_credits():
    allowance = SimpleNamespace(included_units=100, consumed_units=150, remaining_units=0)
    db = AsyncMock()
    db.execute.return_value = MagicMock(scalar_one_or_none=lambda: allowance)
    await WorkspaceUsageAllowanceRepository(db).restore_units(uuid4(), 20)
    assert allowance.consumed_units == 130
    assert allowance.remaining_units == 0


@pytest.mark.asyncio
async def test_subscription_sync_reconciles_allowance_under_consumption_lock():
    svc = service()
    workspace_id = uuid4()
    sub = SimpleNamespace(plan_slug="basic", status="active")
    await _sync_workspace_selected_plan_from_subscription(svc, workspace_id=workspace_id, sub=sub)
    svc._ledger_repo.acquire_consumption_lock.assert_awaited_once_with(workspace_id)
    svc._allowance_repo.reconcile_plan_limit.assert_awaited_once_with(workspace_id, 100)
    svc._ledger_repo.add_entry.assert_not_awaited()


@pytest.mark.asyncio
async def test_catalog_excludes_staff_and_enterprise():
    from src.domain.billing.router import get_available_plans

    svc = service()
    svc.get_subscription_optional = AsyncMock(return_value=None)
    catalog = AsyncMock()
    catalog.list_plans.return_value = [
        SimpleNamespace(slug=s, id=uuid4()) for s in ("staff", "enterprise", "basic")
    ]
    with patch("src.domain.billing.router.subscription_plan_to_public") as convert:
        convert.return_value = dict(
            id=uuid4(),
            name="Basic",
            slug="basic",
            description=None,
            currency="usd",
            features=None,
            limits=None,
            sort_order=0,
            trial_days=0,
        )
        result = await get_available_plans(
            uuid4(), SimpleNamespace(id=uuid4()), catalog, svc, AsyncMock()
        )
    assert [p.slug for p in result.plans] == ["basic"]


@pytest.mark.asyncio
async def test_stripe_update_flow_targets_existing_item():
    from src.integrations.stripe_client import StripeClientImpl

    with (
        patch(
            "src.integrations.stripe_client.stripe.Subscription.retrieve",
            return_value={"customer": "cus_1", "items": {"data": [{"id": "si_1", "quantity": 1}]}},
        ),
        patch(
            "src.integrations.stripe_client.stripe.billing_portal.Session.create",
            return_value=SimpleNamespace(url="https://stripe.test/change"),
        ) as create,
    ):
        result = await StripeClientImpl(api_key="test").create_subscription_update_portal(
            "cus_1", "sub_1", "price_year", "https://app.test/billing"
        )
    assert result == "https://stripe.test/change"
    assert create.call_args.kwargs["flow_data"]["subscription_update_confirm"] == {
        "subscription": "sub_1",
        "items": [{"id": "si_1", "price": "price_year", "quantity": 1}],
    }
