<?php

namespace Tests\Feature\Products;

use App\Models\Auth\User;
use App\Models\Features\FeatureVersion;
use App\Models\Js\JsVersion;
use App\Models\Products\Product;
use App\Models\Resources\ResourceVersion;
use App\Models\Scss\ScssVersion;
use App\Models\ScssCdn\ScssCdnBundle;
use App\Models\Setups\Setup;
use App\Models\Tutorials\TutorialVersion;
use App\Support\EstadoDePublicacion;
use App\Support\TieneEstadoDePublicacion;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * El estado de publicación, **igual en los siete tipos de contenido** (MGR-023).
 *
 * Hasta el 2026-09-10 solo `setups` tenía columna de estado. Los otros seis se servían **en
 * cuanto se creaban**: bastaba que el número de versión le cuadrara al plugin de un sitio
 * para que ese contenido le llegara, a medio escribir o no. No había borrador, ni «preparado
 * sin publicar», ni deprecación — se editaba en producción.
 *
 * Y eso choca con cómo se autoría de verdad: preparar la documentación de una versión de
 * producto lleva días y varias manos. La única forma de no publicar a medias era no crear la
 * versión hasta tenerlo todo, o crearla con un número más alto que ninguna instalación
 * tuviera aún: un truco, no un mecanismo.
 *
 * **Lo que estos tests recorren son los siete a la vez**, a propósito. La asimetría anterior
 * no fue un olvido puntual: fue que la regla vivía en siete sitios. Un test que solo probara
 * uno dejaría que los otros seis volvieran a divergir sin que nada avisara.
 */
class EstadoDePublicacionTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Un usuario de verdad para `created_by`.
     *
     * Las siete tablas tienen clave ajena a `users`, así que un `1` a pelo no pasa del
     * `insert` — y el fallo sale como violación de clave ajena, no como lo que se está
     * probando.
     */
    private function quien(): int
    {
        return $this->quien ??= User::factory()->create()->id;
    }

    private ?int $quien = null;

    private function producto(): Product
    {
        return Product::create([
            'name' => 'Fresk',
            'slug' => 'theme_fresk_' . str()->random(6),
            'actived' => true,
        ]);
    }

    /**
     * Los siete tipos, con lo mínimo para crear una versión de cada uno.
     *
     * @return array<string, array{0: class-string, 1: array<string, mixed>}>
     */
    public static function losSieteTipos(): array
    {
        return [
            'funcionalidades' => [FeatureVersion::class, []],
            'tutoriales' => [TutorialVersion::class, []],
            'recursos' => [ResourceVersion::class, []],
            'scss' => [ScssVersion::class, []],
            'js' => [JsVersion::class, []],
            'bundle scss-cdn' => [ScssCdnBundle::class, []],
            'setups' => [Setup::class, ['yaml' => "plugin: theme_fresk\nversion: 2026010100\n"]],
        ];
    }

    private function version(string $modelo, Product $producto, string $version, string $estado, array $extra = []): object
    {
        return $modelo::create(array_merge([
            'product_id' => $producto->id,
            'version' => $version,
            'status' => $estado,
            'created_by' => $this->quien(),
            'updated_by' => $this->quien(),
        ], $extra));
    }

    /* ============ Lo que se sirve y lo que no ============ */

    /** @dataProvider losSieteTipos */
    public function test_un_borrador_no_se_sirve_aunque_su_numero_cuadre(string $modelo, array $extra): void
    {
        // **El fallo entero, en una frase.** Un borrador con un número que le cuadra al
        // plugin del sitio se servía igual, porque no había nada que filtrar.
        $producto = $this->producto();

        $this->version($modelo, $producto, '2026010100', EstadoDePublicacion::BORRADOR, $extra);

        $this->assertNull(
            $modelo::findCompatibleVersion($producto, '2026010100'),
            $modelo . ' sirve una versión en borrador'
        );
    }

    /** @dataProvider losSieteTipos */
    public function test_una_retirada_tampoco_se_sirve(string $modelo, array $extra): void
    {
        $producto = $this->producto();

        $this->version($modelo, $producto, '2026010100', EstadoDePublicacion::DESHABILITADA, $extra);

        $this->assertNull($modelo::findCompatibleVersion($producto, '2026010100'));
    }

    /** @dataProvider losSieteTipos */
    public function test_una_publicada_si_se_sirve(string $modelo, array $extra): void
    {
        $producto = $this->producto();

        $this->version($modelo, $producto, '2026010100', EstadoDePublicacion::PUBLICADA, $extra);

        $servida = $modelo::findCompatibleVersion($producto, '2026010100');

        $this->assertNotNull($servida);
        $this->assertSame('2026010100', $servida->version);
    }

    /** @dataProvider losSieteTipos */
    public function test_al_pasar_a_borrador_se_cae_a_la_anterior_publicada(string $modelo, array $extra): void
    {
        // **Es la consecuencia que hay que decirle a quien retira una versión**: no se queda
        // sin contenido, se queda con la anterior que le cuadre. Y por eso la fila de la
        // retirada se conserva.
        $producto = $this->producto();

        $this->version($modelo, $producto, '2025010100', EstadoDePublicacion::PUBLICADA, $extra);
        $this->version($modelo, $producto, '2026010100', EstadoDePublicacion::BORRADOR, $extra);

        $servida = $modelo::findCompatibleVersion($producto, '2026010100');

        $this->assertNotNull($servida);
        $this->assertSame('2025010100', $servida->version, 'no ha caído a la anterior publicada');
    }

    /** @dataProvider losSieteTipos */
    public function test_la_coincidencia_exacta_tambien_respeta_el_estado(string $modelo, array $extra): void
    {
        // **El camino que se olvida.** El resolvedor busca primero la coincidencia exacta por
        // igualdad de texto y devuelve esa; si ese atajo no filtrara por estado, un borrador
        // con el número exacto del plugin se serviría igual y los otros tests no lo verían.
        $producto = $this->producto();

        $this->version($modelo, $producto, '2026010100', EstadoDePublicacion::BORRADOR, $extra);

        $this->assertNull($modelo::findCompatibleVersion($producto, '2026010100'));
    }

    /* ============ Y todo lo que ya estaba, publicado ============ */

    /** @dataProvider losSieteTipos */
    public function test_el_defecto_de_la_columna_es_publicada(string $modelo, array $extra): void
    {
        // **La respuesta a la pregunta abierta del issue**: qué pasa con el contenido ya
        // cargado. Todo pasa a `active`, porque cualquier otra cosa dejaría de servir hoy
        // contenido que se sirve, y eso lo notaría el cliente antes que nosotros. La
        // migración no toca las filas: se apoya en el defecto de la columna, y esto lo fija.
        $producto = $this->producto();

        $fila = $modelo::create(array_merge([
            'product_id' => $producto->id,
            'version' => '2026010100',
            'created_by' => $this->quien(),
            'updated_by' => $this->quien(),
        ], $extra));

        $this->assertSame(EstadoDePublicacion::PUBLICADA, $fila->fresh()->status);
    }

    /* ============ Cómo nace una versión ============ */

    /** @dataProvider losSieteTipos */
    public function test_un_duplicado_nace_borrador(string $modelo, array $extra): void
    {
        // Se duplica una versión para preparar la siguiente: publicarla al copiarla es lo
        // contrario de lo que se venía a hacer.
        $producto = $this->producto();

        $original = $this->version($modelo, $producto, '2026010100', EstadoDePublicacion::PUBLICADA, $extra);

        $copia = $original->duplicate();

        $this->assertSame(EstadoDePublicacion::BORRADOR, $copia->fresh()->status);
        // Y la original sigue publicada: duplicar no despublica nada.
        $this->assertSame(EstadoDePublicacion::PUBLICADA, $original->fresh()->status);
    }

    /* ============ La semántica, en un solo sitio ============ */

    public function test_los_siete_comparten_la_misma_definicion_de_estado(): void
    {
        // **Es el test de esta tanda.** La asimetría anterior no fue un olvido: fue que la
        // regla vivía en siete sitios. Si alguien vuelve a escribir los scopes en un modelo
        // suelto, esto no lo caza — pero sí caza que alguno deje de compartir el trait, que
        // es por donde empezaría la divergencia.
        foreach (array_map(fn (array $t) => $t[0], self::losSieteTipos()) as $modelo) {
            $this->assertContains(
                TieneEstadoDePublicacion::class,
                class_uses_recursive($modelo),
                $modelo . ' no usa el estado compartido: es por donde volvería la asimetría de MGR-023'
            );
        }
    }
}
