<?php

namespace Tests\Feature\System;

use App\Mail\ApiBlockAlert;
use App\Mail\ApiBlocksDigest;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Monitoring\ApiBlock;
use App\Models\Products\LicenseToken;
use App\Services\System\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Tests\TestCase;

/**
 * Avisos de bloqueo de la API y su configuración.
 *
 * Lo pedido: "es importante mandar un email cuando hay un bloqueo SIEMPRE", con el
 * matiz de que sea **en la transición a bloqueado** —no un correo por cada rechazo— y
 * con un resumen diario a una hora configurable.
 *
 * Lo que había antes: el aviso existía pero **venía desactivado**
 * (`API_ALERTS_ENABLED=false`), y no había ninguna noción de "esto está bloqueado":
 * solo ventanas de un minuto en `api_rate_events`, que no permiten saber si es la
 * primera vez.
 */
class BlockAlertsTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN = '3IP-TEST-BLOQUEO-01';
    private const DOMAIN = 'https://bloqueo.test';
    private const AVISADOS = 'sistemas@tresipunt.com';

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

        Mail::fake();

        // Aquí se prueba el límite de la licencia; el de IP se apaga para que no se
        // solapen los dos bloqueos.
        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');

        app(Settings::class)->set(Settings::AVISOS_EMAIL, self::AVISADOS);
        app(Settings::class)->set(Settings::AVISOS_ACTIVOS, '1');
    }

    private function licencia(int $limite = 1): LicenseToken
    {
        $cliente = Client::create(['name' => 'Cliente Bloqueo', 'shortname' => 'bloqueo', 'actived' => true]);

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

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

        return $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',
            ],
        ]);
    }

    /* ===================== El aviso ===================== */

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

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

        Mail::assertSent(ApiBlockAlert::class, function ($correo) {
            return $correo->hasTo(self::AVISADOS);
        });
    }

    public function test_no_manda_un_correo_por_cada_rechazo(): void
    {
        // El caso real de producción fueron 437 rechazos en 17 minutos. Un aviso que
        // llega 437 veces se filtra y deja de leerse.
        $this->licencia();

        $this->llamar();

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

        Mail::assertSent(ApiBlockAlert::class, 1);
    }

    public function test_si_se_libera_y_vuelve_a_pasar_avisa_otra_vez(): void
    {
        // Es la razón de que el bloqueo tenga estado: cerrar la incidencia rearma el
        // aviso, como el `Notice::reset()` del Manager antiguo.
        $this->licencia();

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

        ApiBlock::blocked()->firstOrFail()->liberar(null, 'revisado en el test');

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

        Mail::assertSent(ApiBlockAlert::class, 2);
    }

    public function test_se_puede_desactivar_el_aviso(): void
    {
        app(Settings::class)->set(Settings::AVISOS_ACTIVOS, '0');

        $this->licencia();

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

        Mail::assertNothingSent();

        // Pero el bloqueo se registra igual: desactivar el correo no puede dejar de
        // registrar lo que pasa.
        $this->assertSame(1, ApiBlock::blocked()->count());
    }

    public function test_sin_destinatarios_no_se_pierde_el_aviso_en_silencio(): void
    {
        app(Settings::class)->set(Settings::AVISOS_EMAIL, '');

        $this->licencia();

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

        Mail::assertNothingSent();

        // Queda en el log del panel, que es lo que hace que salga en el Dashboard: un
        // aviso que no se puede enviar es un problema de configuración que hay que ver.
        $this->assertDatabaseHas('logs', ['code' => '16007']);

        // Y el bloqueo sigue pendiente de avisar, para que se reintente.
        $this->assertNull(ApiBlock::blocked()->firstOrFail()->notified_at);
    }

    /* ===================== El estado ===================== */

    public function test_el_bloqueo_guarda_de_quien_es_y_desde_cuando(): void
    {
        $licencia = $this->licencia();

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

        $bloqueo = ApiBlock::blocked()->firstOrFail();

        $this->assertSame(ApiBlock::SUBJECT_ENVIRONMENT, $bloqueo->subject_type);
        $this->assertSame(ApiBlock::REASON_TOKEN, $bloqueo->reason);
        $this->assertSame($licencia->id, $bloqueo->license_token_id);
        $this->assertSame(self::DOMAIN, $bloqueo->environment->domain);
        $this->assertNotNull($bloqueo->first_blocked_at);
        $this->assertNotNull($bloqueo->notified_at);
    }

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

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

        $bloqueo = ApiBlock::blocked()->firstOrFail();

        $this->assertSame(3, $bloqueo->hits);
        $this->assertSame(1, ApiBlock::count(), 'No debe crear una fila por rechazo.');
    }

    public function test_liberar_cierra_la_incidencia_y_deja_quien_fue(): void
    {
        $usuario = User::factory()->create();
        $this->licencia();

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

        $bloqueo = ApiBlock::blocked()->firstOrFail();
        $bloqueo->liberar($usuario->id, 'era un entorno de pruebas');

        $bloqueo->refresh();

        $this->assertSame(ApiBlock::STATE_CLEARED, $bloqueo->state);
        $this->assertSame($usuario->id, $bloqueo->cleared_by);
        $this->assertSame('era un entorno de pruebas', $bloqueo->cleared_note);
        $this->assertSame(0, ApiBlock::blocked()->count());
    }

    /* ===================== El resumen diario ===================== */

    public function test_el_resumen_se_envia_aunque_no_haya_bloqueos(): void
    {
        // Deliberado: un resumen que solo llega cuando hay problemas no permite
        // distinguir "todo bien" de "el resumen se ha roto".
        $this->artisan('api-blocks:digest --force')->assertSuccessful();

        Mail::assertSent(ApiBlocksDigest::class, function ($correo) {
            return $correo->hasTo(self::AVISADOS) && $correo->activos->isEmpty();
        });
    }

    public function test_el_resumen_lista_los_bloqueos_activos(): void
    {
        $this->licencia();
        $this->llamar();
        $this->llamar()->assertStatus(429);

        $this->artisan('api-blocks:digest --force')->assertSuccessful();

        Mail::assertSent(ApiBlocksDigest::class, fn ($correo) => $correo->activos->count() === 1);
    }

    public function test_el_resumen_respeta_la_hora_configurada(): void
    {
        // Corre cada hora y decide dentro si le toca, para que cambiar la hora en el
        // panel tenga efecto sin redesplegar.
        $ajustes = app(Settings::class);
        $ajustes->set(Settings::RESUMEN_HORA, (string) ((int) now()->addHours(3)->hour));

        $this->artisan('api-blocks:digest')->assertSuccessful();

        Mail::assertNothingSent();

        $ajustes->set(Settings::RESUMEN_HORA, (string) ((int) now()->hour));

        $this->artisan('api-blocks:digest')->assertSuccessful();

        Mail::assertSent(ApiBlocksDigest::class, 1);
    }

    public function test_el_resumen_se_puede_desactivar(): void
    {
        app(Settings::class)->set(Settings::RESUMEN_ACTIVO, '0');

        $this->artisan('api-blocks:digest')->assertSuccessful();

        Mail::assertNothingSent();
    }
}
