<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\Index as EnvironmentsIndex;
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 Livewire\Livewire;
use ReflectionClass;
use ReflectionProperty;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Los filtros del listado de entornos viven en la URL (MGR-053).
 *
 * **Lo que se arregla no es solo la comodidad.** Con dieciséis filtros y un bloque
 * plegable, no sobrevivir a una recarga significa que quien recarga cree que está viendo
 * el parque completo cuando estaba viendo un recorte, o al revés. Y sin filtros en la URL
 * no se puede pasar «mira estos» por Slack: hay que explicar qué casillas marcar.
 */
class FiltrosEnLaUrlTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Los dieciséis, con su nombre en la URL.
     *
     * Se escriben aquí a mano **a propósito**: si mañana alguien añade un filtro y no lo
     * añade a esta lista, el test de abajo lo caza. Un test que leyera los atributos del
     * componente para comprobar los atributos del componente no probaría nada.
     *
     * @var array<string, string>
     */
    private const FILTROS = [
        'search' => 'q',
        'env' => 'env',
        'typeFilter' => 'tipo',
        'activeFilter' => 'estado',
        'onlyNeverSynced' => 'sin_sync',
        'onlyWithoutLicense' => 'sin_licencia',
        'onlyWithoutSignal' => 'sin_senal',
        'onlyExpiredSupport' => 'soporte_caducado',
        'onlyWithoutSupport' => 'sin_soporte',
        'onlyCoreMajorUpdates' => 'core_mayor',
        'onlyCoreMinorUpdates' => 'core_menor',
        'onlyEnvironmentsWithOutdatedPlugins' => 'plugins_viejos',
        'pluginComponents' => 'plugin',
        'versionFilter' => 'version',
        'onlyWithoutWsToken' => 'sin_token',
        'productFilter' => 'producto',
        // El único que no sale de un dato del sistema: lo escribió una persona.
        'onlyPendingReview' => 'por_revisar',
        // El del KPI de «desincronizados»: nunca han hablado, o hace tres días o más.
        'onlyStale' => 'sin_frescos',
    ];

    private function usuario(): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDeEntornos', 'guard_name' => 'web']);
        $rol->givePermissionTo(Permission::firstOrCreate(
            ['name' => 'admin.environments.index', 'guard_name' => 'web'],
            ['model' => 'environments', 'description' => 'admin.environments.index']
        ));

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

        Cache::flush();

        return $user;
    }

    private function entorno(string $nombre, string $dominio, array $extra = []): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'filtros'],
            ['name' => 'Cliente de filtros', 'actived' => true]
        );

        return Environment::create(array_merge([
            'name' => $nombre,
            'domain' => $dominio,
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ], $extra));
    }

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

        foreach (self::FILTROS as $propiedad => $alias) {
            $casos[$propiedad] = [$propiedad, $alias];
        }

        return $casos;
    }

    /**
     * @dataProvider losFiltros
     */
    public function test_cada_filtro_tiene_su_nombre_en_la_url(string $propiedad, string $alias): void
    {
        $atributos = (new ReflectionProperty(EnvironmentsIndex::class, $propiedad))
            ->getAttributes(\Livewire\Attributes\Url::class);

        $this->assertCount(1, $atributos,
            "el filtro `{$propiedad}` no va a la URL: no sobrevive a una recarga");

        $this->assertSame($alias, $atributos[0]->getArguments()['as'] ?? null,
            "el filtro `{$propiedad}` tendría que llamarse `{$alias}` en la URL");
    }

    public function test_no_hay_ningun_filtro_sin_declarar(): void
    {
        // **El test que caza el filtro nuevo.** `limpiarFiltros()` es la lista de lo que
        // esta pantalla considera un filtro, así que si alguien añade uno ahí y no lo pone
        // en la URL, salta aquí. `pluginSearch` no cuenta: es el buscador del selector.
        $codigo = file_get_contents(
            (new ReflectionClass(EnvironmentsIndex::class))->getFileName()
        );

        preg_match("/public function limpiarFiltros\(\): void.*?\]\);/s", $codigo, $m);
        $this->assertNotEmpty($m, 'no encuentro `limpiarFiltros()`');

        preg_match_all("/'([a-zA-Z]+)'/", $m[0], $nombres);

        $declarados = array_diff($nombres[1], ['pluginSearch']);
        $enLaUrl = array_keys(self::FILTROS);

        $this->assertSame([], array_values(array_diff($declarados, $enLaUrl)),
            'hay filtros que se limpian pero no viajan en la URL');
    }

    public function test_un_enlace_con_filtros_llega_filtrado(): void
    {
        // Lo que hace que se pueda pasar «mira estos» por Slack.
        $this->usuario();

        $this->entorno('Apagado', 'https://apagado.test', ['active' => false]);
        $this->entorno('Encendido', 'https://encendido.test', ['active' => true]);

        $entornos = Livewire::withQueryParams(['estado' => 'no'])
            ->test(EnvironmentsIndex::class)
            ->viewData('environments');

        $nombres = collect($entornos->items())->pluck('name');

        $this->assertTrue($nombres->contains('Apagado'));
        $this->assertFalse($nombres->contains('Encendido'),
            'el filtro de la URL tiene que aplicarse al montar, no solo al pulsarlo');
    }

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

        $this->entorno('Campus de Girona', 'https://girona.test');
        $this->entorno('Campus de Lleida', 'https://lleida.test');

        $entornos = Livewire::withQueryParams(['q' => 'Girona'])
            ->test(EnvironmentsIndex::class)
            ->viewData('environments');

        $this->assertCount(1, $entornos->items());
        $this->assertSame('Campus de Girona', $entornos->items()[0]->name);
    }

    public function test_una_bandera_llega_como_bandera_y_no_como_texto(): void
    {
        // `?sin_licencia=1` llega de la URL como la cadena «1». Si no se convierte a
        // booleano, la casilla se pinta marcada pero la consulta puede no filtrar.
        $this->usuario();

        $componente = Livewire::withQueryParams(['sin_licencia' => true])
            ->test(EnvironmentsIndex::class);

        $this->assertTrue($componente->get('onlyWithoutLicense'));
        $this->assertTrue($componente->instance()->hayFiltros(),
            'la pantalla tiene que saber que llega filtrada, para avisar de que es un recorte');
    }

    public function test_la_pantalla_sin_filtros_no_arrastra_parametros(): void
    {
        // El `except` de cada atributo. Sin él, la URL de la pantalla limpia llevaría los
        // dieciséis parámetros vacíos y no habría forma de enlazar «el listado completo».
        //
        // **Se compara con el `except`, no con «vacío».** Antes se exigía que los dieciséis
        // arrancaran vacíos, y eso valía mientras el defecto de la pantalla fuera «no
        // filtrar nada». Ya no lo es: arranca ocultando los locales (`envs`) y los
        // inactivos (`estado = si`). Lo que importa no es que el valor sea vacío, es que
        // sea **el mismo que su `except`**, que es exactamente la condición que hace que el
        // parámetro no salga en la URL.
        $this->usuario();

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

        foreach (self::FILTROS as $propiedad => $alias) {
            $argumentos = (new ReflectionProperty(EnvironmentsIndex::class, $propiedad))
                ->getAttributes(\Livewire\Attributes\Url::class)[0]
                ->getArguments();

            $this->assertArrayHasKey('except', $argumentos,
                "`{$propiedad}` no declara `except`, así que `{$alias}` saldría siempre en la URL");

            $this->assertSame($argumentos['except'], $componente->get($propiedad),
                "`{$propiedad}` no arranca en su `except`, así que `{$alias}` saldría siempre en la URL");
        }
    }

    public function test_limpiar_filtros_los_quita_tambien_de_la_url(): void
    {
        $this->usuario();
        $this->entorno('Uno', 'https://uno.test');

        $componente = Livewire::withQueryParams(['estado' => 'no', 'sin_licencia' => true])
            ->test(EnvironmentsIndex::class)
            ->call('limpiarFiltros');

        // **Al defecto, no a vacío.** `limpiarFiltros()` deja la pantalla como al entrar,
        // y como al entrar oculta locales e inactivos. Quien quiera verlo todo tiene el
        // «ver todo» del aviso, que es otro botón porque es otra cosa.
        $componente->assertSet('activeFilter', 'si')
            // `env` vacío **es** el defecto: significa «los usos que el catálogo marca
            // como visibles». Ver la propiedad `$env` del componente.
            ->assertSet('env', '')
            ->assertSet('onlyWithoutLicense', false);

        $this->assertFalse($componente->instance()->hayFiltros(),
            'el defecto no puede contar como «filtro puesto»: el botón de quitarlos estaría siempre encendido');
    }

    public function test_elegir_todos_en_un_select_deja_el_estado_limpio(): void
    {
        // Un `<select>` manda `''` al elegir «Todas» y el valor por defecto es `null`: si
        // se quedaran los dos, la URL arrastraría un `version=` vacío que el `except` no
        // puede limpiar porque no coincide con el defecto.
        $this->usuario();

        $componente = Livewire::test(EnvironmentsIndex::class)
            ->set('versionFilter', '5.1')
            ->set('versionFilter', '');

        $this->assertNull($componente->get('versionFilter'));
    }

    public function test_el_control_de_env_deja_la_url_canonica(): void
    {
        // `env` ya no es un `<select>`: son botones que se marcan y se desmarcan, y su
        // estado limpio no es «ninguno» —nunca lo está— sino **el vacío, que significa el
        // defecto configurado en el catálogo**.
        //
        // Así que volver al defecto por el camino largo —quitar un uso y volver a
        // ponerlo— tiene que dejar la cadena vacía otra vez. Si `alternarEnv()` no
        // reordenara al orden del catálogo, quedaría `env=pre,pro,develop`: la misma vista
        // con otra URL y con el parámetro colgando.
        $this->usuario();

        $componente = Livewire::test(EnvironmentsIndex::class)
            ->call('alternarEnv', 'develop')   // quitarlo
            ->call('alternarEnv', 'develop');  // y volver a ponerlo, que entra al final

        $this->assertSame('', $componente->get('env'),
            'volver al defecto por otro camino tiene que dejar la URL limpia');

        $this->assertSame(
            \App\Models\Environments\Env::visiblesPorDefecto(),
            $componente->instance()->envsElegidos()
        );
    }

    public function test_un_env_de_la_url_que_no_existe_no_deja_la_lista_vacia(): void
    {
        // La URL la escribe cualquiera. Un `?env=inventado` filtrando por un uso que no
        // está en el catálogo daría una lista vacía sin decir por qué — y con la barra de
        // filtros diciendo que no hay nada puesto.
        $this->usuario();
        $this->entorno('Uno', 'https://uno.test');

        $componente = Livewire::withQueryParams(['env' => 'inventado'])
            ->test(EnvironmentsIndex::class);

        $this->assertSame(
            \App\Models\Environments\Env::visiblesPorDefecto(),
            $componente->instance()->envsElegidos()
        );
        $this->assertCount(1, $componente->viewData('environments')->items());
    }

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

        $componente = Livewire::withQueryParams(['env' => 'local,pro'])
            ->test(EnvironmentsIndex::class);

        // En el orden del catálogo, que es el contrato: producción primero.
        $this->assertSame(['pro', 'local'], $componente->instance()->envsElegidos());
    }
}
