import uuid
from collections import namedtuple
from collections.abc import Generator
from datetime import datetime, timedelta
from decimal import Decimal
from unittest.mock import AsyncMock, MagicMock, call

import freezegun
import pytest
import pytest_asyncio
import stripe as stripe_lib
from freezegun import freeze_time
from pytest_mock import MockerFixture
from sqlalchemy.util.typing import TypeAlias

from polar.auth.models import AuthSubject
from polar.billing_entry.repository import BillingEntryRepository
from polar.checkout.eventstream import CheckoutEvent
from polar.enums import (
    PaymentProcessor,
    SubscriptionProrationBehavior,
    SubscriptionRecurringInterval,
)
from polar.event.repository import EventRepository
from polar.event.system import SystemEvent
from polar.exceptions import (
    BadRequest,
    PolarRequestValidationError,
    ResourceUnavailable,
)
from polar.kit.pagination import PaginationParams
from polar.kit.trial import TrialInterval
from polar.kit.utils import utc_now
from polar.locker import Locker
from polar.meter.aggregation import AggregationFunction, PropertyAggregation
from polar.meter.filter import Filter, FilterConjunction
from polar.models import (
    Benefit,
    BillingEntry,
    Customer,
    Discount,
    Meter,
    Organization,
    PaymentMethod,
    Product,
    ProductPrice,
    Subscription,
    User,
    UserOrganization,
)
from polar.models.billing_entry import BillingEntryDirection, BillingEntryType
from polar.models.checkout import CheckoutStatus
from polar.models.customer_seat import SeatStatus
from polar.models.discount import DiscountDuration, DiscountType
from polar.models.order import OrderBillingReasonInternal
from polar.models.product_price import ProductPriceSeatUnit
from polar.models.subscription import SubscriptionStatus
from polar.postgres import AsyncSession
from polar.product.guard import (
    MeteredPrice,
    is_fixed_price,
    is_free_price,
    is_metered_price,
)
from polar.subscription.schemas import (
    SubscriptionCreateCustomer,
    SubscriptionCreateExternalCustomer,
)
from polar.subscription.service import (
    AlreadyCanceledSubscription,
    BelowMinimumSeats,
    InactiveSubscription,
    MissingCheckoutCustomer,
    NotARecurringProduct,
    NotASeatBasedSubscription,
    SeatsAlreadyAssigned,
    TrialingSubscription,
)
from polar.subscription.service import subscription as subscription_service
from tests.fixtures.auth import AuthSubjectFixture
from tests.fixtures.database import SaveFixture
from tests.fixtures.random_objects import (
    create_active_subscription,
    create_canceled_subscription,
    create_checkout,
    create_customer,
    create_customer_seat,
    create_discount,
    create_event,
    create_meter,
    create_product,
    create_product_price_seat_unit,
    create_subscription,
    create_subscription_with_seats,
    create_trialing_subscription,
    set_product_benefits,
)

Hooks = namedtuple("Hooks", "updated activated canceled uncanceled revoked")
HookNames = frozenset(Hooks._fields)


def assert_hooks_called_once(subscription_hooks: Hooks, called: set[str]) -> None:
    for hook in called:
        getattr(subscription_hooks, hook).assert_called_once()

    not_called = HookNames - called
    for hook in not_called:
        getattr(subscription_hooks, hook).assert_not_called()


def reset_hooks(subscription_hooks: Hooks) -> None:
    for hook in HookNames:
        getattr(subscription_hooks, hook).reset_mock()


def build_stripe_payment_intent(
    *,
    amount: int = 0,
    status: str = "succeeded",
    customer: str | None = "CUSTOMER_ID",
    payment_method: str | None = "PAYMENT_METHOD_ID",
) -> stripe_lib.PaymentIntent:
    return stripe_lib.PaymentIntent.construct_from(
        {
            "id": "STRIPE_PAYMENT_INTENT_ID",
            "amount": amount,
            "status": status,
            "customer": customer,
            "payment_method": payment_method,
        },
        None,
    )


@pytest.fixture
def subscription_hooks(mocker: MockerFixture) -> Hooks:
    updated = mocker.patch.object(subscription_service, "_on_subscription_updated")
    activated = mocker.patch.object(subscription_service, "_on_subscription_activated")
    canceled = mocker.patch.object(subscription_service, "_on_subscription_canceled")
    uncanceled = mocker.patch.object(
        subscription_service, "_on_subscription_uncanceled"
    )
    revoked = mocker.patch.object(subscription_service, "_on_subscription_revoked")
    return Hooks(
        updated=updated,
        activated=activated,
        canceled=canceled,
        uncanceled=uncanceled,
        revoked=revoked,
    )


@pytest.fixture
def publish_checkout_event_mock(mocker: MockerFixture) -> AsyncMock:
    return mocker.patch("polar.subscription.service.publish_checkout_event")


@pytest.fixture
def enqueue_benefits_grants_mock(mocker: MockerFixture) -> MagicMock:
    return mocker.patch.object(subscription_service, "enqueue_benefits_grants")


@pytest.fixture
def enqueue_job_mock(mocker: MockerFixture) -> MagicMock:
    return mocker.patch("polar.subscription.service.enqueue_job")


@pytest.fixture
def enqueue_email_mock(mocker: MockerFixture) -> MagicMock:
    return mocker.patch("polar.subscription.service.enqueue_email")


@pytest.fixture
def frozen_time() -> Generator[datetime, None]:
    frozen_time = utc_now()
    with freezegun.freeze_time(frozen_time):
        yield frozen_time


@pytest.mark.asyncio
class TestCreate:
    async def test_product_does_not_exist(
        self,
        session: AsyncSession,
        auth_subject: AuthSubject[User],
    ) -> None:
        subscription_create = SubscriptionCreateCustomer(
            product_id=uuid.uuid4(),
            customer_id=uuid.uuid4(),
        )

        with pytest.raises(PolarRequestValidationError) as exc_info:
            await subscription_service.create(
                session, subscription_create, auth_subject
            )

        errors = exc_info.value.errors()
        assert len(errors) == 2
        assert errors[0]["loc"] == ("body", "product_id")
        assert errors[0]["msg"] == "Product does not exist."
        assert errors[1]["loc"] == ("body", "customer_id")
        assert errors[1]["msg"] == "Customer does not exist."

    async def test_product_not_recurring(
        self,
        save_fixture: SaveFixture,
        session: AsyncSession,
        auth_subject: AuthSubject[User],
        product_one_time: Product,
        customer: Customer,
    ) -> None:
        subscription_create = SubscriptionCreateCustomer(
            product_id=product_one_time.id,
            customer_id=customer.id,
        )

        with pytest.raises(PolarRequestValidationError) as exc_info:
            await subscription_service.create(
                session, subscription_create, auth_subject
            )

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("body", "product_id")
        assert errors[0]["msg"] == "Product is not a recurring product."

    async def test_product_not_free(
        self,
        save_fixture: SaveFixture,
        session: AsyncSession,
        auth_subject: AuthSubject[User],
        product: Product,
        customer: Customer,
    ) -> None:
        subscription_create = SubscriptionCreateCustomer(
            product_id=product.id,
            customer_id=customer.id,
        )

        with pytest.raises(PolarRequestValidationError) as exc_info:
            await subscription_service.create(
                session, subscription_create, auth_subject
            )

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("body", "product_id")
        assert (
            errors[0]["msg"]
            == "Product is not free. The customer should go through a checkout to create a paid subscription."
        )

    async def test_customer_does_not_exist_by_id(
        self,
        save_fixture: SaveFixture,
        session: AsyncSession,
        auth_subject: AuthSubject[User],
        product_recurring_free_price: Product,
    ) -> None:
        subscription_create = SubscriptionCreateCustomer(
            product_id=product_recurring_free_price.id,
            customer_id=uuid.uuid4(),
        )

        with pytest.raises(PolarRequestValidationError) as exc_info:
            await subscription_service.create(
                session, subscription_create, auth_subject
            )

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("body", "customer_id")
        assert errors[0]["msg"] == "Customer does not exist."

    async def test_customer_does_not_exist_by_external_id(
        self,
        save_fixture: SaveFixture,
        session: AsyncSession,
        auth_subject: AuthSubject[User],
        product_recurring_free_price: Product,
    ) -> None:
        subscription_create = SubscriptionCreateExternalCustomer(
            product_id=product_recurring_free_price.id,
            external_customer_id="nonexistent",
        )

        with pytest.raises(PolarRequestValidationError) as exc_info:
            await subscription_service.create(
                session, subscription_create, auth_subject
            )

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("body", "external_customer_id")
        assert errors[0]["msg"] == "Customer does not exist."

    async def test_valid_with_customer_id(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        subscription_hooks: Hooks,
        save_fixture: SaveFixture,
        session: AsyncSession,
        auth_subject: AuthSubject[User],
        product_recurring_free_price: Product,
        customer: Customer,
    ) -> None:
        subscription_create = SubscriptionCreateCustomer(
            product_id=product_recurring_free_price.id,
            customer_id=customer.id,
            metadata={"key": "value"},
        )

        subscription = await subscription_service.create(
            session, subscription_create, auth_subject
        )

        assert subscription.status == SubscriptionStatus.active
        assert subscription.product_id == product_recurring_free_price.id
        assert subscription.customer_id == customer.id
        assert subscription.prices == product_recurring_free_price.prices
        assert subscription.amount == 0
        assert subscription.currency == "usd"
        assert subscription.recurring_interval == SubscriptionRecurringInterval.month
        assert subscription.recurring_interval_count == 1
        assert subscription.user_metadata == {"key": "value"}

        assert subscription.started_at is not None
        assert subscription.current_period_start is not None
        assert subscription.current_period_end is not None
        assert subscription.started_at == subscription.current_period_start
        assert subscription.current_period_end > subscription.current_period_start

        assert_hooks_called_once(subscription_hooks, {"activated", "updated"})
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)

    async def test_valid_with_external_customer_id(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        subscription_hooks: Hooks,
        save_fixture: SaveFixture,
        session: AsyncSession,
        auth_subject: AuthSubject[User],
        product_recurring_free_price: Product,
        customer_external_id: Customer,
    ) -> None:
        assert customer_external_id.external_id is not None

        subscription_create = SubscriptionCreateExternalCustomer(
            product_id=product_recurring_free_price.id,
            external_customer_id=customer_external_id.external_id,
        )

        subscription = await subscription_service.create(
            session, subscription_create, auth_subject
        )

        assert subscription.status == SubscriptionStatus.active
        assert subscription.product_id == product_recurring_free_price.id
        assert subscription.customer_id == customer_external_id.id
        assert subscription.prices == product_recurring_free_price.prices

        assert_hooks_called_once(subscription_hooks, {"activated", "updated"})
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)


@pytest.mark.asyncio
class TestCreateOrUpdateFromCheckout:
    async def test_not_recurring_product(
        self,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product_one_time: Product,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product_one_time],
            status=CheckoutStatus.confirmed,
        )
        with pytest.raises(NotARecurringProduct):
            await subscription_service.create_or_update_from_checkout(
                session, checkout, None
            )

    async def test_missing_customer(
        self,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product: Product,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product],
            status=CheckoutStatus.confirmed,
        )
        with pytest.raises(MissingCheckoutCustomer):
            await subscription_service.create_or_update_from_checkout(
                session, checkout, None
            )

    async def test_new_fixed(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        publish_checkout_event_mock: AsyncMock,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product: Product,
        customer: Customer,
        payment_method: PaymentMethod,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product],
            status=CheckoutStatus.confirmed,
            customer=customer,
        )

        (
            subscription,
            created,
        ) = await subscription_service.create_or_update_from_checkout(
            session, checkout, payment_method
        )

        assert created is True

        assert subscription.status == SubscriptionStatus.active
        assert subscription.prices == product.prices
        assert subscription.amount == checkout.total_amount
        assert subscription.payment_method == payment_method

        assert subscription.started_at is not None
        assert subscription.current_period_start is not None
        assert subscription.current_period_end is not None
        assert subscription.started_at == subscription.current_period_start
        assert subscription.current_period_end > subscription.current_period_start

        publish_checkout_event_mock.assert_called_once_with(
            checkout.client_secret, CheckoutEvent.subscription_created
        )
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)

    async def test_new_custom(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        publish_checkout_event_mock: AsyncMock,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product_recurring_custom_price: Product,
        customer: Customer,
        payment_method: PaymentMethod,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product_recurring_custom_price],
            status=CheckoutStatus.confirmed,
            customer=customer,
            amount=4242,
            currency="usd",
        )

        (
            subscription,
            created,
        ) = await subscription_service.create_or_update_from_checkout(
            session, checkout, payment_method
        )

        assert created is True

        assert subscription.status == SubscriptionStatus.active
        assert subscription.prices == product_recurring_custom_price.prices
        assert subscription.amount == checkout.total_amount
        assert subscription.currency == checkout.currency
        assert subscription.payment_method == payment_method

        publish_checkout_event_mock.assert_called_once_with(
            checkout.client_secret, CheckoutEvent.subscription_created
        )
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)

    async def test_new_free(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        publish_checkout_event_mock: AsyncMock,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product_recurring_free_price: Product,
        customer: Customer,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product_recurring_free_price],
            status=CheckoutStatus.confirmed,
            customer=customer,
        )

        (
            subscription,
            created,
        ) = await subscription_service.create_or_update_from_checkout(
            session, checkout, None
        )

        assert created is True

        assert subscription.status == SubscriptionStatus.active
        assert subscription.prices == product_recurring_free_price.prices
        assert subscription.amount == 0
        assert subscription.currency == "usd"
        assert subscription.payment_method is None

        publish_checkout_event_mock.assert_called_once_with(
            checkout.client_secret, CheckoutEvent.subscription_created
        )
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)

    async def test_new_metered(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        publish_checkout_event_mock: AsyncMock,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product_recurring_metered: Product,
        customer: Customer,
        payment_method: PaymentMethod,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product_recurring_metered],
            status=CheckoutStatus.confirmed,
            customer=customer,
        )

        (
            subscription,
            created,
        ) = await subscription_service.create_or_update_from_checkout(
            session, checkout, payment_method
        )

        assert created is True

        assert subscription.status == SubscriptionStatus.active
        assert subscription.prices == product_recurring_metered.prices
        assert subscription.amount == 0
        assert subscription.currency == "usd"
        assert subscription.payment_method == payment_method

        publish_checkout_event_mock.assert_called_once_with(
            checkout.client_secret, CheckoutEvent.subscription_created
        )
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)

        event_repository = EventRepository.from_session(session)
        for subscription_meter in subscription.meters:
            meter_reset = await event_repository.get_latest_meter_reset(
                customer, subscription_meter.meter_id
            )
            assert meter_reset is not None

    async def test_new_custom_discount_percentage_100(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        publish_checkout_event_mock: AsyncMock,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product_recurring_custom_price: Product,
        customer: Customer,
        discount_percentage_100: Discount,
        payment_method: PaymentMethod,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product_recurring_custom_price],
            status=CheckoutStatus.confirmed,
            customer=customer,
            amount=4242,
            currency="usd",
            discount=discount_percentage_100,
        )

        (
            subscription,
            created,
        ) = await subscription_service.create_or_update_from_checkout(
            session, checkout, payment_method
        )

        assert created is True

        assert subscription.status == SubscriptionStatus.active
        assert subscription.prices == product_recurring_custom_price.prices
        assert subscription.amount == 0
        assert subscription.currency == checkout.currency
        assert subscription.payment_method == payment_method

        publish_checkout_event_mock.assert_called_once_with(
            checkout.client_secret, CheckoutEvent.subscription_created
        )
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)

    async def test_upgrade_fixed(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        publish_checkout_event_mock: AsyncMock,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product_recurring_free_price: Product,
        product: Product,
        customer: Customer,
        payment_method: PaymentMethod,
    ) -> None:
        subscription = await create_subscription(
            save_fixture,
            product=product_recurring_free_price,
            customer=customer,
            status=SubscriptionStatus.active,
        )
        checkout = await create_checkout(
            save_fixture,
            products=[product],
            status=CheckoutStatus.confirmed,
            customer=customer,
            subscription=subscription,
        )
        previous_current_period_start = subscription.current_period_start
        previous_current_period_end = subscription.current_period_end
        previous_started_at = subscription.started_at

        (
            updated_subscription,
            created,
        ) = await subscription_service.create_or_update_from_checkout(
            session, checkout, payment_method
        )

        assert created is False

        assert updated_subscription.status == SubscriptionStatus.active
        assert updated_subscription.prices == product.prices
        assert updated_subscription.amount == checkout.total_amount
        assert updated_subscription.currency == checkout.currency
        assert updated_subscription.payment_method == payment_method

        # Started at doesn't change, but current period does
        assert updated_subscription.started_at == previous_started_at
        assert updated_subscription.current_period_start > previous_current_period_start
        assert updated_subscription.current_period_end is not None
        assert previous_current_period_end is not None
        assert updated_subscription.current_period_end > previous_current_period_end

        publish_checkout_event_mock.assert_called_once_with(
            checkout.client_secret, CheckoutEvent.subscription_created
        )
        enqueue_benefits_grants_mock.assert_called_once_with(
            session, updated_subscription
        )

    async def test_trial(
        self,
        enqueue_benefits_grants_mock: MagicMock,
        publish_checkout_event_mock: AsyncMock,
        save_fixture: SaveFixture,
        session: AsyncSession,
        product: Product,
        customer: Customer,
        payment_method: PaymentMethod,
    ) -> None:
        checkout = await create_checkout(
            save_fixture,
            products=[product],
            status=CheckoutStatus.confirmed,
            customer=customer,
            trial_interval=TrialInterval.month,
            trial_interval_count=1,
        )

        (
            subscription,
            created,
        ) = await subscription_service.create_or_update_from_checkout(
            session, checkout, payment_method
        )

        assert created is True

        assert subscription.status == SubscriptionStatus.trialing
        assert subscription.prices == product.prices
        assert subscription.amount == checkout.total_amount
        assert subscription.payment_method == payment_method

        assert subscription.started_at is not None
        assert subscription.current_period_start is not None
        assert subscription.current_period_end is not None
        assert subscription.started_at == subscription.current_period_start
        assert subscription.current_period_end > subscription.current_period_start
        assert subscription.current_period_end == checkout.trial_end
        assert subscription.trial_start == subscription.current_period_start

        publish_checkout_event_mock.assert_called_once_with(
            checkout.client_secret, CheckoutEvent.subscription_created
        )
        enqueue_benefits_grants_mock.assert_called_once_with(session, subscription)


@pytest.mark.asyncio
class TestCycle:
    async def test_inactive(
        self,
        session: AsyncSession,
        save_fixture: SaveFixture,
        product: Product,
        customer: Customer,
    ) -> None:
        subscription = await create_subscription(
            save_fixture, product=product, customer=customer
        )

        with pytest.raises(InactiveSubscription):
            await subscription_service.cycle(session, subscription)

    async def test_fixed_price(
        self,
        session: AsyncSession,
        enqueue_job_mock: MagicMock,
        enqueue_email_mock: MagicMock,
        save_fixture: SaveFixture,
        product: Product,
        customer: Customer,
    ) -> None:
        subscription = await create_active_subscription(
            save_fixture,
            product=product,
            customer=customer,
            scheduler_locked_at=utc_now(),
        )

        previous_current_period_end = subscription.current_period_end

        updated_subscription = await subscription_service.cycle(session, subscription)

        assert updated_subscription.ended_at is None
        assert updated_subscription.current_period_start == previous_current_period_end
        assert updated_subscription.current_period_end is not None
        assert previous_current_period_end is not None
        assert updated_subscription.current_period_end > previous_current_period_end
        assert updated_subscription.scheduler_locked_at is None

        event_repository = EventRepository.from_session(session)
        events = await event_repository.get_all_by_name(SystemEvent.subscription_cycled)
        assert len(events) == 1
        event = events[0]
        assert event.user_metadata["subscription_id"] == str(subscription.id)
        assert event.user_metadata["amount"] == subscription.amount
        assert event.user_metadata["currency"] == subscription.currency
        assert (
            event.user_metadata["recurring_interval"]
            == subscription.recurring_interval.value
        )
        assert (
            event.user_metadata["recurring_interval_count"]
            == subscription.recurring_interval_count
        )
        assert event.customer_id == customer.id
        assert event.organization_id == customer.organization_id

        price = product.prices[0]
        assert is_fixed_price(price)
        billing_entry_repository = BillingEntryRepository.from_session(session)
        billing_entries = await billing_entry_repository.get_pending_by_subscription(
            subscription.id
        )
        assert len(billing_entries) == 1
        billing_entry = billing_entries[0]
        assert (
            billing_entry.start_timestamp == updated_subscription.current_period_start
        )
        assert billing_entry.end_timestamp == updated_subscription.current_period_end
        assert billing_entry.direction == BillingEntryDirection.debit
        assert billing_entry.customer_id == customer.id
        assert billing_entry.product_price_id == price.id
        assert billing_entry.event_id == event.id
        assert billing_entry.amount == price.price_amount
        assert billing_entry.currency == price.price_currency

        enqueue_job_mock.assert_any_call(
            "order.create_subscription_order",
            subscription.id,
            OrderBillingReasonInternal.subscription_cycle,
        )

        enqueue_email_mock.assert_not_called()

    async def test_free_price(
        self,
        session: AsyncSession,
        save_fixture: SaveFixture,
        product_recurring_free_price: Product,
        customer: Customer,
    ) -> None:
        subscription = await create_active_subscription(
            save_fixture,
            product=product_recurring_free_price,
            customer=customer,
            scheduler_locked_at=utc_now(),
        )

        await subscription_service.cycle(session, subscription)

        price = product_recurring_free_price.prices[0]
        assert is_free_price(price)
        billing_entry_repository = BillingEntryRepository.from_session(session)
        billing_entries = await billing_entry_repository.get_pending_by_subscription(
            subscription.id
        )
        assert len(billing_entries) == 1
        billing_entry = billing_entries[0]
        assert billing_entry.amount == 0
        assert billing_entry.currency == subscription.currency

    @freeze_time("2024-01-15")
    async def test_discount_repetition(
        self,
        session: AsyncSession,
        save_fixture: SaveFixture,
        product: Product,
        customer: Customer,
        organization: Organization,
    ) -> None:
        discount = await create_discount(
            save_fixture,
            type=DiscountType.fixed,
            amount=1000,
            currency="usd",
            duration=DiscountDuration.repeating,
            duration_in_months=3,
            organization=organization,
        )
        subscription = await create_active_subscription(
            save_fixture,
            product=product,
            customer=customer,
            discount=discount,
            scheduler_locked_at=utc_now(),
        )

        second_month_subscription = await subscription_service.cycle(
            session, subscription
        )
        assert second_month_subscription.discount == discount

        third_month_subscription = await subscription_service.cycle(
            session, second_month_subscription
        )
        assert third_month_subscription.discount == discount

        fourth_month_subscription = await subscription_service.cycle(
            session, third_month_subscription
        )
        assert fourth_month_subscription.discount is None

        billing_entry_repository = BillingEntryRepository.from_session(session)
        billing_entries = await billing_entry_repository.get_pending_by_subscription(
            subscription.id
        )
        assert len(billing_entries) == 3

        (
            second_month_billing_entry,
            third_month_billing_entry,
            fourth_month_billing_entry,
        ) = billing_entries
        assert second_month_billing_entry.discount == discount
        assert third_month_billing_entry.discount == discount
        assert fourth_month_billing_entry.discount is None

    @freeze_time("2024-01-15")
    async def test_nth_month_cycle(
        self,
        session: AsyncSession,
        save_fixture: SaveFixture,
        product_recurring_every_second_month: Product,
        customer: Customer,
        organization: Organization,
    ) -> None:
        subscription = await create_active_subscription(
            save_fixture,
            product=product_recurring_every_second_month,
            customer=customer,
            scheduler_locked_at=utc_now(),
        )

        first_period_start = subscription.current_period_start
        first_period_end = subscription.current_period_end
        assert first_period_start is not None
        assert first_period_end is not None
        assert first_period_end == first_period_start.replace(month=3)

        second_cycle_subscription = await subscription_service.cycle(
            session, subscription
        )
        second_period_start = second_cycle_subscription.current_period_start
        second_period_end = second_cycle_subscription.current_period_end
        assert second_period_start == first_period_end
        assert second_period_end is not None
        assert second_period_end == first_period_end.replace(month=5)

        third_cycle_subscription = await subscription_service.cycle(
            session, second_cycle_subscription
        )
        assert third_cycle_subscription.current_period_start == second_period_end
        assert third_cycle_subscription.current_period_end is not None
        assert third_cycle_subscription.current_period_end == second_period_end.replace(
            month=7
        )

        billing_entry_repository = BillingEntryRepository.from_session(session)
        billing_entries = await billing_entry_repository.get_pending_by_subscription(
