<?php

namespace Tests\Feature\Clients;

use App\Livewire\Clients\ClientStatusModal;
use App\Mail\LicenceExpiryAlert;
use App\Mail\SupportExpiryAlert;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Products\LicenseToken;
use App\Models\Support\EnvironmentSupport;
use App\Models\Support\Support;
use App\Services\System\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * La baja de un cliente.
 *
 * **Lo que había.** Una casilla `actived` dentro del formulario del cliente, guardada
 * como cualquier otro campo. Desmarcarla hace que el middleware devuelva «Cliente
 * inactivo» (403) a todas sus licencias —sus Moodles dejan de recibir contenido— y no
 * pasaba nada más:
 *
 * - Las licencias y los entornos seguían marcados como **activos** en el panel: la ficha
 *   decía «activa» de algo cortado.
 * - **No quedaba rastro**: ni quién, ni cuándo, ni por qué.
 * - Los avisos de caducidad de licencias y de soportes **seguían enviándose**, porque
 *   ninguna de las dos tareas miraba el cliente. Comercial recibía correos de clientes
 *   que ya no lo eran.
 * - Y un borrado en blando dejaba todo apuntando a un cliente invisible, porque las
 *   cascadas del esquema (`cascadeOnDelete`, `nullOnDelete`) **no se disparan con
 *   SoftDeletes**.
 */
class ClientDeactivationTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(bool $conPermiso = true): User
    {
        foreach (['admin.clients.index', 'admin.clients.edit'] as $nombre) {
            Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'clients', 'description' => $nombre]
            );
        }

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

        return $user;
    }

    /** @return array{0: Client, 1: LicenseToken, 2: Environment} */
    private function clienteCompleto(?string $finLicencia = null): array
    {
        $cliente = Client::create([
            'name' => 'Cliente que se va',
            'shortname' => 'baja' . uniqid(),
            'actived' => true,
        ]);

        $licencia = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia del cliente',
            'token' => '3IP-BAJA-' . strtoupper(uniqid()),
            'active' => true,
            'start_at' => now()->subMonth(),
            'end_at' => $finLicencia ?? now()->addYear(),
        ]);

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

        return [$cliente, $licencia, $entorno];
    }

    /* ===================== La baja ===================== */

    public function test_la_baja_desactiva_las_licencias_y_los_entornos(): void
    {
        $this->usuario();
        [$cliente, $licencia, $entorno] = $this->clienteCompleto();

        Livewire::test(ClientStatusModal::class, ['clientId' => $cliente->id])
            ->set('motivo', 'Fin de contrato en diciembre')
            ->call('darDeBaja')
            ->assertHasNoErrors();

        $this->assertFalse((bool) $cliente->fresh()->actived);
        // Lo que faltaba: el panel decía «activa» de algo que la API ya cortaba.
        $this->assertFalse((bool) $licencia->fresh()->active);
        $this->assertFalse((bool) $entorno->fresh()->active);
    }

    public function test_la_baja_queda_registrada_con_quien_cuando_y_por_que(): void
    {
        $usuario = $this->usuario();
        [$cliente] = $this->clienteCompleto();

        Livewire::test(ClientStatusModal::class, ['clientId' => $cliente->id])
            ->set('motivo', 'Impago de tres facturas')
            ->call('darDeBaja');

        $cliente->refresh();

        $this->assertNotNull($cliente->deactivated_at);
        $this->assertSame($usuario->id, $cliente->deactivated_by);
        $this->assertSame('Impago de tres facturas', $cliente->deactivation_reason);

        // Y en el log del panel, con el impacto.
        $mensaje = \App\Models\Monitoring\Log::where('code', '16019')->value('msg');
        $this->assertStringContainsString('dado de BAJA', $mensaje);
        $this->assertStringContainsString('Impago de tres facturas', $mensaje);
    }

    public function test_el_motivo_es_obligatorio(): void
    {
        // Es lo que se lee seis meses después para saber si el cliente se fue o estaba
        // en una renegociación.
        $this->usuario();
        [$cliente, $licencia] = $this->clienteCompleto();

        Livewire::test(ClientStatusModal::class, ['clientId' => $cliente->id])
            ->set('motivo', '')
            ->call('darDeBaja')
            ->assertHasErrors(['motivo']);

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

    public function test_sin_permiso_no_se_da_de_baja(): void
    {
        // MGR-005: el método viaja por /livewire/update, y esto corta el servicio de un
        // cliente entero.
        $this->usuario(conPermiso: false);
        [$cliente] = $this->clienteCompleto();

        Livewire::test(ClientStatusModal::class, ['clientId' => $cliente->id])
            ->assertForbidden();

        $this->assertTrue((bool) $cliente->fresh()->actived);
    }

    public function test_la_modal_dice_a_cuantos_afecta_antes_de_cortar(): void
    {
        // Lo que hace que alguien lea un aviso es que le diga qué va a pasar y a
        // cuántos, no un «¿estás seguro?».
        $this->usuario();
        [$cliente] = $this->clienteCompleto();

        $impacto = Livewire::test(ClientStatusModal::class, ['clientId' => $cliente->id])
            ->viewData('impacto');

        $this->assertSame(1, $impacto['licencias']);
        $this->assertSame(1, $impacto['entornos']);
    }

    /* ===================== El alta ===================== */

    public function test_el_alta_recupera_lo_que_apago_la_baja(): void
    {
        $this->usuario();
        [$cliente, $licencia, $entorno] = $this->clienteCompleto();

        $cliente->darDeBaja('Prueba', null);
        $cliente->refresh();

        Livewire::test(ClientStatusModal::class, ['clientId' => $cliente->id])
            ->call('reactivar');

        $this->assertTrue((bool) $cliente->fresh()->actived);
        $this->assertTrue((bool) $licencia->fresh()->active);
        $this->assertTrue((bool) $entorno->fresh()->active);
        $this->assertNull($cliente->fresh()->deactivated_at);
    }

    public function test_el_alta_no_enciende_lo_que_ya_estaba_apagado_antes(): void
    {
        // Una licencia que alguien había desactivado a mano antes de la baja tiene que
        // seguir apagada al volver.
        $this->usuario();
        [$cliente, $licencia] = $this->clienteCompleto();

        $otra = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia ya apagada',
            'token' => '3IP-OFF-' . strtoupper(uniqid()),
            'active' => false,
            'end_at' => now()->addYear(),
        ]);

        $cliente->darDeBaja('Prueba', null);
        $cliente->refresh();
        $cliente->reactivar(null);

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

    public function test_el_alta_no_reactiva_una_licencia_caducada_durante_la_baja(): void
    {
        // Reactivarla la dejaría «activa y caducada», que es el estado que la tarea de
        // caducidad existe para evitar —y esa noche mandaría un correo—.
        $this->usuario();
        [$cliente, $licencia] = $this->clienteCompleto(finLicencia: now()->addDays(5)->toDateTimeString());

        $cliente->darDeBaja('Prueba', null);

        // Pasa el tiempo y la licencia vence estando el cliente de baja.
        $licencia->update(['end_at' => now()->subDays(3)]);
        $cliente->refresh();

        $resultado = $cliente->reactivar(null);

        $this->assertTrue((bool) $cliente->fresh()->actived);
        $this->assertFalse((bool) $licencia->fresh()->active);
        $this->assertSame(1, $resultado['caducadas']);
    }

    /* ===================== Los avisos ===================== */

    public function test_no_se_avisa_de_licencias_de_un_cliente_de_baja(): void
    {
        Mail::fake();
        app(Settings::class)->set(Settings::LICENCIAS_EMAIL, 'comercial@tresipunt.test');
        app(Settings::class)->olvidar();

        [$cliente] = $this->clienteCompleto(finLicencia: now()->addDays(5)->toDateTimeString());

        // De alta sí avisa.
        $this->artisan('licences:check-expiry');
        Mail::assertSent(LicenceExpiryAlert::class, 1);

        // Y de baja, no. Antes seguían saliendo: Comercial recibía avisos de clientes
        // que ya no lo eran.
        Mail::fake();
        $cliente->darDeBaja('Se va', null);

        $this->artisan('licences:check-expiry');
        Mail::assertNothingSent();
    }

    public function test_no_se_avisa_de_soportes_de_un_cliente_de_baja(): void
    {
        Mail::fake();
        app(Settings::class)->set(Settings::SOPORTES_EMAIL, 'help@tresipunt.test');
        app(Settings::class)->olvidar();

        [$cliente, , $entorno] = $this->clienteCompleto();

        $tipo = Support::create(['name' => 'Premium', 'shortname' => 'prem', 'description' => 'x']);
        EnvironmentSupport::create([
            'environment_id' => $entorno->id,
            'support_id' => $tipo->id,
            'start_at' => now()->subYear()->format('Y-m-d'),
            'end_at' => now()->addDays(7)->format('Y-m-d'),
        ]);

        $this->artisan('supports:check-expiry');
        Mail::assertSent(SupportExpiryAlert::class, 1);

        Mail::fake();
        $cliente->darDeBaja('Se va', null);

        $this->artisan('supports:check-expiry');
        Mail::assertNothingSent();
    }

    /* ===================== El borrado en blando ===================== */

    public function test_borrar_un_cliente_deja_todo_consistente(): void
    {
        // Las cascadas del esquema (`cascadeOnDelete` en licencias, `nullOnDelete` en
        // entornos) NO se disparan con SoftDeletes: el DELETE nunca llega a la base. Así
        // que un cliente borrado dejaba licencias y entornos **activos** apuntando a un
        // cliente invisible, mientras la API ya devolvía «Cliente inactivo».
        $this->usuario();
        [$cliente, $licencia, $entorno] = $this->clienteCompleto();

        $cliente->delete();

        $this->assertSoftDeleted('clients', ['id' => $cliente->id]);
        $this->assertFalse((bool) $licencia->fresh()->active);
        $this->assertFalse((bool) $entorno->fresh()->active);
        $this->assertNotNull(Client::withTrashed()->find($cliente->id)->deactivated_at);
    }

    public function test_el_listado_de_entornos_aguanta_un_cliente_borrado(): void
    {
        // La tarjeta hacía `$environment->client->name` sin proteger: con el cliente
        // borrado en blando la relación es null y el listado reventaba con «Attempt to
        // read property on null».
        $this->usuario();
        [$cliente, , $entorno] = $this->clienteCompleto();
        $cliente->delete();

        Livewire::test(\App\Livewire\Environments\Components\EnvironmentCard::class, [
            'environment' => $entorno->fresh(),
            'client' => null,
            'showClient' => true,
        ])->assertOk()->assertSee('Cliente borrado');
    }
    /* ===================== Lo que ve el Moodle ===================== */

    public function test_el_moodle_recibe_cliente_inactivo_y_no_token_inactivo(): void
    {
        // La baja desactiva también las licencias, así que si el middleware mirara
        // primero el `active` del token respondería "Token inactivo": un motivo que
        // manda a soporte a revisar la licencia cuando el problema es el cliente.
        [$cliente, $licencia] = $this->clienteCompleto();
        $cliente->darDeBaja('Fin de contrato', null);

        $this->withHeaders([
            'Authorization' => 'Bearer ' . $licencia->token,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'products',
            'host' => $cliente->environments()->withoutGlobalScopes()->first()->domain,
            'plugin' => 'local_tresipunt',
        ])->assertForbidden()->assertJsonPath('error', 'Cliente inactivo');
    }
    /* ===================== El listado ===================== */

    public function test_el_listado_muestra_los_clientes_de_baja(): void
    {
        // No se veía: un cliente de baja se distinguía de uno de alta solo entrando en
        // su ficha, y la columna de estado del listado habla de sus LICENCIAS.
        $this->usuario();
        [$cliente] = $this->clienteCompleto();
        $cliente->darDeBaja('Fin de contrato', null);

        Livewire::test(\App\Livewire\Clients\Index::class)
            ->set('estado', 'baja')
            ->assertSee('DE BAJA')
            ->assertSee($cliente->name);
    }

    public function test_el_listado_esconde_los_de_baja_por_defecto(): void
    {
        // El listado es la herramienta del día a día: los que se han ido estorban ahí,
        // pero tienen que poder encontrarse con el filtro.
        $this->usuario();
        [$deAlta] = $this->clienteCompleto();
        [$deBaja] = $this->clienteCompleto();
        $deBaja->update(['name' => 'Cliente ya ido']);
        $deBaja->darDeBaja('Se fue', null);

        Livewire::test(\App\Livewire\Clients\Index::class)
            ->assertSee($deAlta->name)
            ->assertDontSee('Cliente ya ido')
            ->set('estado', 'todos')
            ->assertSee('Cliente ya ido');
    }
}
