<?php

declare(strict_types=1);

namespace Tests\Feature\Schema;

use App\Enums\SubscriptionStatus;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * One entitling subscription per account, enforced by the database.
 *
 * Nothing previously stopped an account holding two plans at once, and the fee
 * resolver would then have priced against whichever it read first. A retried
 * checkout or a duplicated provider webhook is enough to cause it.
 */
final class SubscriptionIntegrityTest extends TestCase
{
    use RefreshDatabase;

    private function subscribe(User $user, Plan $plan, SubscriptionStatus $status): Subscription
    {
        $subscription = new Subscription;
        $subscription->user_id = $user->getKey();
        $subscription->plan_id = $plan->getKey();
        $subscription->status = $status;
        $subscription->current_period_start = now();
        $subscription->save();

        return $subscription;
    }

    #[Test]
    public function a_second_active_subscription_is_refused(): void
    {
        $user = User::factory()->create();

        $this->subscribe($user, Plan::factory()->create(['code' => 'portfolio']), SubscriptionStatus::Active);

        $this->expectException(UniqueConstraintViolationException::class);

        $this->subscribe($user, Plan::factory()->create(['code' => 'broker']), SubscriptionStatus::Active);
    }

    #[Test]
    public function a_cancelled_subscription_does_not_block_a_new_one(): void
    {
        $user = User::factory()->create();

        $old = $this->subscribe($user, Plan::factory()->create(['code' => 'starter']), SubscriptionStatus::Active);

        $old->status = SubscriptionStatus::Canceled;
        $old->save();

        $new = $this->subscribe($user, Plan::factory()->create(['code' => 'broker']), SubscriptionStatus::Active);

        $this->assertNull($old->fresh()->active_marker);
        $this->assertSame($user->getKey(), (int) $new->fresh()->active_marker);
    }

    #[Test]
    public function different_accounts_may_each_hold_an_active_subscription(): void
    {
        $plan = Plan::factory()->create();

        $this->subscribe(User::factory()->create(), $plan, SubscriptionStatus::Active);
        $this->subscribe(User::factory()->create(), $plan, SubscriptionStatus::Active);

        $this->assertDatabaseCount('subscriptions', 2);
    }

    /**
     * Past-due still entitles -- a missed payment should not instantly revoke a
     * seller's plan -- so it must occupy the single active slot.
     */
    #[Test]
    public function a_past_due_subscription_occupies_the_active_slot(): void
    {
        $user = User::factory()->create();

        $subscription = $this->subscribe($user, Plan::factory()->create(), SubscriptionStatus::PastDue);

        $this->assertSame($user->getKey(), (int) $subscription->fresh()->active_marker);
    }

    #[Test]
    public function an_incomplete_subscription_does_not_occupy_the_slot(): void
    {
        $user = User::factory()->create();

        $subscription = $this->subscribe($user, Plan::factory()->create(), SubscriptionStatus::Incomplete);

        $this->assertNull($subscription->fresh()->active_marker);
    }

    #[Test]
    public function registration_leaves_exactly_one_entitling_subscription(): void
    {
        Notification::fake();

        Plan::factory()->asDefault()->create(['code' => 'starter']);

        $this->asSpa()->postJson('/api/v1/auth/register', [
            'name' => 'Asha Rai',
            'email' => 'asha@example.com',
            'password' => 'correct-horse-battery-91',
            'password_confirmation' => 'correct-horse-battery-91',
            'accept_terms' => true,
        ])->assertCreated();

        $user = User::query()->where('email', 'asha@example.com')->sole();

        $this->assertSame(1, $user->subscriptions()->entitling()->count());
    }
}
