<?php

namespace Tests\Feature\Plugins;

use App\Livewire\Plugins\Ficha;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Plugin;
use App\Models\Products\LicenseToken;
use App\Models\Products\OwnPlugin;
use App\Models\Products\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * La ficha de un plugin (PRODSECU-183), desde `Plugin vista.dc.html`.
 *
 * Lo que se fija: que diga qué es sin inventarse lo que no sabe —estándar o terceros—, que
 * el resumen cuente lo mismo que la tabla, que cada cifra filtre, y los cinco estados del
 * diseño: producto en varios sitios, nuestro en uno, sin clasificar, contratado sin
 * instalar y el 404.
 */
class FichaDePluginTest extends TestCase
{
    use RefreshDatabase;

    private Client $cliente;

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

        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);
        foreach (['admin.plugins.environment', 'admin.products.show', 'admin.license-tokens.index'] as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'plugins', 'description' => $nombre]
            ));
        }
        $user = User::factory()->create();
        $user->assignRole($rol);
        $this->actingAs($user);
        Cache::flush();

        $this->cliente = Client::create(['name' => 'Cliente Ficha', 'shortname' => 'fch', 'actived' => true]);
    }

    private function entorno(string $nombre, string $env = 'pro', bool $activo = true): Environment
    {
        return Environment::create([
            'name' => $nombre,
            'domain' => 'https://' . str()->slug($nombre) . '.ficha.test',
            'env' => $env,
            'version' => '4.5.3',
            'client_id' => $this->cliente->id,
            'active' => $activo,
        ]);
    }

    private function plugin(Environment $entorno, string $component, array $atributos = []): Plugin
    {
        return Plugin::create(array_merge([
            'environment_id' => $entorno->id,
            'name' => 'Fresk',
            'component' => $component,
            'type' => strtok($component, '_'),
            'versiondisk' => '2026061200',
            'versiondb' => '2026061200',
            'release' => '2.3.1',
            'versionrequires' => '2024100700',
            'has_updates' => false,
            'has_dependencies' => false,
        ], $atributos));
    }

    private function ficha(string $component)
    {
        return Livewire::test(Ficha::class, ['component' => $component]);
    }

    /** El parque del diseño: un producto en varios sitios, versiones distintas y problemas. */
    private function parqueDeFresk(): void
    {
        Product::create(['name' => 'Fresk', 'slug' => 'theme_fresk', 'shortname' => 'frk']);

        $this->plugin($this->entorno('Campus Alpha'), 'theme_fresk');
        $this->plugin($this->entorno('Aula Universitat'), 'theme_fresk', ['has_updates' => true, 'availableupdates' => [['version' => '2026090100', 'release' => '2.4.0']]]);
        $this->plugin($this->entorno('Campus Ajuntament'), 'theme_fresk', ['release' => '2.1.0', 'versiondisk' => '2026020300', 'versiondb' => '2026020300', 'has_updates' => true]);
        $this->plugin($this->entorno('Learning Konsup'), 'theme_fresk', ['release' => '2.1.0', 'versiondisk' => '2026020300', 'versiondb' => '2025111000']);
        $this->plugin($this->entorno('Campus Pre', 'pre'), 'theme_fresk');
        $this->plugin($this->entorno('Prueba', 'test'), 'theme_fresk', ['release' => '1.8.2', 'versiondisk' => '2025090300', 'versiondb' => '2026020300']);
        $this->plugin($this->entorno('Aula Exea', 'pro', activo: false), 'theme_fresk', ['release' => '2.1.0', 'versiondisk' => '2026020300', 'versiondb' => '2026020300']);
    }

    /* ============ Qué es ============ */

    public function test_un_producto_dice_que_es_producto_y_enlaza_a_su_ficha(): void
    {
        $this->parqueDeFresk();

        $this->ficha('theme_fresk')
            ->assertSee('PRODUCTO')
            ->assertSee('Ficha de producto')
            ->assertSee('Licencias que lo incluyen');
    }

    public function test_uno_nuestro_dice_que_es_nuestro_con_sus_notas(): void
    {
        OwnPlugin::create(['component' => 'mod_videoconnect', 'name' => 'VideoConnect', 'notes' => 'Videoconferencia integrada en el curso.']);
        $this->plugin($this->entorno('Campus Alpha'), 'mod_videoconnect', ['name' => 'Otro nombre del sitio']);

        $this->ficha('mod_videoconnect')
            ->assertSee('NUESTRO')
            ->assertSee('VideoConnect', false)
            ->assertSee('Videoconferencia integrada en el curso.')
            ->assertSee('Catálogo de plugins propios')
            ->assertSee('Solo en un entorno.');
    }

    public function test_lo_que_no_es_nuestro_no_se_llama_de_terceros(): void
    {
        // El sync no manda `is_standard()`: `mod_assign` es estándar y diría «terceros».
        $this->plugin($this->entorno('Campus Alpha'), 'mod_customcert', ['name' => 'Custom certificate']);

        $this->ficha('mod_customcert')
            ->assertSee('SIN CLASIFICAR')
            ->assertDontSee('TERCEROS');
    }

    /* ============ Estándar o terceros: lo dice cada sitio ============ */

    public function test_si_el_sitio_dice_que_es_estandar_sale_estandar(): void
    {
        $this->plugin($this->entorno('Campus Alpha'), 'mod_assign', ['name' => 'Tarea', 'isstandard' => true]);

        $this->ficha('mod_assign')
            ->assertSee('ESTÁNDAR DE MOODLE')
            ->assertDontSee('SIN CLASIFICAR')
            ->assertSee('Documentación de Moodle')
            ->assertDontSee('marketplace.moodle.com', false);
    }

    public function test_si_el_sitio_dice_que_no_es_estandar_sale_terceros(): void
    {
        $this->plugin($this->entorno('Campus Alpha'), 'mod_customcert', ['isstandard' => false]);

        $this->ficha('mod_customcert')
            ->assertSee('TERCEROS')
            ->assertSee('marketplace.moodle.com', false);
    }

    public function test_si_ningun_sitio_lo_dice_sigue_sin_clasificar(): void
    {
        // Sitios con un local_tresipunt anterior al 2026-10-01.
        $this->plugin($this->entorno('Campus Alpha'), 'mod_customcert', ['isstandard' => null]);

        $this->ficha('mod_customcert')->assertSee('SIN CLASIFICAR');
    }

    public function test_si_los_sitios_no_coinciden_cuenta_como_estandar_y_lo_dice(): void
    {
        // Un plugin que salió del paquete en una versión de Moodle: `mod_chat` en la 5.0.
        $this->plugin($this->entorno('Moodle 4.5'), 'mod_chat', ['isstandard' => true]);
        $this->plugin($this->entorno('Moodle 5.1'), 'mod_chat', ['isstandard' => false]);

        $this->ficha('mod_chat')
            ->assertSee('ESTÁNDAR DE MOODLE')
            ->assertSee('Estándar en 1 de 2 sitios');
    }

    public function test_el_catalogo_manda_sobre_lo_que_diga_el_sitio(): void
    {
        // Un plugin nuestro es «nuestro» aunque su Moodle diga que no es estándar.
        OwnPlugin::create(['component' => 'local_tresipunttools', 'name' => 'Tools']);
        $this->plugin($this->entorno('Campus Alpha'), 'local_tresipunttools', ['isstandard' => false]);

        $this->ficha('local_tresipunttools')->assertSee('NUESTRO')->assertDontSee('TERCEROS');
    }

    public function test_enlaza_a_la_busqueda_del_marketplace_por_component(): void
    {
        // Una búsqueda y no la página: la página va por un número que no llega en el
        // inventario. Vale para cualquier plugin, también los nuestros publicados.
        $this->plugin($this->entorno('Campus Alpha'), 'mod_customcert');
        OwnPlugin::create(['component' => 'local_tresipuntsepelite', 'name' => 'SEPE Lite']);
        $this->plugin($this->entorno('Campus Beta'), 'local_tresipuntsepelite');

        $this->ficha('mod_customcert')->assertSee('https://marketplace.moodle.com/?q=mod_customcert', false);
        $this->ficha('local_tresipuntsepelite')->assertSee('https://marketplace.moodle.com/?q=local_tresipuntsepelite', false);
    }

    public function test_con_la_url_del_directorio_enlaza_a_la_pagina_y_no_a_la_busqueda(): void
    {
        // `local_tresipunt` reenvía el `url` de la actualización que anuncia Moodle.
        $this->plugin($this->entorno('Campus Alpha'), 'mod_customcert', [
            'has_updates' => true,
            'availableupdates' => [['version' => '2025041409', 'release' => '5.0.7', 'url' => 'https://moodle.org/plugins/mod_customcert']],
        ]);

        $this->ficha('mod_customcert')
            ->assertSee('Página en Moodle Marketplace')
            ->assertSee('https://moodle.org/plugins/mod_customcert', false)
            ->assertDontSee('marketplace.moodle.com/?q=', false);
    }

    /** @return array<string, array{string}> */
    public static function urlsQueNoSonDelDirectorio(): array
    {
        return [
            'nuestro servidor de descargas' => ['https://plugins.3ipunt.com/mod_customcert_2025041409.zip'],
            'sin https' => ['http://moodle.org/plugins/mod_customcert'],
            'javascript' => ['javascript:alert(1)'],
            'otro dominio que lo imita' => ['https://moodle.org.evil.test/plugins/mod_customcert'],
        ];
    }

    /**
     * @dataProvider urlsQueNoSonDelDirectorio
     */
    public function test_una_url_que_no_es_del_directorio_no_se_enlaza(string $url): void
    {
        // La URL la manda el Moodle del cliente y se pinta como enlace.
        $this->plugin($this->entorno('Campus Alpha'), 'mod_customcert', [
            'has_updates' => true,
            'availableupdates' => [['version' => '2025041409', 'release' => '5.0.7', 'url' => $url]],
        ]);

        $this->ficha('mod_customcert')
            ->assertSee('https://marketplace.moodle.com/?q=mod_customcert', false)
            ->assertDontSee($url, false);
    }

    /* ============ El resumen ============ */

    public function test_el_resumen_cuenta_sin_los_apagados(): void
    {
        $this->parqueDeFresk();

        $test = $this->ficha('theme_fresk');

        $this->assertSame(6, $test->viewData('instalaciones')->count(), 'Aula Exea está apagado');
        $this->assertSame(4, $test->viewData('nPro'));
        $this->assertSame(2, $test->viewData('nNueva'));
        $this->assertSame(2, $test->viewData('nDesfase'), 'uno sin aplicar y uno retrocedido');
        $this->assertSame(1, $test->viewData('apagadosOcultos'));
    }

    public function test_las_versiones_van_de_la_mas_nueva_a_la_mas_vieja(): void
    {
        $this->parqueDeFresk();

        $versiones = $this->ficha('theme_fresk')->viewData('versiones');

        $this->assertSame(['2.3.1', '2.1.0', '1.8.2'], array_column($versiones, 'release'));
        $this->assertSame([3, 2, 1], array_column($versiones, 'n'));
    }

    public function test_ver_apagados_los_incluye(): void
    {
        $this->parqueDeFresk();

        $test = $this->ficha('theme_fresk')->set('incluirApagados', true);

        $this->assertSame(7, $test->viewData('instalaciones')->count());
        $test->assertSee('APAGADO');
    }

    /* ============ Cada cifra filtra ============ */

    private function nombres($test): array
    {
        return $test->viewData('grupos')->flatMap(fn ($g) => $g['filas'])->map(fn ($f) => $f['entorno']->name)->sort()->values()->all();
    }

    public function test_versión_nueva_filtra_la_tabla(): void
    {
        $this->parqueDeFresk();

        $test = $this->ficha('theme_fresk')->call('alternarFiltro', 'nueva');

        $this->assertSame(['Aula Universitat', 'Campus Ajuntament'], $this->nombres($test));
        $test->assertSee('→ 2.4.0');
    }

    public function test_con_desfase_filtra_y_dice_disco_y_base_de_datos(): void
    {
        $this->parqueDeFresk();

        $test = $this->ficha('theme_fresk')->call('alternarFiltro', 'desfase');

        $this->assertSame(['Learning Konsup', 'Prueba'], $this->nombres($test));
        $test->assertSee('Actualización sin aplicar')
            ->assertSee('Ficheros retrocedidos')
            ->assertSee('disco 2026020300 · BD 2025111000');
    }

    public function test_pulsar_la_misma_cifra_quita_el_filtro(): void
    {
        $this->parqueDeFresk();

        $test = $this->ficha('theme_fresk')->call('alternarFiltro', 'nueva')->call('alternarFiltro', 'nueva');

        $this->assertSame('todos', $test->get('filtro'));
    }

    public function test_un_filtro_que_no_existe_no_cambia_nada(): void
    {
        $this->parqueDeFresk();

        $this->assertSame('todos', $this->ficha('theme_fresk')->call('alternarFiltro', 'inventado')->get('filtro'));
    }

    public function test_versiones_que_conviven_agrupa_por_version(): void
    {
        $this->parqueDeFresk();

        $grupos = $this->ficha('theme_fresk')->call('alternarFiltro', 'vers')->viewData('grupos');

        $this->assertSame(['2.3.1', '2.1.0', '1.8.2'], $grupos->pluck('release')->all());
    }

    public function test_se_filtra_por_uso_y_por_version(): void
    {
        $this->parqueDeFresk();

        $this->assertSame(['Campus Pre'], $this->nombres($this->ficha('theme_fresk')->set('uso', 'pre')));
        $this->assertSame(['Campus Ajuntament', 'Learning Konsup'], $this->nombres($this->ficha('theme_fresk')->set('version', '2.1.0')));
    }

    /* ============ Dependencias ============ */

    public function test_dice_la_dependencia_que_falta_sin_una_consulta_por_fila(): void
    {
        $con = $this->entorno('Con dependencia');
        $sin = $this->entorno('Sin dependencia');
        $this->plugin($con, 'theme_fresk', ['dependencies' => ['format_fresk' => '2026010100']]);
        $this->plugin($con, 'format_fresk', ['name' => 'Formato']);
        $this->plugin($sin, 'theme_fresk', ['dependencies' => ['format_fresk' => '2026010100']]);
        for ($i = 1; $i <= 8; $i++) {
            $this->plugin($this->entorno('Otro ' . $i), 'theme_fresk', ['dependencies' => ['format_fresk' => '2026010100']]);
        }

        DB::enableQueryLog();
        $test = $this->ficha('theme_fresk');
        // La de dependencias es la que acota por entorno **y** por componente a la vez.
        $deDependencias = collect(DB::getQueryLog())->filter(fn ($q) => str_contains($q['query'], '"environment_id" in')
            && str_contains($q['query'], '"component" in'))->count();
        DB::disableQueryLog();

        $test->assertSee('Falta format_fresk');
        $this->assertLessThanOrEqual(1, $deDependencias, 'las dependencias se están consultando por fila');
    }

    /* ============ Contratado sin instalar, y el 404 ============ */

    public function test_un_producto_contratado_sin_instalar_dice_en_que_licencias_esta(): void
    {
        $producto = Product::create(['name' => 'Fundae Report', 'slug' => 'report_fundae', 'shortname' => 'fun']);
        $licencia = LicenseToken::create(['client_id' => $this->cliente->id, 'name' => 'Licencia Alpha', 'token' => '3IP-TEST-FICHA-0001', 'active' => true]);
        $licencia->products()->attach($producto->id);

        $this->ficha('report_fundae')
            ->assertSee('PRODUCTO')
            ->assertSee('Contratado y sin instalar en ningún entorno')
            ->assertSee('Licencia Alpha');
    }

    public function test_un_component_que_no_existe_es_un_404_con_sugerencia(): void
    {
        $this->plugin($this->entorno('Campus Alpha'), 'mod_videoconnect');

        $this->ficha('mod_videoconect')
            ->assertSee('Ningún inventario tiene este plugin')
            ->assertSee('¿Querías decir mod_videoconnect?');
    }

    public function test_si_no_se_parece_a_nada_no_sugiere(): void
    {
        $this->plugin($this->entorno('Campus Alpha'), 'mod_videoconnect');

        $this->ficha('block_xyzabc')
            ->assertSee('Ningún inventario tiene este plugin')
            ->assertDontSee('¿Querías decir');
    }

    /* ============ La ruta ============ */

    public function test_la_ruta_responde_y_exige_el_permiso(): void
    {
        $this->parqueDeFresk();

        $this->get(route('plugins.ficha', 'theme_fresk'))->assertOk()->assertSee('Fresk');

        $this->actingAs(User::factory()->create());
        $this->get(route('plugins.ficha', 'theme_fresk'))->assertForbidden();
    }

    public function test_la_ruta_no_se_come_las_de_la_seccion(): void
    {
        $this->get('/plugins/nuestros')->assertOk();
        $this->get('/plugins/sin-instalar')->assertOk();
    }

    /* ============ La descarga ============ */

    private function csv($test): string
    {
        $descarga = $test->effects['download'] ?? null;
        $this->assertNotNull($descarga, 'la acción no ha devuelto ninguna descarga');

        return base64_decode($descarga['content']);
    }

    public function test_descarga_los_entornos_con_los_filtros_puestos(): void
    {
        $this->parqueDeFresk();

        $csv = $this->csv($this->ficha('theme_fresk')->call('alternarFiltro', 'desfase')->call('descargar'));

        $this->assertStringContainsString('Learning Konsup', $csv);
        $this->assertStringContainsString('Actualización sin aplicar', $csv);
        $this->assertStringContainsString('Prueba', $csv);
        $this->assertStringNotContainsString('Campus Alpha', $csv, 'está al día: el filtro lo quita');
        $this->assertDatabaseHas('logs', ['code' => '16046']);
    }

    public function test_la_descarga_no_lleva_tokens(): void
    {
        $entorno = $this->entorno('Campus Alpha');
        $entorno->forceFill(['moodletoken' => 'token-secreto-del-moodle'])->save();
        $this->plugin($entorno, 'theme_fresk');

        $csv = $this->csv($this->ficha('theme_fresk')->call('descargar'));

        $this->assertStringContainsString('Campus Alpha', $csv);
        $this->assertStringNotContainsString('token-secreto-del-moodle', $csv);
    }

    public function test_el_inventario_enlaza_a_la_ficha(): void
    {
        $this->parqueDeFresk();

        Livewire::test(\App\Livewire\Plugins\Inventario::class)
            ->assertSee(route('plugins.ficha', 'theme_fresk'), false);
    }
}
