<?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 Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;

/**
 * Un error de la API deja rastro fuera de la base (MGR-003).
 *
 * **El síntoma que lo abrió**: un Moodle recibió cuatro `401 Token no válido` seguidos y
 * `tail -f` no enseñó **ni una línea**. El único rastro estaba en `api_request_logs`, o sea
 * en el visor del panel: diagnosticar una integración obligaba a entrar al panel.
 *
 * Y lo que hay que proteger con la misma fuerza es **lo que NO se escribe**. Las otras tres
 * severidades son respuestas correctas —`sin_contenido`, `negocio`, y los códigos `*002` que
 * no son averías (`norma-cero-errores.md`)—. Si el fichero se llena de tráfico normal, nadie
 * lo mira, y «no hay rastro» se convierte en «el rastro no se puede leer», que es peor
 * porque parece resuelto.
 */
class RastroDelErrorTest extends TestCase
{
    use RefreshDatabase;

    /** Lo que el canal `api` ha recibido durante el test. */
    private array $lineas = [];

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

        $this->lineas = [];

        // Se escucha el canal de verdad: `Log::shouldReceive` no distinguiría el canal, y
        // el canal es justo la decisión que se está probando.
        Log::channel('api')->listen(function ($mensaje) {
            $this->lineas[] = $mensaje;
        });
    }

    /**
     * Un `sync` que el contrato acepta.
     *
     * **Sin `plugin` es un 422**, y eso sí es un error —lo dice el validador—, así que
     * usarlo como «petición correcta» probaba lo contrario de lo que se quería.
     *
     * @return array<string, mixed>
     */
    private function sincronizacionValida(): array
    {
        return [
            'action' => 'sync',
            'host' => 'https://sitio-api.test',
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => [
                'version' => '2025100601.03',
                'release' => '5.1.1+ (Build: 20251219)',
                'type' => 'moodle',
                'env' => 'pro',
            ],
            // Un inventario vacío tampoco vale: `data.plugins` es `required`, y un array
            // vacío no lo cumple. Un sitio real siempre manda al menos el suyo.
            'data' => ['plugins' => [[
                'name' => 'Tresipunt',
                'component' => 'local_tresipunt',
                'type' => 'local',
                'versiondisk' => '2026080604',
                'versiondb' => '2026080604',
                'versionrequires' => 2024100700,
                'release' => '1.0.0',
            ]]],
        ];
    }

    /** @return array{LicenseToken, Environment} */
    private function sitioConLicencia(): array
    {
        $cliente = Client::create(['name' => 'Cliente API', 'shortname' => 'apicli', 'actived' => true]);

        $licencia = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia API',
            'token' => LicenseToken::generarToken(),
            'active' => true,
        ]);

        $entorno = Environment::create([
            'name' => 'Sitio API',
            'domain' => 'https://sitio-api.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
            'license_token_id' => $licencia->id,
        ]);

        return [$licencia, $entorno];
    }

    public function test_un_token_invalido_deja_una_linea_en_el_log(): void
    {
        // **El caso del issue, tal cual**: cuatro rechazos y `tail -f` en blanco.
        $respuesta = $this->postJson('/api/v1', [
            'action' => 'sync',
            'host' => 'https://sitio-api.test',
        ], ['Authorization' => 'Bearer 3IP-0000-0000-0000-0000']);

        $this->assertSame(401, $respuesta->getStatusCode());

        $this->assertCount(1, $this->lineas,
            'un 401 tiene que dejar rastro fuera de `api_request_logs`');

        $this->assertSame('Petición de la API con error', $this->lineas[0]->message);
        $this->assertSame(401, $this->lineas[0]->context['http_status']);
    }

    public function test_la_linea_lleva_el_uuid_que_une_con_la_fila(): void
    {
        // `request_uuid` es el mismo identificador que la API le devuelve al plugin
        // (MGR-007): es lo que permite pasar del `tail -f` al detalle del panel.
        $this->postJson('/api/v1', [
            'action' => 'sync',
            'host' => 'https://sitio-api.test',
        ], ['Authorization' => 'Bearer 3IP-0000-0000-0000-0000']);

        $uuid = $this->lineas[0]->context['request_uuid'] ?? null;

        $this->assertNotNull($uuid);
        $this->assertSame(
            $uuid,
            ApiRequestLog::latest('id')->first()->request_uuid,
            'el uuid del log tiene que ser el de la fila'
        );
    }

    public function test_la_linea_dice_a_quien_llamar_sin_abrir_el_panel(): void
    {
        $this->postJson('/api/v1', [
            'action' => 'sync',
            'host' => 'https://sitio-api.test',
            'plugin' => 'local_tresipunt',
        ], ['Authorization' => 'Bearer 3IP-0000-0000-0000-0000']);

        $contexto = $this->lineas[0]->context;

        foreach (['host', 'action', 'plugin', 'code', 'error', 'duration_ms'] as $clave) {
            $this->assertArrayHasKey($clave, $contexto, "falta `$clave` en la línea");
        }

        $this->assertSame('https://sitio-api.test', $contexto['host']);
        $this->assertSame('sync', $contexto['action']);
    }

    public function test_una_peticion_correcta_no_escribe_nada(): void
    {
        // **Lo que no se escribe importa igual.** Si el fichero se llena de tráfico
        // normal, deja de servir para diagnosticar.
        [$licencia] = $this->sitioConLicencia();

        $this->postJson('/api/v1', $this->sincronizacionValida(),
            ['Authorization' => 'Bearer ' . $licencia->token]);

        $this->assertSame([], $this->lineas,
            'una petición correcta no puede ensuciar el log del servidor');
    }

    public function test_sin_contenido_y_negocio_no_son_errores_y_no_se_escriben(): void
    {
        // Las dos severidades que la norma cero errores dice que son respuestas
        // correctas. Se comprueba sobre la fila, que es quien decide la severidad.
        foreach ([
            ['http_status' => 200, 'error_code' => 3002, 'error' => 'Sin contenido'],
            ['http_status' => 403, 'error_code' => 3001, 'error' => 'Sin licencia'],
        ] as $caso) {
            $this->lineas = [];

            ApiRequestLog::create(array_merge([
                'request_uuid' => (string) \Illuminate\Support\Str::uuid(),
                'method' => 'POST',
                'path' => 'api/v1',
                'auth_type' => 'bearer',
                'action' => 'features',
                'duration_ms' => 12,
                'started_at' => now(),
                'ended_at' => now(),
            ], $caso));

            $this->assertSame([], $this->lineas,
                'código ' . $caso['error_code'] . ': es una respuesta correcta, no un error');
        }
    }

    public function test_si_no_se_puede_escribir_el_log_la_api_responde_igual(): void
    {
        // Registrar no puede tumbar una respuesta: la fila ya está guardada, que es lo
        // que no se puede perder.
        [$licencia] = $this->sitioConLicencia();

        Log::shouldReceive('channel')->andThrow(new \RuntimeException('disco lleno'));
        Log::shouldReceive('error')->andReturnNull();

        $respuesta = $this->postJson('/api/v1', $this->sincronizacionValida(),
            ['Authorization' => 'Bearer ' . $licencia->token]);

        $this->assertLessThan(500, $respuesta->getStatusCode(),
            'un fallo al escribir el log no puede convertirse en un 500 para el cliente');
        $this->assertDatabaseCount('api_request_logs', 1);
    }
}
