<?php

namespace Tests\Feature\Monitoring;

use App\Livewire\Monitoring\Notices;
use App\Models\Auth\User;
use App\Models\Monitoring\ApiBlock;
use App\Models\Monitoring\Notice;
use App\Services\System\BlockNotifier;
use App\Services\System\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * El aviso de un corte tiene que salir en la pantalla de Avisos.
 *
 * **Las dos pantallas no decían lo mismo, y así se reportó.** En Acciones del panel aparecía
 * «Aviso de límite de la API enviado» y en Avisos no había nada de eso: `BlockNotifier`
 * mandaba el correo y dejaba rastro en el log, pero **no escribía ninguna fila en
 * `notices`**, que es la tabla que lee esa pantalla.
 *
 * No era solo una pantalla incompleta:
 *
 * - Su propio subtítulo promete «correos automáticos de caducidad **y bloqueo**».
 * - La pregunta para la que existe —«¿se avisó de esto?»— no se podía contestar **del aviso
 *   que más corre**, que es el de un corte.
 * - Y el punto de la pestaña mira si se ha entregado algo últimamente, así que un sistema que
 *   solo mandaba avisos de bloqueo se veía como uno que no manda nada.
 */
class AvisoDeBloqueoTest extends TestCase
{
    use RefreshDatabase;

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

        Mail::fake();

        // El aviso viene encendido de fábrica, pero hace falta destinatario: sin él no hay
        // aviso posible y el servicio se sale antes (queda en el log, no aquí).
        app(Settings::class)->set(Settings::AVISOS_EMAIL, 'sistemas@tresipunt.com');
    }

    private function bloqueo(array $atributos = []): ApiBlock
    {
        return ApiBlock::create(array_merge([
            'subject_type' => ApiBlock::SUBJECT_IP,
            'ip' => '18.201.244.212',
            'reason' => ApiBlock::REASON_FAILURES,
            'state' => ApiBlock::STATE_BLOCKED,
            'enforced' => false,
            'first_blocked_at' => now(),
            'last_blocked_at' => now(),
            'hits' => 10,
            'limit_value' => 20,
        ], $atributos));
    }

    public function test_avisar_de_un_corte_deja_su_fila_en_avisos(): void
    {
        $bloqueo = $this->bloqueo();

        app(BlockNotifier::class)->avisar($bloqueo);

        $aviso = Notice::where('kind', Notice::KIND_BLOCK_ALERT)->first();

        $this->assertNotNull($aviso, 'el aviso de bloqueo no ha dejado fila en notices');
        $this->assertSame(Notice::SUBJECT_API_BLOCK, $aviso->subject_type);
        $this->assertSame($bloqueo->id, (int) $aviso->subject_id);
        $this->assertNotNull($aviso->sent_at, 'se ha registrado como no enviado');
        $this->assertStringContainsString('sistemas@tresipunt.com', (string) $aviso->sent_to);
    }

    public function test_el_aviso_guarda_a_quien_y_si_cortaba(): void
    {
        // **El sujeto y el modo van en el `payload`** porque el episodio se puede borrar con
        // la retención y el aviso tiene que seguir diciendo a quién se avisó. Y si cortaba o
        // solo observaba es lo primero que se pregunta al leerlo.
        app(BlockNotifier::class)->avisar($this->bloqueo(['enforced' => true, 'hits' => 437]));

        $aviso = Notice::where('kind', Notice::KIND_BLOCK_ALERT)->first();

        $this->assertStringContainsString('18.201.244.212', $aviso->payload['sujeto']);
        $this->assertTrue($aviso->payload['corta']);
        $this->assertSame(437, $aviso->payload['rechazos']);
        // El límite cruzado va en `threshold`: es lo que distingue dos avisos del mismo
        // sujeto por motivos distintos.
        $this->assertSame(20, (int) $aviso->threshold);
    }

    public function test_un_corte_avisa_una_sola_vez(): void
    {
        // Misma regla que `notified_at`, dicha en la tabla que se mira: el caso real fueron
        // 437 rechazos en 17 minutos, y un aviso por rechazo es un buzón que se filtra.
        $bloqueo = $this->bloqueo();

        app(BlockNotifier::class)->avisar($bloqueo);
        app(BlockNotifier::class)->avisar($bloqueo->fresh());

        $this->assertSame(1, Notice::where('kind', Notice::KIND_BLOCK_ALERT)->count());
    }

    public function test_si_el_correo_falla_el_aviso_queda_como_fallido(): void
    {
        // **Es la mitad del sentido de esa pantalla**: «los fallidos primero, un aviso que no
        // salió es un cliente sin avisar». Sin esta fila, un SMTP caído no se ve en ninguna
        // parte hasta que alguien echa de menos un correo.
        Mail::shouldReceive('to')->andThrow(new \RuntimeException('SMTP caído'));

        $bloqueo = $this->bloqueo();

        app(BlockNotifier::class)->avisar($bloqueo);

        $aviso = Notice::where('kind', Notice::KIND_BLOCK_ALERT)->first();

        $this->assertNotNull($aviso);
        $this->assertNull($aviso->sent_at, 'un aviso que falló no puede contar como enviado');
        $this->assertStringContainsString('SMTP caído', (string) $aviso->error);

        // Y el episodio sigue pendiente de avisar, para que el siguiente rechazo lo reintente.
        $this->assertNull($bloqueo->fresh()->notified_at);
    }

    public function test_la_pantalla_lo_ensena_con_su_sujeto_y_su_modo(): void
    {
        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);
        $rol->givePermissionTo(Permission::firstOrCreate(
            ['name' => 'admin.api-logs.index', 'guard_name' => 'web'],
            ['model' => 'monitoring', 'description' => 'ver']
        ));

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

        app(BlockNotifier::class)->avisar($this->bloqueo(['enforced' => false]));

        Livewire::test(Notices::class)
            ->assertOk()
            ->assertSee('Bloqueo de la API')
            ->assertSee('18.201.244.212')
            // Y no como «api_block #1», que es lo que salía sin resolver el sujeto.
            ->assertDontSee('api_block #')
            ->assertSee('En observación');
    }

    public function test_el_filtro_de_tipos_ofrece_el_de_bloqueo(): void
    {
        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);
        $rol->givePermissionTo(Permission::firstOrCreate(
            ['name' => 'admin.api-logs.index', 'guard_name' => 'web'],
            ['model' => 'monitoring', 'description' => 'ver']
        ));

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

        $tipos = Livewire::test(Notices::class)->viewData('tipos');

        $this->assertArrayHasKey(Notice::KIND_BLOCK_ALERT, $tipos);
    }
}
