<?php

namespace Tests\Feature\System;

use App\Mail\ApiBlockAlert;
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;

/**
 * Modo observación: los límites miden pero no cortan.
 *
 * **Por qué es el valor de fábrica.** Los umbrales actuales —20 fallos y 600 peticiones
 * por minuto por IP— se eligieron a partir de **un único incidente** de producción, y el
 * tráfico real de todos los entornos no se conoce hasta verlo. Cortar el primer día con
 * un número puesto a ojo significa dejar sin servicio a un cliente legítimo, y eso es
 * peor que no tener límite.
 *
 * Así que se despliega midiendo: los contadores cuentan, las incidencias se registran,
 * los avisos se envían diciendo que **no se ha cortado nada**, y con semanas de datos
 * delante se ajustan los topes y se enciende el corte desde el panel.
 */
class ObservationModeTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN = '3IP-TEST-OBSERVA-01';
    private const DOMAIN = 'https://observa.test';

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

        Mail::fake();
        config(['api.throttle.enabled' => false]);
        RateLimiter::clear('rate_limit:token:' . self::TOKEN);
        Cache::flush();

        app(Settings::class)->set(Settings::AVISOS_EMAIL, 'sistemas@tresipunt.com');
    }

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

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

        Environment::create([
            'name' => 'Entorno Observa',
            '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',
            ],
        ]);
    }

    public function test_de_fabrica_los_limites_no_cortan(): void
    {
        // El test que más importa de este fichero: nadie ha tocado nada y el valor por
        // defecto es el seguro.
        $this->assertFalse(app(Settings::class)->losLimitesCortan());
    }

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

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

        // Se ha pasado del tope y **sigue respondiendo 200**: el sitio del cliente
        // funciona con normalidad.
        $this->llamar()->assertOk();
        $this->llamar()->assertOk();
    }

    public function test_pero_queda_registrado_que_se_habria_bloqueado(): void
    {
        // Es el sentido del modo: sin registro no hay datos con los que ajustar.
        $this->licencia(1);

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

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

        $this->assertFalse($bloqueo->enforced, 'Debe quedar marcado como NO cortado.');
        $this->assertSame('Se habría bloqueado', $bloqueo->estado_texto);
        $this->assertSame(1, $bloqueo->limit_value);
    }

    public function test_y_se_avisa_igual_por_correo(): void
    {
        // Avisar es lo que permite enterarse de que un tope está mal puesto ANTES de
        // activarlo y cortarle el servicio a alguien.
        $this->licencia(1);

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

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

    public function test_el_correo_dice_claramente_que_no_se_ha_cortado(): void
    {
        // Sin esto, el aviso haría salir corriendo a arreglar algo que funciona.
        $this->licencia(1);

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

        Mail::assertSent(ApiBlockAlert::class, function (ApiBlockAlert $correo) {
            $html = $correo->render();

            return str_contains($html, 'modo observación')
                && str_contains($html, 'no se ha cortado nada');
        });
    }

    public function test_al_activar_el_corte_si_corta(): void
    {
        app(Settings::class)->set(Settings::LIMITES_CORTAN, '1');

        $this->licencia(1);

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

        $this->assertTrue(ApiBlock::blocked()->firstOrFail()->enforced);
    }

    public function test_el_correo_del_corte_real_no_habla_de_observacion(): void
    {
        app(Settings::class)->set(Settings::LIMITES_CORTAN, '1');

        $this->licencia(1);

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

        Mail::assertSent(ApiBlockAlert::class, function (ApiBlockAlert $correo) {
            $html = $correo->render();

            return !str_contains($html, 'modo observación')
                && str_contains($html, 'ha dejado de recibir licencias');
        });
    }
}
