<?php

namespace Tests\Feature\Supports;

use App\Livewire\Environments\Supports\Index as SoportesDelEntorno;
use App\Mail\SupportExpiryAlert;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Monitoring\Notice;
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;

/**
 * Soportes de entorno: asignarlos, y avisar antes de que caduquen.
 *
 * Recupera el `support:expires` del Manager antiguo —avisos a 30/15/7/0 días— que se
 * perdió en la migración. Lo que se encontró al empezar:
 *
 * - Las tablas estaban migradas, el catálogo de tipos tenía pantalla y **nada asignaba
 *   un soporte a un entorno**: las 3 filas de la base local venían de la importación y
 *   la última acabó en marzo de 2023.
 * - `environments.environment_support_id` —el puntero al contrato vigente— estaba a
 *   **NULL en los 7 entornos** marcados con `has_support`.
 * - `Support::get_status_support()` devolvía **«Caducado» para todo**, incluido un
 *   soporte que acabase en 2027 (Carbon 3 y su `diffInDays` con signo), y
 *   `is_enabled()` estaba invertido.
 *
 * Este fichero fija las tres cosas y el ciclo de avisos.
 */
class SupportExpiryTest extends TestCase
{
    use RefreshDatabase;

    private const VER = 'admin.environments.supports.index';
    private const CREAR = 'admin.environments.supports.create';
    private const EDITAR = 'admin.environments.supports.edit';
    private const BORRAR = 'admin.environments.supports.destroy';

    private Support $tipo;

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

        $this->tipo = Support::create([
            'name' => 'Soporte Premium',
            'shortname' => 'premium',
            'description' => 'Soporte de pruebas',
        ]);

        app(Settings::class)->set(Settings::SOPORTES_EMAIL, 'help@tresipunt.test');
        app(Settings::class)->olvidar();
    }

    private function usuario(array $permisos): User
    {
        foreach ($permisos as $nombre) {
            Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'environment_support', 'description' => $nombre]
            );
        }

        $user = User::factory()->create();
        $user->givePermissionTo($permisos);
        $this->actingAs($user);
        Cache::flush();

        return $user;
    }

    private function entorno(string $nombre = 'Sitio de prueba'): Environment
    {
        $cliente = Client::create([
            'name' => 'Cliente ' . $nombre,
            'shortname' => 'c' . uniqid(),
            'actived' => true,
        ]);

        return Environment::create([
            'client_id' => $cliente->id,
            'name' => $nombre,
            'domain' => 'https://' . uniqid() . '.test',
            'version' => '5.1.1',
            'env' => 'local',
            'active' => true,
        ]);
    }

    private function contrato(Environment $entorno, int $diasHastaElFin): EnvironmentSupport
    {
        return EnvironmentSupport::create([
            'environment_id' => $entorno->id,
            'support_id' => $this->tipo->id,
            'start_at' => now()->subYear()->format('Y-m-d'),
            'end_at' => now()->addDays($diasHastaElFin)->format('Y-m-d'),
            'comment' => 'Pedido 1234',
        ]);
    }

    /* ===================== El estado del soporte ===================== */

    public function test_un_soporte_con_fin_lejano_no_esta_caducado(): void
    {
        // Devolvía "Caducado" para todo, incluido 2027: `diffInDays` con signo.
        $contrato = $this->contrato($this->entorno(), 200);

        $this->assertSame(5, $contrato->estado());
        $this->assertSame('Activado', $contrato->estado_texto);
        $this->assertFalse($contrato->haCaducado());
        $this->assertSame(200, $contrato->diasRestantes());
    }

    public function test_un_soporte_a_menos_de_treinta_dias_esta_proximo_a_caducar(): void
    {
        $contrato = $this->contrato($this->entorno(), 10);

        $this->assertSame(4, $contrato->estado());
        $this->assertSame('Próximo a caducar', $contrato->estado_texto);
    }

    public function test_el_estado_no_depende_de_la_hora_a_la_que_se_mire(): void
    {
        // Comparando marcas de tiempo, un soporte que acaba mañana daba 0,99 días → 0 →
        // "caducado", y el resultado cambiaba según la hora a la que corriera la tarea.
        $entorno = $this->entorno();
        $contrato = $this->contrato($entorno, 1);

        foreach (['00:01', '13:37', '23:59'] as $hora) {
            $this->travelTo(now()->setTimeFromTimeString($hora));
            $this->assertSame(4, $contrato->estado(), "Falla a las {$hora}");
            $this->assertSame(1, $contrato->diasRestantes(), "Falla a las {$hora}");
        }

        $this->travelBack();
    }

    public function test_is_enabled_no_cuenta_un_caducado_como_activo(): void
    {
        // Estaba como `< 4`, y el 3 es "Caducado": decía true para un soporte vencido.
        $activo = (object) [
            'start_at' => now()->subYear()->format('Y-m-d'),
            'end_at' => now()->addDays(100)->format('Y-m-d'),
        ];
        $caducado = (object) [
            'start_at' => now()->subYears(2)->format('Y-m-d'),
            'end_at' => now()->subDays(40)->format('Y-m-d'),
        ];

        $this->assertTrue(Support::is_enabled($activo));
        $this->assertFalse(Support::is_enabled($caducado));
    }

    /* ===================== El puntero al contrato vigente ===================== */

    public function test_asignar_un_soporte_deja_el_puntero_apuntando(): void
    {
        // Estaba a NULL en todos los entornos de la base real, así que cualquier cosa
        // que dependiera de él no hacía nada y en silencio.
        $entorno = $this->entorno();
        $contrato = $this->contrato($entorno, 100);

        $entorno->refresh();

        $this->assertSame($contrato->id, $entorno->environment_support_id);
        $this->assertTrue((bool) $entorno->has_support);
    }

    public function test_el_vigente_es_el_de_fin_mas_lejano_no_el_ultimo_creado(): void
    {
        // Si alguien da de alta la renovación del año que viene y luego corrige un
        // contrato viejo, el orden de creación miente y el de fechas no.
        $entorno = $this->entorno();
        $renovacion = $this->contrato($entorno, 400);
        $this->contrato($entorno, 20);

        $entorno->refresh();

        $this->assertSame($renovacion->id, $entorno->environment_support_id);
    }

    public function test_al_quitar_el_ultimo_soporte_el_entorno_se_queda_sin_soporte(): void
    {
        $entorno = $this->entorno();
        $contrato = $this->contrato($entorno, 100);

        $contrato->delete();
        $entorno->refresh();

        $this->assertNull($entorno->environment_support_id);
        $this->assertFalse((bool) $entorno->has_support);
    }

    /* ===================== La pantalla ===================== */

    public function test_sin_permiso_no_se_entra(): void
    {
        $this->usuario(['admin.dashboard.index']);
        $entorno = $this->entorno();

        $this->get(route('environments.supports.index', $entorno))->assertForbidden();
    }

    public function test_se_asigna_un_soporte_desde_la_pantalla(): void
    {
        $this->usuario([self::VER, self::CREAR]);
        $entorno = $this->entorno();

        Livewire::test(SoportesDelEntorno::class, ['environment' => $entorno])
            ->call('nuevo')
            ->set('supportId', $this->tipo->id)
            ->set('startAt', now()->format('Y-m-d'))
            ->set('endAt', now()->addYear()->format('Y-m-d'))
            ->set('comment', 'Pedido 9876')
            ->call('guardar')
            ->assertHasNoErrors();

        $contrato = EnvironmentSupport::where('environment_id', $entorno->id)->firstOrFail();

        $this->assertSame('Pedido 9876', $contrato->comment);
        $this->assertSame($contrato->id, $entorno->fresh()->environment_support_id);
        $this->assertDatabaseHas('logs', ['code' => '16015']);
    }

    public function test_no_se_acepta_un_periodo_al_reves(): void
    {
        $this->usuario([self::VER, self::CREAR]);
        $entorno = $this->entorno();

        Livewire::test(SoportesDelEntorno::class, ['environment' => $entorno])
            ->call('nuevo')
            ->set('supportId', $this->tipo->id)
            ->set('startAt', now()->format('Y-m-d'))
            ->set('endAt', now()->subMonth()->format('Y-m-d'))
            ->call('guardar')
            ->assertHasErrors(['endAt']);

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

    public function test_quien_solo_puede_ver_no_puede_asignar(): void
    {
        // MGR-005: el método viaja por /livewire/update, donde el permiso de la ruta no
        // se reaplica.
        $this->usuario([self::VER]);
        $entorno = $this->entorno();

        Livewire::test(SoportesDelEntorno::class, ['environment' => $entorno])
            ->call('nuevo')
            ->assertForbidden();

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

    public function test_no_se_puede_editar_el_soporte_de_otro_entorno(): void
    {
        // MGR-006: sin acotar al entorno de la URL, cambiando el id se editaría el
        // contrato de otro cliente desde esta pantalla.
        $this->usuario([self::VER, self::EDITAR]);

        $mio = $this->entorno('El mío');
        $ajeno = $this->contrato($this->entorno('El de otro'), 100);

        $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class);

        Livewire::test(SoportesDelEntorno::class, ['environment' => $mio])
            ->call('editar', $ajeno->id);
    }

    public function test_se_quita_un_soporte_con_confirmacion(): void
    {
        $this->usuario([self::VER, self::BORRAR]);
        $entorno = $this->entorno();
        $contrato = $this->contrato($entorno, 100);

        Livewire::test(SoportesDelEntorno::class, ['environment' => $entorno])
            ->call('pedirBaja', $contrato->id)
            // El camino real: la modal dispara el evento con un array. Ver MGR-042.
            ->dispatch('environmentSupportDeleteConfirmed', ['itemId' => $contrato->id]);

        $this->assertNull($contrato->fresh());
        $this->assertDatabaseHas('logs', ['code' => '16016']);
    }

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

    public function test_avisa_al_llegar_al_umbral(): void
    {
        Mail::fake();
        $entorno = $this->entorno();
        $this->contrato($entorno, 15);

        $this->artisan('supports:check-expiry')->assertSuccessful();

        Mail::assertSent(SupportExpiryAlert::class, 1);
        Mail::assertSent(
            SupportExpiryAlert::class,
            fn (SupportExpiryAlert $correo) => $correo->dias === 15 && !$correo->haCaducado()
        );
    }

    public function test_no_avisa_de_un_soporte_que_esta_lejos(): void
    {
        Mail::fake();
        $this->contrato($this->entorno(), 200);

        $this->artisan('supports:check-expiry')->assertSuccessful();

        Mail::assertNothingSent();
    }

    public function test_el_mismo_umbral_no_avisa_dos_veces(): void
    {
        // Sin esto, una tarea diaria manda el mismo correo 15 días seguidos y al tercero
        // nadie lo lee.
        Mail::fake();
        $this->contrato($this->entorno(), 15);

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

        Mail::assertSent(SupportExpiryAlert::class, 1);
    }

    public function test_cada_umbral_avisa_una_vez(): void
    {
        // El ciclo completo: 30 → 15 → 7 → 0, un correo en cada escalón.
        Mail::fake();
        $entorno = $this->entorno();
        $contrato = $this->contrato($entorno, 30);

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

        foreach ([15, 7, 0] as $i => $restantes) {
            $contrato->update(['end_at' => now()->addDays($restantes)->format('Y-m-d')]);
            // Sin este `update` de la fecha de fin, el `reference` cambiaría y rearmaría
            // los avisos; aquí se mueve la fecha a propósito para simular el paso del
            // tiempo, así que se comprueba el umbral, no la referencia.
            $this->artisan('supports:check-expiry');
        }

        $this->assertSame(
            4,
            Notice::where('subject_type', Notice::SUBJECT_ENVIRONMENT_SUPPORT)
                ->where('subject_id', $contrato->id)
                ->whereNotNull('sent_at')
                ->count()
        );
    }

    public function test_renovar_rearma_los_avisos(): void
    {
        // Es la propiedad que el Manager antiguo conseguía con un `reset()` a mano desde
        // cinco sitios. Aquí sale de que `reference` es la fecha de fin.
        Mail::fake();
        $contrato = $this->contrato($this->entorno(), 15);

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

        // Renovación: un año más. Con la fecha nueva no toca ningún umbral todavía.
        $nuevoFin = now()->addYear();
        $contrato->update(['end_at' => $nuevoFin->format('Y-m-d')]);
        $this->artisan('supports:check-expiry');
        Mail::assertSent(SupportExpiryAlert::class, 1);

        // Y al acercarse la fecha nueva, avisa otra vez. Se viaja en el tiempo en vez de
        // mover la fecha de fin: moverla a 15 días de hoy daría la MISMA referencia que
        // el primer aviso, y entonces lo que se estaría probando es el antiduplicado.
        $this->travelTo($nuevoFin->copy()->subDays(15));
        $this->artisan('supports:check-expiry');
        Mail::assertSent(SupportExpiryAlert::class, 2);
        $this->travelBack();
    }

    public function test_avisa_de_uno_ya_caducado_con_el_umbral_cero(): void
    {
        Mail::fake();
        $this->contrato($this->entorno(), -3);

        $this->artisan('supports:check-expiry')->assertSuccessful();

        Mail::assertSent(
            SupportExpiryAlert::class,
            fn (SupportExpiryAlert $correo) => $correo->haCaducado()
        );
    }

    public function test_no_resucita_soportes_terminados_hace_meses(): void
    {
        // En la base real había contratos acabados en 2021: sin el corte inferior, la
        // primera ejecución mandaría un correo por cada uno.
        Mail::fake();
        $this->contrato($this->entorno(), -400);

        $this->artisan('supports:check-expiry')->assertSuccessful();

        Mail::assertNothingSent();
    }

    public function test_si_se_salta_un_umbral_avisa_del_que_corresponde(): void
    {
        // La tarea ha estado parada y el soporte pasó de 30 a 12 días. Toca el de 15
        // —el más cercano alcanzado—, y NO un correo por cada umbral perdido.
        Mail::fake();
        $this->contrato($this->entorno(), 12);

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

        Mail::assertSent(SupportExpiryAlert::class, 1);
        $this->assertSame(
            15,
            Notice::where('subject_type', Notice::SUBJECT_ENVIRONMENT_SUPPORT)->value('threshold')
        );
    }

    public function test_no_avisa_si_esta_desactivado(): void
    {
        Mail::fake();
        app(Settings::class)->set(Settings::SOPORTES_ACTIVO, '0');
        app(Settings::class)->olvidar();

        $this->contrato($this->entorno(), 7);

        $this->artisan('supports:check-expiry')->assertSuccessful();

        Mail::assertNothingSent();
    }

    public function test_sin_destinatarios_no_envia_y_lo_dice(): void
    {
        Mail::fake();
        $ajustes = app(Settings::class);
        $ajustes->set(Settings::SOPORTES_EMAIL, '');
        $ajustes->set(Settings::LICENCIAS_EMAIL, '');
        $ajustes->olvidar();

        $this->contrato($this->entorno(), 7);

        $this->artisan('supports:check-expiry')
            ->expectsOutputToContain('No hay destinatarios')
            ->assertSuccessful();

        Mail::assertNothingSent();
    }

    public function test_los_destinatarios_caen_a_los_de_licencias(): void
    {
        $ajustes = app(Settings::class);
        $ajustes->set(Settings::SOPORTES_EMAIL, '');
        $ajustes->set(Settings::LICENCIAS_EMAIL, 'comercial@tresipunt.test');
        $ajustes->olvidar();

        $this->assertSame(['comercial@tresipunt.test'], app(Settings::class)->destinatariosDeSoportes());
    }

    public function test_el_dry_run_no_envia_nada(): void
    {
        Mail::fake();
        $this->contrato($this->entorno(), 7);

        $this->artisan('supports:check-expiry --dry-run')->assertSuccessful();

        Mail::assertNothingSent();
        $this->assertSame(0, Notice::count());
    }

    public function test_los_umbrales_se_limpian_al_leerlos(): void
    {
        // Se acepta cualquier cosa por el formulario y se saneа al leer: duplicados,
        // negativos, y un tope para que nadie configure trescientos avisos.
        $ajustes = app(Settings::class);
        $ajustes->set(Settings::SOPORTES_DIAS, '30, 30, -5, 900, 7');
        $ajustes->olvidar();

        $this->assertSame([30, 7], app(Settings::class)->diasDeAvisoDeSoportes());
    }

    public function test_unos_umbrales_vacios_vuelven_a_los_de_siempre(): void
    {
        // Un aviso sin umbrales no avisaría nunca, y en silencio.
        $ajustes = app(Settings::class);
        $ajustes->set(Settings::SOPORTES_DIAS, '');
        $ajustes->olvidar();

        $this->assertSame([30, 15, 7, 0], app(Settings::class)->diasDeAvisoDeSoportes());
    }
}
