<?php

namespace Tests\Feature\Environments;

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

/**
 * Los filtros del listado de entornos.
 *
 * El que motiva el fichero: **el filtro de actualización de core no se quedaba puesto**.
 * La vista lo hacía con dos `$set` encadenados en el mismo `wire:click` —una bandera para
 * «mayor» y otra para «menor»—, y eso son **dos peticiones a Livewire**, cada una con su
 * propia instantánea del componente. Se pisaban, y al elegir «Menor» el control volvía a
 * «Todas».
 *
 * Es un fallo que no se ve en un test que llame a los métodos por separado: hay que
 * comprobar el estado **después de la acción que usa la pantalla**.
 */
class ListFiltersTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Nombres de los entornos que muestra el listado.
     *
     * **No se comprueba el HTML** porque cada fila es un componente Livewire hijo
     * (`environment-card`), y en un test del padre los hijos se renderizan como marca
     * de posición: su contenido nunca está en el HTML del padre. Comprobar `assertSee`
     * aquí daría siempre falso negativo.
     *
     * @return array<int, string>
     */
    private function listado(\Livewire\Features\SupportTesting\Testable $componente): array
    {
        return $componente->viewData('environments')->pluck('name')->all();
    }

    private function usuario(): User
    {
        Permission::firstOrCreate(
            ['name' => 'admin.environments.index', 'guard_name' => 'web'],
            ['model' => 'environments', 'description' => 'ver']
        );

        $user = User::factory()->create();
        $user->givePermissionTo('admin.environments.index');
        $this->actingAs($user);
        Cache::flush();

        return $user;
    }

    private function entorno(string $nombre, array $atributos = []): Environment
    {
        $cliente = Client::create([
            'name' => 'Cliente ' . uniqid(),
            'shortname' => 'c' . uniqid(),
            'actived' => true,
        ]);

        return Environment::create(array_merge([
            'client_id' => $cliente->id,
            'name' => $nombre,
            'domain' => 'https://' . uniqid() . '.test',
            'version' => '4.5.3',
            'env' => 'pro',
            'active' => true,
        ], $atributos));
    }

    /* ===================== Actualización de core ===================== */

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

        Livewire::test(Index::class)
            ->call('filtrarActualizacion', 'menor')
            ->assertSet('onlyCoreMinorUpdates', true)
            ->assertSet('onlyCoreMajorUpdates', false);
    }

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

        Livewire::test(Index::class)
            ->call('filtrarActualizacion', 'mayor')
            ->assertSet('onlyCoreMajorUpdates', true)
            ->assertSet('onlyCoreMinorUpdates', false);
    }

    public function test_los_dos_no_pueden_estar_puestos_a_la_vez(): void
    {
        // Son excluyentes: un entorno no puede tener «solo mayor» y «solo menor» al mismo
        // tiempo, y con las dos puestas la lista salía vacía sin explicación.
        $this->usuario();

        Livewire::test(Index::class)
            ->call('filtrarActualizacion', 'mayor')
            ->call('filtrarActualizacion', 'menor')
            ->assertSet('onlyCoreMajorUpdates', false)
            ->assertSet('onlyCoreMinorUpdates', true)
            ->call('filtrarActualizacion', '')
            ->assertSet('onlyCoreMajorUpdates', false)
            ->assertSet('onlyCoreMinorUpdates', false);
    }

    public function test_el_filtro_menor_filtra_de_verdad(): void
    {
        $this->usuario();
        $this->entorno('Con menor', ['lastminor' => '4.5.6']);
        $this->entorno('Con mayor', ['lastversion' => '5.0']);
        $this->entorno('Al dia');

        $nombres = $this->listado(
            Livewire::test(Index::class)->call('filtrarActualizacion', 'menor')
        );

        $this->assertSame(['Con menor'], $nombres);
    }

    /* ===================== Los chips de triaje ===================== */

    public function test_el_chip_de_sin_licencia_filtra(): void
    {
        $this->usuario();
        $this->entorno('Sin su licencia');

        $conLicencia = $this->entorno('Con su licencia');
        $token = \App\Models\Products\LicenseToken::create([
            'client_id' => $conLicencia->client_id,
            'name' => 'Licencia',
            'token' => '3IP-FILTRO-' . strtoupper(uniqid()),
            'active' => true,
        ]);
        $conLicencia->update(['license_token_id' => $token->id]);

        $nombres = $this->listado(
            Livewire::test(Index::class)->set('onlyWithoutLicense', true)
        );

        $this->assertSame(['Sin su licencia'], $nombres);
    }

    public function test_el_chip_de_sin_senal_no_incluye_los_que_nunca_sincronizaron(): void
    {
        // Es la distinción que costó descubrir: «nunca sincronizado» es un alta sin
        // terminar y «sin señal» es una avería. Meterlos en el mismo filtro haría que el
        // número del chip no correspondiera con lo que se ve.
        $this->usuario();
        $this->entorno('Nunca sincronizado');
        $this->entorno('Sin senal', ['refresh_at' => now()->subDays(9)]);
        $this->entorno('Al dia hoy', ['refresh_at' => now()->subHour()]);

        $nombres = $this->listado(
            Livewire::test(Index::class)->set('onlyWithoutSignal', true)
        );

        $this->assertSame(['Sin senal'], $nombres);
    }

    /* ===================== Limpiar ===================== */

    public function test_limpiar_filtros_los_quita_todos(): void
    {
        // Con nueve filtros, quitarlos a mano es imposible de acertar, y quedarse uno
        // puesto sin darse cuenta hace leer una lista recortada como si fuera todo.
        $this->usuario();

        Livewire::test(Index::class)
            ->set('search', 'algo')
            ->call('alternarEnv', 'local')
            ->set('onlyWithoutLicense', true)
            ->set('onlyWithoutSignal', true)
            ->call('filtrarActualizacion', 'mayor')
            ->call('anadirPlugin', 'block_tresipuntsepe')
            ->assertSet('search', 'algo')
            ->call('limpiarFiltros')
            ->assertSet('search', '')
            ->assertSet('env', '')
            ->assertSet('onlyWithoutLicense', false)
            ->assertSet('onlyWithoutSignal', false)
            ->assertSet('onlyCoreMajorUpdates', false)
            ->assertSet('pluginComponents', [])
            ->assertSet('versionFilter', null)
            ->assertSet('onlyWithoutWsToken', false);
    }

    public function test_el_resumen_cuenta_sobre_todo_el_parque_y_no_sobre_lo_filtrado(): void
    {
        // Es el «a qué hay que mirar hoy»: si cambiara al filtrar dejaría de servir para
        // eso.
        $this->usuario();
        $this->entorno('Sin senal uno', ['refresh_at' => now()->subDays(9)]);
        $this->entorno('Otro al dia', ['refresh_at' => now()->subHour()]);

        $componente = Livewire::test(Index::class);
        $resumenSinFiltrar = $componente->viewData('resumen');

        Cache::flush();

        $componente->set('search', 'Otro al dia');

        $this->assertSame($resumenSinFiltrar['sinSenal'], $componente->viewData('resumen')['sinSenal']);
        $this->assertSame(1, $componente->viewData('resumen')['sinSenal']);
    }
    /* ===================== Los filtros que faltaban ===================== */

    public function test_se_puede_filtrar_por_plataforma(): void
    {
        // «¿Qué sitios son Goodle?» era una pregunta sin respuesta en esta pantalla: la
        // barra solo filtraba por `env`, que es otra cosa.
        $this->usuario();

        $moodle = \App\Models\Environments\Type::create(['name' => 'Moodle LMS', 'shortname' => 'moodle']);
        $goodle = \App\Models\Environments\Type::create(['name' => 'Goodle', 'shortname' => 'goodle']);

        $this->entorno('Uno de moodle', ['type_id' => $moodle->id]);
        $this->entorno('Uno de goodle', ['type_id' => $goodle->id]);

        $nombres = $this->listado(
            Livewire::test(Index::class)->set('typeFilter', $goodle->id)
        );

        $this->assertSame(['Uno de goodle'], $nombres);
    }

    public function test_el_desplegable_de_plataforma_no_ofrece_tipos_sin_entornos(): void
    {
        // Ofrecer «Goodle» cuando no hay ninguno lleva a una lista vacía y a pensar que
        // el filtro está roto.
        $this->usuario();

        $conEntornos = \App\Models\Environments\Type::create(['name' => 'Moodle LMS', 'shortname' => 'moodle']);
        \App\Models\Environments\Type::create(['name' => 'Sin usar', 'shortname' => 'sinusar']);
        $this->entorno('Uno', ['type_id' => $conEntornos->id]);

        $tipos = Livewire::test(Index::class)->viewData('tipos');

        $this->assertSame(['Moodle LMS'], array_column($tipos, 'name'));
        $this->assertSame([1], array_column($tipos, 'cuantos'),
            'la opción lleva cuántos entornos hay: es lo que dice si vale la pena pulsarla');
    }

    public function test_se_pueden_aislar_los_inactivos(): void
    {
        // No había forma de aislarlos ni de quitarlos de en medio: salían atenuados
        // entre los demás.
        $this->usuario();
        $this->entorno('El activo');
        $this->entorno('El apagado', ['active' => false]);

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

        // **Por defecto solo los activos.** Antes se veían los dos, porque esconder filas
        // sin decirlo es peor que mostrarlas; ese motivo sigue en pie y por eso el recorte
        // se dice —lo comprueba `test_el_defecto_dice_cuantas_filas_esconde`—.
        $this->assertSame(['El activo'], $this->listado($componente));
        $this->assertSame(['El apagado'], $this->listado($componente->set('activeFilter', 'no')));
        $this->assertCount(2, $this->listado($componente->set('activeFilter', '')));
    }

    public function test_se_pueden_ver_los_que_nunca_han_sincronizado(): void
    {
        // En la base real son 20 de 21: el grupo más grande y el único sin atajo, que es
        // lo que hacía extraño ver «0 sin señal».
        $this->usuario();
        $this->entorno('Nunca dio senal');
        $this->entorno('Este si', ['refresh_at' => now()->subHour()]);

        $nombres = $this->listado(
            Livewire::test(Index::class)->call('alternarSenal', 'nunca')
        );

        $this->assertSame(['Nunca dio senal'], $nombres);
    }

    public function test_nunca_sincronizado_y_sin_senal_son_excluyentes(): void
    {
        // Uno pide `refresh_at` nulo y el otro no nulo: con los dos puestos la lista
        // saldría vacía sin explicación. Misma trampa que el filtro de actualización.
        $this->usuario();

        Livewire::test(Index::class)
            ->call('alternarSenal', 'nunca')
            ->assertSet('onlyNeverSynced', true)
            ->call('alternarSenal', 'senal')
            ->assertSet('onlyNeverSynced', false)
            ->assertSet('onlyWithoutSignal', true);
    }
    /* ===================== Plugins, versión y token ===================== */

    private function plugin(Environment $entorno, string $componente): void
    {
        \App\Models\Environments\Plugin::create([
            'environment_id' => $entorno->id,
            'component' => $componente,
            'name' => $componente,
            // `type` y `name` son NOT NULL: el inventario que manda el plugin siempre
            // los trae, así que la tabla los exige.
            'type' => explode('_', $componente)[0],
            'versiondisk' => '2026010100',
            'versiondb' => '2026010100',
            'versionrequires' => 2024042200,
        ]);
    }

    public function test_el_buscador_de_plugins_pide_dos_letras(): void
    {
        // Con una sola letra la consulta devuelve casi todo y no ayuda a elegir.
        $this->usuario();
        $this->plugin($this->entorno('Uno'), 'block_tresipuntsepe');

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

        $this->assertSame([], $componente->set('pluginSearch', 'b')->instance()->sugerenciasDePlugins());
        $this->assertNotSame([], $componente->set('pluginSearch', 'bl')->instance()->sugerenciasDePlugins());
    }

    public function test_las_sugerencias_dicen_en_cuantos_entornos_esta(): void
    {
        // Distingue un producto nuestro repartido de un plugin de un solo sitio.
        $this->usuario();
        $this->plugin($this->entorno('Uno'), 'local_tresipunt');
        $this->plugin($this->entorno('Dos'), 'local_tresipunt');

        $sugerencias = Livewire::test(Index::class)
            ->set('pluginSearch', 'tresipunt')
            ->instance()->sugerenciasDePlugins();

        $this->assertSame('local_tresipunt', $sugerencias[0]->component);
        $this->assertSame(2, (int) $sugerencias[0]->entornos);
    }

    public function test_un_plugin_ya_elegido_no_se_vuelve_a_ofrecer(): void
    {
        $this->usuario();
        $this->plugin($this->entorno('Uno'), 'local_tresipunt');

        $sugerencias = Livewire::test(Index::class)
            ->call('anadirPlugin', 'local_tresipunt')
            ->set('pluginSearch', 'tresipunt')
            ->instance()->sugerenciasDePlugins();

        $this->assertSame([], $sugerencias);
    }

    public function test_varios_plugins_se_acumulan_con_y(): void
    {
        // «Los que tienen el SEPE Y el ImportGC». Con un `whereIn` saldrían los que
        // tienen cualquiera de los dos, y añadir criterios daría MÁS resultados, que es
        // lo contrario de lo que espera quien filtra.
        $this->usuario();

        $losDos = $this->entorno('Tiene los dos');
        $this->plugin($losDos, 'block_tresipuntsepe');
        $this->plugin($losDos, 'local_tresipuntimportgc');

        $soloUno = $this->entorno('Tiene uno');
        $this->plugin($soloUno, 'block_tresipuntsepe');

        $componente = Livewire::test(Index::class)->call('anadirPlugin', 'block_tresipuntsepe');
        $this->assertCount(2, $this->listado($componente));

        $componente->call('anadirPlugin', 'local_tresipuntimportgc');
        $this->assertSame(['Tiene los dos'], $this->listado($componente));
    }

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

        Livewire::test(Index::class)
            ->call('anadirPlugin', 'local_tresipunt')
            ->assertSet('pluginComponents', ['local_tresipunt'])
            ->call('quitarPlugin', 'local_tresipunt')
            ->assertSet('pluginComponents', []);
    }

    public function test_la_version_filtra_por_familia(): void
    {
        // «Quién sigue en 4.1» incluye 4.1.11 y 4.1.13, y no 4.5.3.
        $this->usuario();
        $this->entorno('Vieja once', ['version' => '4.1.11']);
        $this->entorno('Vieja trece', ['version' => '4.1.13']);
        $this->entorno('Nueva', ['version' => '4.5.3']);

        $nombres = $this->listado(Livewire::test(Index::class)->set('versionFilter', '4.1'));
        sort($nombres);

        $this->assertSame(['Vieja once', 'Vieja trece'], $nombres);
    }

    public function test_las_familias_ofrecidas_salen_de_lo_que_hay(): void
    {
        $this->usuario();
        $this->entorno('Una', ['version' => '4.1.11']);
        $this->entorno('Otra', ['version' => '5.1.1']);

        $familias = Livewire::test(Index::class)->viewData('familias');

        // De la más nueva a la más vieja, con su recuento.
        $this->assertSame(['5.1' => 1, '4.1' => 1], $familias);
    }

    public function test_se_pueden_ver_los_que_no_tienen_token_de_servicios_web(): void
    {
        // Es la lista de trabajo de «a quién hay que pedirle el token»: hasta ahora solo
        // se veía entrando fila a fila.
        $this->usuario();
        $this->entorno('Sin token');
        $this->entorno('En blanco', ['moodletoken' => '']);
        $this->entorno('Con token', ['moodletoken' => 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4']);

        $nombres = $this->listado(Livewire::test(Index::class)->set('onlyWithoutWsToken', true));
        sort($nombres);

        // La cadena vacía cuenta como sin token: tampoco sirve para sincronizar.
        $this->assertSame(['En blanco', 'Sin token'], $nombres);
    }
    /* ===================== Soporte y agrupación ===================== */

    public function test_se_pueden_ver_los_que_nunca_han_tenido_soporte(): void
    {
        // La pregunta de Comercial: a quién se le puede ofrecer. Es distinta de
        // «caducado», que ya fue cliente de soporte.
        $this->usuario();

        $sinSoporte = $this->entorno('Nunca tuvo');
        $conSoporte = $this->entorno('Tuvo y caduco');

        $tipo = \App\Models\Support\Support::create(['name' => 'Premium', 'shortname' => 'prem' . uniqid(), 'description' => 'x']);
        \App\Models\Support\EnvironmentSupport::create([
            'environment_id' => $conSoporte->id,
            'support_id' => $tipo->id,
            'start_at' => now()->subYears(2)->format('Y-m-d'),
            'end_at' => now()->subMonths(3)->format('Y-m-d'),
        ]);

        $nombres = $this->listado(
            Livewire::test(Index::class)->set('onlyWithoutSupport', true)
        );

        // El caducado NO sale: tuvo soporte, y esa es otra lista.
        $this->assertSame(['Nunca tuvo'], $nombres);
    }

    public function test_el_bloque_plegado_dice_cuantos_filtros_esconde(): void
    {
        // Esconder un filtro activo es lo peor que puede hacer un listado: se lee una
        // lista recortada como si fuera todo el parque. El recuento es lo que lo evita,
        // y además abre el bloque solo.
        $this->usuario();

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

        $componente->set('onlyWithoutWsToken', true);
        $this->assertSame(1, $componente->viewData('filtrosAvanzados'));

        $componente->set('versionFilter', '4.1')->call('anadirPlugin', 'local_tresipunt');
        $this->assertSame(3, $componente->viewData('filtrosAvanzados'));
    }

    public function test_los_filtros_de_la_fila_visible_no_cuentan_como_escondidos(): void
    {
        // Buscar, entorno y activo están siempre a la vista: contarlos en el botón de
        // «más filtros» diría que hay algo escondido que no lo está.
        $this->usuario();

        $componente = Livewire::test(Index::class)
            ->set('search', 'algo')
            ->call('alternarEnv', 'local')
            ->set('activeFilter', 'no');

        $this->assertSame(0, $componente->viewData('filtrosAvanzados'));
        $this->assertTrue($componente->viewData('hayFiltros'));
    }

    /* ===================== `env`: varios a la vez ===================== */

    public function test_se_pueden_ver_pro_y_pre_a_la_vez(): void
    {
        // **Es el caso que motiva el cambio.** El control era excluyente, y la pregunta
        // que se hace cuando pasa algo en un cliente es «pro y pre»: producción y su
        // previo. Con un solo valor había que elegir entre ver de más o mirar dos veces.
        $this->usuario();
        $this->entorno('El de pro', ['env' => 'pro']);
        $this->entorno('El de pre', ['env' => 'pre']);
        $this->entorno('El de develop', ['env' => 'develop']);

        $componente = Livewire::test(Index::class)
            ->call('alternarEnv', 'develop');

        $this->assertSame(['pro', 'pre'], $componente->instance()->envsElegidos());

        // Ordenados como los ordena el listado, no como se han pulsado.
        $nombres = $this->listado($componente);
        sort($nombres);

        $this->assertSame(['El de pre', 'El de pro'], $nombres);
    }

    public function test_quitar_el_ultimo_env_no_hace_nada(): void
    {
        // Si «ninguno» significara «todos», deseleccionar mostraría **más** filas, que es
        // lo contrario de lo que espera cualquiera; y si significara «ninguno», el control
        // tendría un estado que solo lleva a un callejón. Así que lo que está marcado es
        // exactamente lo que se ve, siempre.
        $this->usuario();
        $this->entorno('El de pro', ['env' => 'pro']);

        $componente = Livewire::test(Index::class)
            ->call('alternarEnv', 'pre')
            ->call('alternarEnv', 'develop');

        $this->assertSame(['pro'], $componente->instance()->envsElegidos());

        $componente->call('alternarEnv', 'pro');

        $this->assertSame(['pro'], $componente->instance()->envsElegidos(),
            'quitar el último dejaría la lista vacía, y una lista vacía no significa nada');
        $this->assertSame(['El de pro'], $this->listado($componente));
    }

    public function test_un_env_inventado_no_entra(): void
    {
        // `alternarEnv` llega de un `wire:click`, y en Livewire eso es una petición que
        // cualquiera puede repetir con el valor que quiera. Un `env` que no existe no
        // rompería nada —la consulta saldría vacía— pero se quedaría en la URL y en el
        // control, que es peor: una lista vacía sin saber por qué.
        $this->usuario();
        $this->entorno('El de pro');

        $componente = Livewire::test(Index::class)->call('alternarEnv', 'produccion');

        $this->assertSame('', $componente->get('env'));
        $this->assertSame(Env::visiblesPorDefecto(), $componente->instance()->envsElegidos());
    }

    public function test_todos_los_envs_incluye_los_locales(): void
    {
        $this->usuario();
        $this->entorno('El de pro', ['env' => 'pro']);
        $this->entorno('El local', ['env' => 'local']);

        $componente = Livewire::test(Index::class)->call('todosLosEnvs');

        $this->assertSame(Env::todos(), $componente->instance()->envsElegidos());
        $this->assertCount(2, $this->listado($componente));
    }

    public function test_el_defecto_lo_decide_el_catalogo_y_no_el_codigo(): void
    {
        // **Lo que se ve de entrada ya no está en el código.** Era la constante
        // `ENVS_POR_DEFECTO` —«todos menos local»—; ahora es la casilla «sale de entrada en
        // el filtro» de cada fila del catálogo, y por tanto una decisión de quien gestiona
        // el parque en vez de un despliegue.
        $this->usuario();
        $this->entorno('De cliente', ['env' => 'pro']);
        $this->entorno('El local', ['env' => 'local']);

        $this->assertSame(['pro', 'pre', 'develop'], Env::visiblesPorDefecto(),
            'la semilla del catálogo es la que había en el código: todos menos local');

        // Y al marcarlo en el catálogo, el listado lo enseña sin tocar nada más.
        Env::query()->where('shortname', 'local')->first()->update(['visible_por_defecto' => true]);

        $this->assertCount(2, $this->listado(Livewire::test(Index::class)));
    }

    public function test_si_el_catalogo_no_marca_ninguno_valen_todos(): void
    {
        // **El estado imposible tiene que tener una salida.** Si alguien desmarca los
        // cuatro, el filtro se quedaría sin ningún uso puesto: el listado no podría
        // enseñar nada y la pantalla no tendría más salida que adivinar la URL. «No se ha
        // decidido nada» se interpreta como «valen todos», que es lo inofensivo.
        $this->usuario();
        $this->entorno('El local', ['env' => 'local']);

        Env::query()->update(['visible_por_defecto' => false]);
        Env::olvidarCatalogo();

        $this->assertSame(Env::todos(), Env::visiblesPorDefecto());
        $this->assertCount(1, $this->listado(Livewire::test(Index::class)));
    }

    public function test_un_uso_nuevo_del_catalogo_aparece_en_el_filtro(): void
    {
        // Es la mitad del sentido de que sea catálogo: dar de alta «staging» y que salga
        // en la barra sin tocar la plantilla ni desplegar.
        $this->usuario();

        Env::create([
            'shortname' => 'staging', 'name' => 'Staging', 'orden' => 25,
            'visible_por_defecto' => true,
            'color_text' => 'var(--warning-500)', 'color_bg' => 'var(--warning-50)',
        ]);

        $this->entorno('El de staging', ['env' => 'staging']);

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

        $this->assertContains('staging', $componente->instance()->envsElegidos());
        $this->assertSame(['El de staging'], $this->listado($componente));
        $this->assertContains('staging', $componente->viewData('usos')->pluck('shortname')->all());
    }

    public function test_el_defecto_oculta_los_locales(): void
    {
        // En el parque real los locales son ruido de desarrollo, y salían mezclados con
        // los sitios de clientes en la misma lista.
        $this->usuario();
        $this->entorno('De cliente', ['env' => 'pro']);
        $this->entorno('El de mi maquina', ['env' => 'local']);

        $this->assertSame(['De cliente'], $this->listado(Livewire::test(Index::class)));
    }

    /* ===================== Lo que el defecto esconde, dicho ===================== */

    public function test_el_defecto_dice_cuantas_filas_esconde(): void
    {
        // **Es la mitad del cambio.** El motivo por el que esta pantalla no ocultaba nada
        // estaba escrito en el código: *esconder filas sin decirlo es peor que mostrarlas*.
        // Sigue siendo bueno, así que el recorte no se ha hecho a cambio de quitarlo: se
        // dice, con el número y con la salida al lado.
        $this->usuario();
        $this->entorno('Visible', ['env' => 'pro', 'active' => true]);
        $this->entorno('Local encendido', ['env' => 'local', 'active' => true]);
        $this->entorno('Pro apagado', ['env' => 'pro', 'active' => false]);
        $this->entorno('Local apagado', ['env' => 'local', 'active' => false]);

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

        $this->assertSame(['Visible'], $this->listado($componente));

        // **Un número, y exacto.** Hay 2 locales y 2 inactivos, pero solo 3 filas
        // escondidas: «Local apagado» está en los dos grupos. Decir «2 locales y 2
        // inactivos» sería prometer que al pulsar «ver todo» aparecerían cuatro.
        $this->assertSame(3, $componente->viewData('ocultos'));

        $componente->call('verTodo');

        $this->assertCount(4, $this->listado($componente));
        $this->assertSame(0, $componente->viewData('ocultos'),
            'con el recorte quitado no hay nada escondido de lo que avisar');
    }

    public function test_el_aviso_se_cuenta_sobre_el_resto_de_filtros(): void
    {
        // Si dijera «hay 12 locales» mientras se está buscando «reus», el número no se
        // referiría a lo que se tiene delante y no serviría para decidir nada.
        $this->usuario();
        $this->entorno('Campus Reus', ['env' => 'pro']);
        $this->entorno('Campus Reus de pruebas', ['env' => 'local']);
        $this->entorno('Campus Girona de pruebas', ['env' => 'local']);

        $componente = Livewire::test(Index::class)->set('search', 'Reus');

        $this->assertSame(['Campus Reus'], $this->listado($componente));
        $this->assertSame(1, $componente->viewData('ocultos'),
            'los locales que no cumplen la búsqueda no aparecerían al pulsar «ver todo»');
    }

    public function test_quitar_filtros_no_es_lo_mismo_que_ver_todo(): void
    {
        // Son dos botones porque son dos cosas: «Quitar filtros» devuelve la pantalla **al
        // defecto** —que esconde— y «ver todo» quita el recorte del defecto.
        $this->usuario();
        $this->entorno('El local', ['env' => 'local']);

        $componente = Livewire::test(Index::class)->call('verTodo');

        $this->assertCount(1, $this->listado($componente));

        $componente->call('limpiarFiltros');

        $this->assertSame([], $this->listado($componente));
        $this->assertSame('', $componente->get('env'));
        $this->assertSame('si', $componente->get('activeFilter'));
    }

    /* ===================== Las opciones, acotadas a lo filtrado ===================== */

    public function test_las_plataformas_ofrecidas_son_las_de_lo_que_se_esta_viendo(): void
    {
        // **Lo que pide el cambio:** en los demás filtros solo salen las opciones de los
        // elementos que ya está filtrando la búsqueda. Antes venían del catálogo entero:
        // con el listado acotado se ofrecían plataformas que no tenía ninguno de esos
        // sitios, y elegirlas llevaba a una lista vacía sin saber si el filtro fallaba.
        $this->usuario();

        $deCliente = \App\Models\Environments\Type::create(['name' => 'Moodle LMS', 'shortname' => 'moodle']);
        $soloLocal = \App\Models\Environments\Type::create(['name' => 'Workplace', 'shortname' => 'workplace']);

        $this->entorno('De cliente', ['env' => 'pro', 'type_id' => $deCliente->id]);
        $this->entorno('De mi maquina', ['env' => 'local', 'type_id' => $soloLocal->id]);

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

        $this->assertSame(['Moodle LMS'], array_column($componente->viewData('tipos'), 'name'),
            'Workplace solo la tiene un local, y los locales no se están viendo');

        // Y al quitar el recorte vuelve a ofrecerse, porque ya hay un sitio que la tiene.
        $componente->call('verTodo');

        $this->assertSame(['Moodle LMS', 'Workplace'], array_column($componente->viewData('tipos'), 'name'));
    }

    public function test_una_plataforma_puesta_no_esconde_las_demas(): void
    {
        // **La regla de cualquier buscador por facetas**, y la diferencia entre acotar y
        // quedarse encerrado: las opciones de «plataforma» se calculan con todos los
        // filtros puestos **menos el de plataforma**. Si se calcularan con el suyo dentro,
        // al elegir Goodle la única opción ofrecida sería Goodle y no habría forma de
        // cambiar de valor sin limpiar el filtro.
        $this->usuario();

        $moodle = \App\Models\Environments\Type::create(['name' => 'Moodle LMS', 'shortname' => 'moodle']);
        $goodle = \App\Models\Environments\Type::create(['name' => 'Goodle', 'shortname' => 'goodle']);

        $this->entorno('Uno', ['type_id' => $moodle->id]);
        $this->entorno('Otro', ['type_id' => $goodle->id]);

        $tipos = Livewire::test(Index::class)
            ->set('typeFilter', $goodle->id)
            ->viewData('tipos');

        $this->assertSame(['Goodle', 'Moodle LMS'], array_column($tipos, 'name'));
    }

    public function test_las_familias_de_version_son_las_de_lo_que_se_esta_viendo(): void
    {
        $this->usuario();
        $this->entorno('De cliente', ['env' => 'pro', 'version' => '4.5.3']);
        $this->entorno('De mi maquina', ['env' => 'local', 'version' => '5.1.1']);

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

        $this->assertSame(['4.5' => 1], $componente->viewData('familias'));

        $componente->call('verTodo');

        // De la más nueva a la más vieja: la pregunta es «quién se ha quedado atrás».
        $this->assertSame(['5.1' => 1, '4.5' => 1], $componente->viewData('familias'));
    }

    public function test_los_productos_ofrecidos_son_los_que_llegan_a_lo_que_se_esta_viendo(): void
    {
        // Un entorno no tiene productos: los hereda de su licencia cruzando dos pivotes.
        // Por eso la opción **no lleva recuento** —sería un número aproximado en una
        // pantalla que existe para acotar—, pero acotarse tiene que acotarse igual.
        $this->usuario();

        $deCliente = \App\Models\Products\Product::create([
            'name' => 'Marketplace', 'slug' => 'local_tresipuntmarket', 'shortname' => 'market',
        ]);
        $soloLocal = \App\Models\Products\Product::create([
            'name' => 'Laboratorio', 'slug' => 'local_tresipuntlab', 'shortname' => 'lab',
        ]);

        foreach ([['pro', $deCliente], ['local', $soloLocal]] as [$env, $producto]) {
            $entorno = $this->entorno('El de ' . $env, ['env' => $env]);

            $token = \App\Models\Products\LicenseToken::create([
                'client_id' => $entorno->client_id,
                'name' => 'Licencia de ' . $env,
                'token' => '3IP-FACETA-' . strtoupper(uniqid()),
                'active' => true,
            ]);
            $token->products()->attach($producto->id);
            $entorno->update(['license_token_id' => $token->id]);
        }

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

        $this->assertSame(['Marketplace'], array_column($componente->viewData('productos'), 'name'),
            'el Laboratorio solo llega a un local, y los locales no se están viendo');

        $componente->call('verTodo');

        $this->assertSame(['Laboratorio', 'Marketplace'], array_column($componente->viewData('productos'), 'name'));
    }
}
