<?php

use App\Models\PlanPrice;
use App\Models\StripeAddonPrice;
use App\Models\StripePlanPrice;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use Illuminate\Contracts\Console\Kernel;

/**
 * The orphans an operator can finally act on.
 *
 * The command lists EVERY active recurring Price at one of our Products that no
 * row knows — adoptable or not; the only question it asks is whether a register
 * row holds the id. That is wider than the log line AdoptStripePrice writes, and
 * it is why stripe:sync-catalogue has to run first: see the command's own
 * docblock, which had to be corrected away from the narrower claim.
 *
 * The fixtures below are all at the narrow end anyway — planted with no metadata
 * at all, so nothing could have adopted them, which is what a person clicking
 * through Stripe's dashboard leaves behind. That keeps each test about the
 * sweep's own question (does a row know this id?) rather than about what the
 * recognition step would have done with it.
 */
beforeEach(function () {
    $this->stripe = new FakeStripeClient;
    app()->instance(StripeClient::class, $this->stripe);
    app(Kernel::class)->call('stripe:sync-catalogue');

    $this->product = (string) StripeAddonPrice::query()
        ->where('addon_key', 'priority_support')
        ->value('stripe_product_id');

    $this->known = (string) StripeAddonPrice::query()
        ->where('addon_key', 'priority_support')
        ->where('interval', 'month')
        ->where('reverse_charge', false)
        ->value('stripe_price_id');

    $this->stripe->plantPrice('price_orphan', $this->product, 9900, 'EUR', 'month', []);
});

it('reports the orphan and leaves it alone', function () {
    $this->artisan('stripe:sweep-orphan-prices')
        ->expectsOutputToContain('price_orphan')
        ->assertSuccessful();

    expect($this->stripe->archived)->toBe([]);
});

it('never reports a price a row knows', function () {
    $this->artisan('stripe:sweep-orphan-prices')
        ->doesntExpectOutputToContain($this->known)
        ->assertSuccessful();
});

it('sees the plan side too: a family product orphan is reported, a known plan price never', function () {
    // Every other test in this file plants its orphan under the MODULE Product,
    // so both halves of the plan side went untested: the PlanFamily half of
    // products() and the StripePlanPrice half of the $known set. Blind to
    // either, the command misses the LARGER half — a family Product collects a
    // Price per version, term, treatment and rate change, a module Product four.
    //
    // Both assertions sit in one test on purpose: each fails for one of the two
    // lines. Drop the PlanFamily pluck from products() and the family Product is
    // never visited, so the orphan below goes unreported. Drop the
    // StripePlanPrice pluck from $known and the row's own Price becomes an
    // orphan the command names.
    $row = PlanPrice::query()->firstOrFail();
    $familyProduct = (string) $row->version->family->stripe_product_id;

    $knownPlanPrice = (string) StripePlanPrice::query()
        ->where('plan_price_id', $row->id)
        ->where('reverse_charge', false)
        ->value('stripe_price_id');

    // At the family's Product and at an amount no row of ours holds — what the
    // sync leaves behind when it dies between Stripe's create and our insert,
    // seen from the sweep's side.
    $this->stripe->plantPrice('price_family_orphan', $familyProduct,
        12300, (string) $row->currency, 'month', []);

    $this->artisan('stripe:sweep-orphan-prices')
        ->expectsOutputToContain('price_family_orphan')
        ->doesntExpectOutputToContain($knownPlanPrice)
        ->assertSuccessful();

    expect($familyProduct)->not->toBe('')
        ->and($knownPlanPrice)->not->toBe('')
        // Nothing was touched: this is the reporting path, not --archive.
        ->and($this->stripe->archived)->toBe([]);
});

it('stops selling the orphan when asked to', function () {
    $this->artisan('stripe:sweep-orphan-prices', ['--archive' => true])
        ->assertSuccessful();

    // Archiving is harmless for the same reason it always is here: Stripe goes
    // on billing every subscription already on an archived Price, it merely
    // stops being sold.
    expect($this->stripe->archived)->toBe(['price_orphan']);
});

it('touches nothing on a dry run, even when asked to archive', function () {
    $this->artisan('stripe:sweep-orphan-prices', ['--archive' => true, '--dry-run' => true])
        ->expectsOutputToContain('price_orphan')
        ->assertSuccessful();

    expect($this->stripe->archived)->toBe([]);
});

it('fails cleanly on a dry run when Stripe is not configured, rather than throwing', function () {
    // Unlike stripe:sync-catalogue, this command has no local-only path: the
    // report itself is a live activePricesFor() call, dry-run or not. A guard
    // that only fired outside --dry-run would let this fall straight into
    // HttpStripeClient's own "not configured" exception instead of the clean
    // error below.
    $this->stripe->configured = false;

    $this->artisan('stripe:sweep-orphan-prices', ['--dry-run' => true])
        ->expectsOutputToContain('Stripe is not configured (STRIPE_SECRET is empty). Nothing was read.')
        ->assertFailed();
});
