<?php

declare(strict_types=1);

namespace Tests\Feature\Performance;

use App\Http\Resources\PublicSellerResource;
use App\Http\Resources\UserResource;
use App\Models\SellerProfile;
use App\Models\User;
use App\Models\UserVerification;
use Illuminate\Database\LazyLoadingViolationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * Guards against per-row query cost in the serialisation layer.
 *
 * The resources previously called loadMissing() inside model helpers, which is
 * a per-model eager load: serialising N sellers issued 2N+1 queries. Worse, it
 * slipped past Model::preventLazyLoading() entirely, because an explicit load is
 * not a lazy-load violation -- the guardrail gave false confidence against
 * exactly the pattern used to satisfy it.
 *
 * These tests assert on cost rather than on output, which is the only way to
 * see that class of regression.
 */
final class QueryCountTest extends TestCase
{
    use RefreshDatabase;

    private function request(): Request
    {
        return Request::create('/', 'GET');
    }

    /** @return list<SellerProfile> */
    private function sellers(int $count): array
    {
        $sellers = [];

        for ($i = 0; $i < $count; $i++) {
            $user = User::factory()->create();
            UserVerification::factory()->identityVerified()->create(['user_id' => $user->getKey()]);
            $sellers[] = SellerProfile::factory()->create(['user_id' => $user->getKey()]);
        }

        return $sellers;
    }

    #[Test]
    public function serialising_public_sellers_does_not_scale_with_collection_size(): void
    {
        $this->assertQueryCountDoesNotScale(
            seed: fn (int $count) => $this->sellers($count),
            operation: function (int $count): void {
                $sellers = SellerProfile::query()
                    ->withPublicBadges()
                    ->limit($count)
                    ->get();

                // json_encode forces every nested resource to resolve, which is
                // where a per-row query would actually fire.
                json_encode(PublicSellerResource::collection($sellers)->toArray($this->request()));
            },
        );
    }

    #[Test]
    public function serialising_users_does_not_scale_with_collection_size(): void
    {
        $this->assertQueryCountDoesNotScale(
            seed: fn (int $count) => User::factory()->count($count)->create(),
            operation: function (int $count): void {
                $users = User::query()->withTrustSignals()->limit($count)->get();

                json_encode(UserResource::collection($users)->toArray($this->request()));
            },
        );
    }

    /**
     * The measured figure, pinned. A seller grid must cost a constant number of
     * queries regardless of how many sellers it shows.
     */
    #[Test]
    public function a_seller_grid_costs_a_constant_number_of_queries(): void
    {
        $this->sellers(12);

        $run = $this->measureQueries(function (): void {
            $sellers = SellerProfile::query()->withPublicBadges()->limit(12)->get();
            json_encode(PublicSellerResource::collection($sellers)->toArray($this->request()));
        });

        // One for the sellers, one for their users, one for the verifications.
        $this->assertLessThanOrEqual(3, count($run['queries']), sprintf(
            "Expected at most 3 queries for a 12-seller grid, got %d:\n  %s",
            count($run['queries']),
            implode("\n  ", $run['queries']),
        ));
    }

    /**
     * The guardrail must now actually fire on a collection.
     *
     * This is the protection that loadMissing() was suppressing: forgetting to
     * eager-load is the mistake that produces an N+1, and it has to be an error
     * a developer sees immediately rather than a silent per-row cost.
     *
     * Note the scope. Laravel arms lazy-load prevention only on models hydrated
     * from a multi-row result -- see Builder::hydrate(), which sets the flag
     * when `count($items) > 1`. That is the right scope: a lazy load on a single
     * model is one extra query, not an N+1. So the guard is asserted here
     * against a collection, which is where it matters.
     */
    #[Test]
    public function forgetting_to_eager_load_throws_when_serialising_a_collection(): void
    {
        $this->sellers(3);

        $sellers = SellerProfile::query()->get();

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

        foreach ($sellers as $seller) {
            $seller->publicBadges();
        }
    }

    /**
     * The complement: eager-loading satisfies the guard.
     */
    #[Test]
    public function eager_loading_satisfies_the_guard(): void
    {
        $this->sellers(3);

        $sellers = SellerProfile::query()->withPublicBadges()->get();

        foreach ($sellers as $seller) {
            $this->assertIsArray($seller->publicBadges());
        }
    }

    /**
     * A single-model read is deliberately exempt, and the account endpoints rely
     * on that -- they serialise one user and load its relations explicitly.
     */
    #[Test]
    public function a_single_model_read_is_not_treated_as_a_violation(): void
    {
        $this->sellers(1);

        $seller = SellerProfile::query()->firstOrFail();

        $this->assertIsArray($seller->publicBadges());
    }
}
