<?php

use App\Models\Operator;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Models\User;
use App\Provisioning\Jobs\AdvanceRunJob;
use Illuminate\Support\Facades\Queue;

function stripeEvent(string $id = 'evt_1', string $plan = 'team'): array
{
    return [
        'id' => $id,
        'type' => 'checkout.session.completed',
        'data' => ['object' => [
            'id' => 'cs_'.$id, // checkout session id = idempotency key
            'customer' => 'cus_1',
            'payment_status' => 'paid',
            'customer_details' => ['email' => 'kunde@example.com', 'name' => 'Kanzlei Berger'],
            'amount_total' => 4900,
            'currency' => 'eur',
            'metadata' => ['plan' => $plan, 'datacenter' => 'fsn'],
        ]],
    ];
}

it('creates a customer, order and run from a paid checkout', function () {
    Queue::fake();

    $this->postJson(route('webhooks.stripe'), stripeEvent())->assertOk();

    $order = Order::query()->where('stripe_event_id', 'cs_evt_1')->first();
    expect($order)->not->toBeNull()
        ->and($order->plan)->toBe('team')
        ->and($order->status)->toBe('paid')
        ->and(ProvisioningRun::query()->where('subject_id', $order->id)->where('pipeline', 'customer')->exists())->toBeTrue();
    Queue::assertPushed(AdvanceRunJob::class, 1);
});

it('is idempotent — a duplicate webhook starts only one run', function () {
    Queue::fake();

    $this->postJson(route('webhooks.stripe'), stripeEvent('evt_dup'))->assertOk();
    $this->postJson(route('webhooks.stripe'), stripeEvent('evt_dup'))->assertOk();

    expect(Order::query()->where('stripe_event_id', 'cs_evt_dup')->count())->toBe(1);
    Queue::assertPushed(AdvanceRunJob::class, 1);
});

it('ignores unrelated event types', function () {
    Queue::fake();

    $this->postJson(route('webhooks.stripe'), ['id' => 'evt_x', 'type' => 'payout.paid', 'data' => ['object' => []]])
        ->assertOk()
        ->assertJson(['ignored' => true]);

    Queue::assertNothingPushed();
});

it('answers a billing event for a subscription it does not have, without retrying forever', function () {
    Queue::fake();

    // Stripe delivers events for objects created by hand in the dashboard, and
    // for other environments pointed at the same endpoint. A non-2xx here would
    // have them retry something that can never succeed.
    $this->postJson(route('webhooks.stripe'), [
        'id' => 'evt_orphan', 'type' => 'invoice.paid',
        'data' => ['object' => ['id' => 'in_1', 'subscription' => 'sub_unknown', 'amount_paid' => 21480]],
    ])->assertOk()->assertJson(['handled' => 'invoice.paid', 'applied' => false]);

    Queue::assertNothingPushed();
});

it('ignores a paid checkout without a customer email', function () {
    Queue::fake();

    $event = stripeEvent('evt_noemail');
    unset($event['data']['object']['customer_details'], $event['data']['object']['customer_email'], $event['data']['object']['metadata']['email']);

    $this->postJson(route('webhooks.stripe'), $event)->assertOk()->assertJson(['ignored' => 'no_customer_email']);

    expect(Order::query()->where('stripe_event_id', 'evt_noemail')->exists())->toBeFalse();
    Queue::assertNothingPushed();
});

it('provisions an async payment that succeeds later', function () {
    Queue::fake();

    $event = stripeEvent('evt_async');
    $event['type'] = 'checkout.session.async_payment_succeeded';
    unset($event['data']['object']['payment_status']); // async success is inherently paid

    $this->postJson(route('webhooks.stripe'), $event)->assertOk();

    expect(Order::query()->where('stripe_event_id', 'cs_evt_async')->exists())->toBeTrue();
    Queue::assertPushed(AdvanceRunJob::class, 1);
});

it('ignores a completed but unpaid checkout session', function () {
    Queue::fake();

    $event = stripeEvent('evt_unpaid');
    $event['data']['object']['payment_status'] = 'unpaid';

    $this->postJson(route('webhooks.stripe'), $event)->assertOk()->assertJson(['ignored' => true]);

    expect(Order::query()->where('stripe_event_id', 'evt_unpaid')->exists())->toBeFalse();
    Queue::assertNothingPushed();
});

it('still provisions a paid order when the customer email already belongs to an operator, but withholds the portal login', function () {
    // R21: one address must not represent both an operator and a customer.
    // The payment already happened, so — same principle as openContract()'s
    // own "the order is the record that money changed hands" — the order and
    // run are still created; only the colliding portal LOGIN is withheld.
    Queue::fake();

    Operator::factory()->create(['email' => 'kunde@example.com']);

    $this->postJson(route('webhooks.stripe'), stripeEvent('evt_opcollide'))->assertOk();

    $order = Order::query()->where('stripe_event_id', 'cs_evt_opcollide')->first();
    expect($order)->not->toBeNull()
        ->and(ProvisioningRun::query()->where('subject_id', $order->id)->where('pipeline', 'customer')->exists())->toBeTrue()
        ->and(User::query()->where('email', 'kunde@example.com')->exists())->toBeFalse();

    Queue::assertPushed(AdvanceRunJob::class, 1);
});

it('rejects an invalid signature when a secret is configured', function () {
    config()->set('services.stripe.webhook_secret', 'whsec_test');

    $this->postJson(route('webhooks.stripe'), stripeEvent('evt_sig'), ['Stripe-Signature' => 't=1,v1=deadbeef'])
        ->assertStatus(400);
});

it('accepts a valid signature', function () {
    Queue::fake();
    config()->set('services.stripe.webhook_secret', 'whsec_test');

    $payload = json_encode(stripeEvent('evt_ok'));
    $timestamp = time(); // within Stripe's replay tolerance
    $signature = hash_hmac('sha256', $timestamp.'.'.$payload, 'whsec_test');

    $this->call(
        'POST',
        route('webhooks.stripe'),
        [], [], [],
        ['CONTENT_TYPE' => 'application/json', 'HTTP_STRIPE_SIGNATURE' => "t={$timestamp},v1={$signature}"],
        $payload,
    )->assertOk();

    expect(Order::query()->where('stripe_event_id', 'cs_evt_ok')->exists())->toBeTrue();
});
