<?php

namespace Tests\Feature\Auth;

use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * Ningún permiso puede existir sin gatear nada, salvo que se declare que es intención.
 *
 * **El hallazgo que lo motiva.** El reparto tenía **27 permisos que ninguna ruta, ningún
 * `authorize()` y ningún `@can` comprobaba**. Unos eran duplicados de otro con nombre
 * distinto —`admin.types.edit` frente a `admin.configurations.environments-types.edit`—,
 * otros restos del Manager anterior, y otros intención de verdad. Veinte se borraron; siete
 * se quedaron declarados en `RoleSeeder::PERMISOS_DE_ROADMAP`.
 *
 * **Y este test es la única parte que arregla el problema de fondo.** Borrar veinte filas
 * limpia el hoy; lo que hacía falta es que la lista **no pueda volver a crecer en
 * silencio**. Es el séptimo caso del mismo patrón en este release —MGR-039, MGR-045,
 * MGR-060, MGR-062, MGR-070, PRODSECU-198 y esto—: *funcionalidad que parecía existir y no
 * tenía puerta*. Los seis anteriores se arreglaron uno a uno; este cierra la fábrica.
 *
 * **Qué cuenta como «tener puerta»**: aparecer en `routes/web.php` como `permission:`, en un
 * `authorize()` o `can()` de un componente, o en un `@can` de una vista. Si un permiso está
 * en el reparto y no aparece en ninguno de los tres, o le falta la pantalla o sobra la
 * línea — y las dos cosas hay que decidirlas, no dejarlas.
 */
class PermisosSinPantallaTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Todas las cadenas de permiso que aparecen en el código.
     *
     * Se lee el código y no la base de datos a propósito: la pregunta es «¿algo comprueba
     * este permiso?», y eso solo lo puede contestar el código.
     *
     * @return list<string>
     */
    private function permisosQueApareceEnElCodigo(): array
    {
        $usados = [];

        foreach ([app_path(), base_path('routes'), resource_path('views')] as $donde) {
            $ficheros = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($donde)
            );

            foreach ($ficheros as $fichero) {
                if (! in_array($fichero->getExtension(), ['php'], true)) {
                    continue;
                }

                $contenido = file_get_contents($fichero->getPathname());

                // `admin.algo.otro` y `api.algo.otro`, con puntos y guiones.
                preg_match_all('/\b(?:admin|api)\.[a-z0-9.-]+/', $contenido, $coincidencias);

                foreach ($coincidencias[0] as $permiso) {
                    $usados[$permiso] = true;
                }
            }
        }

        return array_keys($usados);
    }

    public function test_ningun_permiso_del_reparto_se_queda_sin_puerta(): void
    {
        $this->seed(RoleSeeder::class);

        $usados = $this->permisosQueApareceEnElCodigo();
        $declarados = array_keys(RoleSeeder::PERMISOS_DE_ROADMAP);

        $huerfanos = Permission::pluck('name')
            ->reject(fn (string $permiso) => in_array($permiso, $usados, true))
            ->reject(fn (string $permiso) => in_array($permiso, $declarados, true))
            ->sort()
            ->values()
            ->all();

        $this->assertSame(
            [],
            $huerfanos,
            "Estos permisos existen y no los comprueba nada:\n  " . implode("\n  ", $huerfanos)
            . "\n\nO les falta la pantalla, o sobra la línea del seeder. Si son intención de "
            . 'verdad, decláralos en RoleSeeder::PERMISOS_DE_ROADMAP con el motivo — estar en '
            . 'esa lista es lo que distingue una función pendiente de un olvido.'
        );
    }

    public function test_los_declarados_como_roadmap_siguen_sin_pantalla(): void
    {
        // **El test simétrico, y hace falta.** Cuando a uno de los siete se le ponga por fin
        // su pantalla, hay que **sacarlo de la lista**: dejarlo ahí lo marcaría «sin pantalla
        // todavía» en la pantalla de Permisos para siempre, que es mentir al revés.
        $usados = $this->permisosQueApareceEnElCodigo();

        $yaTienenPantalla = array_values(array_filter(
            array_keys(RoleSeeder::PERMISOS_DE_ROADMAP),
            fn (string $permiso) => in_array($permiso, $usados, true)
        ));

        $this->assertSame(
            [],
            $yaTienenPantalla,
            "Estos ya se comprueban en el código, así que no son roadmap:\n  "
            . implode("\n  ", $yaTienenPantalla)
            . "\n\nQuítalos de RoleSeeder::PERMISOS_DE_ROADMAP."
        );
    }

    public function test_cada_permiso_de_roadmap_dice_por_que_sigue_ahi(): void
    {
        // Una lista de claves sin motivo se convierte en un cajón: nadie sabe si sigue
        // haciendo falta y nadie se atreve a quitar nada.
        foreach (RoleSeeder::PERMISOS_DE_ROADMAP as $permiso => $motivo) {
            $this->assertIsString($permiso);
            $this->assertGreaterThan(
                15,
                mb_strlen($motivo),
                "El motivo de {$permiso} tiene que explicar algo."
            );
        }
    }

    public function test_los_diecinueve_borrados_no_vuelven(): void
    {
        // Si alguien los reañade al seeder «por si acaso», esto lo caza. Son duplicados de
        // otro permiso con nombre distinto, restos de una pantalla eliminada, o de la API
        // —que autentica por token y **no comprueba permisos**—.
        //
        // **Eran veinte y son diecinueve.** `admin.supports.destroy` volvió el mismo día,
        // y volvió **bien**: el usuario pidió poder borrar un tipo de soporte del
        // catálogo, así que se hizo la pantalla —`BorrarSoporteModal`, con su impacto y su
        // bloqueo— y el permiso entró con ella. Este test lo cazó al reañadirlo, que es
        // exactamente para lo que está.
        //
        // Y es la demostración de que la regla sirve: **el permiso no era la función**.
        // Estuvo declarado meses sin que el catálogo pudiera borrar nada.
        $this->seed(RoleSeeder::class);

        foreach ([
            'admin.types.index',
            'admin.types.create',
            'admin.types.edit',
            'admin.types.destroy',
            'admin.types.list',
            'admin.scss.create',
            'admin.scss.edit',
            'admin.scss.show',
            'admin.js.create',
            'admin.js.edit',
            'admin.js.show',
            'admin.scss-cdn.edit',
            'admin.scss-cdn.show',
            'admin.configurations.products.create',
            'admin.configurations.products.edit',
            'admin.notices.index',
            'api.datas.store',
            'api.plugins.store',
            'api.updates.store',
        ] as $muerto) {
            $this->assertNull(
                Permission::where('name', $muerto)->first(),
                "{$muerto} volvió al seeder. No gatea nada: o es un duplicado de otro con "
                . 'nombre distinto, o es de algo que ya no existe.'
            );
        }
    }
}
