<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\PedirDatosModal;
use App\Livewire\Environments\Uso;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Services\Moodle\MoodleSyncService;
use App\Services\Moodle\QueSeLePide;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * El Manager le pide al Moodle su foto de uso.
 *
 * **El hueco.** «Uso del entorno» salía vacía —«último dato recibido: nunca»— y no había
 * nada que hacer desde el panel: la foto la manda `sync_stats_task` del plugin, sobre las
 * 02:00, así que la única salida era esperar a la madrugada o entrar al Moodle del cliente
 * a lanzar la tarea a mano. Es el mismo problema que resolvió «Sincronizar ahora» para el
 * inventario, y la misma solución con otra función de servicio web.
 *
 * **Y lo que hay que no perder de vista**: la respuesta del Moodle **no trae los datos**.
 * Contesta que acepta el encargo y nos los manda después, en una petición aparte a nuestra
 * API. Un test que diera por hecho que al terminar ya hay foto estaría probando algo que
 * no pasa.
 */
class PedirDatosDeUsoTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN_WS = 'aaaaaaaabbbbbbbbccccccccdddddddd';

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

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

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

        Cache::flush();

        return $user;
    }

    private function entorno(array $extra = []): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'usocli'],
            ['name' => 'Cliente de uso', 'actived' => true]
        );

        return Environment::create(array_merge([
            'name' => 'Campus con datos',
            'domain' => 'https://campus-uso.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
            'moodletoken' => self::TOKEN_WS,
        ], $extra));
    }

    /* ===================== El servicio ===================== */

    public function test_llama_a_la_funcion_de_estadisticas_del_plugin(): void
    {
        // **La función es el contrato.** `local_tresipunt_stats_data` es la que dispara
        // `tip::stats()` en el Moodle; con la de `sync` se traería el inventario de
        // plugins y la pantalla de uso seguiría vacía.
        Http::fake([
            '*' => Http::response(['success' => true, 'data' => []], 200),
        ]);

        $entorno = $this->entorno();

        $resultado = app(MoodleSyncService::class)->pedirDatosDeUso($entorno);

        $this->assertTrue($resultado['ok']);

        Http::assertSent(function ($peticion) {
            return str_contains($peticion->url(), '/webservice/rest/server.php')
                && $peticion['wsfunction'] === 'local_tresipunt_stats_data'
                && $peticion['wstoken'] === self::TOKEN_WS;
        });
    }

    public function test_sincronizar_sigue_llamando_a_la_suya(): void
    {
        // **Lo que no se puede romper al generalizar el servicio.** Las dos puertas
        // comparten cuerpo, así que un cruce de funciones no daría ningún error: haría
        // silenciosamente lo otro.
        Http::fake(['*' => Http::response(['success' => true], 200)]);

        app(MoodleSyncService::class)->lanzar($this->entorno());

        Http::assertSent(fn ($p) => $p['wsfunction'] === 'local_tresipunt_auth_sync');
    }

    public function test_pedir_datos_no_queda_bloqueado_por_una_sincronizacion(): void
    {
        // **El cerrojo va por entorno Y por lo que se pide.** Son dos trabajos distintos
        // en el Moodle, y compartir cerrojo haría que pedir la foto fallara sin motivo
        // porque alguien acabara de lanzar una sincronización.
        Http::fake(['*' => Http::response(['success' => true], 200)]);

        $entorno = $this->entorno();
        $servicio = app(MoodleSyncService::class);

        $this->assertTrue($servicio->lanzar($entorno)['ok']);
        $this->assertTrue($servicio->pedirDatosDeUso($entorno)['ok'],
            'pedir la foto es otro encargo: no lo bloquea el cerrojo del inventario');

        Http::assertSentCount(2);
    }

    public function test_dos_peticiones_seguidas_de_datos_solo_mandan_una(): void
    {
        // Y dentro de lo suyo, el cerrojo sí manda: cortesía con el sitio del cliente.
        Http::fake(['*' => Http::response(['success' => true], 200)]);

        $entorno = $this->entorno();
        $servicio = app(MoodleSyncService::class);

        $primero = $servicio->pedirDatosDeUso($entorno);
        $segundo = $servicio->pedirDatosDeUso($entorno);

        $this->assertTrue($primero['ok']);
        $this->assertFalse($segundo['ok']);
        $this->assertStringContainsString('Ya se ha pedido los datos de uso', $segundo['mensaje']);
        Http::assertSentCount(1);
    }

    public function test_sin_token_no_sale_ninguna_llamada_y_se_explica(): void
    {
        // No es una avería: es que falta un dato, y la acción a tomar es otra.
        Http::fake();

        $resultado = app(MoodleSyncService::class)
            ->pedirDatosDeUso($this->entorno(['moodletoken' => null]));

        $this->assertFalse($resultado['ok']);
        $this->assertStringContainsString('token de servicios web', $resultado['mensaje']);
        $this->assertStringContainsString('Conexión con el Moodle', $resultado['mensaje'],
            'el mensaje tiene que decir dónde se guarda');

        Http::assertNothingSent();
    }

    public function test_el_interruptor_apaga_las_dos_puertas(): void
    {
        // Lo que se apaga es la salida a internet del Manager, no una función concreta.
        config()->set('moodle.sync.enabled', false);
        Http::fake();

        $entorno = $this->entorno();
        $servicio = app(MoodleSyncService::class);

        $this->assertFalse($servicio->pedirDatosDeUso($entorno)['ok']);
        $this->assertFalse($servicio->lanzar($entorno)['ok']);

        Http::assertNothingSent();
    }

    public function test_un_rechazo_del_plugin_dice_que_se_estaba_pidiendo(): void
    {
        // «Ha rechazado la sincronización» cuando lo que se pedía era la foto manda a
        // mirar donde no es.
        Http::fake(['*' => Http::response(['success' => false, 'error' => 'sin permiso'], 200)]);

        $resultado = app(MoodleSyncService::class)->pedirDatosDeUso($this->entorno());

        $this->assertFalse($resultado['ok']);
        $this->assertStringContainsString('los datos de uso', $resultado['mensaje']);
        $this->assertStringContainsString('sin permiso', $resultado['mensaje']);
    }

    public function test_los_servicios_web_apagados_se_explican_igual_que_en_sync(): void
    {
        // **Las nueve traducciones de fallo son lo que se comparte**, y es la razón de no
        // haber duplicado el servicio: un `errorcode` de Moodle tiene que explicarse igual
        // se pida lo que se pida.
        Http::fake(['*' => Http::response(['exception' => 'x', 'errorcode' => 'servicenotavailable'], 200)]);

        $resultado = app(MoodleSyncService::class)->pedirDatosDeUso($this->entorno());

        $this->assertFalse($resultado['ok']);
        $this->assertNotSame('', trim($resultado['mensaje']));
        $this->assertNotNull($resultado['detalle'], 'el detalle técnico se guarda para el ticket');
    }

    public function test_queda_registrado_en_el_log_del_panel(): void
    {
        Http::fake(['*' => Http::response(['success' => true], 200)]);

        $entorno = $this->entorno();

        app(MoodleSyncService::class)->pedirDatosDeUso($entorno);

        $entrada = MonitoringLog::where('code', QueSeLePide::datosDeUso()->codigoOk)->first();

        $this->assertNotNull($entrada, 'pedirle algo al Moodle de un cliente deja rastro');
        $this->assertStringContainsString($entorno->domain, $entrada->msg);
    }

    public function test_los_codigos_nuevos_estan_en_el_registro(): void
    {
        // Sin esto el visor del panel pinta «Código 16032» en lugar del texto, tanto en la
        // fila como en el desplegable de filtros.
        foreach ([QueSeLePide::datosDeUso(), QueSeLePide::sincronizacion()] as $que) {
            $this->assertArrayHasKey($que->codigoOk, MonitoringLog::CODIGOS);
            $this->assertArrayHasKey($que->codigoFallo, MonitoringLog::CODIGOS);
        }
    }

    /* ===================== La modal ===================== */

    public function test_la_modal_pide_y_avisa_a_la_pantalla(): void
    {
        Http::fake(['*' => Http::response(['success' => true], 200)]);

        $this->usuario();
        $entorno = $this->entorno();

        Livewire::test(PedirDatosModal::class, ['environmentId' => $entorno->id])
            ->assertOk()
            ->call('pedir')
            ->assertSet('ok', true)
            ->assertDispatched('datosDeUsoPedidos');
    }

    public function test_la_modal_no_ofrece_pedirlo_sin_token(): void
    {
        $this->usuario();
        $entorno = $this->entorno(['moodletoken' => null]);

        Livewire::test(PedirDatosModal::class, ['environmentId' => $entorno->id])
            ->assertOk()
            ->assertDontSee('Pedirlos ahora')
            ->assertSee('no tiene guardado su token');
    }

    public function test_la_modal_dice_que_los_datos_llegan_despues(): void
    {
        // **Lo que evita el «pues no ha funcionado».** Sin esta frase, quien pulse verá
        // «correcto» y la pantalla igual de vacía.
        $this->usuario();
        $entorno = $this->entorno();

        Livewire::test(PedirDatosModal::class, ['environmentId' => $entorno->id])
            ->assertSee('no vuelven en esta respuesta');
    }

    public function test_un_fallo_deja_ver_el_detalle_tecnico(): void
    {
        Http::fake(['*' => Http::response('<html>no soy json</html>', 200)]);

        $this->usuario();
        $entorno = $this->entorno();

        $componente = Livewire::test(PedirDatosModal::class, ['environmentId' => $entorno->id])
            ->call('pedir')
            ->assertSet('ok', false)
            ->assertSee('Detalle técnico');

        $this->assertNotNull($componente->get('detalle'));
    }

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

        Livewire::test(PedirDatosModal::class, ['environmentId' => $entorno->id])
            ->assertForbidden();
    }

    /* ===================== El botón ===================== */

    public function test_el_boton_sale_en_la_pantalla_de_uso(): void
    {
        $this->usuario(['admin.environments.refresh', 'admin.environments.uso']);
        $entorno = $this->entorno();

        Livewire::test(Uso::class, ['environment' => $entorno])
            ->assertOk()
            ->assertSee('Pedir ahora');
    }

    public function test_sin_permiso_de_refrescar_no_hay_boton(): void
    {
        $this->usuario(['admin.environments.uso']);
        $entorno = $this->entorno();

        Livewire::test(Uso::class, ['environment' => $entorno])
            ->assertOk()
            ->assertDontSee('Pedir ahora')
            ->assertDontSee('Pedirle los datos ahora');
    }

    public function test_un_entorno_de_baja_no_ofrece_pedir_nada(): void
    {
        // Su histórico está congelado a propósito: pedirle datos a un sitio dado de baja
        // volvería a moverlo.
        $this->usuario(['admin.environments.refresh', 'admin.environments.uso']);
        $entorno = $this->entorno();
        $entorno->delete();

        Livewire::test(Uso::class, ['environment' => $entorno->fresh()])
            ->assertOk()
            ->assertDontSee('Pedir ahora');
    }
}
