<?php

namespace Tests\Feature\Api;

use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Monitoring\ApiRateEvent;
use App\Models\Products\LicenseToken;
use App\Services\System\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Tests\TestCase;

/**
 * El límite de peticiones **por licencia** (MGR-013).
 *
 * El campo se llamaba `usage_limit` y la interfaz decía "Límite de uso", pero la
 * ventana siempre ha sido de 60 segundos. Un token con 100 no tenía una cuota de 100
 * llamadas: tenía **100 por minuto, renovables para siempre** —144.000 al día—. En
 * esta misma base había una licencia llamada "Licencia de Prueba Temporal" con 100,
 * creada por alguien que entendía "100 llamadas y se acaba".
 *
 * **Decisión**: el comportamiento se queda (un límite por minuto es lo que se quiere)
 * y lo que cambia es que deje de engañar: el nombre, la interfaz, y —sobre todo— que
 * el corte **deje rastro**, porque antes no lo dejaba en ninguna parte.
 *
 * No confundir con el límite por IP de `ThrottleApiRequests` (MGR-008): son dos
 * mecanismos distintos y por eso sus mensajes de 429 ahora se distinguen.
 */
class TokenRateLimitTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN = '3IP-TEST-RATELIMIT-1';
    private const DOMAIN = 'https://ratelimit.test';

    private LicenseToken $licencia;

    protected function setUp(): void
    {
        parent::setUp();

        // El límite por IP se desactiva: aquí se prueba el de la licencia, y con los
        // dos activos no se sabría cuál ha cortado.
        config(['api.throttle.enabled' => false]);

        RateLimiter::clear('rate_limit:token:' . self::TOKEN);
        Cache::flush();

        // Estos tests prueban el CORTE, así que hay que activarlo: por defecto los
        // límites solo observan. Tener que encenderlo aquí es, en sí, la prueba de que
        // el valor de fábrica es el seguro.
        app(Settings::class)->set(Settings::LIMITES_CORTAN, '1');
    }

    private function licencia(?int $limite): LicenseToken
    {
        $cliente = Client::create(['name' => 'Cliente Límite', 'shortname' => 'limite', 'actived' => true]);

        $this->licencia = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia con tope',
            'token' => self::TOKEN,
            'active' => true,
            'rate_limit_per_minute' => $limite,
        ]);

        Environment::create([
            'name' => 'Entorno Límite',
            'domain' => self::DOMAIN,
            'version' => '5.1.1',
            'env' => 'local',
            'client_id' => $cliente->id,
            'license_token_id' => $this->licencia->id,
            'active' => true,
        ]);

        return $this->licencia;
    }

    private function llamar(): \Illuminate\Testing\TestResponse
    {
        return $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'products',
            'host' => self::DOMAIN,
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => [
                'version' => '2025100601.03',
                'release' => '5.1.1+ (Build: 20251219)',
                'type' => 'moodle',
                'env' => 'local',
            ],
        ]);
    }

    public function test_corta_al_pasarse_del_tope(): void
    {
        $this->licencia(3);

        for ($i = 1; $i <= 3; $i++) {
            $this->llamar()->assertOk();
        }

        $this->llamar()->assertStatus(429);
    }

    public function test_el_mensaje_dice_que_ha_cortado_la_licencia_y_con_que_tope(): void
    {
        // Los dos mecanismos de límite devolvían textos casi idénticos, así que era
        // imposible saber cuál había actuado. Y el plugin escribe este texto en su log:
        // es lo único que ve soporte.
        $this->licencia(2);

        $this->llamar();
        $this->llamar();

        $error = $this->llamar()->assertStatus(429)->json('error');

        $this->assertStringContainsString('licencia', $error);
        $this->assertStringContainsString('2/minuto', $error);
    }

    public function test_el_corte_queda_registrado(): void
    {
        // Lo que antes no pasaba: el 429 se iba sin dejar rastro y la única forma de
        // enterarse era que el cliente llamara.
        $licencia = $this->licencia(1);

        $this->llamar()->assertOk();
        $this->llamar()->assertStatus(429);

        $this->assertDatabaseHas('api_rate_events', [
            'license_token_id' => $licencia->id,
            'limit_kind' => ApiRateEvent::LIMIT_TOKEN,
            'kind' => ApiRateEvent::KIND_BLOCKED,
            'limit_value' => 1,
        ]);
    }

    public function test_muchos_rechazos_no_generan_muchas_filas(): void
    {
        // Un cliente en bucle que se pase mil veces en un minuto no puede llenar la
        // tabla: una fila por licencia y minuto, con el contador de rechazos.
        $licencia = $this->licencia(1);

        $this->llamar();

        for ($i = 0; $i < 6; $i++) {
            $this->llamar()->assertStatus(429);
        }

        $this->assertSame(
            1,
            ApiRateEvent::where('license_token_id', $licencia->id)->count(),
            'Debería haber una sola fila para esta licencia y este minuto.'
        );
    }

    public function test_sin_tope_no_corta(): void
    {
        // Tres de las cuatro licencias reales están así: sin tope propio, con el techo
        // por defecto de 10.000/minuto, que en la práctica es sin límite.
        $this->licencia(null);

        for ($i = 1; $i <= 5; $i++) {
            $this->llamar()->assertOk();
        }

        $this->assertDatabaseCount('api_rate_events', 0);
    }

    public function test_el_contador_es_por_minuto_no_una_cuota(): void
    {
        // El corazón de MGR-013: al cambiar de minuto el contador vuelve a cero. Es lo
        // que hace que NO sea una cuota total, y es lo que la interfaz ocultaba.
        $this->licencia(2);

        $this->llamar()->assertOk();
        $this->llamar()->assertOk();
        $this->llamar()->assertStatus(429);

        // Pasado el minuto, el contador se ha vaciado y vuelve a servir.
        RateLimiter::clear('rate_limit:token:' . self::TOKEN);

        $this->llamar()->assertOk();
    }
}
