<?php

namespace Tests\Feature\Api;

use App\Livewire\Environments\Show;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Data;
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;

/**
 * `site.availableupdatesstatus`: cómo fue la comprobación de actualizaciones del core
 * (PRODSECU-195).
 *
 * **El problema.** Un `availableupdates` vacío era ambiguo: el sitio estaba al día, o la
 * comprobación había fallado —sin salida a internet, `moodle.org` caído—, o el administrador
 * la tenía desactivada. El Manager lo leía siempre como «al día»: borraba las releases y
 * dejaba `lastversion`/`lastminor` como estaban. Ahora el plugin dice cuál de las tres es:
 *
 * - `ok`: lo que llega es la foto buena. Con `core` vacío, está al día.
 * - `failed` / `disabled`: no se sabe nada nuevo y **no se toca nada** de lo que sale de la
 *   comprobación.
 * - Sin la clave (clientes antiguos): como siempre, salvo que no borra las releases si el
 *   subárbol no viene.
 */
class ComprobacionDeActualizacionesTest extends TestCase
{
    use RefreshDatabase;

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

    private Environment $entorno;

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

        $cliente = Client::create(['name' => 'Cliente Comprobación', 'shortname' => 'cmp', 'actived' => true]);

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

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

    /**
     * Un `sync`. Lo que se pase en `$site` se añade al sitio; `null` en una clave la quita.
     *
     * @param  array<string, mixed>  $site
     * @param  array<string, mixed>  $arriba  claves de primer nivel del payload
     */
    private function sincronizar(array $site = [], array $arriba = []): \Illuminate\Testing\TestResponse
    {
        $base = [
            'version' => '2025100601.03',
            'release' => '5.1.1+ (Build: 20251219)',
            'type' => 'moodle',
            'env' => 'pro',
        ];

        return $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', array_merge([
            'action' => 'sync',
            'host' => self::DOMAIN,
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => array_merge($base, $site),
            // `data.plugins` es obligatorio en el `sync`: aquí no es lo que se prueba.
            'data' => ['plugins' => [[
                'name' => 'Un plugin',
                'component' => 'mod_algo',
                'type' => 'mod',
                'versiondisk' => '2026010100',
                'versiondb' => '2026010100',
            ]]],
        ], $arriba));
    }

    private function release(string $version, string $release): array
    {
        return [
            'version' => $version,
            'release' => $release,
            'maturity' => 200,
            'url' => 'https://moodle.org/download/releases/' . $release,
            'download' => 'https://download.moodle.org/download.php/stable/moodle-' . $release . '.zip',
        ];
    }

    /** Un `sync` bueno con dos releases, para tener un estado previo que no se debe perder. */
    private function estadoPrevioBueno(): void
    {
        $this->sincronizar([
            'availableupdatesstatus' => 'ok',
            'availableupdatesfetch' => 1767225600, // 2026-01-01
            'availableupdates' => ['core' => [
                $this->release('2025100602.00', '5.1.2'),
                $this->release('2026010100.00', '6.0'),
            ]],
        ])->assertSuccessful();
    }

    private function releases(): int
    {
        return Update::where('environment_id', $this->entorno->id)->count();
    }

    private function datos(): ?Data
    {
        return Data::where('environment_id', $this->entorno->id)->first();
    }

    /* ============ Los cinco casos ============ */

    public function test_ok_con_releases_guarda_la_foto(): void
    {
        $this->estadoPrevioBueno();

        $this->entorno->refresh();
        $this->assertSame('6.0', $this->entorno->lastversion);
        $this->assertSame('5.1.2', $this->entorno->lastminor);
        $this->assertSame('1767225600', $this->datos()->availableupdatesfetch);
        $this->assertSame('ok', $this->datos()->availableupdatesstatus);
        $this->assertSame(2, $this->releases());
    }

    public function test_ok_sin_releases_esta_al_dia(): void
    {
        $this->estadoPrevioBueno();

        $this->sincronizar([
            'availableupdatesstatus' => 'ok',
            'availableupdatesfetch' => 1769904000, // 2026-02-01
            'availableupdates' => ['core' => []],
        ])->assertSuccessful();

        $this->entorno->refresh();
        // Lo último disponible es lo que tiene: la release normalizada actual.
        $this->assertSame('5.1.1', $this->entorno->lastversion);
        $this->assertSame('5.1.1', $this->entorno->lastminor);
        $this->assertSame('1769904000', $this->datos()->availableupdatesfetch);
        $this->assertSame(0, $this->releases());
    }

    public function test_ok_con_core_nulo_tambien_esta_al_dia(): void
    {
        $this->estadoPrevioBueno();

        $this->sincronizar([
            'availableupdatesstatus' => 'ok',
            'availableupdates' => ['core' => null],
        ])->assertSuccessful();

        $this->entorno->refresh();
        $this->assertSame('5.1.1', $this->entorno->lastversion);
        $this->assertSame('5.1.1', $this->entorno->lastminor);
        $this->assertSame(0, $this->releases());
    }

    /** @return array<string, array{string}> */
    public static function estadosSinDatos(): array
    {
        return ['fallida' => ['failed'], 'desactivada' => ['disabled']];
    }

    /**
     * @dataProvider estadosSinDatos
     */
    public function test_sin_datos_no_se_toca_nada_de_la_comprobacion(string $estado): void
    {
        $this->estadoPrevioBueno();

        $this->sincronizar([
            'availableupdatesstatus' => $estado,
            'availableupdatesfetch' => 1769904000,
            'availableupdates' => ['core' => []],
        ])->assertSuccessful();

        $this->entorno->refresh();
        $this->assertSame('6.0', $this->entorno->lastversion, 'lastversion no se toca');
        $this->assertSame('5.1.2', $this->entorno->lastminor, 'lastminor no se toca');
        $this->assertSame('1767225600', $this->datos()->availableupdatesfetch, 'se queda la fecha del último resultado bueno');
        $this->assertSame(2, $this->releases(), 'las releases no se borran');
        $this->assertSame($estado, $this->datos()->availableupdatesstatus);
    }

    public function test_cliente_antiguo_sin_la_clave_no_borra_las_releases(): void
    {
        $this->estadoPrevioBueno();

        // Ni `availableupdatesstatus` ni `availableupdates`: es un plugin anterior. No está
        // diciendo «no hay actualizaciones», y antes eso borraba la foto.
        $this->sincronizar()->assertSuccessful();

        $this->entorno->refresh();
        $this->assertSame('6.0', $this->entorno->lastversion);
        $this->assertSame('5.1.2', $this->entorno->lastminor);
        $this->assertSame(2, $this->releases());
        $this->assertSame('ok', $this->datos()->availableupdatesstatus, 'el estado anterior no se pisa con nada');
    }

    public function test_cliente_antiguo_que_manda_el_subarbol_sigue_como_siempre(): void
    {
        $this->estadoPrevioBueno();

        $this->sincronizar([
            'availableupdates' => ['core' => [$this->release('2025100603.00', '5.1.3')]],
        ])->assertSuccessful();

        $this->entorno->refresh();
        $this->assertSame('5.1.3', $this->entorno->lastversion);
        $this->assertSame('5.1.3', $this->entorno->lastminor);
        $this->assertSame(1, $this->releases());
    }

    public function test_el_subarbol_arriba_del_todo_de_los_clientes_antiguos_se_lee(): void
    {
        // El fallback existía y nunca funcionó: el closure de la transacción no recibía
        // `$payload`, y el `??` se tragaba la variable indefinida.
        $this->sincronizar([], ['availableupdates' => ['core' => [$this->release('2025100602.00', '5.1.2')]]])
            ->assertSuccessful();

        $this->assertSame('5.1.2', $this->entorno->fresh()->lastminor);
        $this->assertSame(1, $this->releases());
    }

    /* ============ El contrato ============ */

    /** @return array<string, array{mixed}> */
    public static function estadosMalFormados(): array
    {
        return [
            'desconocido' => ['unknown'],
            'vacío' => [''],
            'número' => [1],
            'booleano' => [true],
            'lista' => [['ok']],
        ];
    }

    /**
     * @dataProvider estadosMalFormados
     */
    public function test_un_estado_mal_formado_no_tumba_el_sync_y_se_ignora(mixed $malo): void
    {
        // **No puede tumbar un `sync`**: un plugin que lo mande mal perdería el inventario
        // del día con un 422. Se ignora y se sigue como un cliente antiguo.
        $this->estadoPrevioBueno();

        $this->sincronizar([
            'availableupdatesstatus' => $malo,
            'availableupdatesfetch' => 1769904000,
        ])->assertSuccessful();

        $this->entorno->refresh();
        $this->assertSame('6.0', $this->entorno->lastversion, 'como un cliente antiguo sin el subárbol: no se toca');
        $this->assertSame(2, $this->releases(), 'las releases no se borran');
        $this->assertSame('ok', $this->datos()->availableupdatesstatus, 'el último estado bueno no se pisa con basura');
    }

    public function test_el_estado_admite_mayusculas_y_espacios(): void
    {
        $this->estadoPrevioBueno();

        $this->sincronizar(['availableupdatesstatus' => ' FAILED '])->assertSuccessful();

        $this->assertSame('failed', $this->datos()->availableupdatesstatus);
        $this->assertSame(2, $this->releases());
    }

    public function test_un_estado_mal_formado_en_environment_tampoco_tumba_el_sync(): void
    {
        // El payload antiguo, el de los plugins viejos, que es donde más probable es.
        $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'sync',
            'host' => self::DOMAIN,
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'environment' => [
                'version' => '2025100601.03',
                'release' => '5.1.1',
                'availableupdatesstatus' => ['lo que sea'],
            ],
            'data' => ['plugins' => [['name' => 'P', 'component' => 'mod_p', 'type' => 'mod', 'versiondisk' => '1', 'versiondb' => '1']]],
        ])->assertSuccessful();

        $this->assertNull($this->datos()?->availableupdatesstatus);
    }

    public function test_tambien_se_acepta_en_environment(): void
    {
        // El payload antiguo mandaba el sitio en `environment` en vez de `site`.
        $respuesta = $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'sync',
            'host' => self::DOMAIN,
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'environment' => [
                'version' => '2025100601.03',
                'release' => '5.1.1',
                'availableupdatesstatus' => 'disabled',
            ],
            'data' => ['plugins' => [['name' => 'P', 'component' => 'mod_p', 'type' => 'mod', 'versiondisk' => '1', 'versiondb' => '1']]],
        ]);

        $respuesta->assertSuccessful();
        $this->assertSame('disabled', $this->datos()->availableupdatesstatus);
    }

    public function test_la_respuesta_del_sync_no_cambia(): void
    {
        $antigua = $this->sincronizar()->json();
        $nueva = $this->sincronizar(['availableupdatesstatus' => 'failed'])->json();

        $this->assertSame(
            array_keys(\Illuminate\Support\Arr::dot($antigua)),
            array_keys(\Illuminate\Support\Arr::dot($nueva))
        );
    }

    /* ============ La ficha ============ */

    private function verFicha()
    {
        $rol = Role::firstOrCreate(['name' => 'VeEntornos', 'guard_name' => 'web']);
        foreach (['admin.environments.index', 'admin.environments.show'] 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 Livewire::test(Show::class, ['environment' => $this->entorno->fresh()]);
    }

    public function test_la_ficha_avisa_de_una_comprobacion_fallida_con_la_fecha_buena(): void
    {
        $this->estadoPrevioBueno();
        $this->sincronizar(['availableupdatesstatus' => 'failed'])->assertSuccessful();

        $this->verFicha()
            ->assertSee('Comprobación de actualizaciones: fallida')
            ->assertSee('Último resultado correcto: ' . \Carbon\Carbon::createFromTimestamp(1767225600)->format('d/m/Y H:i'));
    }

    public function test_la_ficha_avisa_de_una_comprobacion_desactivada_sin_resultado_previo(): void
    {
        $this->sincronizar(['availableupdatesstatus' => 'disabled'])->assertSuccessful();

        $this->verFicha()
            ->assertSee('Comprobación de actualizaciones: desactivada')
            ->assertSee('No consta ningún resultado correcto');
    }

    public function test_con_la_comprobacion_bien_la_ficha_no_avisa(): void
    {
        $this->estadoPrevioBueno();

        $this->verFicha()->assertDontSee('Comprobación de actualizaciones:');
    }
}
