<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\BorrarEntornoModal;
use App\Livewire\Environments\Components\EnvironmentCard;
use App\Livewire\Environments\Show;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Plugin;
use App\Models\ApiRequestLog;
use App\Models\Products\LicenseToken;
use App\Models\Support\EnvironmentSupport;
use App\Models\Support\Support;
use App\Support\EntornoBorrable;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Borrar un entorno, **solo si está limpio**: sin licencia y sin nada que cuelgue de él,
 * peticiones a la API incluidas. Solo Admin. Ver `EntornoBorrable`.
 */
class BorrarEntornoTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(array $permisos = ['admin.environments.index', 'admin.environments.show', 'admin.environments.destroy']): User
    {
        $rol = Role::firstOrCreate(['name' => 'BorradorDeEntornos', 'guard_name' => 'web']);

        foreach ($permisos as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'environments', 'description' => $nombre]
            ));
        }

        $user = User::factory()->create();
        $user->assignRole($rol);
        $this->actingAs($user);

        Cache::flush();

        return $user;
    }

    private function entorno(string $nombre = 'Creado por error', array $extra = []): Environment
    {
        $cliente = Client::firstOrCreate(['shortname' => 'borr'], ['name' => 'Cliente borrar', 'actived' => true]);

        return Environment::create(array_merge([
            'name' => $nombre,
            'domain' => 'https://' . Str::slug($nombre) . '.borrar.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ], $extra));
    }

    private function peticionDe(Environment $entorno): void
    {
        ApiRequestLog::create([
            'request_uuid' => (string) Str::uuid(),
            'method' => 'POST',
            'path' => 'api/v1',
            'ip' => '203.0.113.20',
            'auth_type' => 'bearer',
            'http_status' => 200,
            'action' => 'products',
            'host' => $entorno->domain,
            'environment_id' => $entorno->id,
            'started_at' => now(),
            'ended_at' => now(),
            'duration_ms' => 20,
        ]);
    }

    private function modal(Environment $entorno)
    {
        return Livewire::test(BorrarEntornoModal::class, ['environmentId' => $entorno->id]);
    }

    /* ============ La regla ============ */

    public function test_un_entorno_limpio_se_borra_del_todo(): void
    {
        $this->usuario();
        $entorno = $this->entorno();

        $this->modal($entorno)
            ->assertSee('Borrar el entorno')
            ->call('borrar')
            ->assertRedirect(route('environments.index'));

        // Del todo, no a la papelera: si no, su dominio seguiría ocupado en el índice único.
        $this->assertSame(0, Environment::withTrashed()->whereKey($entorno->id)->count());
    }

    public function test_su_dominio_queda_libre(): void
    {
        $this->usuario();
        $entorno = $this->entorno();

        $this->modal($entorno)->call('borrar');

        $this->entorno('Creado por error');
        $this->assertSame(1, Environment::where('domain', $entorno->domain)->count());
    }

    public function test_deja_rastro_antes_de_borrar(): void
    {
        $this->usuario();
        $entorno = $this->entorno();

        $this->modal($entorno)->call('borrar');

        $this->assertDatabaseHas('logs', ['code' => '16045', 'entity' => 'Environment', 'entity_id' => (string) $entorno->id]);
    }

    public function test_con_licencia_no_se_borra(): void
    {
        $this->usuario();
        $entorno = $this->entorno();
        $token = LicenseToken::create([
            'token' => '3IP-BORR-0001-BORR-BORR',
            'client_id' => $entorno->client_id,
            'active' => true,
        ]);
        $entorno->update(['license_token_id' => $token->id]);

        $this->modal($entorno)
            ->assertSee('no se puede borrar')
            ->assertSee('Tiene una licencia asociada')
            ->call('borrar');

        $this->assertNotNull($entorno->fresh());
    }

    public function test_con_peticiones_a_la_api_no_se_borra(): void
    {
        // **Cuentan**: un entorno sin licencia pero con peticiones estuvo funcionando, y su
        // historial se quedaría sin entorno.
        $this->usuario();
        $entorno = $this->entorno();
        $this->peticionDe($entorno);

        $this->modal($entorno)
            ->assertSee('1 petición a la API registrada')
            ->call('borrar');

        $this->assertNotNull($entorno->fresh());
    }

    public function test_con_plugins_sincronizados_no_se_borra(): void
    {
        $this->usuario();
        $entorno = $this->entorno();
        Plugin::create([
            'environment_id' => $entorno->id,
            'name' => 'Tresipunt',
            'component' => 'local_tresipunt',
            'type' => 'local',
            'versiondisk' => '2026010100',
            'versiondb' => '2026010100',
            'versionrequires' => '2024100700',
            'release' => '2.0.0',
            'has_updates' => false,
            'has_dependencies' => false,
        ]);

        $this->assertSame(['tiene 1 plugin sincronizado'], EntornoBorrable::motivos($entorno));
    }

    public function test_con_un_contrato_de_soporte_no_se_borra(): void
    {
        $this->usuario();
        $entorno = $this->entorno();
        $tipo = Support::create(['name' => 'Premium', 'shortname' => 'prem' . uniqid(), 'description' => 'x']);
        EnvironmentSupport::create([
            'environment_id' => $entorno->id,
            'support_id' => $tipo->id,
            'start_at' => now()->subYear()->format('Y-m-d'),
            'end_at' => now()->addYear()->format('Y-m-d'),
        ]);

        $this->assertSame(['tiene 1 contrato de soporte'], EntornoBorrable::motivos($entorno));
    }

    public function test_si_se_ensucia_con_la_modal_abierta_no_se_borra(): void
    {
        // Entre abrir y pulsar, el Moodle puede haber llamado a la API.
        $this->usuario();
        $entorno = $this->entorno();

        $modal = $this->modal($entorno)->assertSee('Borrar el entorno');
        $this->peticionDe($entorno);
        $modal->call('borrar');

        $this->assertNotNull($entorno->fresh());
        $this->assertDatabaseMissing('logs', ['code' => '16045']);
    }

    public function test_todas_las_tablas_con_environment_id_estan_en_la_regla(): void
    {
        // Si mañana se crea una tabla que cuelga de un entorno y no se añade aquí, se podría
        // borrar un entorno que no está limpio, y sus filas se irían en cascada sin avisar.
        $conEntorno = collect(Schema::getTables())
            ->pluck('name')
            ->filter(fn (string $tabla) => Schema::hasColumn($tabla, 'environment_id'))
            ->sort()->values()->all();

        $enLaRegla = collect(array_keys(EntornoBorrable::TABLAS))->sort()->values()->all();

        $this->assertSame($enLaRegla, $conEntorno);
    }

    public function test_el_listado_y_la_ficha_dicen_lo_mismo(): void
    {
        // Dos caminos para la misma regla: la subconsulta del listado y `motivos()`.
        $this->usuario();
        $limpio = $this->entorno('Limpio');
        $sucio = $this->entorno('Con peticiones');
        $this->peticionDe($sucio);

        $columna = Environment::query()
            ->select('id')
            ->addSelect(['esta_limpio' => EntornoBorrable::subconsultaEstaLimpio()])
            ->pluck('esta_limpio', 'id');

        $this->assertSame(1, (int) $columna[$limpio->id]);
        $this->assertSame(0, (int) $columna[$sucio->id]);
        $this->assertTrue(EntornoBorrable::estaLimpio($limpio));
        $this->assertFalse(EntornoBorrable::estaLimpio($sucio));
    }

    /* ============ Quién ============ */

    public function test_sin_el_permiso_no_se_abre(): void
    {
        $this->usuario(['admin.environments.index']);

        $this->modal($this->entorno())->assertForbidden();
    }

    public function test_solo_admin_tiene_el_permiso(): void
    {
        $this->seed(RoleSeeder::class);

        $roles = Role::whereHas('permissions', fn ($q) => $q->where('name', 'admin.environments.destroy'))
            ->pluck('name')->all();

        $this->assertSame(['Admin'], $roles);
    }

    /* ============ Dónde ============ */

    public function test_la_ficha_ensena_el_boton_con_candado_si_no_esta_limpio(): void
    {
        $this->usuario();
        $sucio = $this->entorno('Con peticiones');
        $this->peticionDe($sucio);

        Livewire::test(Show::class, ['environment' => $sucio])
            ->assertSeeHtml('wire:click="abrirBorrar"')
            ->assertSee('No se puede borrar: tiene 1 petición a la API registrada');
    }

    public function test_la_ficha_sin_permiso_no_ensena_el_boton(): void
    {
        $this->usuario(['admin.environments.index', 'admin.environments.show']);

        Livewire::test(Show::class, ['environment' => $this->entorno()])
            ->assertDontSeeHtml('wire:click="abrirBorrar"');
    }

    public function test_la_tarjeta_usa_el_dato_del_listado_sin_consultar(): void
    {
        $this->usuario();
        $entorno = $this->entorno();

        $conColumna = Environment::query()
            ->addSelect(['environments.*', 'esta_limpio' => EntornoBorrable::subconsultaEstaLimpio()])
            ->findOrFail($entorno->id);

        DB::enableQueryLog();
        Livewire::test(EnvironmentCard::class, ['environment' => $conColumna, 'client' => $conColumna->client])
            ->assertSee('Borrar entorno…');
        $consultas = collect(DB::getQueryLog())->filter(fn ($q) => str_contains($q['query'], 'api_request_logs'));
        DB::disableQueryLog();

        $this->assertCount(0, $consultas, 'con la columna precalculada no tiene que contar nada');
    }

    public function test_la_tarjeta_ofrece_borrar(): void
    {
        $this->usuario();
        $entorno = $this->entorno();

        Livewire::test(EnvironmentCard::class, ['environment' => $entorno, 'client' => $entorno->client])
            ->assertSee('Borrar entorno…')
            ->call('abrirBorrar')
            ->assertDispatched('openModal');
    }
}
