<?php

use App\Livewire\Admin\PlanVersions;
use App\Models\PlanVersion;
use App\Models\Subscription;
use App\Models\User;
use App\Services\Billing\PlanCatalogue;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

/**
 * Paying for a year, and what that is worth saying out loud.
 *
 * Both prices have existed since the catalogue did — one `plan_prices` row per
 * term, each with its own Stripe Price — but only the monthly one was ever shown.
 * A customer could not choose the term, the console could only type the yearly
 * TOTAL as a second free-form figure, and nothing in the system knew why 588
 * belonged to a package costing 49 a month. So no page could say "two months
 * free" without a person working it out again by hand.
 *
 * `free_months` on the version is that missing fact. The total is derived from
 * it, which is the only arrangement in which the sentence a customer reads and
 * the amount Stripe charges cannot drift apart.
 */
beforeEach(function () {
    $this->stripe = new FakeStripeClient;
    app()->instance(StripeClient::class, $this->stripe);

    // A DIFFERENT id per term, unlike the other checkout suites: this one is
    // about which of the two is picked, and one id shared by both would make
    // every assertion here pass whatever the code did. Two statements rather
    // than one concat(): the suite runs on SQLite, which spells that ||.
    foreach ([Subscription::TERM_MONTHLY, Subscription::TERM_YEARLY] as $term) {
        DB::table('plan_prices')->where('term', $term)->update(['stripe_price_id' => 'price_'.$term]);
    }
});

function termBuyer(): User
{
    return User::factory()->create(['email_verified_at' => now()]);
}

it('derives the year from the month and the free months', function () {
    // The arithmetic, in the one place both the form preview and the draft use.
    expect(PlanVersions::yearlyCents(4900, 0))->toBe(58800)
        ->and(PlanVersions::yearlyCents(4900, 2))->toBe(49000)
        ->and(PlanVersions::yearlyCents(17900, 1))->toBe(196900)
        // Out of range is clamped rather than turned into a negative price.
        ->and(PlanVersions::yearlyCents(4900, 99))->toBe(0);
});

it('offers both terms in the shop, with the figure behind the saving', function () {
    $plan = app(PlanCatalogue::class)->sellable()['team'];

    expect($plan)->toHaveKeys(['price_cents', 'yearly_price_cents', 'free_months'])
        ->and($plan['price_cents'])->toBe(7900)
        // Zwei Freimonate auf der neuen Leiter — was hier zählt, ist, dass die
        // Zahl GELESEN und nicht angenommen wird.
        ->and($plan['free_months'])->toBe(2)
        ->and($plan['yearly_price_cents'])->toBe(7900 * (12 - $plan['free_months']));
});

it('freezes the free months when the version is published', function () {
    // It is part of the terms a customer bought, not presentation: the yearly
    // amount was derived from it, and changing it afterwards would rewrite what
    // a running contract says it agreed to.
    $version = PlanVersion::query()->whereNotNull('published_at')->first();

    expect(fn () => $version->update(['free_months' => 3]))
        ->toThrow(RuntimeException::class);
});

it('lets a customer pick the term on the order page', function () {
    $page = $this->actingAs(termBuyer())->get(route('order'))->assertOk()->getContent();

    // Both figures rendered, one shown: switching costs no round trip.
    expect($page)->toContain("term = 'yearly'")
        ->and($page)->toContain(__('order.term_yearly'));
});

it('links to the checkout with one query string, not two question marks', function () {
    // Both parameters are query parameters, so route() + '?term=' built
    // `?plan=team?term=monthly` and the package arrived as the whole string
    // "team?term=monthly". Every click landed on "this package is not on sale",
    // and nothing in the page or the log said why.
    $page = $this->actingAs(termBuyer())->get(route('order'))->assertOk()->getContent();

    preg_match_all('/href="([^"]*checkout[^"]*)"/', $page, $plain);
    preg_match_all('/x-bind:href="\x27([^\x27]*)\x27/', $page, $bound);

    expect($plain[1])->not->toBeEmpty()
        ->and($bound[1])->not->toBeEmpty();

    foreach (array_merge($plain[1], $bound[1]) as $href) {
        expect(substr_count($href, '?'))->toBe(1, $href);
    }
});

it('keeps the saving above the switch, where it cannot move the buttons', function () {
    // It hung underneath and was hidden on the yearly view, so choosing a term
    // moved the very buttons that choose it.
    $page = File::get(resource_path('views/livewire/order.blade.php'));

    expect(strpos($page, 'order.free_months_hint'))->toBeLessThan(strpos($page, "order.term_'.\$option"))
        // And it is always there: one line, two wordings, no appearing and
        // disappearing.
        ->and($page)->toContain('order.free_months_taken');
});

it('carries the chosen term into the checkout page and its form', function () {
    $page = $this->actingAs(termBuyer())
        ->get(route('checkout', ['plan' => 'team', 'term' => 'yearly']))
        ->assertOk()
        ->getContent();

    expect($page)->toContain('name="term" value="yearly"')
        // And the yearly figure is the one on the summary: 79,00 × 10 gross.
        ->and($page)->toContain('948,00');
});

it('refuses a term nobody sells even in the address bar', function () {
    // A URL is a string somebody can retype. An unknown term falls back to the
    // month rather than showing a page priced from nothing.
    $page = $this->actingAs(termBuyer())
        ->get(route('checkout', ['plan' => 'team', 'term' => 'weekly']))
        ->assertOk()
        ->getContent();

    expect($page)->toContain('name="term" value="monthly"');
});

it('opens the checkout on the yearly Stripe Price when yearly was chosen', function () {
    // The one that matters: a term shown but not carried through would charge a
    // month at the yearly price or the other way round.
    $version = app(PlanCatalogue::class)->currentVersion('team');
    $yearly = $version->priceFor(Subscription::TERM_YEARLY);

    $this->actingAs(termBuyer())->post(route('checkout.start'), [
        'plan' => 'team',
        'term' => Subscription::TERM_YEARLY,
        'terms_accepted' => '1',
    ]);

    expect($this->stripe->checkouts)->toHaveCount(1)
        ->and($this->stripe->checkouts[0]['price'])->toBe($yearly->stripe_price_id);
});

it('defaults to the month when no term was sent at all', function () {
    // A form with JavaScript off posts an empty term, and "empty" must not mean
    // "whichever row comes first".
    $version = app(PlanCatalogue::class)->currentVersion('team');

    $this->actingAs(termBuyer())->post(route('checkout.start'), ['plan' => 'team', 'terms_accepted' => '1']);

    expect($this->stripe->checkouts[0]['price'])
        ->toBe($version->priceFor(Subscription::TERM_MONTHLY)->stripe_price_id);
});

it('refuses a term nobody sells', function () {
    $this->actingAs(termBuyer())
        ->post(route('checkout.start'), ['plan' => 'team', 'term' => 'weekly', 'terms_accepted' => '1'])
        ->assertSessionHasErrors('term');

    expect($this->stripe->checkouts)->toBeEmpty();
});

it('shows the yearly price on the public sheet as well', function () {
    // "auf der website sowie im userpanel ersichtlich" — the sheet quotes the
    // same two figures, from the same catalogue, so the switch a visitor used
    // outside and the one in the panel cannot disagree.
    $page = $this->get('/')->assertOk()->getContent();

    expect($page)->toContain("term = 'yearly'")
        // The yearly PRICE as the headline, in the unit it is charged in — the
        // monthly equivalent as the headline did not move at all on a package
        // with no free months, so the sheet looked like it ignored the click.
        ->and($page)->toContain('/Jahr')
        ->and($page)->toContain('entspricht');
});

it('states the term on the billing card, not just a monthly figure', function () {
    // A yearly contract is charged once a year while the card says "per month".
    // Without this line the customer read a number that never appears on a
    // statement.
    expect(File::get(resource_path('views/livewire/billing.blade.php')))
        ->toContain("__('billing.term_yearly_note'")
        ->and(File::get(app_path('Livewire/Billing.php')))
        ->toContain("'term' => (string) \$subscription->term");
});

// ---- The console ----

it('asks for free months rather than a second price', function () {
    // Typing both totals let them disagree, and nothing then knew which was
    // meant. The form takes the monthly price and the months that are free.
    $component = File::get(app_path('Livewire/Admin/PlanVersions.php'));

    expect($component)->toContain('public int $freeMonths')
        ->and($component)->not->toContain('public string $yearlyPrice')
        ->and($component)->toContain("'freeMonths' => 'required|integer|min:0|max:6'");

    expect(File::get(resource_path('views/livewire/admin/plan-versions.blade.php')))
        ->toContain("__('plans.free_months')")
        ->toContain('yearlyPreview');
});
