<?php

namespace Tests\Feature\Api;

use App\Livewire\Plugins\Core as Parque;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Update;
use App\Models\Products\LicenseToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Las releases de core disponibles de cada sitio: se guardan y se enseñan.
 *
 * **Es el módulo `updates` del Manager anterior, retomado.** Allí era una fila por release
 * disponible para cada sitio, con su madurez y su descarga, alimentada por `api.updates.store`
 * y pintada en `/admin/updates/{id}`. En el Manager nuevo la tabla existía con las mismas
 * columnas y **vacía**: el `availableupdates.core` llegaba en cada `sync`, se leía para
 * deducir `lastversion` y `lastminor`, y **el resto se tiraba** — qué releases concretas hay,
 * su madurez y de dónde se descargan.
 *
 * Se descartó a conciencia en su momento —`fixes/PRODSECU-152/decisions.md`: «no tiene UI ni
 * endpoint; persistirlo sin consumidor crea deuda. Cuando se aborde ese módulo se
 * retomará»— y esto es retomarlo, con su pantalla: la pestaña de versiones de Moodle del
 * parque, que ya cargaba la relación esperándolo.
 */
class UpdatesDelCoreTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN = '3IP-TEST-UPDATES';
    private const DOMAIN = 'https://updates.test';

    private Environment $entorno;

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

        $cliente = Client::create(['name' => 'Cliente Updates', 'shortname' => 'upd', 'actived' => true]);

        $token = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia Updates',
            'token' => self::TOKEN,
            'active' => true,
        ]);

        $this->entorno = Environment::create([
            'name' => 'Campus Updates',
            'domain' => self::DOMAIN,
            'version' => '5.1.1',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'license_token_id' => $token->id,
            'active' => true,
        ]);
    }

    /**
     * Un `sync` con el subárbol de actualizaciones del core tal y como lo manda Moodle.
     *
     * @param  array<int, array<string, mixed>>|null  $delCore
     */
    private function sincronizar(?array $delCore, ?array $plugins = null): \Illuminate\Testing\TestResponse
    {
        // `data.plugins` es obligatorio en el `sync` y un array vacío no cumple el
        // `required`: se manda un plugin cualquiera, que aquí no es lo que se prueba.
        $plugins ??= [[
            'name' => 'Un plugin',
            'component' => 'mod_algo',
            'type' => 'mod',
            'versiondisk' => '2026010100',
            'versiondb' => '2026010100',
        ]];

        $site = [
            'version' => '2025100601.03',
            'release' => '5.1.1+ (Build: 20251219)',
            'type' => 'moodle',
            'env' => 'pro',
        ];

        if ($delCore !== null) {
            $site['availableupdates'] = ['core' => $delCore];
        }

        return $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'sync',
            'host' => self::DOMAIN,
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => $site,
            'data' => ['plugins' => $plugins],
        ]);
    }

    /** Una release como la describe el comprobador de Moodle. */
    private function release(string $version, string $release, int $maturity = 200): array
    {
        return [
            'version' => $version,
            'release' => $release,
            'maturity' => $maturity,
            'url' => 'https://moodle.org/download/releases/' . $release,
            'download' => 'https://download.moodle.org/download.php/stable/moodle-' . $release . '.zip',
            'downloadmd5' => md5($release),
        ];
    }

    public function test_las_releases_disponibles_se_guardan_con_su_madurez_y_su_descarga(): void
    {
        // **Lo que se tiraba.** El subárbol llegaba entero y solo se leían dos números.
        $this->sincronizar([
            $this->release('2025100602.00', '5.1.2'),
            $this->release('2026010100.00', '6.0', maturity: 150),
        ])->assertSuccessful();

        $filas = Update::where('environment_id', $this->entorno->id)->get()->keyBy('release');

        $this->assertCount(2, $filas);
        $this->assertSame('2025100602.00', $filas['5.1.2']->version);
        $this->assertSame(200, $filas['5.1.2']->maturity);
        $this->assertStringContainsString('moodle-5.1.2.zip', $filas['5.1.2']->download);
        $this->assertSame(md5('5.1.2'), $filas['5.1.2']->downloadmd5);
        // La release candidate se guarda igual: es el dato que decide si se instala o se
        // espera, y esconderla sería decidir por quien lo lea.
        $this->assertSame(150, $filas['6.0']->maturity);
    }

    public function test_la_release_se_normaliza_igual_que_la_del_sitio(): void
    {
        // Si una se guardara como «5.1.2+ (Build: …)» y la otra como «5.1.2», no se podrían
        // comparar entre sí — y compararlas es justo para lo que sirven.
        $this->sincronizar([
            ['version' => '2025100602.00', 'release' => '5.1.2+ (Build: 20260102)'],
        ])->assertSuccessful();

        $this->assertSame('5.1.2', Update::where('environment_id', $this->entorno->id)->value('release'));
    }

    public function test_cada_sync_reemplaza_la_foto_anterior(): void
    {
        // **No es un histórico: es lo que Moodle ofrece ahora.** Una release que ya no
        // aparece es una que ya no está disponible, y conservarla ofrecería una descarga que
        // puede no existir.
        $this->sincronizar([$this->release('2025100602.00', '5.1.2')])->assertSuccessful();
        $this->sincronizar([$this->release('2025100603.00', '5.1.3')])->assertSuccessful();

        $filas = Update::where('environment_id', $this->entorno->id)->get();

        $this->assertCount(1, $filas);
        $this->assertSame('5.1.3', $filas->first()->release);
    }

    public function test_el_reemplazo_no_apila_filas_borradas(): void
    {
        // La tabla tiene borrado lógico, y un `delete()` aquí dejaría una generación de
        // filas muertas por sitio **y por sincronización**, que son diarias. Nadie las lee.
        for ($i = 1; $i <= 3; $i++) {
            $this->sincronizar([$this->release('202510060' . $i . '.00', '5.1.' . $i)])->assertSuccessful();
        }

        $this->assertSame(
            1,
            Update::withTrashed()->where('environment_id', $this->entorno->id)->count(),
            'el reemplazo está apilando filas con borrado lógico'
        );
    }

    public function test_un_sitio_sin_actualizaciones_disponibles_no_deja_filas(): void
    {
        $this->sincronizar([$this->release('2025100602.00', '5.1.2')])->assertSuccessful();

        // El siguiente sync ya no trae nada: el sitio está al día.
        $this->sincronizar(null)->assertSuccessful();

        $this->assertSame(0, Update::where('environment_id', $this->entorno->id)->count());
    }

    public function test_una_release_sin_version_no_se_guarda_y_no_tumba_el_sync(): void
    {
        // `version` es lo único que identifica la fila; el resto del subárbol es opcional en
        // el payload. Y un payload raro no puede tumbar una sincronización.
        $this->sincronizar([
            ['release' => '5.1.2'],
            $this->release('2025100603.00', '5.1.3'),
        ])->assertSuccessful();

        $filas = Update::where('environment_id', $this->entorno->id)->get();

        $this->assertCount(1, $filas);
        $this->assertSame('5.1.3', $filas->first()->release);
    }

    public function test_los_dos_escalares_de_siempre_siguen_saliendo(): void
    {
        // Lo que ya funcionaba no se toca: de aquí salen «le faltan parches de su rama» y
        // «hay un salto de rama», que es lo que ordena la pestaña.
        $this->sincronizar([
            $this->release('2025100602.00', '5.1.2'),
            $this->release('2026010100.00', '6.0'),
        ])->assertSuccessful();

        $this->entorno->refresh();

        $this->assertSame('6.0', $this->entorno->lastversion);
        $this->assertSame('5.1.2', $this->entorno->lastminor);
    }

    /* ============ Y la pantalla ============ */

    public function test_el_parque_las_ensena_con_sus_enlaces(): void
    {
        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);

        foreach (['admin.plugins.environment', 'admin.environments.index', 'admin.clients.index',
                  'admin.configs.index', 'admin.dashboard.index'] as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'plugins', 'description' => $nombre]
            ));
        }

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

        $this->sincronizar([$this->release('2025100602.00', '5.1.2')])->assertSuccessful();

        $componente = Livewire::test(Parque::class);

        $fila = collect($componente->viewData('core'))->firstWhere('entorno.id', $this->entorno->id);

        $this->assertCount(1, $fila['disponibles'], 'la pantalla no recibe las releases disponibles');

        $componente
            ->assertSee('release disponible')
            ->assertSee('Descargar')
            ->assertSee('Estable');
    }

    public function test_sin_releases_disponibles_la_pantalla_no_ofrece_desplegar_nada(): void
    {
        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);
        $rol->givePermissionTo(Permission::firstOrCreate(
            ['name' => 'admin.plugins.environment', 'guard_name' => 'web'],
            ['model' => 'plugins', 'description' => 'ver']
        ));

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

        $this->sincronizar(null)->assertSuccessful();

        Livewire::test(Parque::class)
            ->assertDontSee('releases disponibles');
    }
}
