// test_visual — golden-image diff de las demos de primitives_gallery.
//
// Asume que existen PNGs en `cpp/tests/golden/<demo>.png` generados por
// `cpp/scripts/update_goldens.sh`. Si no existen, los SKIPea con INFO.
//
// El binario primitives_gallery se localiza relativo al directorio de build:
//   <build>/apps/primitives_gallery/primitives_gallery --capture <tmpdir>
//
// Si el entorno no puede crear contexto GL (WSL minimo, container sin Mesa),
// el binario falla; el test reporta SKIP en lugar de FAIL para no bloquear
// CI en entornos donde el render headless no es posible.

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "png_diff.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <filesystem>
#include <string>
#include <sys/stat.h>
#include <vector>

namespace fs = std::filesystem;

// Defaults inyectados por CMake en el target test_visual:
//   FN_TEST_GOLDEN_DIR     — cpp/tests/golden (absoluto)
//   FN_TEST_GALLERY_BIN    — path absoluto al binario primitives_gallery
//   FN_TEST_TMP_DIR        — directorio temporal donde la corrida actual
//                            escribe sus PNGs (build/tests/visual_actual)
#ifndef FN_TEST_GOLDEN_DIR
#define FN_TEST_GOLDEN_DIR "cpp/tests/golden"
#endif
#ifndef FN_TEST_GALLERY_BIN
#define FN_TEST_GALLERY_BIN ""
#endif
#ifndef FN_TEST_TMP_DIR
#define FN_TEST_TMP_DIR "/tmp/primitives_gallery_visual"
#endif
#ifndef FN_TEST_REPO_ROOT
#define FN_TEST_REPO_ROOT "."
#endif

static std::vector<std::string> list_pngs(const std::string& dir) {
    std::vector<std::string> out;
    if (!fs::exists(dir) || !fs::is_directory(dir)) return out;
    for (const auto& entry : fs::directory_iterator(dir)) {
        if (!entry.is_regular_file()) continue;
        const auto path = entry.path();
        if (path.extension() == ".png") {
            out.push_back(path.stem().string());
        }
    }
    return out;
}

static bool file_exists(const std::string& p) {
    struct stat st{};
    return ::stat(p.c_str(), &st) == 0;
}

TEST_CASE("primitives_gallery visual goldens", "[visual]") {
    const std::string golden_dir = FN_TEST_GOLDEN_DIR;
    const std::string gallery_bin = FN_TEST_GALLERY_BIN;
    const std::string tmp_dir = FN_TEST_TMP_DIR;

    INFO("golden dir: " << golden_dir);
    INFO("gallery bin: " << gallery_bin);
    INFO("tmp dir: " << tmp_dir);

    auto goldens = list_pngs(golden_dir);
    if (goldens.empty()) {
        WARN("No goldens found in '" << golden_dir << "'. "
             "Run cpp/scripts/update_goldens.sh to generate them. "
             "Visual diff test SKIPPED.");
        SUCCEED("no goldens — skipped");
        return;
    }

    if (gallery_bin.empty() || !file_exists(gallery_bin)) {
        WARN("primitives_gallery binary not found at '" << gallery_bin
             << "'. Build target primitives_gallery first. SKIPPED.");
        SUCCEED("gallery binary missing — skipped");
        return;
    }

    // Crear tmp_dir y correr captura.
    std::error_code ec;
    fs::create_directories(tmp_dir, ec);

    // Ejecutar binario en modo --capture desde la raiz del repo. Algunas
    // demos resuelven paths relativos (p.ej. sql_workbench busca registry.db);
    // correr desde la raiz garantiza determinismo entre maquinas.
    //
    // LIBGL_ALWAYS_SOFTWARE=1 fuerza llvmpipe igual que update_goldens.sh, asi
    // demos que muestran strings del driver (gl_info) no fallan por diferencias
    // entre llvmpipe / d3d12 / drivers vendor.
    std::string cmd = std::string("cd '") + FN_TEST_REPO_ROOT + "' && "
                      + "LIBGL_ALWAYS_SOFTWARE=1 '"
                      + gallery_bin + "' --capture '" + tmp_dir + "' 2>&1";
    INFO("capture cmd: " << cmd);
    const int rc = std::system(cmd.c_str());
    if (rc != 0) {
        WARN("primitives_gallery --capture exited with rc=" << rc
             << " (likely no GL context — WSL/headless). "
             << "Run with LIBGL_ALWAYS_SOFTWARE=1 or install Mesa. SKIPPED.");
        SUCCEED("capture failed — environment lacks GL — skipped");
        return;
    }

    int matched = 0, missing_actual = 0, diffed = 0;
    for (const auto& demo_id : goldens) {
        const std::string g_path = golden_dir + "/" + demo_id + ".png";
        const std::string a_path = tmp_dir + "/" + demo_id + ".png";
        if (!file_exists(a_path)) {
            WARN("No actual capture for '" << demo_id << "' at " << a_path);
            ++missing_actual;
            continue;
        }
        auto r = fn_test::pixel_diff_ratio(g_path, a_path, /*channel_threshold=*/5);
        INFO("demo: " << demo_id
             << " diff_ratio=" << r.diff_ratio
             << " (" << r.pixels_different << "/" << r.pixels_total << ")");
        // Tolerancia 1% por defecto.
        if (r.diff_ratio > 0.01) {
            ++diffed;
            FAIL_CHECK("Visual diff for '" << demo_id << "' exceeds 1%: "
                       << (r.diff_ratio * 100.0) << "%. "
                       << "Compare " << a_path << " vs " << g_path
                       << " — if the change is intentional, run "
                       << "cpp/scripts/update_goldens.sh.");
        } else {
            ++matched;
        }
    }

    INFO("visual goldens — matched=" << matched
         << " diffed=" << diffed
         << " missing_actual=" << missing_actual);
    REQUIRE(diffed == 0);
}
