<?php

namespace Tests\Feature\Api;

use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Products\LicenseToken;
use App\Models\Products\Product;
use App\Services\Api\Actions\FeaturesAction;
use App\Services\Api\Exceptions\FeaturesException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

/**
 * Homogeneidad del contrato con `local_tresipunt`, revisado contra el código del
 * plugin (`moodle45/local/tresipunt`, versión 2026080603).
 *
 * El dato que cambia el criterio de todo este fichero: **el plugin SÍ ramifica por
 * nuestro `code`**. En `classes/output/index/base_index_page.php`:
 *
 *   CODES_NO_CONTENT = [3002, 4002, 5002]                       → pinta "sin contenido"
 *   CODES_NO_LICENCE = [401, 403, 2001, 2002, 3001, 4001, 5001] → pinta la promo
 *
 * O sea que un código mal puesto no es cosmético: cambia lo que ve el cliente.
 */
class ContractHomogeneityTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN = '3IP-TEST-CONTRACT-1';
    private const DOMAIN = 'https://contrato.test';
    private const OTRO_DOMAIN = 'https://el-otro-cliente.test';

    private function sembrar(): array
    {
        $client = Client::create(['name' => 'Cliente Contrato', 'shortname' => 'contrato', 'actived' => true]);

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

        $environment = Environment::create([
            'name' => 'Entorno Contrato',
            'domain' => self::DOMAIN,
            'version' => '5.1.1',
            'env' => 'local',
            'client_id' => $client->id,
            'license_token_id' => $token->id,
            'active' => true,
        ]);

        // Un segundo entorno del MISMO token: es lo que el mensaje de error
        // enumeraba y no debe volver a salir por la red.
        Environment::create([
            'name' => 'Otro entorno del cliente',
            'domain' => self::OTRO_DOMAIN,
            'version' => '4.5.3',
            'env' => 'pro',
            'client_id' => $client->id,
            'license_token_id' => $token->id,
            'active' => true,
        ]);

        return [$client, $token, $environment];
    }

    private function llamar(array $extra = [], string $host = self::DOMAIN): \Illuminate\Testing\TestResponse
    {
        return $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', array_merge([
            'action' => 'products',
            'host' => $host,
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => [
                'version' => '2025100601.03',
                'release' => '5.1.1+ (Build: 20251219)',
                'type' => 'moodle',
                'env' => 'local',
            ],
        ], $extra));
    }

    public function test_el_error_de_host_no_enumera_los_dominios_del_cliente(): void
    {
        // MGR-009. El mensaje decía "Available hosts: a, b, c" y `data` los repetía en
        // `available_hosts`. Cualquier sitio con el token —o comprometido— obtenía el
        // listado de dominios de su cliente, y el plugin además lo escribe en su log.
        $this->sembrar();

        $respuesta = $this->llamar([], 'https://no-registrado.test');

        $cuerpo = $respuesta->getContent();

        $this->assertStringNotContainsString('Available hosts', $cuerpo);
        $this->assertStringNotContainsString('el-otro-cliente.test', $cuerpo);
        $respuesta->assertJsonMissingPath('data.available_hosts');

        // Lo que SÍ se queda: el host buscado, que es el dominio de quien llama.
        $respuesta->assertJsonPath('data.searched_host', 'https://no-registrado.test');
    }

    public function test_features_usa_su_familia_de_codigos(): void
    {
        // FeaturesAction lanzaba ScssException con 3000/3002/3003. Su familia es la
        // 5xxx, y el código importa: 5002 está en CODES_NO_CONTENT del plugin, así
        // que "no hay features publicadas" se pinta como lista vacía y no como avería.
        $producto = Product::create([
            'name' => 'Producto Features',
            'slug' => 'theme_prueba',
            'shortname' => 'prueba',
        ]);

        $peticion = Request::create('/api/v1', 'POST', ['action' => 'features']);
        $peticion->merge(['_validated_product' => $producto]);

        try {
            (new FeaturesAction())->execute($peticion, ['version' => '2026080604']);
            $this->fail('Se esperaba una FeaturesException: no hay versión de features.');
        } catch (FeaturesException $e) {
            $this->assertSame(5002, $e->getApiCode());
            $this->assertSame(404, $e->getHttpStatus());
        }
    }

    public function test_features_valida_el_formato_de_version_con_su_codigo(): void
    {
        $producto = Product::create([
            'name' => 'Producto Features 2',
            'slug' => 'theme_prueba2',
            'shortname' => 'prueba2',
        ]);

        $peticion = Request::create('/api/v1', 'POST', []);
        $peticion->merge(['_validated_product' => $producto]);

        try {
            (new FeaturesAction())->execute($peticion, ['version' => 'no-es-una-version']);
            $this->fail('Se esperaba una FeaturesException por formato de versión.');
        } catch (FeaturesException $e) {
            $this->assertSame(5003, $e->getApiCode());
        }
    }

    public function test_la_accion_stats_ya_no_existe(): void
    {
        // Retirada en la 2.0.0: ni el plugin 2.0.0 ni la 1.1.3 la llamaban. La tarea
        // diaria envía `action = 'data'` (tip.php, método stats()).
        $this->sembrar();

        $this->llamar([
            'action' => 'stats',
            'date' => '2026-08-27',
            'data' => ['users' => 10],
        ])->assertStatus(422);
    }

    public function test_la_accion_data_guarda_una_fila_por_dia(): void
    {
        // Sin la fila diaria no hay serie que pintar: cada envío machacaba el
        // anterior. La escribe `data` porque es la acción que el plugin llama.
        [, , $environment] = $this->sembrar();

        $this->llamar([
            'action' => 'data',
            'date' => '2026-08-27',
            'data' => ['users' => 120, 'courses' => 34],
        ])->assertOk()->assertJsonPath('data.date', '2026-08-27');

        $this->assertTrue($this->hayFotaDelDia($environment->id, '2026-08-27'));

        // Y el estado actual se sigue guardando en `data`.
        $this->assertDatabaseHas('data', ['environment_id' => $environment->id, 'users' => 120]);
    }

    public function test_dos_envios_el_mismo_dia_no_duplican_la_fila(): void
    {
        [, , $environment] = $this->sembrar();

        foreach ([100, 150] as $usuarios) {
            $this->llamar([
                'action' => 'data',
                'date' => '2026-08-27',
                'data' => ['users' => $usuarios],
            ])->assertOk();
        }

        $this->assertSame(1, $this->fotosDelDia($environment->id, '2026-08-27'));
    }

    public function test_sin_fecha_la_foto_se_guarda_con_la_de_hoy(): void
    {
        // Una versión antigua del plugin, o una llamada manual, no envían `date`.
        [, , $environment] = $this->sembrar();

        $this->llamar([
            'action' => 'data',
            'data' => ['users' => 7],
        ])->assertOk();

        $this->assertTrue($this->hayFotaDelDia($environment->id, now()->format('Y-m-d')));
    }

    public function test_se_registra_que_plugin_origina_la_llamada(): void
    {
        // MGR-036: el cliente envía `callerplugin` cuando quien pide no es quien
        // firma. Se tiraba, y con solo `plugin` no se puede responder "¿quién ha
        // hecho estas 400 peticiones?".
        $this->sembrar();

        $this->llamar(['callerplugin' => 'theme_fresk'])->assertOk();

        $this->assertDatabaseHas('api_request_logs', [
            'action' => 'products',
            'plugin' => 'local_tresipunt',
            'callerplugin' => 'theme_fresk',
        ]);
    }

    /** Cuántas fotos hay de ese entorno y ese día, comparando por fecha. */
    private function fotosDelDia(int $environmentId, string $dia): int
    {
        return DB::table('environment_stats')
            ->where('environment_id', $environmentId)
            ->whereDate('date', $dia)
            ->count();
    }

    private function hayFotaDelDia(int $environmentId, string $dia): bool
    {
        return $this->fotosDelDia($environmentId, $dia) === 1;
    }
}
