<?php

use App\Models\PlanVersion;
use App\Models\Subscription;
use App\Services\Billing\PlanCatalogue;
use App\Services\Billing\PlanChange;
use Illuminate\Support\Carbon;

it('freezes what the customer bought against later price rises', function () {
    $subscription = Subscription::factory()->plan('team')->create();
    $bookedPrice = $subscription->price_cents;

    // The owner doubles what Team costs, which means publishing a new version:
    // the price of a published one is fixed, because someone may be standing at
    // the checkout looking at it.
    $catalogue = app(PlanCatalogue::class);
    $current = $catalogue->currentVersion('team');
    $currency = Subscription::catalogueCurrency();
    $catalogue->schedule($current, $current->available_from, now());

    $dearer = PlanVersion::query()->create([
        ...$current->only(['plan_family_id', 'quota_gb', 'traffic_gb', 'seats', 'ram_mb', 'cores', 'disk_gb', 'performance', 'template_vmid']),
        'version' => 3, 'features' => $current->features, 'available_from' => now(),
    ]);
    $dearer->prices()->create(['term' => 'monthly', 'amount_cents' => $bookedPrice * 2, 'currency' => $currency]);
    $dearer->prices()->create(['term' => 'yearly', 'amount_cents' => $bookedPrice * 24, 'currency' => $currency]);
    $catalogue->publish($dearer, now());

    // The existing customer keeps their terms — that is the entire point.
    expect($subscription->fresh()->price_cents)->toBe($bookedPrice)
        ->and(Subscription::snapshotFrom('team')['price_cents'])->toBe($bookedPrice * 2);
});

it('refuses to rewrite a snapshot after the fact', function () {
    $subscription = Subscription::factory()->plan('team')->create();

    expect(fn () => $subscription->update(['price_cents' => 1]))
        ->toThrow(RuntimeException::class, 'immutable');

    expect($subscription->fresh()->price_cents)->toBe(Subscription::snapshotFrom('team')['price_cents']);

    // The rejected value is still on the in-memory model, so it has to be
    // discarded before anything else can be saved — otherwise the next write
    // carries the forbidden change along and is refused too.
    $subscription->refresh();

    // Operational fields still move.
    $subscription->update(['pending_plan' => 'start']);
    expect($subscription->fresh()->pending_plan)->toBe('start');
});

it('charges only the remaining days when upgrading mid-month', function () {
    $start = Carbon::parse('2026-08-01');
    $subscription = Subscription::factory()->plan('start')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);

    $change = PlanChange::evaluate($subscription, 'team', Carbon::parse('2026-08-16'));

    $startPrice = Subscription::snapshotFrom('start')['price_cents'];
    $teamPrice = Subscription::snapshotFrom('team')['price_cents'];
    $expected = (int) round($teamPrice * 16 / 31) - (int) round($startPrice * 16 / 31);

    expect($change->isUpgrade)->toBeTrue()
        ->and($change->allowedNow)->toBeTrue()
        ->and($change->chargeCents)->toBe($expected)
        ->and($change->chargeCents)->toBeLessThan($teamPrice)
        ->and($change->chargeCents)->toBeGreaterThan(0);
});

it('makes an upgrade on the last day cost almost nothing', function () {
    $start = Carbon::parse('2026-08-01');
    $subscription = Subscription::factory()->plan('start')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);

    $change = PlanChange::evaluate($subscription, 'business', Carbon::parse('2026-08-31'));

    // One day of the difference, not a month of it.
    expect($change->remainingDays)->toBe(1)
        ->and($change->chargeCents)->toBeLessThan(2000);
});

it('holds a downgrade until the term the customer paid for is over', function () {
    $start = Carbon::parse('2026-01-01');
    $yearly = Subscription::factory()->plan('business', Subscription::TERM_YEARLY)->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addYear(),
    ]);

    $change = PlanChange::evaluate($yearly, 'start', Carbon::parse('2026-03-15'));

    // Someone on a yearly contract bought a year, not three months.
    expect($change->isUpgrade)->toBeFalse()
        ->and($change->allowedNow)->toBeFalse()
        ->and($change->chargeCents)->toBe(0)
        ->and($change->effectiveAt->toDateString())->toBe('2027-01-01');
});

it('lets a monthly customer move down at the end of the month', function () {
    $start = Carbon::parse('2026-08-01');
    $monthly = Subscription::factory()->plan('business')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);

    expect(PlanChange::evaluate($monthly, 'team', Carbon::parse('2026-08-10'))->effectiveAt->toDateString())
        ->toBe('2026-09-01');
});

it('credits only the unused part of the difference for a goodwill downgrade', function () {
    $start = Carbon::parse('2026-08-01');
    $subscription = Subscription::factory()->plan('business')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);

    // 11 days left of 31: they used the expensive plan for the other 20.
    $credit = PlanChange::goodwillCredit($subscription, 'team', Carbon::parse('2026-08-21'));

    $difference = Subscription::snapshotFrom('business')['price_cents'] - Subscription::snapshotFrom('team')['price_cents'];
    expect($credit)->toBe((int) round($difference * 11 / 31))
        ->and($credit)->toBeLessThan($difference);
});

it('will not call an upgrade a credit', function () {
    $subscription = Subscription::factory()->plan('start')->create();

    expect(fn () => PlanChange::goodwillCredit($subscription, 'business'))
        ->toThrow(RuntimeException::class);
});

it('prices a yearly term as the ten months the catalogue charges for', function () {
    $monthly = Subscription::snapshotFrom('team');
    $yearly = Subscription::snapshotFrom('team', Subscription::TERM_YEARLY);

    // Any discount belongs in the catalogue, not hidden in the conversion — and
    // here it is both halves of one decision: two free months on the version,
    // ten months of the monthly figure in the price row beside it.
    expect($yearly['price_cents'])->toBe($monthly['price_cents'] * 10)
        ->and(app(PlanCatalogue::class)->sellable()['team']['free_months'])->toBe(2);
});

it('reads the direction from the plan rank, not from a grandfathered price', function () {
    $start = Carbon::parse('2026-08-01');

    // A business customer from back when business cost less than today's team.
    $subscription = Subscription::factory()->plan('business')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);
    $subscription->forceFill(['price_cents' => 4900])->saveQuietly();   // grandfathered

    // The premise, asserted rather than written into config('provisioning.plans.*')
    // — a key that has not existed since the catalogue moved into the database, so
    // that line set nothing and this test never built the scenario it is named for.
    // Today's team has to cost MORE than this customer's grandfathered business
    // price, or a price-led reading of the direction would agree with the rank one
    // and the test would prove nothing.
    expect(Subscription::snapshotFrom('team')['price_cents'])->toBeGreaterThan(4900);

    $change = PlanChange::evaluate($subscription->fresh(), 'team', Carbon::parse('2026-08-15'));

    // Moving to team takes resources away — that is a downgrade whatever the
    // prices say, and it waits for the end of the term.
    expect($change->isUpgrade)->toBeFalse()
        ->and($change->allowedNow)->toBeFalse()
        ->and($change->chargeCents)->toBe(0);
});

it('lets a scheduled downgrade actually happen once the term is over', function () {
    $start = Carbon::parse('2026-08-01');
    $subscription = Subscription::factory()->plan('business')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);

    // Mid-term: not yet.
    expect(PlanChange::evaluate($subscription, 'team', Carbon::parse('2026-08-15'))->allowedNow)
        ->toBeFalse();

    // On the boundary and after it: the job carrying it out has to be able to.
    foreach (['2026-09-01', '2026-09-03'] as $when) {
        $change = PlanChange::evaluate($subscription, 'team', Carbon::parse($when));
        expect($change->allowedNow)->toBeTrue()
            ->and($change->effectiveAt->toDateString())->toBe($when);
    }
});

it('never invoices a customer for downgrading', function () {
    $start = Carbon::parse('2026-08-01');
    $subscription = Subscription::factory()->plan('business')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);
    // Grandfathered below today's smaller plan — asserted, for the reason given in
    // the direction test above: the config write this replaces was a no-op.
    $subscription->forceFill(['price_cents' => 4900])->saveQuietly();
    expect(Subscription::snapshotFrom('team')['price_cents'])->toBeGreaterThan(4900);

    $credit = PlanChange::goodwillCredit($subscription->fresh(), 'team', Carbon::parse('2026-08-15'));

    expect($credit)->toBe(0);
});

it('does not give away an upgrade in the last hours of a term', function () {
    $start = Carbon::parse('2026-08-01');
    $subscription = Subscription::factory()->plan('start')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);

    // Six hours left: still service, so still a charge.
    $change = PlanChange::evaluate($subscription, 'business', Carbon::parse('2026-08-31 18:00'));

    expect($change->remainingDays)->toBe(1)
        ->and($change->chargeCents)->toBeGreaterThan(0);
});

it('sends an upgrade requested after the term into the next one', function () {
    $start = Carbon::parse('2026-08-01');
    $subscription = Subscription::factory()->plan('start')->create([
        'current_period_start' => $start,
        'current_period_end' => $start->copy()->addMonth(),
    ]);

    // Nothing left to prorate — charging zero and switching immediately would
    // hand out the bigger plan for free.
    $change = PlanChange::evaluate($subscription, 'business', Carbon::parse('2026-09-05'));

    expect($change->allowedNow)->toBeFalse()
        ->and($change->chargeCents)->toBe(0)
        ->and($change->effectiveAt->toDateString())->toBe('2026-09-01');
});

it('has nothing to change once a subscription is cancelled', function () {
    $subscription = Subscription::factory()->plan('start')->create([
        'status' => 'cancelled',
        'cancelled_at' => now(),
    ]);

    // A caller trusting allowedNow would otherwise provision and bill an
    // upgrade for a customer who has already left.
    $change = PlanChange::evaluate($subscription, 'business');

    expect($change->allowedNow)->toBeFalse()
        ->and($change->chargeCents)->toBe(0)
        ->and($change->effectiveAt)->toBeNull();
});

it('refuses a term it does not know', function () {
    expect(fn () => Subscription::snapshotFrom('team', 'yearlyy'))
        ->toThrow(RuntimeException::class, 'Unknown term');
});
