<?php

declare(strict_types=1);

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\DB;

abstract class TestCase extends BaseTestCase
{
    /*
     * The rate limiter is backed by the cache, and the suite runs on the array
     * store, so throttle counters are discarded between tests automatically.
     * Nothing needs resetting here -- and clearing the limiter instance would
     * discard the limiter definitions registered during boot.
     */

    /**
     * Drops the resolved user cached on each auth guard.
     *
     * A real HTTP request boots a fresh application and resolves the guard from
     * scratch. Within one test the application is reused, so a guard keeps the
     * user it already resolved -- which would make a revoked token or a
     * suspended account appear to still work. Call this between the request
     * that revokes a credential and the request that proves it is dead.
     */
    protected function forgetAuthGuards(): static
    {
        $this->app->make('auth')->forgetGuards();

        return $this;
    }

    /**
     * Counts the queries a callable issues.
     *
     * Exists because the original suite asserted on responses but never on cost,
     * and an N+1 sat undetected in the resource layer as a result: serialising
     * two sellers issued five queries, which would have been ~49 on a listing
     * grid. A response-shape assertion cannot see that; a query count can.
     *
     * @template TReturn
     *
     * @param  callable(): TReturn  $callback
     * @return array{result: TReturn, queries: list<string>}
     */
    protected function measureQueries(callable $callback): array
    {
        DB::flushQueryLog();
        DB::enableQueryLog();

        try {
            $result = $callback();
        } finally {
            $queries = array_map(
                fn (array $entry): string => (string) $entry['query'],
                DB::getQueryLog(),
            );

            DB::disableQueryLog();
            DB::flushQueryLog();
        }

        return ['result' => $result, 'queries' => $queries];
    }

    /**
     * Asserts that an operation's query count does not grow with the number of
     * rows it processes.
     *
     * Fixtures are built by `$seed` OUTSIDE the measurement, and only `$operation`
     * is counted -- otherwise the inserts dominate and the assertion measures
     * nothing useful.
     *
     * @param  callable(int): void  $seed  Creates the given number of rows.
     * @param  callable(int): void  $operation  Reads and serialises them.
     */
    protected function assertQueryCountDoesNotScale(
        callable $seed,
        callable $operation,
        int $small = 2,
        int $large = 8,
    ): void {
        $seed($small);
        $smallRun = $this->measureQueries(fn () => $operation($small));

        // Top up to the larger size rather than rebuilding, so the second run
        // reads a strictly larger set.
        $seed($large - $small);
        $largeRun = $this->measureQueries(fn () => $operation($large));

        $smallCount = count($smallRun['queries']);
        $largeCount = count($largeRun['queries']);

        $this->assertSame($smallCount, $largeCount, sprintf(
            "Query count scales with the number of rows: %d rows took %d queries, %d rows took %d.\n".
            "This is an N+1. Queries for the larger run:\n  %s",
            $small,
            $smallCount,
            $large,
            $largeCount,
            implode("\n  ", $largeRun['queries']),
        ));
    }

    /**
     * Marks the SPA as the caller so stateful, cookie-based routes behave the
     * way they do in the browser.
     */
    protected function asSpa(): static
    {
        $this->withHeader('Referer', (string) config('app.frontend_url'));

        return $this;
    }
}
