<?php

namespace Tests\Feature\Licences;

use App\Livewire\LicenseTokens\RateLimitModal;
use App\Livewire\LicenseTokens\Show;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Monitoring\ApiRateEvent;
use App\Models\Products\LicenseToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Cambiar el límite de peticiones de una licencia desde su ficha.
 *
 * Lo que se fija: que **el número se decida con los datos delante**. Bajar este tope corta
 * servicio —el Moodle del cliente deja de recibir licencias y contenido en cuanto se
 * pasa—, y en el formulario de edición ese campo se toca sin ver cuánto gasta ese cliente
 * ni si ya se le ha cortado alguna vez.
 */
class RateLimitModalTest extends TestCase
{
    use RefreshDatabase;

    private function admin(array $permisos = ['admin.license-tokens.index', 'admin.license-tokens.edit']): User
    {
        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);

        foreach ($permisos as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'license-tokens', 'description' => $nombre]
            ));
        }

        $user = User::factory()->create();
        $user->assignRole($rol);
        $this->actingAs($user);

        Cache::flush();

        return $user;
    }

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

        return LicenseToken::create([
            // Único por llamada: hay casos que crean dos licencias y `token` lo es.
            'token' => '3IP-RRRR-LLLL-MMMM-' . str_pad((string) (LicenseToken::count() + 1), 4, '0', STR_PAD_LEFT),
            'client_id' => $cliente->id,
            'actived' => true,
            'active' => true,
            'rate_limit_per_minute' => $tope,
        ]);
    }

    /** Una ventana de un minuto en la que ese cliente gastó `$observadas` peticiones. */
    private function trafico(LicenseToken $licencia, int $observadas, int $rechazadas = 0): ApiRateEvent
    {
        return ApiRateEvent::create([
            'license_token_id' => $licencia->id,
            // NOT NULL: cada ventana anota de qué IP vino, aunque el límite sea por token.
            'ip' => '10.0.0.1',
            'window_started_at' => now()->subDay(),
            'limit_kind' => ApiRateEvent::LIMIT_TOKEN,
            'kind' => 'token',
            'observed' => $observadas,
            'rejected' => $rechazadas,
            'limit_value' => $licencia->rate_limit_per_minute ?? 10000,
        ]);
    }

    /* ===================== Que se llegue ===================== */

    public function test_desde_la_ficha_se_abre_la_modal(): void
    {
        // **Es la razón de que exista.** El campo estaba solo en el formulario de edición,
        // donde no se ve el consumo ni los cortes de ese cliente.
        $this->admin();
        $licencia = $this->licencia(100);

        Livewire::test(Show::class, ['license_token' => $licencia])
            ->call('abrirLimite')
            ->assertDispatched('openModal');
    }

    public function test_la_ficha_ofrece_poner_un_limite_cuando_no_hay_ninguno(): void
    {
        // «Cambiar el límite» sobre una licencia que no tiene ninguno mandaría a buscar un
        // número que no existe.
        $this->admin();

        Livewire::test(Show::class, ['license_token' => $this->licencia(null)])
            ->assertSee('Poner un límite');

        Livewire::test(Show::class, ['license_token' => $this->licencia(100)])
            ->assertSee('Cambiar el límite');
    }

    /* ===================== Guardar ===================== */

    public function test_cambiar_el_tope_lo_guarda(): void
    {
        $this->admin();
        $licencia = $this->licencia(100);

        Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '250')
            ->call('guardar')
            ->assertHasNoErrors();

        $this->assertSame(250, $licencia->fresh()->rate_limit_per_minute);
    }

    public function test_vaciarlo_quita_el_tope_propio(): void
    {
        // Vacío significa «que se le aplique el techo por defecto», que es un estado
        // distinto de tener un tope alto: hay que poder volver a él.
        $this->admin();
        $licencia = $this->licencia(100);

        Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '')
            ->call('guardar')
            ->assertHasNoErrors();

        $this->assertNull($licencia->fresh()->rate_limit_per_minute);
    }

    public function test_un_cero_no_se_acepta_y_se_explica_por_que(): void
    {
        // **Es la confusión que hay que evitar.** Un 0 dejaría al cliente sin poder llamar,
        // y quien lo escribe casi siempre quiere decir «sin tope». El mensaje manda al
        // campo vacío en vez de dejar pasar un número que corta a todo el mundo.
        $this->admin();
        $licencia = $this->licencia(100);

        Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '0')
            ->call('guardar')
            ->assertHasErrors(['tope']);

        $this->assertSame(100, $licencia->fresh()->rate_limit_per_minute);
    }

    public function test_el_cambio_queda_en_el_log_con_el_antes_y_el_despues(): void
    {
        // Sin los dos números, el log no sirve para lo único que se consulta: por qué un
        // cliente empezó a recibir 429 un martes por la tarde.
        $this->admin();
        $licencia = $this->licencia(100);

        Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '50')
            ->call('guardar');

        $this->assertDatabaseHas('logs', ['code' => '16030']);

        $mensaje = \App\Models\Monitoring\Log::where('code', '16030')->value('msg');
        $this->assertStringContainsString('100/min', $mensaje);
        $this->assertStringContainsString('50/min', $mensaje);
    }

    public function test_guardar_lo_mismo_no_deja_rastro_de_un_cambio_que_no_hubo(): void
    {
        // Un log de auditoría con entradas de cambios que no cambiaron nada es un log que
        // hay que leer entero para encontrar lo que importa.
        $this->admin();
        $licencia = $this->licencia(100);

        Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '100')
            ->call('guardar');

        $this->assertDatabaseMissing('logs', ['code' => '16030']);
    }

    /* ===================== Lo que avisa antes de cortar ===================== */

    public function test_avisa_cuando_el_tope_queda_por_debajo_de_lo_que_el_cliente_gasta(): void
    {
        // **El caso que corta servicio.** Si el tope queda por debajo del pico de la
        // semana, ese cliente empieza a recibir 429 en cuanto se guarde, y su sitio deja de
        // recibir licencias y contenido. El aviso lo dice con el número delante.
        $this->admin();
        $licencia = $this->licencia(500);
        $this->trafico($licencia, observadas: 340);

        $componente = Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '100');

        $consecuencia = $componente->viewData('consecuencia');

        $this->assertTrue($consecuencia['grave']);
        $this->assertStringContainsString('340', $consecuencia['aviso']);
        $componente->assertSee('empezará a recibir 429');
    }

    public function test_un_tope_holgado_no_se_presenta_como_grave(): void
    {
        // Si todo lo que se toca sale en rojo, el rojo deja de significar algo.
        $this->admin();
        $licencia = $this->licencia(500);
        $this->trafico($licencia, observadas: 40);

        $consecuencia = Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '400')
            ->viewData('consecuencia');

        $this->assertFalse($consecuencia['grave']);
    }

    public function test_se_dice_que_ya_se_le_ha_cortado_esta_semana(): void
    {
        // Quien abre esta modal suele venir de una queja del cliente. Que la pantalla diga
        // que ya se le ha cortado ahorra la pregunta.
        $this->admin();
        $licencia = $this->licencia(100);
        $this->trafico($licencia, observadas: 100, rechazadas: 25);

        $componente = Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id]);

        $this->assertSame(25, $componente->viewData('consumo')['cortes']);
        $componente->assertSee('peticiones rechazadas');
    }

    public function test_quitar_el_tope_dice_que_pasa_a_no_tener_limite(): void
    {
        $this->admin();
        $licencia = $this->licencia(100);

        $consecuencia = Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->set('tope', '')
            ->viewData('consecuencia');

        $this->assertFalse($consecuencia['grave']);
        $this->assertStringContainsString('techo por defecto', $consecuencia['aviso']);
    }

    public function test_el_pico_se_mide_sobre_lo_gastado_y_no_sobre_lo_rechazado(): void
    {
        // Comparar el tope contra `rejected` diría que un cliente al que nunca se ha
        // cortado no gasta nada, y dejaría poner un tope por debajo de su consumo real sin
        // ningún aviso.
        $this->admin();
        $licencia = $this->licencia(500);
        $this->trafico($licencia, observadas: 380, rechazadas: 0);

        $this->assertSame(380, Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->viewData('consumo')['pico']);
    }

    /* ===================== Permisos ===================== */

    public function test_quien_solo_mira_no_abre_la_modal_ni_guarda(): void
    {
        // MGR-005: el permiso de la ruta no se reaplica en `/livewire/update`, y esta modal
        // escribe un número que corta servicio.
        $this->admin(['admin.license-tokens.index']);
        $licencia = $this->licencia(100);

        Livewire::test(Show::class, ['license_token' => $licencia])
            ->call('abrirLimite')
            ->assertForbidden();

        Livewire::test(RateLimitModal::class, ['licenseTokenId' => $licencia->id])
            ->assertForbidden();

        $this->assertSame(100, $licencia->fresh()->rate_limit_per_minute);
    }

    public function test_quien_solo_mira_no_ve_el_boton(): void
    {
        $this->admin(['admin.license-tokens.index']);

        Livewire::test(Show::class, ['license_token' => $this->licencia(100)])
            ->assertDontSee('Cambiar el límite');
    }
}
