<?php

namespace Tests\Feature\Api\Admin;

use App\Livewire\Forms\LoginForm;
use App\Models\ApiRequestLog;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Support\ApiDeAdministracion;
use Illuminate\Support\Facades\Hash;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;

/**
 * Un test por cada hallazgo de la auditoría de seguridad de la API de administración
 * (PRODSECU-215, 2026-10-30). Si alguno vuelve a fallar, el hueco ha vuelto.
 *
 * Ver `.tresipunt/…/manager/norma-seguridad-api-admin.md`.
 */
class AuditoriaDeSeguridadTest extends ApiDeAdministracionTestCase
{
    // ============ M1: nada de `javascript:` guardado para pintarlo como enlace ============

    public function test_jira_tiene_que_ser_una_url_http(): void
    {
        $this->api('POST', 'clients', [
            'name' => 'X', 'shortname' => 'x', 'jira' => 'javascript:alert(document.cookie)',
        ], $this->token(['admin.clients.write']))
            ->assertStatus(422)
            ->assertJsonValidationErrors('jira');

        $this->assertSame(0, Client::count());
    }

    public function test_un_local_no_admite_un_dominio_que_no_sea_http(): void
    {
        $cliente = $this->cliente();
        $token = $this->token(['admin.environments.write']);

        foreach (['javascript:alert(document.domain)//', 'moodle.test', 'ftp://moodle.test'] as $dominio) {
            $this->api('POST', 'clients/' . $cliente->id . '/environments', [
                'name' => 'X', 'domain' => $dominio, 'env' => 'local', 'version' => '4.5',
            ], $token)->assertStatus(422)->assertJsonValidationErrors('domain');
        }

        // Pasar un `pro` a `local` con un `javascript:` tampoco.
        $entorno = $this->entorno($cliente, 'https://campus.test');
        $this->api('PATCH', 'environments/' . $entorno->id, ['env' => 'local', 'domain' => 'javascript:alert(1)'], $token)
            ->assertStatus(422);

        // `http://` sigue valiendo en los local.
        $this->api('POST', 'clients/' . $cliente->id . '/environments', [
            'name' => 'Mi local', 'domain' => 'http://moodle.test', 'env' => 'local', 'version' => '4.5',
        ], $token)->assertCreated();
    }

    public function test_un_dominio_antiguo_que_no_es_http_no_se_pinta_como_enlace(): void
    {
        $entorno = new Environment(['domain' => 'javascript:alert(1)']);
        $this->assertNull($entorno->enlaceDelDominio());
        $this->assertNull($entorno->enlaceDeLogin());

        $bueno = new Environment(['domain' => 'https://campus.test']);
        $this->assertSame('https://campus.test', $bueno->enlaceDelDominio());
    }

    // ============ B1: escribir exige también leer ============

    public function test_un_token_de_solo_escritura_no_lee_por_la_puerta_de_atras(): void
    {
        $cliente = $this->cliente('ajeno', ['cif' => 'B12345678']);
        $soloEscribe = $this->tokenExacto(['admin.clients.write']);

        // Un PATCH vacío devolvía la ficha completa.
        $this->api('PATCH', 'clients/' . $cliente->id, [], $soloEscribe)->assertForbidden();
        // Y el 409 de un alta duplicada, también.
        $this->api('POST', 'clients', ['name' => 'X', 'shortname' => 'ajeno'], $soloEscribe)->assertForbidden();
    }

    // ============ B2: nada de 500 con traza ============

    public function test_una_pagina_desorbitada_es_422_y_no_500(): void
    {
        $this->api('GET', 'environments?search=x&page=9223372036854775807&per_page=100', [], $this->token(['admin.environments.read']))
            ->assertStatus(422)
            ->assertJsonValidationErrors('page');
    }

    // ============ B4: la API no puede quitar productos ============

    public function test_anadir_productos_no_quita_uno_puesto_desde_el_panel_mientras_tanto(): void
    {
        $a = $this->producto('a');
        $b = $this->producto('b');
        $licencia = $this->licencia($this->cliente());
        $token = $this->token(['admin.license-tokens.write']);

        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', ['products' => [['product_id' => $a->id]]], $token)->assertOk();

        // `b` lo añade el panel; la API, con la lista de antes más nada, no lo toca: 409.
        $licencia->products()->attach($b->id, ['status' => 'active']);
        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', ['products' => [['product_id' => $a->id]]], $token)
            ->assertStatus(409);

        $this->assertEqualsCanonicalizing([$a->id, $b->id], $licencia->products()->pluck('products.id')->all());
    }

    // ============ B5: entradas acotadas ============

    public function test_las_entradas_tienen_tope(): void
    {
        $cliente = $this->cliente();
        $licencia = $this->licencia($cliente);
        $token = $this->token(['admin.license-tokens.write', 'admin.environments.read']);

        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', [
            'products' => array_map(fn ($i) => ['product_id' => $i], range(1, 101)),
        ], $token)->assertStatus(422)->assertJsonValidationErrors('products');

        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', [
            'metadata' => ['grande' => str_repeat('x', 17000)],
        ], $token)->assertStatus(422)->assertJsonValidationErrors('metadata');

        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', [
            'metadata' => array_fill_keys(array_map(fn ($i) => 'k' . $i, range(1, 51)), 1),
        ], $token)->assertStatus(422)->assertJsonValidationErrors('metadata');

        $this->api('GET', 'environments?search=' . str_repeat('a', 101), [], $token)->assertStatus(422);
    }

    // ============ Errores sin tripas ============

    public function test_un_fallo_inesperado_responde_sin_traza(): void
    {
        config(['app.debug' => true]);

        // Un fallo de verdad en mitad de la petición: la tabla de clientes no está.
        \Illuminate\Support\Facades\Schema::drop('clients');

        $respuesta = $this->api('GET', 'clients', [], $this->token(['admin.clients.read']));

        $respuesta->assertStatus(500)->assertJsonStructure(['message', 'request_uuid']);
        $this->assertStringNotContainsString('SQL', $respuesta->getContent());
        $this->assertStringNotContainsString('trace', $respuesta->getContent());
        $this->assertStringNotContainsString('clients', $respuesta->getContent());
    }

    // ============ El usuario de servicio ============

    public function test_el_usuario_de_servicio_no_inicia_sesion_en_el_panel(): void
    {
        $servicio = ApiDeAdministracion::usuarioDeServicio();
        $servicio->forceFill(['password' => Hash::make('conocida')])->save();

        Livewire::test('pages.auth.login')
            ->set('form.email', ApiDeAdministracion::EMAIL_DEL_USUARIO)
            ->set('form.password', 'conocida')
            ->call('login')
            ->assertHasErrors('form.email');

        $this->assertGuest();
    }

    public function test_no_se_adopta_como_usuario_de_servicio_a_una_persona_con_rol(): void
    {
        $persona = User::factory()->create(['email' => strtoupper(ApiDeAdministracion::EMAIL_DEL_USUARIO)]);
        $persona->assignRole(Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']));

        $this->expectException(\RuntimeException::class);
        ApiDeAdministracion::usuarioDeServicio();
    }

    public function test_el_usuario_de_servicio_se_reconoce_sin_distinguir_mayusculas(): void
    {
        $usuario = new User(['email' => strtoupper(ApiDeAdministracion::EMAIL_DEL_USUARIO)]);

        $this->assertTrue(ApiDeAdministracion::esUsuarioDeServicio($usuario));
    }

    // ============ Credenciales fuera de los logs ============

    public function test_un_token_puesto_en_la_url_no_queda_en_claro_en_el_log(): void
    {
        $this->api('GET', 'types?token=secreto-en-la-url', [], $this->token(['admin.catalogs.read']));

        $fila = ApiRequestLog::latest('id')->firstOrFail();
        $this->assertStringNotContainsString('secreto-en-la-url', json_encode($fila->query));
    }
}
