<?php

namespace Tests\Feature\Deploy;

use App\Livewire\Environments\Components\EnvironmentCard;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * Qué pasa si cambia la `APP_KEY`.
 *
 * **No es hipotético.** El pipeline de despliegue de pre ejecuta
 * `php artisan key:generate` en cada pasada, después de copiar el `.env`. Si esa clave se
 * regenera, todo lo cifrado con la anterior queda **ilegible para siempre**.
 *
 * Lo único cifrado hoy es `environments.moodletoken` (MGR-038), el token de servicios web
 * del Moodle de cada cliente. Y se lee en el render de **cada tarjeta** del listado de
 * entornos, así que con el cast `encrypted` de Laravel una rotación de clave no daría un
 * fallo discreto: daría un **500 en la pantalla principal del panel**, sin ninguna pista
 * de que la causa está en el despliegue.
 *
 * Estos tests fijan la degradación elegida: **el valor se pierde —no hay forma de
 * recuperarlo— pero la aplicación sigue en pie** y el entorno se comporta como uno que
 * nunca dio su token, que es exactamente lo que es a partir de ese momento.
 *
 * Ver `deploy-y-cicd.md`.
 */
class KeyRotationTest extends TestCase
{
    use RefreshDatabase;

    private function entorno(): Environment
    {
        $cliente = Client::create([
            'name' => 'Cliente Clave',
            'shortname' => 'clave' . uniqid(),
            'actived' => true,
        ]);

        return Environment::create([
            'client_id' => $cliente->id,
            'name' => 'Sitio con token',
            'domain' => 'https://' . uniqid() . '.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'moodletoken' => 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
        ]);
    }

    /**
     * Simula la rotación: se deja en la columna un valor cifrado con OTRA clave.
     *
     * Es más fiel que cambiar `APP_KEY` en caliente —el encriptador ya está resuelto en
     * el contenedor— y prueba exactamente lo mismo: un valor que este encriptador no
     * puede descifrar.
     */
    private function romperElCifrado(Environment $entorno): void
    {
        DB::table('environments')
            ->where('id', $entorno->id)
            ->update(['moodletoken' => 'eyJpdiI6InJvdG8iLCJ2YWx1ZSI6InJvdG8iLCJtYWMiOiJyb3RvIn0=']);
    }

    public function test_un_token_ilegible_no_lanza_excepcion(): void
    {
        $entorno = $this->entorno();
        $this->romperElCifrado($entorno);

        // Con el cast `encrypted` de Laravel esto lanzaría `DecryptException`.
        $this->assertNull($entorno->fresh()->moodletoken);
    }

    public function test_el_listado_de_entornos_sigue_funcionando(): void
    {
        // Es el escenario real: la tarjeta lee el token en cada render, así que el fallo
        // no afectaría a un entorno sino a la pantalla entera.
        Permission::firstOrCreate(
            ['name' => 'admin.environments.refresh', 'guard_name' => 'web'],
            ['model' => 'environments', 'description' => 'refrescar']
        );
        $user = User::factory()->create();
        $user->givePermissionTo('admin.environments.refresh');
        $this->actingAs($user);
        Cache::flush();

        $entorno = $this->entorno();
        $this->romperElCifrado($entorno);

        Livewire::test(EnvironmentCard::class, [
            'environment' => $entorno->fresh(),
            'client' => $entorno->client,
            'showClient' => true,
        ])->assertOk();
    }

    public function test_el_entorno_pasa_a_comportarse_como_uno_sin_token(): void
    {
        // Y es lo correcto: a partir de esa rotación no tiene token, porque el valor
        // viejo no se puede recuperar. Hay que volver a pegarlo.
        Permission::firstOrCreate(
            ['name' => 'admin.environments.refresh', 'guard_name' => 'web'],
            ['model' => 'environments', 'description' => 'refrescar']
        );
        $user = User::factory()->create();
        $user->givePermissionTo('admin.environments.refresh');
        $this->actingAs($user);
        Cache::flush();

        $entorno = $this->entorno();

        $componente = fn (Environment $e) => Livewire::test(EnvironmentCard::class, [
            'environment' => $e,
            'client' => $e->client,
        ]);

        $this->assertTrue($componente($entorno)->instance()->puedeSincronizar);

        $this->romperElCifrado($entorno);

        $this->assertFalse($componente($entorno->fresh())->instance()->puedeSincronizar);
    }

    public function test_la_sincronizacion_lo_explica_en_vez_de_reventar(): void
    {
        $entorno = $this->entorno();
        $this->romperElCifrado($entorno);

        $resultado = app(\App\Services\Moodle\MoodleSyncService::class)->lanzar($entorno->fresh());

        $this->assertFalse($resultado['ok']);
        // El mismo mensaje que un entorno que nunca ha enviado su token, que es en lo que
        // se ha convertido.
        $this->assertStringContainsString('token de servicios web', $resultado['mensaje']);
    }

    public function test_un_token_nuevo_se_guarda_y_se_lee_bien(): void
    {
        // Que el cast siga haciendo su trabajo: cifra al guardar y descifra al leer.
        $entorno = $this->entorno();

        $this->assertSame('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', $entorno->fresh()->moodletoken);

        $enBruto = DB::table('environments')->where('id', $entorno->id)->value('moodletoken');

        $this->assertNotSame('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', $enBruto);
        $this->assertStringNotContainsString('a1b2c3d4', (string) $enBruto);
    }

    public function test_un_valor_vacio_no_se_confunde_con_uno_roto(): void
    {
        $entorno = $this->entorno();
        DB::table('environments')->where('id', $entorno->id)->update(['moodletoken' => '']);

        $this->assertNull($entorno->fresh()->moodletoken);
    }
}
