<?php

namespace Tests\Feature\LicenseTokens;

use App\Livewire\LicenseTokens\Show;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Products\LicenseToken;
use App\Models\Products\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * La vigencia de cada producto dentro de una licencia, a la vista.
 *
 * **Hay dos fechas y solo se veía una.** La de la licencia (`license_tokens.end_at`) y la
 * de cada producto dentro de ella (`license_token_product.end_at`). El middleware corta
 * por las dos, y con mensajes distintos:
 *
 * - por la de la licencia → «Token expirado»
 * - por la del producto → «Licence does not include this plugin» (código 2001)
 *
 * Las del producto **no aparecían en ninguna pantalla** (`grep license_token_product
 * resources/views` no devolvía nada), así que un producto caducado dentro de una licencia
 * vigente cortaba el servicio de ese plugin con un mensaje que apunta a otro sitio —a los
 * productos contratados, no a las fechas—. Y no avisa nadie: `licences:check-expiry` mira
 * solo la fecha de la licencia.
 *
 * Además importa para las renovaciones: en la base local **11 de 13 filas** traen fecha
 * propia (vienen de la importación del Manager antiguo), y renovar la licencia cambiando
 * su `end_at` no toca esas fechas.
 */
class ProductValidityTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(): User
    {
        foreach (['admin.license-tokens.index', 'admin.license-tokens.edit'] as $nombre) {
            Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'license_tokens', 'description' => $nombre]
            );
        }

        $user = User::factory()->create();
        $user->givePermissionTo(['admin.license-tokens.index', 'admin.license-tokens.edit']);
        $this->actingAs($user);
        Cache::flush();

        return $user;
    }

    private function licencia(): LicenseToken
    {
        $cliente = Client::create([
            'name' => 'Cliente Vigencias',
            'shortname' => 'vig' . uniqid(),
            'actived' => true,
        ]);

        return LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia de prueba',
            'token' => '3IP-VIG-' . strtoupper(uniqid()),
            'active' => true,
            'start_at' => now()->subMonths(2),
            'end_at' => now()->addMonths(6),
        ]);
    }

    private function producto(string $slug): Product
    {
        return Product::create([
            'name' => 'Producto ' . $slug,
            'slug' => $slug,
            'shortname' => substr($slug, 0, 20),
        ]);
    }

    /** @return array<string, mixed> */
    private function vigencias(LicenseToken $licencia): array
    {
        return Livewire::test(Show::class, ['license_token' => $licencia])->viewData('vigencias');
    }

    /* ===================== Los estados ===================== */

    public function test_un_producto_sin_fecha_propia_vale_lo_que_la_licencia(): void
    {
        // Es como nacen los productos añadidos desde el panel: el asistente y la
        // pantalla de la licencia solo escriben `status` y `assigned_by`.
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->products()->attach($this->producto('block_uno')->id, ['status' => 'active']);

        $vigencia = $this->vigencias($licencia)[0];

        $this->assertSame('sin_fecha', $vigencia['estado']);
        $this->assertStringContainsString('mientras valga la licencia', $vigencia['texto']);
    }

    public function test_un_producto_vigente_dice_hasta_cuando(): void
    {
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->products()->attach($this->producto('block_dos')->id, [
            'status' => 'active',
            'start_at' => now()->subMonth(),
            'end_at' => $licencia->end_at,
        ]);

        $vigencia = $this->vigencias($licencia)[0];

        $this->assertSame('vigente', $vigencia['estado']);
    }

    public function test_un_producto_caducado_en_una_licencia_vigente_se_ve(): void
    {
        // El caso que cortaba servicio en silencio.
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->products()->attach($this->producto('block_tres')->id, [
            'status' => 'active',
            'end_at' => now()->subDays(20),
        ]);

        $vigencia = $this->vigencias($licencia)[0];

        $this->assertSame('caducado', $vigencia['estado']);
        $this->assertStringContainsString('20 día(s)', $vigencia['texto']);
    }

    public function test_un_producto_que_caduca_antes_que_la_licencia_se_avisa(): void
    {
        // Todavía funciona, pero va a dejar de hacerlo antes que el resto: es lo que
        // convierte una renovación aparentemente correcta en un corte.
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->products()->attach($this->producto('block_cuatro')->id, [
            'status' => 'active',
            'end_at' => now()->addDays(10),
        ]);

        $vigencia = $this->vigencias($licencia)[0];

        $this->assertSame('antes', $vigencia['estado']);
        $this->assertStringContainsString('antes que la licencia', $vigencia['texto']);
    }

    public function test_un_producto_desactivado_en_el_pivote_se_ve(): void
    {
        // `status` distinto de `active` también corta, y tampoco se veía.
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->products()->attach($this->producto('block_cinco')->id, [
            'status' => 'suspended',
            'end_at' => now()->addYear(),
        ]);

        $vigencia = $this->vigencias($licencia)[0];

        $this->assertSame('inactivo', $vigencia['estado']);
    }

    /* ===================== La pantalla lo dice ===================== */

    public function test_la_pantalla_avisa_de_los_productos_desalineados(): void
    {
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->products()->attach($this->producto('block_seis')->id, [
            'status' => 'active',
            'end_at' => now()->subDay(),
        ]);

        Livewire::test(Show::class, ['license_token' => $licencia])
            ->assertSee('no siguen la fecha de la licencia')
            // Y explica el mensaje que recibe el Moodle, que es el que despista.
            ->assertSee('Licence does not include this plugin')
            // Y que renovar la licencia no lo arregla.
            ->assertSee('Renovar la licencia no cambia estas');
    }

    public function test_sin_desalineados_no_se_mete_miedo(): void
    {
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->products()->attach($this->producto('block_siete')->id, ['status' => 'active']);

        Livewire::test(Show::class, ['license_token' => $licencia])
            ->assertDontSee('no siguen la fecha de la licencia');
    }
    /* ===================== Renovación ===================== */

    private function renovar(LicenseToken $licencia, int $meses): void
    {
        Livewire::test(Show::class, ['license_token' => $licencia])
            ->call('pedirRenovacion', $meses)
            // El camino real: la modal dispara el evento con un array. Ver MGR-042.
            ->dispatch('licenceRenewConfirmed', ['itemId' => $meses]);
    }

    public function test_renovar_una_licencia_viva_suma_desde_su_fecha_de_fin(): void
    {
        // Renovar un mes antes de caducar no debe regalar ni quitar tiempo: contar desde
        // hoy le robaría ese mes al cliente.
        $this->usuario();
        $licencia = $this->licencia();
        $finAnterior = $licencia->end_at->copy();

        $this->renovar($licencia, 12);

        $this->assertSame(
            $finAnterior->addMonths(12)->format('Y-m-d'),
            $licencia->fresh()->end_at->format('Y-m-d')
        );
    }

    public function test_renovar_una_licencia_caducada_cuenta_desde_hoy(): void
    {
        // Si contara desde su fecha de fin, «renovar 1 año» sobre una licencia vencida
        // hace tres meses dejaría nueve.
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->update(['end_at' => now()->subMonths(3), 'active' => false]);

        $this->renovar($licencia, 12);

        $this->assertSame(
            now()->addMonths(12)->format('Y-m-d'),
            $licencia->fresh()->end_at->format('Y-m-d')
        );
    }

    public function test_renovar_reactiva_la_licencia(): void
    {
        // `licences:check-expiry` la desactiva al caducar, y el middleware mira `active`
        // ANTES que las fechas: renovar solo la fecha dejaba al cliente cortado con
        // «Token inactivo».
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->update(['end_at' => now()->subDay(), 'active' => false]);

        $this->renovar($licencia, 6);

        $this->assertTrue($licencia->fresh()->active);
    }

    public function test_renovar_alinea_los_productos_que_caducaban_antes(): void
    {
        // El caso de las licencias importadas: 11 de 13 filas traen fecha propia, y
        // renovar solo la licencia las dejaba caducadas cortando ese plugin.
        $this->usuario();
        $licencia = $this->licencia();
        $producto = $this->producto('block_renovar');
        $licencia->products()->attach($producto->id, [
            'status' => 'active',
            'end_at' => now()->subDays(10),
        ]);

        $this->renovar($licencia, 12);

        $licencia->refresh();
        $pivot = $licencia->products()->where('products.id', $producto->id)->first()->pivot;

        $this->assertSame(
            $licencia->end_at->format('Y-m-d'),
            \Carbon\Carbon::parse($pivot->end_at)->format('Y-m-d')
        );
    }

    public function test_renovar_no_pone_fecha_a_un_producto_que_no_la_tenia(): void
    {
        // Sin fecha propia = vale mientras valga la licencia. Ponerle una sería
        // restringirlo, no renovarlo.
        $this->usuario();
        $licencia = $this->licencia();
        $producto = $this->producto('block_sinfecha');
        $licencia->products()->attach($producto->id, ['status' => 'active']);

        $this->renovar($licencia, 12);

        $pivot = $licencia->fresh()->products()->where('products.id', $producto->id)->first()->pivot;

        $this->assertNull($pivot->end_at);
    }

    public function test_renovar_no_acorta_un_producto_que_iba_mas_lejos(): void
    {
        // Una renovación no puede quitar servicio.
        $this->usuario();
        $licencia = $this->licencia();
        $producto = $this->producto('block_maslejos');
        $masLejos = now()->addYears(3)->format('Y-m-d');
        $licencia->products()->attach($producto->id, ['status' => 'active', 'end_at' => $masLejos]);

        $this->renovar($licencia, 6);

        $pivot = $licencia->fresh()->products()->where('products.id', $producto->id)->first()->pivot;

        $this->assertSame($masLejos, \Carbon\Carbon::parse($pivot->end_at)->format('Y-m-d'));
    }

    public function test_la_renovacion_queda_registrada_con_las_dos_fechas(): void
    {
        $this->usuario();
        $licencia = $this->licencia();
        $finAnterior = $licencia->end_at->format('d/m/Y');

        $this->renovar($licencia, 12);

        $mensaje = \App\Models\Monitoring\Log::where('code', '16018')->value('msg');

        $this->assertStringContainsString($finAnterior, $mensaje);
        $this->assertStringContainsString($licencia->fresh()->end_at->format('d/m/Y'), $mensaje);
    }

    public function test_renovar_rearma_el_aviso_de_caducidad(): void
    {
        // La referencia del aviso es la fecha de fin, así que al renovar ningún aviso
        // viejo casa y el ciclo empieza de nuevo. Sin `reset()` que nadie tiene que
        // acordarse de llamar.
        $this->usuario();
        $licencia = $this->licencia();
        $licencia->update(['end_at' => now()->addDays(5)]);

        \App\Models\Monitoring\Notice::registrar(
            \App\Models\Monitoring\Notice::SUBJECT_LICENSE_TOKEN,
            $licencia->id,
            \App\Models\Monitoring\Notice::KIND_LICENCE_EXPIRING,
            10,
            $licencia->end_at->format('Y-m-d'),
            ['comercial@tresipunt.test']
        );

        $this->renovar($licencia, 12);

        $this->assertFalse(\App\Models\Monitoring\Notice::yaEnviado(
            \App\Models\Monitoring\Notice::SUBJECT_LICENSE_TOKEN,
            $licencia->id,
            \App\Models\Monitoring\Notice::KIND_LICENCE_EXPIRING,
            10,
            $licencia->fresh()->end_at->format('Y-m-d')
        ));
    }

    public function test_un_periodo_que_no_se_ofrece_no_se_aplica(): void
    {
        // `pedirRenovacion` y `renovar` llegan del navegador: un 999 por ahí no puede
        // acabar en la base.
        $this->usuario();
        $licencia = $this->licencia();
        $fin = $licencia->end_at->format('Y-m-d');

        $this->renovar($licencia, 999);

        $this->assertSame($fin, $licencia->fresh()->end_at->format('Y-m-d'));
    }

    public function test_sin_permiso_no_se_renueva(): void
    {
        // MGR-005: el método viaja por /livewire/update.
        Permission::firstOrCreate(
            ['name' => 'admin.license-tokens.index', 'guard_name' => 'web'],
            ['model' => 'license_tokens', 'description' => 'ver']
        );
        $user = User::factory()->create();
        $user->givePermissionTo('admin.license-tokens.index');
        $this->actingAs($user);
        Cache::flush();

        $licencia = $this->licencia();
        $fin = $licencia->end_at->format('Y-m-d');

        Livewire::test(Show::class, ['license_token' => $licencia])
            ->call('pedirRenovacion', 12)
            ->assertForbidden();

        $this->assertSame($fin, $licencia->fresh()->end_at->format('Y-m-d'));
    }
    /* ===================== Quitar un producto ===================== */

    /** @return array{0: LicenseToken, 1: Product} */
    private function conProducto(string $slug = 'block_quitar'): array
    {
        $licencia = $this->licencia();
        $producto = $this->producto($slug);
        $licencia->products()->attach($producto->id, ['status' => 'active', 'assigned_by' => null]);

        return [$licencia, $producto];
    }

    private function modal(LicenseToken $licencia, Product $producto)
    {
        return Livewire::test(\App\Livewire\LicenseTokens\ProductRemovalModal::class, [
            'licenseTokenId' => $licencia->id,
            'productId' => $producto->id,
        ]);
    }

    private function pivote(LicenseToken $licencia, Product $producto): ?\App\Models\Products\LicenseTokenProduct
    {
        return \App\Models\Products\LicenseTokenProduct::withTrashed()
            ->where('license_token_id', $licencia->id)
            ->where('product_id', $producto->id)
            ->first();
    }

    public function test_el_interruptor_ya_no_quita_de_un_clic(): void
    {
        // Era un toggle: un clic y el plugin dejaba de funcionar en TODOS los entornos de
        // la licencia, sin confirmación y sin decir a cuántos.
        $this->usuario();
        [$licencia, $producto] = $this->conProducto();

        Livewire::test(Show::class, ['license_token' => $licencia])
            ->call('toggleProduct', $producto->id)
            ->assertDispatched('openModal');

        $this->assertTrue($licencia->fresh()->products->contains('id', $producto->id));
    }

    public function test_quitar_ahora_es_un_borrado_en_blando(): void
    {
        // La tabla tiene `deleted_at` y el modelo `SoftDeletes`, y `detach()` hacía un
        // DELETE de verdad: se iban las fechas, el `observation` y el `assigned_by`.
        $this->usuario();
        [$licencia, $producto] = $this->conProducto();

        $this->modal($licencia, $producto)->call('quitarAhora');

        $this->assertSoftDeleted('license_token_product', [
            'license_token_id' => $licencia->id,
            'product_id' => $producto->id,
        ]);
        $this->assertDatabaseHas('logs', ['code' => '16021']);
    }

    public function test_un_producto_quitado_deja_de_servirse_por_la_api(): void
    {
        // Es lo que hace que el borrado en blando signifique algo: `belongsToMany` no sabe
        // nada de `deleted_at`, así que sin el filtro en la relación el producto seguiría
        // dando servicio —peor que no tener borrado en blando—.
        $this->usuario();
        [$licencia, $producto] = $this->conProducto();

        $this->assertTrue($licencia->products()->where('products.id', $producto->id)->exists());

        $this->modal($licencia, $producto)->call('quitarAhora');

        $this->assertFalse($licencia->fresh()->products()->where('products.id', $producto->id)->exists());
    }

    public function test_quitar_al_final_del_periodo_le_pone_fecha_de_fin(): void
    {
        // Lo que pide un cliente que ha pagado hasta diciembre y no renueva. El middleware
        // ya respetaba `end_at` del pivote: era posible en los datos e imposible en la
        // pantalla.
        $this->usuario();
        [$licencia, $producto] = $this->conProducto();

        $this->modal($licencia, $producto)->call('quitarAlFinal');

        $pivot = $licencia->fresh()->products()->where('products.id', $producto->id)->first()->pivot;

        // Sigue vinculado, y con la fecha de fin de la licencia.
        $this->assertSame(
            $licencia->end_at->format('Y-m-d'),
            \Carbon\Carbon::parse($pivot->end_at)->format('Y-m-d')
        );
    }

    public function test_sin_fecha_futura_no_se_ofrece_quitar_al_final(): void
    {
        // Sin fecha no se sabría qué poner, así que la pantalla lo explica en vez de
        // ofrecer un botón que no funcionaría.
        $this->usuario();
        [$licencia, $producto] = $this->conProducto();
        $licencia->update(['end_at' => null]);

        $componente = $this->modal($licencia->fresh(), $producto);

        $this->assertNull($componente->viewData('finDelPeriodo'));
        $componente->assertSee('No se puede ofrecer');
    }

    public function test_volver_a_anadirlo_recupera_su_fila(): void
    {
        // El índice único `(license_token_id, product_id)` no incluye `deleted_at`, así que
        // no puede haber dos filas para el mismo par: hay que restaurar, y un
        // `syncWithoutDetaching` reventaría contra el índice.
        $this->usuario();
        [$licencia, $producto] = $this->conProducto();

        $idFila = $this->pivote($licencia, $producto)->id;

        $this->modal($licencia, $producto)->call('quitarAhora');

        Livewire::test(Show::class, ['license_token' => $licencia->fresh()])
            ->call('toggleProduct', $producto->id);

        $fila = $this->pivote($licencia, $producto);

        // La MISMA fila, no una nueva.
        $this->assertSame($idFila, $fila->id);
        $this->assertNull($fila->deleted_at);
    }

    public function test_un_producto_que_vuelve_no_vuelve_caducado(): void
    {
        // Restaurar con una `end_at` pasada lo dejaría «activo y caducado», el estado que
        // evitamos en las renovaciones.
        $this->usuario();
        [$licencia, $producto] = $this->conProducto();

        $fila = $this->pivote($licencia, $producto);
        $fila->end_at = now()->subMonths(2);
        $fila->save();
        $fila->delete();

        Livewire::test(Show::class, ['license_token' => $licencia->fresh()])
            ->call('toggleProduct', $producto->id);

        $this->assertNull($this->pivote($licencia, $producto)->end_at);
    }

    public function test_sin_permiso_no_se_quita_un_producto(): void
    {
        // MGR-005: el método viaja por /livewire/update y esto corta servicio.
        Permission::firstOrCreate(
            ['name' => 'admin.license-tokens.index', 'guard_name' => 'web'],
            ['model' => 'license_tokens', 'description' => 'ver']
        );
        $user = User::factory()->create();
        $user->givePermissionTo('admin.license-tokens.index');
        $this->actingAs($user);
        Cache::flush();

        [$licencia, $producto] = $this->conProducto();

        $this->modal($licencia, $producto)->assertForbidden();

        $this->assertTrue($licencia->fresh()->products->contains('id', $producto->id));
    }
}
