<?php

namespace Tests\Feature\ApiLogs;

use App\Models\ApiRequestLog;
use App\Services\Api\Severidad;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;

/**
 * Que el `case` de SQL clasifique exactamente igual que `Severidad::de()`.
 *
 * **Por qué hace falta este test.** La misma decisión está escrita dos veces: en PHP,
 * que es la que se ejecuta al registrar cada petición, y en SQL, que es la que rellenó
 * las filas que ya existían cuando se creó la columna. Se bajó a SQL porque el relleno
 * fila a fila dejó colgada la migración en pre —el coste no depende de cuántos errores
 * haya sino de cuántas filas hay que mirar—, y `api_request_logs` es la tabla que más
 * crece del sistema.
 *
 * Dos copias de una regla se separan siempre, y si estas dos se separan el síntoma es el
 * peor posible: las filas viejas clasificadas de una manera y las nuevas de otra, en la
 * misma pantalla, sin nada que lo indique. El porcentaje de error dejaría de significar
 * algo y nadie sabría por qué.
 */
class SeveridadEnSqlTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Los casos que hay que clasificar: todos los códigos de las cuatro familias por
     * cada último dígito, los HTTP a secas que llegan sin `error_code`, y los mensajes
     * de los que depende la decisión.
     *
     * @return list<array{int, int|null, string|null}>
     */
    private function casos(): array
    {
        $casos = [];

        // Los dos textos que cambian el resultado, más uno que no dice nada.
        $mensajes = [
            null,
            '',
            'Licence does not include this plugin',
            'Plugin not found',
            'Invalid version format',
        ];

        // Las cuatro familias por los cuatro últimos dígitos documentados, más un
        // quinto sin documentar para que el `else` también entre.
        foreach ([2, 3, 4, 5] as $familia) {
            foreach ([0, 1, 2, 3, 4] as $ultimo) {
                $codigo = $familia * 1000 + $ultimo;

                foreach ([400, 401, 403, 404, 422, 429, 500] as $status) {
                    foreach ($mensajes as $mensaje) {
                        $casos[] = [$status, $codigo, $mensaje];
                    }
                }
            }
        }

        // Sin `error_code`: el código efectivo pasa a ser el HTTP, que no es de
        // ninguna familia. Es el caso del 403 del middleware y del 401 sin token.
        foreach ([200, 201, 204, 304, 400, 401, 403, 404, 422, 429, 500, 502, 503] as $status) {
            foreach ($mensajes as $mensaje) {
                $casos[] = [$status, null, $mensaje];
                // Y con el HTTP repetido en `error_code`, que es como lo escribe el
                // logger cuando no hay código de familia. El 403/403 es negocio.
                $casos[] = [$status, $status, $mensaje];
            }
        }

        return $casos;
    }

    public function test_el_sql_clasifica_igual_que_el_php_en_todos_los_casos(): void
    {
        $casos = $this->casos();

        foreach ($casos as $indice => [$status, $codigo, $mensaje]) {
            ApiRequestLog::create([
                'request_uuid' => (string) Str::uuid(),
                'method' => 'POST',
                'path' => '/api/v1',
                'auth_type' => 'token',
                'http_status' => $status,
                'error_code' => $codigo,
                'error' => $mensaje,
                'duration_ms' => 10,
                'started_at' => now(),
                'ended_at' => now(),
                'action' => 'licence',
                // El id explícito ata cada fila a su caso sin depender del orden.
                'id' => $indice + 1,
            ]);
        }

        // Se pide al SQL que clasifique **todas** las filas, no solo las de error: el
        // `case` también tiene que decir `ok` de las que no fallaron, porque en la
        // migración esas se quedan con el valor por defecto de la columna.
        $delSql = DB::table('api_request_logs')
            ->selectRaw('id, ' . Severidad::comoCase() . ' as calculada')
            ->pluck('calculada', 'id');

        $discrepancias = [];

        foreach ($casos as $indice => [$status, $codigo, $mensaje]) {
            $esperada = Severidad::de($status, $codigo, $mensaje);
            $obtenida = $delSql[$indice + 1];

            if ($esperada !== $obtenida) {
                $discrepancias[] = sprintf(
                    'http=%d code=%s error=%s → php dice «%s» y sql dice «%s»',
                    $status,
                    $codigo === null ? 'null' : $codigo,
                    $mensaje === null ? 'null' : "«$mensaje»",
                    $esperada,
                    $obtenida
                );
            }
        }

        $this->assertSame([], $discrepancias, sprintf(
            "El case de SQL y Severidad::de() no dicen lo mismo en %d de %d casos:\n%s",
            count($discrepancias),
            count($casos),
            implode("\n", array_slice($discrepancias, 0, 15))
        ));
    }

    public function test_todos_los_codigos_que_existen_caen_en_un_digito_probado(): void
    {
        // La comparación de arriba prueba los últimos dígitos 0–4 de cada familia,
        // porque son los que la API usa: 0 producto sin validar, 1 licencia o plugin,
        // 2 sin contenido, 3 formato inválido. Si algún día se añade un `X005`, la
        // comparación no lo vería —clasificaría bien por casualidad o mal en silencio—
        // y este test avisa de que hay que ampliar `casos()`.
        //
        // La lista es literal a propósito: los códigos están repartidos por las
        // excepciones de cada acción y no hay un catálogo del que leerlos. Cuando lo
        // haya —§4 de la norma de cero errores, que también renumera las familias—,
        // esto se sustituye por el catálogo.
        $existentes = [
            2000, 2001, 2002,
            3000, 3001, 3002, 3003,
            4000, 4001, 4002, 4003,
            5000, 5001, 5002, 5003,
        ];

        $probados = [0, 1, 2, 3, 4];

        $fuera = array_values(array_filter(
            $existentes,
            fn (int $codigo) => ! in_array($codigo % 1000, $probados, true)
        ));

        $this->assertSame([], $fuera,
            'Hay códigos con un último dígito que la comparación PHP↔SQL no prueba: '
            . implode(', ', $fuera) . '. Amplía casos() en este test.');
    }
}
