<?php

namespace Tests\Feature\Features;

use App\Livewire\Products\BorrarElementoModal;
use App\Livewire\Products\BorrarVersionModal;
use App\Livewire\Features\VersionShow;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Plugin;
use App\Models\Features\Feature;
use App\Models\Features\FeatureVersion;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Models\Products\LicenseToken;
use App\Models\Products\Product;
use App\Support\VersionBorrable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Borrar una versión de Funcionalidades, y borrar una de dentro (PRODSECU-198).
 *
 * **Por qué esto no era «poner un botón».** Una versión de contenido se está sirviendo a
 * los Moodles de los clientes: la API le da a cada sitio la versión más alta menor o igual
 * a la que pide su plugin. Así que borrarla **cambia qué recibe un cliente en su siguiente
 * petición**, sin que él haya tocado nada.
 *
 * Y el caso que más importa es el silencioso: si se borra la última versión, la API responde
 * «sin contenido», que `norma-cero-errores.md` clasifica como **respuesta correcta**. No
 * salta ninguna alarma. El sitio se queda vacío y nadie se entera.
 */
class BorrarVersionTest extends TestCase
{
    use RefreshDatabase;

    private Product $producto;

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

        $this->producto = Product::create([
            'name' => 'Producto con contenido',
            'slug' => 'local_tresipunt',
            'actived' => true,
        ]);
    }

    private function usuario(array $permisos): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDeBorrado', 'guard_name' => 'web']);

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

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

        Cache::flush();

        return $user;
    }

    private function version(string $numero, int $cuantas = 1, string $estado = 'published'): FeatureVersion
    {
        $version = FeatureVersion::create([
            'product_id' => $this->producto->id,
            'version' => $numero,
            'description' => 'Versión ' . $numero,
        ]);

        // `$cuantas = 0` crea una versión vacía, que es la única que la regla deja
        // borrar.
        for ($i = 1; $i <= $cuantas; $i++) {
            Feature::create([
                'feature_version_id' => $version->id,
                'title' => 'Funcionalidad ' . $i . ' de ' . $numero,
                'content_html' => '<p>Contenido</p>',
                'status' => $estado,
                'sort_order' => $i,
            ]);
        }

        return $version;
    }

    /**
     * Un entorno con el plugin instalado en la versión dada.
     *
     * El cruce es por `slug` del producto contra `component` de Moodle, y la versión que
     * cuenta es `versiondb`: es la que el plugin dice tener migrada.
     */
    private function entornoCon(string $versionDelPlugin, string $nombre): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'borrado'],
            ['name' => 'Cliente de borrado', 'actived' => true]
        );

        $licencia = LicenseToken::firstOrCreate(
            ['token' => '3IP-BORR-ADO0-0000-0001'],
            ['client_id' => $cliente->id, 'name' => 'Licencia de borrado', 'active' => true]
        );

        $licencia->products()->syncWithoutDetaching([$this->producto->id]);

        $entorno = Environment::create([
            'name' => $nombre,
            'domain' => 'https://' . \Illuminate\Support\Str::slug($nombre) . '.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
            'license_token_id' => $licencia->id,
        ]);

        Plugin::create([
            'environment_id' => $entorno->id,
            'component' => $this->producto->slug,
            'name' => 'Tresipunt',
            'type' => 'local',
            'versiondb' => $versionDelPlugin,
            'versiondisk' => $versionDelPlugin,
            // NOT NULL en la tabla: lo manda el inventario de Moodle en cada `sync`.
            'versionrequires' => 2024100700,
            'release' => '1.0.0',
        ]);

        return $entorno;
    }

    /* ============ El impacto ============ */

    public function test_dice_a_que_version_cae_cada_entorno_afectado(): void
    {
        // **El dato que convierte esto en una decisión.** Un plugin en 2026030100 recibe
        // hoy la 2026020100; si se borra, cae a la 2026010100.
        $this->usuario(['admin.features.version.destroy']);

        $this->version('2026010100');
        $borrar = $this->version('2026020100');
        $this->entornoCon('2026030100', 'Campus al dia');

        $impacto = Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $borrar->id,
        ])->viewData('impacto');

        $this->assertCount(1, $impacto['afectados']);
        $this->assertSame('2026010100', $impacto['afectados'][0]['pasaraA']);
        $this->assertSame(0, $impacto['seQuedanSinNada']);
        $this->assertFalse($impacto['esLaUnica']);
    }

    public function test_no_se_puede_borrar_si_algun_entorno_la_recibe(): void
    {
        // **La regla, primera mitad.** Borrarla les cambiaría el contenido sin que ellos
        // hayan tocado nada, así que no se ofrece: se dice qué tiene que pasar antes.
        $this->usuario(['admin.features.version.destroy']);

        $borrar = $this->version('2026010100', 0);
        $this->version('2026060100', 0);
        $this->entornoCon('2026020100', 'Campus que la recibe');

        $componente = Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $borrar->id,
        ]);

        $impacto = $componente->viewData('impacto');

        $this->assertFalse($impacto['sePuedeBorrar']);
        $this->assertTrue($impacto['laUsaAlguien']);
        $this->assertFalse($impacto['tieneElementos']);

        // **Los fragmentos son de una sola línea a propósito**: `assertSee` compara texto
        // literal y el blade parte las frases largas en varias líneas, así que una frase
        // entera nunca coincide.
        $componente->assertSee('no se puede borrar todavía')
            ->assertSee('entorno la está recibiendo')
            // Y el camino, que es lo que hay que dar: no un «no».
            ->assertSee('Campus que la recibe');
    }

    public function test_no_se_puede_borrar_si_todavia_tiene_elementos(): void
    {
        // **La regla, segunda mitad.** Y el motivo de que sea así está en la modal:
        // obliga a mirar uno por uno qué se está tirando.
        $this->usuario(['admin.features.version.destroy']);

        $borrar = $this->version('2026010100', 3);

        $componente = Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $borrar->id,
        ]);

        $impacto = $componente->viewData('impacto');

        $this->assertFalse($impacto['sePuedeBorrar']);
        $this->assertTrue($impacto['tieneElementos']);
        $this->assertSame(3, $impacto['elementos']);

        $componente->assertSee('no se puede borrar todavía')
            ->assertSee('funcionalidades dentro');
    }

    public function test_los_dos_motivos_se_dicen_juntos(): void
    {
        // Si fallan las dos condiciones hay que decir las dos: arreglar una y volver a
        // encontrarse el botón bloqueado es lo que hace que la gente deje de leer.
        $this->usuario(['admin.features.version.destroy']);

        $borrar = $this->version('2026010100', 2);
        $this->entornoCon('2026020100', 'Campus con las dos');

        $impacto = Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $borrar->id,
        ])->viewData('impacto');

        $this->assertCount(2, $impacto['motivos']);
        $this->assertStringContainsString('recibiendo', implode(' | ', $impacto['motivos']));
        $this->assertStringContainsString('elementos dentro', implode(' | ', $impacto['motivos']));
    }

    public function test_una_version_vacia_que_nadie_recibe_si_se_puede_borrar(): void
    {
        // El caso que la regla permite, y por eso ya no pide escribir el número: no hay
        // nada que perder.
        $this->usuario(['admin.features.version.destroy']);

        $vacia = $this->version('2026010100', 0);
        $this->version('2026060100', 1);
        $this->entornoCon('2026070100', 'Campus en la nueva');

        $componente = Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $vacia->id,
        ]);

        $this->assertTrue($componente->viewData('impacto')['sePuedeBorrar']);

        $componente->assertSee('está vacía')
            ->assertDontSee('no se puede borrar todavía');
    }
    public function test_un_entorno_sin_el_plugin_no_cuenta_como_afectado(): void
    {
        // No recibe nada de este producto, así que borrar no le cambia nada. Contarlo
        // llenaría la lista de sitios que no tienen que ver con la decisión.
        $this->usuario(['admin.features.version.destroy']);

        $borrar = $this->version('2026010100');
        $this->version('2026020100');

        $cliente = Client::create(['name' => 'Sin plugin', 'shortname' => 'sinplug', 'actived' => true]);
        $licencia = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia sin plugin',
            'token' => '3IP-SINP-LUG0-0000-0001',
            'active' => true,
        ]);
        $licencia->products()->syncWithoutDetaching([$this->producto->id]);

        Environment::create([
            'name' => 'Campus sin plugin',
            'domain' => 'https://sin-plugin.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
            'license_token_id' => $licencia->id,
        ]);

        $impacto = Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $borrar->id,
        ])->viewData('impacto');

        $this->assertSame([], $impacto['afectados']);
    }

    /* ============ El borrado ============ */

    public function test_pulsar_borrar_con_elementos_dentro_no_borra_nada(): void
    {
        // **No basta con esconder el botón** (MGR-005). Y aquí hay un motivo propio: entre
        // abrir la modal y pulsar pueden pasar minutos, y en medio un Moodle puede haber
        // sincronizado y empezar a recibir esta versión.
        $this->usuario(['admin.features.version.destroy']);

        $version = $this->version('2026010100', 2);

        Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $version->id,
        ])->call('borrar');

        $this->assertDatabaseHas('feature_versions', ['id' => $version->id]);
        $this->assertSame(2, Feature::count());
        // El mensaje de error no se comprueba aquí: **el flash no se lee desde un test de
        // Livewire** en este proyecto —está documentado en `ManualSyncTest`—. Lo que importa
        // y sí se puede afirmar es que no se borró nada.
        $this->assertFalse(
            Livewire::test(BorrarVersionModal::class, [
                'tipo' => 'features',
                'productId' => $this->producto->id,
                'versionId' => $version->id,
            ])->instance()->puedeBorrar()
        );
    }

    public function test_una_version_vacia_se_borra(): void
    {
        $this->usuario(['admin.features.version.destroy']);

        $version = $this->version('2026010100', 0);

        Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $version->id,
        ])->call('borrar');

        $this->assertDatabaseMissing('feature_versions', ['id' => $version->id]);
    }

    public function test_el_rastro_dice_que_no_se_corto_ningun_servicio(): void
    {
        // El rastro ya no cuenta impacto porque no hay: **dice la condición**, que es la
        // información útil al leerlo meses después —«¿esto rompió algo?»—.
        $this->usuario(['admin.features.version.destroy']);

        $version = $this->version('2026010100', 0);

        Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $version->id,
        ])->call('borrar');

        $entrada = MonitoringLog::where('code', '16034')->first();

        $this->assertNotNull($entrada);
        $this->assertStringContainsString('2026010100', $entrada->msg);
        $this->assertStringContainsString('estaba vacía y ningún entorno la recibía', $entrada->msg);
    }
    public function test_sin_permiso_no_se_puede_abrir(): void
    {
        $this->usuario(['admin.features.version.show']);

        $version = $this->version('2026010100');

        Livewire::test(BorrarVersionModal::class, [
            'tipo' => 'features',
            'productId' => $this->producto->id,
            'versionId' => $version->id,
        ])->assertForbidden();
    }

    public function test_una_version_de_otro_producto_es_un_404(): void
    {
        // Acotado en la consulta, no comparando después (MGR-006).
        $this->usuario(['admin.features.version.destroy']);

        $otro = Product::create(['name' => 'Otro', 'slug' => 'otro', 'actived' => true]);
        $ajena = FeatureVersion::create([
            'product_id' => $otro->id,
            'version' => '2026010100',
            'description' => 'De otro',
        ]);

        try {
            Livewire::test(BorrarVersionModal::class, [
                'tipo' => 'features',
                'productId' => $this->producto->id,
                'versionId' => $ajena->id,
            ]);

            $this->fail('Se esperaba que la versión de otro producto no se encontrara.');
        } catch (ModelNotFoundException) {
            // Es el resultado correcto.
        }

        // Y sobre todo: la versión ajena sigue ahí.
        $this->assertDatabaseHas('feature_versions', ['id' => $ajena->id]);
    }

    /* ============ El botón en la pantalla ============ */

    public function test_el_boton_sale_con_su_permiso(): void
    {
        $this->usuario(['admin.features.version.show', 'admin.features.version.destroy']);

        $version = $this->version('2026010100');

        Livewire::test(VersionShow::class, ['product' => $this->producto, 'featureVersion' => $version])
            ->assertOk()
            ->assertSee('Borrar versión');
    }

    public function test_sin_permiso_no_hay_boton(): void
    {
        $this->usuario(['admin.features.version.show']);

        $version = $this->version('2026010100');

        Livewire::test(VersionShow::class, ['product' => $this->producto, 'featureVersion' => $version])
            ->assertOk()
            ->assertDontSee('Borrar versión');
    }

    /* ============ La modal del elemento ============ */

    public function test_una_funcionalidad_en_borrador_no_la_ve_nadie(): void
    {
        // **La corrección del `confirm()` que había.** Avisaba de «los N entornos que
        // reciben la versión» aunque el elemento estuviera en borrador, y eso no era
        // verdad: en borrador no se sirve.
        $this->usuario(['admin.features.destroy']);

        $version = $this->version('2026010100', 1, 'draft');
        $this->entornoCon('2026030100', 'Campus con plugin');

        $feature = Feature::first();

        $componente = Livewire::test(BorrarElementoModal::class, [
            'tipo' => 'features',
            'versionId' => $version->id,
            'elementoId' => $feature->id,
        ]);

        $this->assertSame(0, $componente->viewData('loVen'));
        $componente->assertSee('No lo está viendo nadie')
            ->assertSee('Borrador');
    }

    public function test_una_publicada_dice_cuantos_la_ven(): void
    {
        $this->usuario(['admin.features.destroy']);

        $version = $this->version('2026010100', 1, 'published');
        $this->entornoCon('2026030100', 'Campus que la ve');

        $feature = Feature::first();

        $componente = Livewire::test(BorrarElementoModal::class, [
            'tipo' => 'features',
            'versionId' => $version->id,
            'elementoId' => $feature->id,
        ]);

        $this->assertSame(1, $componente->viewData('loVen'));
        $componente->assertSee('lo está recibiendo ahora mismo');
    }

    public function test_borrar_la_funcionalidad_deja_rastro(): void
    {
        // Antes no se registraba: borrar contenido que se está sirviendo tiene que decir
        // quién y qué se llevó.
        $this->usuario(['admin.features.destroy']);

        $version = $this->version('2026010100', 2);
        $feature = Feature::first();
        $titulo = $feature->title;

        Livewire::test(BorrarElementoModal::class, [
            'tipo' => 'features',
            'versionId' => $version->id,
            'elementoId' => $feature->id,
        ])->call('borrar');

        $this->assertDatabaseMissing('features', ['id' => $feature->id]);
        $this->assertSame(1, Feature::count(), 'la otra se queda');

        $entrada = MonitoringLog::where('code', '16035')->first();

        $this->assertNotNull($entrada);
        $this->assertStringContainsString($titulo, $entrada->msg);
    }

    public function test_una_funcionalidad_de_otra_version_es_un_404(): void
    {
        $this->usuario(['admin.features.destroy']);

        $version = $this->version('2026010100');
        $otra = $this->version('2026020100');

        $ajena = Feature::where('feature_version_id', $otra->id)->first();

        try {
            Livewire::test(BorrarElementoModal::class, [
                'tipo' => 'features',
                'versionId' => $version->id,
                'elementoId' => $ajena->id,
            ]);

            $this->fail('Se esperaba que la funcionalidad de otra versión no se encontrara.');
        } catch (ModelNotFoundException) {
            // Es el resultado correcto.
        }

        $this->assertDatabaseHas('features', ['id' => $ajena->id]);
    }

    public function test_los_codigos_de_log_estan_registrados(): void
    {
        // Sin esto el visor pinta «Código 16034» en lugar del texto.
        $this->assertArrayHasKey('16034', MonitoringLog::CODIGOS);
        $this->assertArrayHasKey('16035', MonitoringLog::CODIGOS);
    }

    /* ============ Los cinco tipos ============ */

    /**
     * @dataProvider losCincoTipos
     */
    public function test_el_registro_de_cada_tipo_es_coherente(string $clave): void
    {
        // **El test que sostiene la generalización.** Una modal para cinco tipos solo
        // funciona si el registro dice la verdad: modelo, relación y clave ajena. Un dedazo
        // aquí no da error de sintaxis, da una modal que borra lo que no debe.
        $ficha = VersionBorrable::de($clave);

        $this->assertTrue(class_exists($ficha['modelo']), $ficha['modelo'] . ' no existe');
        $this->assertTrue(class_exists($ficha['modeloElemento']), $ficha['modeloElemento'] . ' no existe');

        $version = new $ficha['modelo']();

        $this->assertTrue(method_exists($version, $ficha['relacion']),
            $ficha['modelo'] . ' no tiene la relación ' . $ficha['relacion'] . '()');

        // La clave ajena tiene que existir de verdad en la tabla del elemento: es con la
        // que se acota la búsqueda para que no se pueda borrar el elemento de otra versión.
        $elemento = new $ficha['modeloElemento']();

        $this->assertContains(
            $ficha['claveDeVersion'],
            \Illuminate\Support\Facades\Schema::getColumnListing($elemento->getTable()),
            $elemento->getTable() . ' no tiene la columna ' . $ficha['claveDeVersion']
        );

        // Y la ruta del listado, que es a donde se redirige tras borrar.
        $this->assertTrue(\Illuminate\Support\Facades\Route::has($ficha['rutaListado']),
            'la ruta ' . $ficha['rutaListado'] . ' no existe');
    }

    /**
     * @dataProvider losCincoTipos
     */
    public function test_ningun_tipo_usa_softdeletes(string $clave): void
    {
        // **De esto depende que la modal no mienta.** Dice «esto no se puede deshacer» y
        // escribe el rastro antes de borrar. Si algún día un modelo se pasa a `SoftDeletes`,
        // las dos cosas dejan de tener sentido y hay que revisar la modal — que es lo que
        // este test obliga a hacer.
        $modelo = VersionBorrable::de($clave)['modelo'];

        $this->assertNotContains(
            \Illuminate\Database\Eloquent\SoftDeletes::class,
            class_uses_recursive($modelo),
            $modelo . ' usa SoftDeletes: la modal de borrado dice que no se puede deshacer'
        );
    }

    /**
     * @dataProvider losCincoTipos
     */
    public function test_cada_tipo_tiene_su_permiso_de_borrar_version(string $clave): void
    {
        // El permiso lo crea la migración `add_version_destroy_permissions`. Si falta, el
        // botón no se pinta nunca y nadie sabe por qué.
        $permiso = VersionBorrable::de($clave)['permisoVersion'];

        $this->assertDatabaseHas('permissions', ['name' => $permiso, 'guard_name' => 'web']);
    }

    /** @return array<string, array{string}> */
    public static function losCincoTipos(): array
    {
        $casos = [];

        foreach (VersionBorrable::claves() as $clave) {
            $casos[$clave] = [$clave];
        }

        return $casos;
    }

    public function test_son_cinco_y_no_estan_setups_ni_scss_cdn(): void
    {
        // **Los dos que no entran, y no es un olvido**: `setups` no versiona por número
        // —tiene un `active` y la resolución es otra— y `scss-cdn` son bundles con su propio
        // ciclo. Si alguien los añade a la ligera, este test lo para y le hace leer el
        // docblock de `VersionBorrable`.
        $this->assertSame(
            ['features', 'tutorials', 'resources', 'scss', 'js'],
            VersionBorrable::claves()
        );
    }

    public function test_un_tipo_que_no_existe_revienta_en_vez_de_dar_una_modal_vacia(): void
    {
        // Con un `?? []` silencioso, una clave mal escrita daría una modal en blanco que
        // borra lo que no debe.
        $this->expectException(\InvalidArgumentException::class);

        VersionBorrable::de('setups');
    }
}