<?php

namespace Tests\Feature\ApiLogs;

use App\Livewire\ApiLogs\Index;
use App\Livewire\ApiLogs\LogDetailModal;
use App\Models\ApiRequestLog;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Services\Api\Severidad;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Marcar una petición como vista.
 *
 * Sin forma de decir «esto ya lo he mirado», la lista de errores no se puede ir vaciando,
 * y una lista que no se vacía se deja de mirar. Es el mismo razonamiento de
 * `norma-cero-errores.md` aplicado al trabajo de revisar, no al de contar.
 *
 * **Se marca la petición, no el tipo de error.** Fue una decisión de producto explícita, y
 * tiene una consecuencia que estos tests dejan escrita: el mismo error mañana es una fila
 * nueva sin marcar, así que revisar no reduce el problema del día siguiente.
 */
class MarcarComoVistoTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(array $permisos = ['admin.api-logs.index']): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDeRevision', 'guard_name' => 'web']);

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

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

        Cache::flush();

        return $user;
    }

    private function entorno(): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'revision'],
            ['name' => 'Cliente de la revisión', 'actived' => true]
        );

        return Environment::create([
            'name' => 'Campus a revisar',
            'domain' => 'https://sitio-' . (Environment::withTrashed()->count() + 1) . '.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ]);
    }

    /** Un error de verdad: la API no reconoce el dominio desde el que llama. */
    private function error(Environment $entorno, array $atributos = []): ApiRequestLog
    {
        return ApiRequestLog::create(array_merge([
            'request_uuid' => (string) Str::uuid(),
            'method' => 'POST',
            'path' => '/api/v1',
            'auth_type' => 'token',
            'environment_id' => $entorno->id,
            'client_id' => $entorno->client_id,
            'host' => $entorno->domain,
            'action' => 'licence',
            'plugin' => 'block_tresipuntsepe',
            'http_status' => 401,
            'error_code' => 2002,
            'error' => 'Environment not found for this host',
            'duration_ms' => 34,
            'started_at' => now()->subHour(),
            'ended_at' => now()->subHour(),
        ], $atributos));
    }

    public function test_marcar_deja_quien_y_cuando(): void
    {
        // El motivo de que existan las dos columnas: dentro de un mes, «esto ya se miró»
        // no vale sin saber quién lo dijo y cuándo.
        $usuario = $this->usuario();
        $peticion = $this->error($this->entorno());

        Livewire::test(Index::class)->call('alternarRevision', $peticion->id);

        $fresca = $peticion->fresh();

        $this->assertNotNull($fresca->reviewed_at);
        $this->assertSame($usuario->id, $fresca->reviewed_by);
        $this->assertTrue($fresca->estaRevisada());
    }

    public function test_es_un_interruptor_y_se_puede_desmarcar(): void
    {
        // Un «visto» que no se puede quitar convierte un clic de más en un dato falso para
        // siempre.
        $this->usuario();
        $peticion = $this->error($this->entorno());

        $componente = Livewire::test(Index::class);

        $componente->call('alternarRevision', $peticion->id);
        $this->assertTrue($peticion->fresh()->estaRevisada());

        $componente->call('alternarRevision', $peticion->id);
        $this->assertFalse($peticion->fresh()->estaRevisada());
        $this->assertNull($peticion->fresh()->reviewed_by);
    }

    public function test_sin_el_permiso_del_visor_no_se_puede_marcar(): void
    {
        // **Lo que se prueba aquí es la segunda capa**, la de MGR-005: el `authorize()` del
        // propio método, que es la que aguanta un POST a `/livewire/update` cuando el
        // `permission:` de la ruta no se reaplica.
        //
        // Por eso se monta CON permiso y se le quita después: montando sin él, el
        // `authorize()` del `mount()` corta antes y no se llega a probar el del método
        // —que es justo el que protege el caso real—.
        $usuario = $this->usuario();
        $peticion = $this->error($this->entorno());

        $componente = Livewire::test(Index::class)->assertOk();

        $usuario->syncRoles([]);
        app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();

        $componente->call('alternarRevision', $peticion->id)->assertForbidden();

        $this->assertFalse($peticion->fresh()->estaRevisada());
    }

    public function test_el_indicador_baja_al_marcar_y_el_total_no(): void
    {
        // **Los dos números.** El grande es lo que queda por mirar; el total sigue ahí
        // porque marcar no arregla: sigue habiendo dos errores.
        $this->usuario();
        $entorno = $this->entorno();
        $uno = $this->error($entorno);
        $this->error($entorno, ['started_at' => now()->subMinutes(30)]);

        $componente = Livewire::test(Index::class);

        $this->assertSame(2, $componente->viewData('errores'));
        $this->assertSame(2, $componente->viewData('erroresSinRevisar'));

        $componente->call('alternarRevision', $uno->id);

        $this->assertSame(2, $componente->viewData('errores'), 'sigue habiendo dos errores');
        $this->assertSame(1, $componente->viewData('erroresSinRevisar'), 'pero uno ya está visto');
    }

    public function test_marcar_invalida_la_cache_de_indicadores(): void
    {
        // Los indicadores se cachean cinco minutos: sin olvidar la clave, marcar como
        // visto no cambiaría el número hasta cinco minutos después. Es la lección de
        // MGR-073, y aquí se nota más porque el usuario acaba de pulsar.
        $this->usuario();
        $entorno = $this->entorno();
        $peticion = $this->error($entorno);

        $componente = Livewire::test(Index::class);
        $this->assertSame(1, $componente->viewData('erroresSinRevisar'));

        $componente->call('alternarRevision', $peticion->id);

        $this->assertSame(0, $componente->viewData('erroresSinRevisar'),
            'el indicador tiene que responder al clic, no cinco minutos después');
    }

    public function test_el_filtro_sin_revisar_saca_de_la_lista_lo_ya_visto(): void
    {
        // Es la otra mitad de marcar: que la lista se pueda vaciar. **Y ahora viene puesto
        // de entrada**, que es lo que hace que marcar cambie algo de lo que se tiene
        // delante: con el defecto anterior —se ven todas— revisar una petición no la
        // quitaba de en medio y la lista no se podía vaciar nunca.
        $this->usuario();
        $entorno = $this->entorno();
        $visto = $this->error($entorno);
        $this->error($entorno, ['started_at' => now()->subMinutes(30)]);

        $componente = Livewire::test(Index::class)->call('alternarRevision', $visto->id);

        $this->assertCount(1, $componente->viewData('logs'),
            'la vista ya no sale: el filtro viene puesto');
        $this->assertSame(1, $componente->viewData('vistosOcultos'),
            'y se dice cuántas está escondiendo, para que no sea un recorte invisible');

        // Y quitándolo se vuelven a ver las dos.
        $componente->set('onlyUnreviewed', false);

        $this->assertCount(2, $componente->viewData('logs'));
        $this->assertSame(0, $componente->viewData('vistosOcultos'));
    }

    public function test_el_filtro_vive_en_la_url(): void
    {
        // Como los otros dieciséis: una investigación se pasa a un compañero con un enlace.
        $reflexion = new \ReflectionProperty(Index::class, 'queryString');
        $reflexion->setAccessible(true);

        $queryString = $reflexion->getValue(new Index);

        // **Como clave y no como valor**: la entrada pasó a llevar `['except' => true]`
        // cuando el filtro empezó a venir puesto de entrada, para que la pantalla recién
        // abierta no arrastre `onlyUnreviewed=1` en cada enlace que se comparta.
        $this->assertArrayHasKey('onlyUnreviewed', $queryString);
        $this->assertSame(['except' => true], $queryString['onlyUnreviewed']);
    }

    public function test_la_modal_ofrece_marcar_y_luego_dice_quien_lo_vio(): void
    {
        $usuario = $this->usuario();
        $peticion = $this->error($this->entorno());

        Livewire::test(LogDetailModal::class, ['logId' => $peticion->id])
            ->assertOk()
            ->assertSee('Marcar como visto')
            ->call('alternarRevision')
            ->assertSee('Visto el')
            ->assertSee($usuario->name);

        $this->assertTrue($peticion->fresh()->estaRevisada());
    }

    public function test_una_peticion_correcta_no_se_puede_marcar_porque_no_hay_nada_que_revisar(): void
    {
        // El botón sería ruido en el 88 % de las filas, que son correctas.
        $this->usuario();
        $entorno = $this->entorno();

        $ok = $this->error($entorno, [
            'http_status' => 200,
            'error_code' => 0,
            'error' => null,
        ]);

        $this->assertSame(Severidad::OK, $ok->severity);

        Livewire::test(LogDetailModal::class, ['logId' => $ok->id])
            ->assertOk()
            ->assertDontSee('Marcar como visto');
    }
}
