<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\Index as ListadoDeEntornos;
use App\Livewire\Environments\Plugins\Index as InventarioDelEntorno;
use App\Livewire\Plugins\Index as Parque;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Plugin;
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;

/**
 * Un plugin ausente del disco tiene que verse en las **cuatro** pantallas.
 *
 * El inventario de un entorno lo marcaba desde el 2026-09-10, pero ahí solo lo ve quien ya
 * ha entrado a ese sitio a mirar. Las otras tres son las que hacen que alguien entre:
 *
 * - **El listado de entornos**: cuál de los veintiún sitios tiene el problema.
 * - **La ficha del entorno**: es donde se mira «cómo está este sitio».
 * - **El parque de plugins**: si le pasa a varios a la vez, que es cuando huele a despliegue
 *   incompleto y no a un borrado a mano. Es la pantalla de Sistemas.
 *
 * Y la regla **está en un solo sitio** —`Plugin::scopeAusenteDelDisco()`— porque cuatro copias
 * de la misma condición acaban diciendo cuatro cosas distintas y sin forma de saber cuál
 * miente. El último test de este fichero es el que lo fija.
 */
class PluginsAusentesVisiblesTest extends TestCase
{
    use RefreshDatabase;

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

        foreach (['admin.environments.index', 'admin.plugins.environment', 'admin.clients.index',
                  'admin.products.index', 'admin.configs.index', 'admin.dashboard.index'] as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'environments', 'description' => $nombre]
            ));
        }

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

        Cache::flush();

        return $user;
    }

    private function entorno(string $nombre, array $atributos = []): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'aus'],
            ['name' => 'Cliente Ausentes', 'actived' => true]
        );

        return Environment::create(array_merge([
            'name' => $nombre,
            'domain' => 'https://' . str()->slug($nombre) . '.aus.test',
            'env' => 'pro',
            'version' => '5.1.1',
            'client_id' => $cliente->id,
            'active' => true,
            'refresh_at' => now()->subHour(),
        ], $atributos));
    }

    /** Un plugin ausente del disco tal y como lo guarda la API. */
    private function ausente(Environment $entorno, string $component): Plugin
    {
        return $this->plugin($entorno, [
            'component' => $component,
            // `'0'` es lo que ponen las acciones cuando el payload no trae la versión de
            // disco: las columnas son NOT NULL, así que la ausencia no llega como `null`.
            'versiondisk' => '0',
            'versiondb' => '2024032700',
        ]);
    }

    private function plugin(Environment $entorno, array $atributos): Plugin
    {
        return Plugin::create(array_merge([
            'environment_id' => $entorno->id,
            'name' => 'moodlecheck',
            'component' => 'local_moodlecheck',
            'type' => 'local',
            'versiondisk' => '2026010100',
            'versiondb' => '2026010100',
            'versionrequires' => '2025041400',
            'has_updates' => false,
            'has_dependencies' => false,
        ], $atributos));
    }

    /* ============ El listado de entornos ============ */

    public function test_el_listado_dice_cuantos_plugins_ausentes_tiene_cada_sitio(): void
    {
        $this->usuario();

        $roto = $this->entorno('Campus roto');
        $sano = $this->entorno('Campus sano');

        $this->ausente($roto, 'local_moodlecheck');
        $this->ausente($roto, 'mod_otro');
        $this->plugin($sano, ['component' => 'local_bien']);

        $filas = Livewire::test(ListadoDeEntornos::class)
            ->viewData('environments')
            ->keyBy('name');

        $this->assertSame(2, (int) $filas['Campus roto']->missing_plugins_count);
        $this->assertSame(0, (int) $filas['Campus sano']->missing_plugins_count);
    }

    public function test_el_recuento_del_listado_no_es_una_consulta_por_fila(): void
    {
        // Veintiún entornos son veintiuna consultas si el recuento se hace en la fila, y
        // esta pantalla ya arrastró ese problema una vez (MGR-015).
        $this->usuario();

        for ($i = 1; $i <= 6; $i++) {
            $this->ausente($this->entorno('Sitio ' . $i), 'local_x' . $i);
        }

        $consultas = 0;
        \Illuminate\Support\Facades\DB::listen(function () use (&$consultas) {
            $consultas++;
        });

        Livewire::test(ListadoDeEntornos::class);

        \Illuminate\Support\Facades\DB::listen(fn () => null);

        $this->assertLessThan(
            30,
            $consultas,
            "Pintar el listado ha costado {$consultas} consultas: el recuento de ausentes se está haciendo por fila."
        );
    }

    /* ============ El parque, que es la pantalla de Sistemas ============ */

    public function test_el_parque_avisa_de_los_ausentes_y_dice_en_que_sitios(): void
    {
        $this->usuario();

        $uno = $this->entorno('Campus uno');
        $otro = $this->entorno('Campus dos');

        $this->ausente($uno, 'local_moodlecheck');
        $this->ausente($uno, 'mod_otro');
        $this->ausente($otro, 'local_moodlecheck');

        $aviso = collect(Livewire::test(Parque::class)->viewData('atencion'))
            ->firstWhere('etiqueta', 'Plugins ausentes del disco');

        $this->assertNotNull($aviso, 'el parque no avisa de los plugins ausentes');
        $this->assertSame('crit', $aviso['nivel']);
        $this->assertStringContainsString('3 plugins', $aviso['texto']);
        $this->assertStringContainsString('2 sitios', $aviso['texto']);
        $this->assertStringContainsString('Campus uno', $aviso['texto']);

        // **Y dice qué hacer, que es lo que separa un aviso de un adorno.** Ni el cron ni
        // subir una versión nueva lo arreglan, y mandar a eso pierde el tiempo de quien lo lea.
        $this->assertStringContainsString('varios sitios', $aviso['arreglo']);
        $this->assertStringNotContainsString('pasa el cron', $aviso['arreglo']);
    }

    public function test_sin_ausentes_el_parque_no_inventa_el_aviso(): void
    {
        $this->usuario();

        $this->plugin($this->entorno('Campus sano'), ['component' => 'local_bien']);

        $etiquetas = collect(Livewire::test(Parque::class)->viewData('atencion'))->pluck('etiqueta');

        $this->assertNotContains('Plugins ausentes del disco', $etiquetas->all());
    }

    public function test_el_parque_no_cuenta_los_ausentes_de_un_entorno_apagado(): void
    {
        // Mismo recorte que el resto de la pantalla (MGR-112): un sitio de baja conserva su
        // inventario, y reclamar un plugin ausente de un sitio que ya no se atiende es
        // mandar a alguien a arreglar algo que no hay que arreglar.
        $this->usuario();

        $apagado = $this->entorno('Campus apagado', ['active' => false]);
        $this->ausente($apagado, 'local_moodlecheck');

        $etiquetas = collect(Livewire::test(Parque::class)->viewData('atencion'))->pluck('etiqueta');

        $this->assertNotContains('Plugins ausentes del disco', $etiquetas->all());

        // Y con el interruptor puesto vuelve a contar.
        $etiquetasConApagados = collect(
            Livewire::test(Parque::class)->set('incluirApagados', true)->viewData('atencion')
        )->pluck('etiqueta');

        $this->assertContains('Plugins ausentes del disco', $etiquetasConApagados->all());
    }

    /* ============ La ficha del entorno ============ */

    public function test_la_ficha_del_entorno_lo_dice_en_su_bloque_de_plugins(): void
    {
        // Es donde se mira «cómo está este sitio». De este bloque, lo único que ya está roto
        // es esto: lo demás —una actualización pendiente— es trabajo planificable.
        $this->usuario();

        $entorno = $this->entorno('Campus con hueco');

        $this->ausente($entorno, 'local_moodlecheck');
        $this->plugin($entorno, ['component' => 'local_bien']);

        $componente = Livewire::test(\App\Livewire\Environments\Show::class, ['environment' => $entorno]);

        $this->assertSame(1, $componente->viewData('plugins')['ausentes']);
        $componente->assertSee('ausente del disco');
    }

    public function test_sin_ausentes_la_ficha_sigue_diciendo_que_no_hay_nada_que_hacer(): void
    {
        // El estado tranquilo tiene que seguir leyéndose: con el badge nuevo por delante,
        // el «ninguno requiere acción» no puede desaparecer cuando de verdad no hay nada.
        $this->usuario();

        $entorno = $this->entorno('Campus sano');
        $this->plugin($entorno, ['component' => 'local_bien']);

        Livewire::test(\App\Livewire\Environments\Show::class, ['environment' => $entorno])
            ->assertSee('Ninguno requiere acción')
            ->assertDontSee('ausente del disco');
    }

    /* ============ Y las cuatro dicen lo mismo ============ */

    public function test_el_scope_y_la_clasificacion_del_inventario_no_pueden_discrepar(): void
    {
        // **El test que importa de este fichero.** El inventario de un entorno clasifica una
        // fila ya cargada, en PHP; el listado y el parque filtran en SQL con el scope. Dos
        // definiciones de la misma condición se separan siempre, y aquí el síntoma sería el
        // peor: el listado diciendo «2 ausentes» y el inventario de ese sitio sin marcar
        // ninguno, sin nada que lo explique.
        $this->usuario();

        $entorno = $this->entorno('Campus mezcla');

        // Los cuatro casos que hay que distinguir, uno por fila.
        $this->plugin($entorno, ['component' => 'a_ausente', 'versiondisk' => '0', 'versiondb' => '2024032700']);
        $this->plugin($entorno, ['component' => 'b_vacio', 'versiondisk' => '', 'versiondb' => '2024032700']);
        $this->plugin($entorno, ['component' => 'c_sin_nada', 'versiondisk' => '0', 'versiondb' => '0']);
        $this->plugin($entorno, ['component' => 'd_normal', 'versiondisk' => '2026010100', 'versiondb' => '2026010100']);

        $porSql = Plugin::query()->ausenteDelDisco()->pluck('component')->sort()->values()->all();

        $componente = Livewire::test(InventarioDelEntorno::class, ['environment' => $entorno]);

        $porPhp = collect($componente->viewData('atencion'))
            ->filter(fn (array $fila) => $fila['etiqueta'] === 'Ausente del disco')
            ->map(fn (array $fila) => $fila['plugin']->component)
            ->sort()
            ->values()
            ->all();

        $this->assertSame(['a_ausente', 'b_vacio'], $porSql);
        $this->assertSame($porSql, $porPhp, 'el scope de SQL y la clasificación en PHP no coinciden');
    }
}
