<?php

namespace Tests\Feature\Features;

use App\Services\Features\HtmlSanitizer;
use Tests\TestCase;

/**
 * Criterio de PRODSECU-193: el autor maqueta con componentes Bootstrap por clase.
 * No se admiten estilos de maquetación, ni `<style>`, ni `<link>`, ni clases que
 * cambiaron de nombre entre Bootstrap 4 y 5 (el Manager sirve el mismo contenido a
 * entornos Moodle 4.x y 5.x), ni utilidades de posicionamiento con las que un
 * contenido podría maquetar encima de la página del cliente.
 */
class HtmlSanitizerTest extends TestCase
{
    private HtmlSanitizer $sanitizer;

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

        $this->sanitizer = app(HtmlSanitizer::class);
    }

    public static function componentesAdmitidos(): array
    {
        return [
            'alert' => ['<div class="alert alert-info">Aviso</div>'],
            'card' => ['<div class="card"><div class="card-body"><h5 class="card-title">T</h5></div></div>'],
            'grid' => ['<div class="row"><div class="col-md-6">A</div></div>'],
            'badge' => ['<span class="badge bg-primary">Nuevo</span>'],
            'tabla' => ['<table class="table table-striped"><tbody><tr><td>A</td></tr></tbody></table>'],
            'lista' => ['<ul class="list-group"><li class="list-group-item">A</li></ul>'],
            'espaciado' => ['<div class="mb-3 p-4">Hola</div>'],
            'centrado' => ['<p class="text-center lead">Hola</p>'],
            'definiciones' => ['<dl class="mb-3"><dt>Término</dt><dd>Explicación.</dd></dl>'],
            'tarjeta con cabecera y pie' => ['<div class="card"><div class="card-header">C</div><div class="card-body">B</div><div class="card-footer text-muted">P</div></div>'],
            'tres columnas' => ['<div class="row"><div class="col-md-4">A</div><div class="col-md-4">B</div><div class="col-md-4">C</div></div>'],
            'aviso con titulo' => ['<div class="alert alert-warning"><h5 class="alert-heading">T</h5><p class="mb-0">X</p></div>'],
            'lista con estados' => ['<ul class="list-group"><li class="list-group-item list-group-item-success">OK</li></ul>'],
            'cita' => ['<blockquote class="blockquote"><p>Cita</p><p class="blockquote-footer mb-0">Fuente</p></blockquote>'],
        ];
    }

    /**
     * @dataProvider componentesAdmitidos
     */
    public function test_los_componentes_bootstrap_se_conservan_intactos(string $html): void
    {
        $this->assertSame($html, $this->sanitizer->sanitize($html));
    }

    public static function clasesRechazadas(): array
    {
        return [
            'ms-3 es solo de Bootstrap 5' => ['ms-3'],
            'me-3 es solo de Bootstrap 5' => ['me-3'],
            'fw-bold es solo de Bootstrap 5' => ['fw-bold'],
            'text-end es solo de Bootstrap 5' => ['text-end'],
            'ml-3 es solo de Bootstrap 4' => ['ml-3'],
            'badge-primary es solo de Bootstrap 4' => ['badge-primary'],
            'position-fixed permite tapar la pagina' => ['position-fixed'],
            'fixed-top permite tapar la pagina' => ['fixed-top'],
            'navbar es del tema del cliente' => ['navbar'],
            'clase inventada' => ['mi-clase-propia'],
        ];
    }

    /**
     * @dataProvider clasesRechazadas
     */
    public function test_las_clases_no_admitidas_se_eliminan(string $clase): void
    {
        $salida = $this->sanitizer->sanitize('<div class="' . $clase . '">X</div>');

        $this->assertStringNotContainsString($clase, $salida);
    }

    public function test_no_se_admiten_estilos_de_maquetacion(): void
    {
        $salida = $this->sanitizer->sanitize('<div style="padding:20px;width:50%">X</div>');

        $this->assertSame('<div>X</div>', $salida);
    }

    public function test_se_conserva_el_color_que_genera_el_editor(): void
    {
        // Los botones de color y resaltado de Jodit producen `<span style="color:…">`.
        // Quitar el `style` de span rompería esa función del editor.
        $salida = $this->sanitizer->sanitize('<span style="color:red">Rojo</span>');

        $this->assertStringContainsString('color', $salida);
        $this->assertStringContainsString('Rojo', $salida);
    }

    public function test_no_se_admiten_bloques_style_ni_link(): void
    {
        $this->assertStringNotContainsString(
            'padding',
            $this->sanitizer->sanitize('<style>.card{padding:20px}</style><p>Hola</p>')
        );

        $this->assertSame(
            '',
            trim($this->sanitizer->sanitize('<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">'))
        );
    }

    public function test_los_componentes_de_la_barra_del_editor_sobreviven_al_saneador(): void
    {
        // La barra de componentes del editor (bootstrap-toolbar.blade.php) inserta
        // fragmentos ya montados. Si alguien añade uno con una clase que el saneador
        // no admite, el autor lo insertaría y lo perdería al guardar sin enterarse.
        // Este test recorre la barra real y comprueba que nada se cae.
        $vista = resource_path('views/livewire/features/components/bootstrap-toolbar.blade.php');

        $this->assertFileExists($vista);

        preg_match_all("/'([^']+)' => '(<[^']+)',/", file_get_contents($vista), $coincidencias, PREG_SET_ORDER);

        $this->assertNotEmpty($coincidencias, 'No se han podido leer los fragmentos de la barra.');

        foreach ($coincidencias as [, $nombre, $fragmento]) {
            $limpio = $this->sanitizer->sanitize($fragmento);

            // Se comparan las clases: son lo que aporta el componente
            preg_match_all('/class="([^"]+)"/', $fragmento, $antes);
            preg_match_all('/class="([^"]+)"/', $limpio, $despues);

            $this->assertSame(
                $antes[1],
                $despues[1],
                "El componente «{$nombre}» de la barra pierde clases al guardar."
            );
        }
    }

    public static function componentesInteractivosDelPlugin(): array
    {
        // Markup exacto que espera `amd/src/remotecomponents.js` de local_tresipunt.
        // Los nombres son BEM con doble guión bajo: con guión simple el plugin no los
        // reconoce y el contenido llega al cliente como divs sueltos.
        return [
            'acordeon' => [
                '<div class="tip-accordion">'
                . '<div class="tip-accordion__item">'
                . '<h5 class="tip-accordion__title">Título</h5>'
                . '<div class="tip-accordion__body"><p>Contenido.</p></div>'
                . '</div></div>',
            ],
            'pestanas' => [
                '<div class="tip-tabs">'
                . '<ul class="tip-tabs__nav"><li class="tip-tabs__tab">Primera</li></ul>'
                . '<div class="tip-tabs__panel"><p>Contenido.</p></div>'
                . '</div>',
            ],
        ];
    }

    /**
     * Contrato con `local_tresipunt`: el plugin monta acordeón y pestañas con su
     * propio JavaScript a partir de estas clases, porque el purificador de Moodle
     * borra cualquier `data-*`. Si estas clases se cayeran aquí, el componente
     * llegaría al Moodle del cliente como divs sueltos.
     *
     * @dataProvider componentesInteractivosDelPlugin
     */
    public function test_las_clases_de_los_componentes_del_plugin_se_conservan(string $html): void
    {
        $this->assertSame($html, $this->sanitizer->sanitize($html));
    }

    public function test_figure_no_se_ofrece_porque_moodle_lo_elimina(): void
    {
        // <figure>/<figcaption> son HTML5 y el purificador de Moodle los borra
        // (doctype XHTML 1.0 Transitional), así que tampoco se admiten aquí: es
        // mejor que el autor no pueda insertarlos que que se pierdan por el camino.
        $salida = $this->sanitizer->sanitize('<figure class="figure"><figcaption class="figure-caption">Pie</figcaption></figure>');

        $this->assertStringNotContainsString('figure', $salida);
    }

    public function test_sigue_sin_admitirse_script(): void
    {
        $salida = $this->sanitizer->sanitize('<p>Hola</p><script>alert(1)</script>');

        $this->assertStringNotContainsString('alert(1)', $salida);
        $this->assertStringNotContainsString('<script', $salida);
    }
}
