<?php

namespace Tests\Feature\Api;

use App\Models\ApiRequestLog;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Products\LicenseToken;
use App\Services\Api\Severidad;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * El cuerpo de la respuesta, guardado solo cuando algo ha ido mal.
 *
 * **Lo que había**: el logger leía la respuesta, le sacaba el `code` y el `error`, medía su
 * tamaño y **descartaba el cuerpo**. Así que el detalle de una petición no podía enseñar
 * qué se le respondió al cliente, y cuando la respuesta no era un JSON con la forma
 * esperada —una excepción que devuelve HTML, un 502 de un proxy— **no quedaba ningún
 * rastro de lo que pasó**.
 *
 * Y lo que estos tests protegen es el límite: **solo las que fallan**. Las correctas son
 * las grandes —`scss`, `js` y `setup` devuelven ficheros— y guardarlas multiplicaría por
 * el peso del contenido servido la tabla que más crece del sistema. Si alguien quita esa
 * condición, el primer test cae.
 */
class CuerpoDeLaRespuestaTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN = '3IP-TEST-CUERPO-01';
    private const DOMAIN = 'https://cuerpo.test';

    protected function setUp(): void
    {
        parent::setUp();

        $cliente = Client::create([
            'name' => 'Cliente del cuerpo',
            'shortname' => 'cuerpo',
            'actived' => true,
        ]);

        $token = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia del cuerpo',
            'token' => self::TOKEN,
            'active' => true,
        ]);

        Environment::create([
            'name' => 'Entorno del cuerpo',
            'domain' => self::DOMAIN,
            'version' => '5.1.1',
            'env' => 'local',
            'client_id' => $cliente->id,
            'license_token_id' => $token->id,
            'active' => true,
        ]);
    }

    private function llamar(array $extra = []): \Illuminate\Testing\TestResponse
    {
        return $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', array_merge([
            'action' => 'products',
            'host' => self::DOMAIN,
            'plugin' => 'local_tresipunt',
            'version' => '2026090301',
            'site' => [
                'version' => '2025100601.03',
                'release' => '5.1.1+ (Build: 20251219)',
                'type' => 'moodle',
                'env' => 'local',
            ],
        ], $extra));
    }

    public function test_una_respuesta_correcta_no_guarda_el_cuerpo(): void
    {
        // **El límite que justifica todo el diseño.** `scss`, `js` y `setup` devuelven
        // ficheros: guardar sus cuerpos multiplicaría la tabla por el peso del contenido
        // servido, y cuando la respuesta va bien `response_size` ya dice lo que hace falta.
        $this->llamar()->assertOk();

        $peticion = ApiRequestLog::latest('id')->firstOrFail();

        $this->assertSame(Severidad::OK, $peticion->severity);
        $this->assertNull($peticion->response_body, 'una respuesta correcta no guarda cuerpo');
        // Pero su tamaño sí, que es el dato útil de una respuesta que fue bien.
        $this->assertNotNull($peticion->response_size);
    }

    public function test_una_respuesta_fallida_guarda_su_cuerpo(): void
    {
        // Un host que no es de este token: la API responde con su JSON de error, y **ese**
        // es el cuerpo que hace falta para diagnosticar.
        $this->llamar(['host' => 'https://otro-sitio.test'])->assertStatus(401);

        $peticion = ApiRequestLog::latest('id')->firstOrFail();

        $this->assertNotSame(Severidad::OK, $peticion->severity);
        $this->assertNotNull($peticion->response_body, 'la respuesta fallida tiene que quedar guardada');

        // Y es el cuerpo de verdad, no un resumen: se puede leer lo que vio el cliente.
        $this->assertStringContainsString('"code"', $peticion->response_body);
    }

    public function test_el_cuerpo_guardado_va_saneado(): void
    {
        // Se pasa por el mismo redactor que los `headers` y el `payload`. Son errores de
        // nuestra propia API y no llevan credenciales, pero el redactor es barato y la
        // garantía vale más que el ahorro.
        // 422: es lo que devuelve la API cuando la validación rechaza el payload.
        $this->llamar(['action' => 'accion-que-no-existe'])->assertStatus(422);

        $peticion = ApiRequestLog::latest('id')->firstOrFail();

        $this->assertNotNull($peticion->response_body);
        $this->assertStringNotContainsString(self::TOKEN, $peticion->response_body,
            'el token nunca puede acabar en el cuerpo guardado');
    }

    public function test_el_cuerpo_se_recorta_y_se_dice_que_se_ha_recortado(): void
    {
        // El tope es de 2.000 caracteres: un error de la API cabe de sobra, una traza de
        // PHP no —y tampoco hace falta, para eso está `laravel.log`—. Se comprueba sobre
        // el método, porque provocar una respuesta de 2 KB desde la API es montar un caso
        // artificial para probar un `mb_substr`.
        $metodo = new \ReflectionMethod(\App\Http\Middleware\ApiRequestLogger::class, 'cuerpoAGuardar');
        $metodo->setAccessible(true);

        $largo = str_repeat('á', 5000);
        $recortado = $metodo->invoke(null, $largo, 500, null);

        $this->assertStringEndsWith('[recortado]', $recortado);
        // Por caracteres y no por bytes: cortar a mitad de un UTF-8 deja basura que
        // después no se puede ni pintar. Con acentos, 2.000 caracteres son 4.000 bytes.
        $this->assertSame(2000, mb_strlen(str_replace("\n…[recortado]", '', $recortado)));
    }

    public function test_una_respuesta_que_no_es_json_tambien_se_guarda(): void
    {
        // **Es el caso que hoy no dejaba ningún rastro**: si el cuerpo no decodifica como
        // JSON —una excepción que devuelve HTML, un 502 de un proxy—, el logger no podía
        // sacar ni `code` ni `error`, así que de esa petición no quedaba nada.
        $metodo = new \ReflectionMethod(\App\Http\Middleware\ApiRequestLogger::class, 'cuerpoAGuardar');
        $metodo->setAccessible(true);

        $html = '<!DOCTYPE html><title>502 Bad Gateway</title><h1>502</h1>';

        $this->assertSame($html, $metodo->invoke(null, $html, 502, null));
    }
}
