<?php

namespace Tests\Feature\ApiLogs;

use App\Livewire\ApiLogs\Index;
use App\Livewire\ApiLogs\LimpiezaDelHistoricoModal as Limpieza;
use App\Models\ApiRequestLog;
use App\Models\Auth\User;
use App\Services\System\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * Borrado del log de la API: lo que se borra, quién puede y qué queda escrito.
 *
 * Esto borra rastro de auditoría, así que hay tres cosas que fijar aquí y que se
 * rompen solas en cuanto alguien toca el filtro del visor:
 *
 * 1. **Que borre lo que dice que borra.** El visor tiene su propio filtro de fechas, y
 *    los métodos de purga comparten la consulta con él: un despiste y «borrar del 1 al
 *    3 de agosto» borra lo que hubiera en pantalla.
 * 2. **Que no lo haga cualquiera.** El botón está detrás de un `@can`, pero el método
 *    viaja por `/livewire/update`, donde el permiso de la ruta no se reaplica (MGR-005).
 * 3. **Que quede rastro.** Un borrado de logs que no se registra es exactamente lo que
 *    haría alguien tapando huellas.
 */
class PurgeTest extends TestCase
{
    use RefreshDatabase;

    private const VER = 'admin.api-logs.index';
    private const BORRAR = 'admin.api-logs.destroy';

    private function usuario(array $permisos): User
    {
        foreach ($permisos as $nombre) {
            Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'api-logs', 'description' => $nombre]
            );
        }

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

        return $user;
    }

    private function log(string $cuando): ApiRequestLog
    {
        return ApiRequestLog::create([
            'request_uuid' => (string) Str::uuid(),
            'method' => 'POST',
            'path' => 'api/v1',
            'ip' => '203.0.113.7',
            'auth_type' => 'bearer',
            'http_status' => 200,
            'action' => 'products',
            'host' => 'https://sitio.test',
            'started_at' => $cuando,
            'ended_at' => $cuando,
            'duration_ms' => 20,
        ]);
    }

    /* ============ Lo que la modal dice antes de borrar ============ */

    public function test_un_atajo_sin_nada_que_borrar_sale_apagado_y_dice_por_que(): void
    {
        // **El fallo que se arregló aquí**: con la petición más antigua de hace diez días,
        // pulsar «Más antiguos de 1 mes» no hacía absolutamente nada. Ahora el botón sale
        // apagado y la modal dice cuál es la más antigua, que es el dato que lo explica
        // sin tener que pulsar.
        $this->usuario([self::VER, self::BORRAR]);
        $this->log(now()->subDays(10)->toDateTimeString());

        Livewire::test(Limpieza::class)
            ->assertOk()
            ->assertSee('La petición más antigua que queda es del')
            ->assertSee(now()->subDays(10)->format('d/m/Y'))
            ->assertSee('No hay ninguna petición tan antigua');
    }

    public function test_elegir_un_criterio_ensena_cuanto_se_borraria_antes_de_borrar(): void
    {
        // **El orden importa y es la razón de la modal.** Antes cada botón abría
        // directamente el «¿seguro?», así que elegir el criterio y decidir el borrado iban
        // en el mismo clic. Aquí se elige, sale el número, y el botón rojo viene después.
        $this->usuario([self::VER, self::BORRAR]);
        $this->log(now()->subDays(200)->toDateTimeString());
        $this->log(now()->subDays(150)->toDateTimeString());

        Livewire::test(Limpieza::class)
            ->call('elegirAntiguedad', 30)
            ->assertSet('criterio', 'antiguedad')
            ->assertSee('Se van a borrar 2 petición(es)')
            ->assertSee('todo lo anterior a 30 días')
            // Y nada se ha borrado todavía.
            ->assertNotDispatched('apiLogsLimpiar');

        $this->assertSame(2, ApiRequestLog::count());
    }

    public function test_un_criterio_que_no_coge_nada_lo_dice_y_no_ofrece_borrar(): void
    {
        // Vacío es una respuesta, no un error: un botón que no hace nada deja pensando si
        // ha fallado algo.
        $this->usuario([self::VER, self::BORRAR]);
        $this->log(now()->subDays(10)->toDateTimeString());

        Livewire::test(Limpieza::class)
            ->call('elegirAntiguedad', 365)
            ->assertSee('no se borra nada')
            ->assertDontSee('Borrar 0 petición(es)');
    }

    public function test_el_numero_que_ensena_la_modal_es_el_que_se_borra(): void
    {
        // **La razón de que la consulta viva en `ApiLogPurger::consultaDe()`.** La modal
        // cuenta y el visor borra, en dos componentes distintos: si cada uno armara su
        // propia consulta, el día que una cambiara el aviso diría un número y el borrado
        // se llevaría otro — y esto no tiene vuelta atrás.
        $this->usuario([self::VER, self::BORRAR]);

        for ($i = 1; $i <= 3; $i++) {
            $this->log(now()->subDays(100 + $i)->toDateTimeString());
        }
        $this->log(now()->subDays(5)->toDateTimeString());

        $anunciado = Livewire::test(Limpieza::class)
            ->call('elegirAntiguedad', 30)
            ->instance()
            ->impacto()['total'];

        Livewire::test(Index::class)
            ->call('limpiar', ['criterio' => 'antiguedad', 'dias' => 30])
            ->assertSee($anunciado . ' registro(s) borrados');

        $this->assertSame(3, $anunciado);
        $this->assertSame(1, ApiRequestLog::count());
    }

    public function test_la_modal_manda_la_orden_y_se_cierra(): void
    {
        // La modal no borra: manda la orden al visor, que es quien tiene los filtros y
        // quien ya deja el rastro. Ver el docblock de `LimpiezaDelHistoricoModal`.
        $this->usuario([self::VER, self::BORRAR]);
        $this->log(now()->subDays(200)->toDateTimeString());

        Livewire::test(Limpieza::class)
            ->call('elegirAntiguedad', 90)
            ->call('borrar')
            ->assertDispatched('apiLogsLimpiar')
            ->assertDispatched('closeModal');

        // Y no ha borrado ella.
        $this->assertSame(1, ApiRequestLog::count());
    }

    public function test_al_borrar_la_pantalla_dice_cuantos(): void
    {
        // **Se borraban las filas y la pantalla no decía nada**: el `success` de `purge()`
        // no se pintaba. Borrar cuarenta mil registros del log en silencio es lo contrario
        // de dejar rastro.
        $this->usuario([self::VER, self::BORRAR]);
        $this->log(now()->subDays(200)->toDateTimeString());
        $this->log(now()->subDays(150)->toDateTimeString());

        Livewire::test(Index::class)
            ->call('limpiar', ['criterio' => 'antiguedad', 'dias' => 30])
            ->assertSee('2 registro(s) borrados');

        $this->assertSame(0, ApiRequestLog::count());
    }

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

    public function test_quien_solo_puede_mirar_no_puede_borrar(): void
    {
        $this->usuario([self::VER]);
        $this->log(now()->subYears(2)->toDateTimeString());

        // Ni abrir el formulario…
        Livewire::test(Index::class)
            ->call('abrirLimpieza')
            ->assertForbidden();

        // …ni mandar la orden a mano. El evento viaja por el navegador, así que se puede
        // provocar sin pasar por la modal (MGR-005).
        Livewire::test(Index::class)
            ->call('limpiar', ['criterio' => 'antiguedad', 'dias' => 30])
            ->assertForbidden();

        Livewire::test(Index::class)
            ->call('purge')
            ->assertForbidden();

        $this->assertSame(1, ApiRequestLog::count());
    }

    public function test_la_modal_de_limpieza_no_se_monta_sin_permiso(): void
    {
        $this->usuario([self::VER]);

        // Livewire convierte el `authorize()` del `mount()` en una respuesta 403, no en
        // una excepción: se comprueba el estado.
        Livewire::test(Limpieza::class)->assertForbidden();
    }

    /* ===================== Por antigüedad ===================== */

    public function test_el_atajo_por_antiguedad_ignora_el_filtro_del_visor(): void
    {
        // El visor viene con «últimos 30 días» puesto. Si la purga usara ese filtro,
        // «borrar los anteriores a 30 días» no borraría nada: justo lo contrario.
        $this->usuario([self::VER, self::BORRAR]);

        $viejo = $this->log(now()->subDays(90)->toDateTimeString());
        $reciente = $this->log(now()->subDays(2)->toDateTimeString());

        Livewire::test(Index::class)
            ->call('limpiar', ['criterio' => 'antiguedad', 'dias' => 30]);

        $this->assertNull($viejo->fresh());
        $this->assertNotNull($reciente->fresh());
    }

    /* ===================== Rango manual ===================== */

    public function test_el_rango_manual_incluye_los_dos_dias_extremos(): void
    {
        // El fallo natural aquí es comparar contra las 00:00 del día «hasta», con lo que
        // ese día no se borra y el rango de un solo día no borra nada.
        $this->usuario([self::VER, self::BORRAR]);

        $antes = $this->log(now()->subDays(20)->startOfDay()->subSecond()->toDateTimeString());
        $primerDia = $this->log(now()->subDays(20)->setTime(0, 5)->toDateTimeString());
        $ultimoDia = $this->log(now()->subDays(18)->setTime(23, 55)->toDateTimeString());
        $despues = $this->log(now()->subDays(17)->startOfDay()->toDateTimeString());

        Livewire::test(Index::class)->call('limpiar', [
            'criterio' => 'rango',
            'desde' => now()->subDays(20)->format('Y-m-d'),
            'hasta' => now()->subDays(18)->format('Y-m-d'),
        ]);

        $this->assertNull($primerDia->fresh());
        $this->assertNull($ultimoDia->fresh());
        $this->assertNotNull($antes->fresh());
        $this->assertNotNull($despues->fresh());
    }

    public function test_el_rango_al_reves_no_se_acepta(): void
    {
        $this->usuario([self::VER, self::BORRAR]);
        $log = $this->log(now()->subDays(10)->toDateTimeString());

        Livewire::test(Limpieza::class)
            ->set('desde', now()->subDays(5)->format('Y-m-d'))
            ->set('hasta', now()->subDays(20)->format('Y-m-d'))
            ->call('elegirRango')
            ->assertHasErrors(['hasta'])
            // Y no se pasa al paso de borrar con un rango que no vale.
            ->assertSet('criterio', '');

        $this->assertNotNull($log->fresh());
    }

    public function test_elegir_un_criterio_borra_el_anterior(): void
    {
        // **Los criterios son excluyentes y antes eran tres propiedades sueltas**: cada
        // acción tenía que acordarse de anular las otras dos, y olvidarse de una era
        // borrar otra cosa. Ahora la orden entra entera.
        $this->usuario([self::VER, self::BORRAR]);

        $viejo = $this->log(now()->subDays(300)->toDateTimeString());
        $delRango = $this->log(now()->subDays(10)->toDateTimeString());

        $orden = Livewire::test(Limpieza::class)
            ->call('elegirAntiguedad', 30)
            ->set('desde', now()->subDays(11)->format('Y-m-d'))
            ->set('hasta', now()->subDays(9)->format('Y-m-d'))
            ->call('elegirRango')
            ->assertSet('criterio', 'rango')
            ->assertSet('dias', null)
            ->instance();

        Livewire::test(Index::class)->call('limpiar', [
            'criterio' => 'rango',
            'desde' => $orden->desde,
            'hasta' => $orden->hasta,
        ]);

        $this->assertNull($delRango->fresh());
        $this->assertNotNull($viejo->fresh());
    }

    /* ===================== Rastro ===================== */

    public function test_el_borrado_queda_registrado_con_el_criterio(): void
    {
        $this->usuario([self::VER, self::BORRAR]);
        $this->log(now()->subDays(400)->toDateTimeString());

        Livewire::test(Index::class)
            ->call('limpiar', ['criterio' => 'antiguedad', 'dias' => 365]);

        $this->assertDatabaseHas('logs', ['code' => '16002']);
        $this->assertStringContainsString(
            'anteriores a 365 días',
            \App\Models\Monitoring\Log::where('code', '16002')->value('msg')
        );
    }
    /* ===================== Retención automática ===================== */

    public function test_sin_retencion_configurada_la_tarea_no_borra_nada(): void
    {
        // Es el estado con el que se despliega: la tarea corre cada noche y no toca nada
        // hasta que alguien decide cuánto histórico quiere conservar.
        $log = $this->log(now()->subYears(3)->toDateTimeString());

        $this->artisan('api-logs:purge --retention')
            ->expectsOutputToContain('No hay retención configurada')
            ->assertSuccessful();

        $this->assertNotNull($log->fresh());
    }

    public function test_con_retencion_configurada_la_tarea_borra_lo_anterior(): void
    {
        app(Settings::class)->set(Settings::LOGS_RETENCION, '30');

        $viejo = $this->log(now()->subDays(60)->toDateTimeString());
        $reciente = $this->log(now()->subDays(5)->toDateTimeString());

        $this->artisan('api-logs:purge --retention')->assertSuccessful();

        $this->assertNull($viejo->fresh());
        $this->assertNotNull($reciente->fresh());
        $this->assertStringContainsString(
            'por retención automática',
            \App\Models\Monitoring\Log::where('code', '16002')->value('msg')
        );
    }

    public function test_la_retencion_no_se_combina_con_los_otros_criterios(): void
    {
        $this->artisan('api-logs:purge --retention --days=10')->assertFailed();
    }

    public function test_el_panel_guarda_la_retencion_y_deja_rastro(): void
    {
        $this->usuario([self::VER, 'admin.configs.edit']);

        Livewire::test(\App\Livewire\Configurations\Manager\SettingModal::class, ['clave' => Settings::LOGS_RETENCION])
            ->set('valor', '120')
            ->call('guardar')
            ->assertHasNoErrors();

        $this->assertSame(120, app(Settings::class)->diasDeRetencionDeLogs());

        // Queda registrado qué ajuste se cambió, no solo que «se guardó algo».
        $this->assertDatabaseHas('logs', ['code' => '16014', 'entity_id' => Settings::LOGS_RETENCION]);
    }

    public function test_sin_permiso_no_se_cambia_la_retencion(): void
    {
        $this->usuario([self::VER]);

        // MGR-005: ni desde la pantalla ni montando la modal a mano.
        Livewire::test(\App\Livewire\Configurations\Manager\Index::class)
            ->call('editar', Settings::LOGS_RETENCION)
            ->assertForbidden();

        Livewire::test(\App\Livewire\Configurations\Manager\SettingModal::class, ['clave' => Settings::LOGS_RETENCION])
            ->assertForbidden();

        $this->assertSame(0, app(Settings::class)->diasDeRetencionDeLogs());
    }
    /* ===================== Limpieza inmediata desde la configuración ===================== */

    public function test_la_limpieza_a_mano_aplica_la_retencion_guardada(): void
    {
        // Es lo que hacía falta: guardar 30 días y no tener que esperar a las 03:30 —ni
        // depender de que el planificador esté puesto en el cron— para ver el efecto.
        $this->usuario([self::VER, self::BORRAR, 'admin.configs.edit']);
        app(Settings::class)->set(Settings::LOGS_RETENCION, '30');

        $viejo = $this->log(now()->subDays(90)->toDateTimeString());
        $reciente = $this->log(now()->subDays(3)->toDateTimeString());

        Livewire::test(\App\Livewire\Configurations\Manager\Index::class)
            ->call('pedirLimpiezaAhora')
            ->dispatch('logsPurgeNowConfirmed', ['itemId' => 30, 'itemName' => '1 registro(s)']);

        $this->assertNull($viejo->fresh());
        $this->assertNotNull($reciente->fresh());

        // Y queda claro en el log que fue a mano, no la tarea de la noche.
        $this->assertStringContainsString(
            'a mano',
            \App\Models\Monitoring\Log::where('code', '16002')->value('msg')
        );
    }

    public function test_sin_retencion_configurada_no_borra_nada_a_mano(): void
    {
        // Sin retención no hay criterio, y "borra ahora" no puede significar "borra todo".
        $this->usuario([self::VER, self::BORRAR, 'admin.configs.edit']);

        $log = $this->log(now()->subYears(2)->toDateTimeString());

        Livewire::test(\App\Livewire\Configurations\Manager\Index::class)
            ->call('pedirLimpiezaAhora')
            ->dispatch('logsPurgeNowConfirmed', ['itemId' => 30, 'itemName' => '1 registro(s)']);

        $this->assertNotNull($log->fresh());
        $this->assertDatabaseMissing('logs', ['code' => '16002']);
    }

    public function test_configurar_la_retencion_no_da_derecho_a_borrar_ahora(): void
    {
        // Decidir cuánto histórico se guarda y borrarlo de golpe no son la misma cosa:
        // esto segundo exige `admin.api-logs.destroy`.
        $this->usuario([self::VER, 'admin.configs.edit']);
        app(Settings::class)->set(Settings::LOGS_RETENCION, '30');

        $viejo = $this->log(now()->subDays(90)->toDateTimeString());

        Livewire::test(\App\Livewire\Configurations\Manager\Index::class)
            ->call('pedirLimpiezaAhora')
            ->assertForbidden();

        Livewire::test(\App\Livewire\Configurations\Manager\Index::class)
            ->dispatch('logsPurgeNowConfirmed', ['itemId' => 30, 'itemName' => '1 registro(s)'])
            ->assertForbidden();

        $this->assertNotNull($viejo->fresh());
    }

    public function test_la_limpieza_a_mano_no_toca_lo_que_esta_dentro_de_la_retencion(): void
    {
        // El borde exacto: el día que cumple la retención se queda.
        $this->usuario([self::VER, self::BORRAR, 'admin.configs.edit']);
        app(Settings::class)->set(Settings::LOGS_RETENCION, '30');

        $justoDentro = $this->log(now()->subDays(30)->addMinutes(5)->toDateTimeString());
        $justoFuera = $this->log(now()->subDays(30)->subMinutes(5)->toDateTimeString());

        Livewire::test(\App\Livewire\Configurations\Manager\Index::class)
            ->call('pedirLimpiezaAhora')
            ->dispatch('logsPurgeNowConfirmed', ['itemId' => 30, 'itemName' => '1 registro(s)']);

        $this->assertNotNull($justoDentro->fresh());
        $this->assertNull($justoFuera->fresh());
    }
}
