<?php

namespace Tests\Feature\Api\Admin;

use App\Models\Monitoring\Log;
use App\Models\Products\LicenseToken;
use App\Models\Products\LicenseTokenProduct;
use App\Support\ApiDeAdministracion;

/**
 * Licencias y sus productos en la API de administración (PRODSECU-215).
 */
class LicenciasTest extends ApiDeAdministracionTestCase
{
    /** El token solo sale en `GET /license-tokens/{id}`: ni en el alta ni en el listado. */
    public function test_el_token_solo_sale_al_pedir_la_licencia(): void
    {
        $cliente = $this->cliente();
        $token = $this->token(['admin.license-tokens.write', 'admin.license-tokens.read']);

        $respuesta = $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', [
            'name' => 'Universidad de Ejemplo — Moodle',
            'start_at' => '2026-11-01',
            'end_at' => '2027-10-31',
            // Se ignoran:
            'token' => '3IP-PUESTO-POR-MI',
            'active' => false,
            'client_id' => 999,
        ], $token);

        $respuesta->assertCreated()
            ->assertJsonPath('data.client_id', $cliente->id)
            ->assertJsonPath('data.active', true)
            ->assertJsonPath('data.start_at', '2026-11-01')
            ->assertJsonPath('data.end_at', '2027-10-31')
            ->assertJsonPath('data.environment_ids', [])
            ->assertJsonPath('data.products', [])
            ->assertJsonMissingPath('data.token');

        $licencia = LicenseToken::firstOrFail();
        $this->assertMatchesRegularExpression('/^3IP-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}$/', $licencia->token);
        $this->assertStringNotContainsString($licencia->token, $respuesta->getContent());
        $this->assertSame(ApiDeAdministracion::usuarioDeServicio()->id, $licencia->created_by);

        $listado = $this->api('GET', 'license-tokens', [], $token);
        $listado->assertOk()->assertJsonCount(1, 'data')->assertJsonMissingPath('data.0.token');
        $this->assertStringNotContainsString($licencia->token, $listado->getContent());

        // La única puerta, y sin caché: lleva una credencial.
        $ver = $this->api('GET', 'license-tokens/' . $licencia->id, [], $token)
            ->assertOk()
            ->assertJsonPath('data.token', $licencia->token);
        $this->assertStringContainsString('no-store', (string) $ver->headers->get('Cache-Control'));

        $this->assertSame(1, Log::where('code', '16050')->count());
    }

    /** Las rutas que devolvían el token desde un entorno o un dominio ya no existen. */
    public function test_no_hay_otras_puertas_al_token(): void
    {
        $cliente = $this->cliente();
        $licencia = $this->licencia($cliente);
        $entorno = $this->entorno($cliente, 'https://campus.uejemplo.es', ['license_token_id' => $licencia->id]);
        $token = $this->token(['admin.license-tokens.read', 'admin.environments.read']);

        $this->api('GET', 'environments/' . $entorno->id . '/license-token', [], $token)->assertNotFound();
        $this->api('GET', 'license-tokens/by-domain?domain=' . urlencode('https://campus.uejemplo.es'), [], $token)->assertNotFound();

        // Desde el entorno se llega al id, y con el id, al token.
        $this->api('GET', 'environments?domain=' . urlencode('https://campus.uejemplo.es/'), [], $token)
            ->assertJsonPath('data.0.license_token_id', $licencia->id)
            ->assertJsonMissingPath('data.0.token');
    }

    public function test_no_se_crean_licencias_en_un_cliente_de_baja(): void
    {
        $cliente = $this->cliente('debaja', ['actived' => false]);

        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', ['name' => 'X'], $this->token(['admin.license-tokens.write']))
            ->assertStatus(409);

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

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

        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', [
            'name' => 'X', 'start_at' => '2027-01-01', 'end_at' => '2026-01-01',
        ], $token)->assertStatus(422)->assertJsonStructure(['errors' => ['end_at']]);

        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', [
            'name' => 'X', 'start_at' => '01/01/2027',
        ], $token)->assertStatus(422)->assertJsonStructure(['errors' => ['start_at']]);
    }

    public function test_filtrar_licencias(): void
    {
        $uno = $this->cliente('uno');
        $dos = $this->cliente('dos');
        $this->licencia($uno, '3IP-A');
        $this->licencia($dos, '3IP-B')->update(['active' => false]);

        $token = $this->token(['admin.license-tokens.read']);

        $this->api('GET', 'license-tokens?client_id=' . $uno->id, [], $token)->assertJsonCount(1, 'data');
        $this->api('GET', 'license-tokens?active=false', [], $token)->assertJsonCount(1, 'data')->assertJsonPath('data.0.client_id', $dos->id);
        $this->api('GET', 'license-tokens?active=quizas', [], $token)->assertStatus(422);
    }

    public function test_editar_licencia_no_toca_active_ni_el_token(): void
    {
        $licencia = $this->licencia($this->cliente());

        $this->api('PATCH', 'license-tokens/' . $licencia->id, [
            'name' => 'Nuevo nombre',
            'end_at' => '2030-01-01',
            'active' => false,
            'token' => '3IP-OTRO',
        ], $this->token(['admin.license-tokens.write']))
            ->assertOk()
            ->assertJsonPath('data.name', 'Nuevo nombre')
            ->assertJsonPath('data.end_at', '2030-01-01')
            ->assertJsonMissingPath('data.token');

        $licencia->refresh();
        $this->assertTrue($licencia->active);
        $this->assertSame('3IP-TEST-0000-0000-0001', $licencia->token);
    }

    public function test_fijar_productos_es_idempotente(): void
    {
        $moodle = $this->tipo('moodle');
        $a = $this->producto('a', [$moodle]);
        $b = $this->producto('b', [$moodle]);
        $cliente = $this->cliente();
        $licencia = $this->licencia($cliente);
        $this->entorno($cliente, 'https://campus.test', ['type_id' => $moodle->id, 'license_token_id' => $licencia->id]);

        $token = $this->token(['admin.license-tokens.write']);
        $cuerpo = ['products' => [['product_id' => $a->id], ['product_id' => $b->id]]];

        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', $cuerpo, $token)
            ->assertOk()
            ->assertJsonCount(2, 'data.products')
            ->assertJsonPath('data.products.0.slug', 'a')
            ->assertJsonPath('data.products.0.status', 'active');

        $filas = LicenseTokenProduct::count();
        $logs = Log::count();

        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', $cuerpo, $token)
            ->assertOk()
            ->assertJsonCount(2, 'data.products');

        $this->assertSame($filas, LicenseTokenProduct::count());
        $this->assertSame($logs, Log::count(), 'Repetir la petición no escribe logs');

        $fila = LicenseTokenProduct::firstOrFail();
        $this->assertSame(ApiDeAdministracion::usuarioDeServicio()->id, $fila->assigned_by);
    }

    /** Quitar un producto corta servicio: por la API es 409 y no se cambia nada. */
    public function test_no_se_pueden_quitar_productos_por_la_api(): void
    {
        $a = $this->producto('a');
        $b = $this->producto('b');
        $c = $this->producto('c');
        $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], ['product_id' => $b->id]]], $token)->assertOk();

        // Falta `b` y se pide `c`: ni se quita `b` ni se añade `c`.
        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', ['products' => [['product_id' => $a->id], ['product_id' => $c->id]]], $token)
            ->assertStatus(409)
            ->assertJsonPath('message', 'Quitar productos de una licencia se hace desde el panel. Faltan en la petición: b.');

        $this->assertEqualsCanonicalizing([$a->id, $b->id], $licencia->products()->pluck('products.id')->all());
        $this->assertSame(0, Log::where('code', '16021')->count());

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

        // Con los que ya tiene más el nuevo, sí.
        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', ['products' => [['product_id' => $a->id], ['product_id' => $b->id], ['product_id' => $c->id]]], $token)
            ->assertOk()
            ->assertJsonCount(3, 'data.products');
    }

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

        $respuesta = $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', [
            'name' => 'Campus',
            'observation' => 'Alta automática',
            'mode' => 'standard',
            'metadata' => ['origen' => 'sistemas', 'plan' => 'pro'],
            'rate_limit_per_minute' => 120,
            // No se aceptan:
            'token' => '3IP-ELEG-IDOX-XXXX-XXXX',
            'active' => false,
        ], $token)->assertCreated()
            // Lo que se escribe, se devuelve.
            ->assertJsonPath('data.observation', 'Alta automática')
            ->assertJsonPath('data.mode', 'standard')
            ->assertJsonPath('data.metadata', ['origen' => 'sistemas', 'plan' => 'pro'])
            ->assertJsonPath('data.rate_limit_per_minute', 120);

        $licencia = LicenseToken::findOrFail($respuesta->json('data.id'));
        $this->assertSame('Alta automática', $licencia->observation);
        $this->assertSame('standard', $licencia->mode);
        $this->assertSame(['origen' => 'sistemas', 'plan' => 'pro'], $licencia->metadata);
        $this->assertSame(120, (int) $licencia->rate_limit_per_minute);
        $this->assertNotSame('3IP-ELEG-IDOX-XXXX-XXXX', $licencia->token);
        $this->assertTrue($licencia->active);

        // Al editar, el tope no se toca: bajarlo puede cortar servicio y va por el panel.
        $this->api('PATCH', 'license-tokens/' . $licencia->id, ['observation' => 'Otra', 'rate_limit_per_minute' => 1], $token)->assertOk();
        $licencia->refresh();
        $this->assertSame('Otra', $licencia->observation);
        $this->assertSame(120, (int) $licencia->rate_limit_per_minute);

        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', ['name' => 'X', 'metadata' => 'texto', 'rate_limit_per_minute' => 0], $token)
            ->assertStatus(422)
            ->assertJsonValidationErrors(['metadata', 'rate_limit_per_minute']);
    }

    /**
     * Las fechas no pueden cortar el servicio (auditoría PRODSECU-215): `end_at` se guarda
     * como el inicio del día, así que un fin de hoy ya está vencido; y un `start_at` futuro
     * deja el Moodle con «Token aún no válido» en el acto.
     */
    public function test_las_fechas_no_cortan_el_servicio(): void
    {
        $cliente = $this->cliente();
        $licencia = $this->licencia($cliente);
        $token = $this->token(['admin.license-tokens.write']);
        $ayer = now()->subDay()->format('Y-m-d');
        $hoy = now()->format('Y-m-d');

        foreach ([$ayer, $hoy] as $fin) {
            $this->api('PATCH', 'license-tokens/' . $licencia->id, ['end_at' => $fin], $token)
                ->assertStatus(422)
                ->assertJsonValidationErrors('end_at');
        }

        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', ['name' => 'X', 'end_at' => $hoy], $token)->assertStatus(422);

        $this->api('PATCH', 'license-tokens/' . $licencia->id, ['end_at' => now()->addDay()->format('Y-m-d')], $token)->assertOk();

        // `start_at` al editar se ignora: la licencia sigue dando servicio.
        $this->api('PATCH', 'license-tokens/' . $licencia->id, ['start_at' => '2099-01-01'], $token)->assertOk();
        $this->assertNull($licencia->fresh()->start_at);
        $this->assertTrue($licencia->fresh()->isValidNow());

        // Al crear, sí se acepta.
        $this->api('POST', 'clients/' . $cliente->id . '/license-tokens', ['name' => 'Futura', 'start_at' => '2027-01-01'], $token)
            ->assertCreated()
            ->assertJsonPath('data.start_at', '2027-01-01');
    }

    /**
     * El índice único del pivote no incluye `deleted_at`: volver a poner un producto quitado
     * restaura su fila, y una `end_at` pasada se limpia.
     */
    public function test_volver_a_poner_un_producto_quitado_restaura_su_fila(): void
    {
        $a = $this->producto('a');
        $licencia = $this->licencia($this->cliente());
        $fila = LicenseTokenProduct::create([
            'license_token_id' => $licencia->id,
            'product_id' => $a->id,
            'status' => 'active',
            'end_at' => now()->subMonth(),
        ]);
        $fila->delete();

        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', ['products' => [['product_id' => $a->id]]], $this->token(['admin.license-tokens.write']))
            ->assertOk()
            ->assertJsonCount(1, 'data.products')
            ->assertJsonPath('data.products.0.end_at', null);

        $this->assertSame(1, LicenseTokenProduct::withTrashed()->count());
        $this->assertNull($fila->fresh()->deleted_at);
    }

    public function test_un_producto_incompatible_es_422_y_no_cambia_nada(): void
    {
        $moodle = $this->tipo('moodle');
        $workplace = $this->tipo('workplace');
        $compatible = $this->producto('compatible', [$moodle, $workplace]);
        $soloMoodle = $this->producto('solo-moodle', [$moodle]);

        $cliente = $this->cliente();
        $licencia = $this->licencia($cliente);
        $this->entorno($cliente, 'https://lms.test', ['type_id' => $moodle->id, 'license_token_id' => $licencia->id]);
        $this->entorno($cliente, 'https://wp.test', ['type_id' => $workplace->id, 'license_token_id' => $licencia->id]);

        $respuesta = $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', [
            'products' => [['product_id' => $compatible->id], ['product_id' => $soloMoodle->id]],
        ], $this->token(['admin.license-tokens.write']));

        $respuesta->assertStatus(422)->assertJsonStructure(['message', 'errors' => ['products.1.product_id']]);
        $this->assertStringContainsString('workplace', $respuesta->json('errors')['products.1.product_id'][0]);
        $this->assertSame(0, LicenseTokenProduct::count());
    }

    public function test_un_producto_no_vendible_no_se_puede_anadir(): void
    {
        $apagado = $this->producto('apagado', [], ['enabled' => false]);
        $licencia = $this->licencia($this->cliente());

        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', ['products' => [['product_id' => $apagado->id]]], $this->token(['admin.license-tokens.write']))
            ->assertStatus(422);

        $this->api('PUT', 'license-tokens/' . $licencia->id . '/products', ['products' => [['product_id' => 99999]]], $this->token(['admin.license-tokens.write']))
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['products.0.product_id']]);
    }
}
