// Unit tests para gltf_load_mesh (issue: gltf_load_mesh_cpp_gfx).
// Cubre: reject magic invalido, triangulo con POSITION+indices sin NORMAL
// (normales generadas correctamente), quad (2 tris), load desde memoria.
// No requiere contexto GL — logica CPU pura.

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "gfx/gltf_load_mesh.h"

#include <cstdint>
#include <cstring>
#include <string>
#include <vector>

// ---------------------------------------------------------------------------
// GLB builder helpers (minimal — construye GLB en memoria para tests)
// ---------------------------------------------------------------------------

// nlohmann is in vendor/ but test includes functions/ and framework/ by default.
// Build a GLB manually via byte helpers to avoid adding another include path.

static void write_u32le(std::vector<uint8_t>& buf, uint32_t v) {
    buf.push_back(v & 0xFF);
    buf.push_back((v >> 8) & 0xFF);
    buf.push_back((v >> 16) & 0xFF);
    buf.push_back((v >> 24) & 0xFF);
}

static void write_f32le(std::vector<uint8_t>& buf, float v) {
    uint32_t u; std::memcpy(&u, &v, 4);
    write_u32le(buf, u);
}

// Align 'buf' to 4-byte boundary by appending 0x20 (space) padding.
static void align4(std::vector<uint8_t>& buf) {
    while (buf.size() % 4 != 0) buf.push_back(0x20);
}

// Build a minimal GLB with:
//   positions: flat float xyz array (length = nv*3)
//   indices:   uint16 array  (length = ni)
//   normals:   optional float xyz array (length = nv*3, nullptr = omit)
// Returns the complete GLB byte vector.
static std::vector<uint8_t> build_glb(const float* positions, size_t nv,
                                        const uint16_t* indices, size_t ni,
                                        const float* normals = nullptr) {
    // BIN chunk: positions | indices | (normals)
    std::vector<uint8_t> bin;
    size_t pos_offset = 0;
    size_t pos_byteLen = nv * 3 * 4;
    for (size_t i = 0; i < nv*3; ++i) write_f32le(bin, positions[i]);

    // pad before indices so they start at 4-byte alignment
    while (bin.size() % 4 != 0) bin.push_back(0);
    size_t idx_offset = bin.size();
    size_t idx_byteLen = ni * 2;
    for (size_t i = 0; i < ni; ++i) {
        bin.push_back(indices[i] & 0xFF);
        bin.push_back((indices[i] >> 8) & 0xFF);
    }

    size_t nrm_offset = 0, nrm_byteLen = 0;
    if (normals) {
        while (bin.size() % 4 != 0) bin.push_back(0);
        nrm_offset  = bin.size();
        nrm_byteLen = nv * 3 * 4;
        for (size_t i = 0; i < nv*3; ++i) write_f32le(bin, normals[i]);
    }
    // GLB chunk length must be multiple of 4
    while (bin.size() % 4 != 0) bin.push_back(0);

    // Build JSON
    // accessor 0: POSITION (vec3 float, bufferView 0)
    // accessor 1: indices  (scalar uint16, bufferView 1)
    // accessor 2: NORMAL   (vec3 float, bufferView 2) — if normals present
    std::string json = "{";
    json += "\"asset\":{\"version\":\"2.0\"},";
    json += "\"buffers\":[{\"byteLength\":" + std::to_string(bin.size()) + "}],";

    // bufferViews
    json += "\"bufferViews\":[";
    json += "{\"buffer\":0,\"byteOffset\":" + std::to_string(pos_offset) +
            ",\"byteLength\":" + std::to_string(pos_byteLen) + "}";
    json += ",{\"buffer\":0,\"byteOffset\":" + std::to_string(idx_offset) +
            ",\"byteLength\":" + std::to_string(idx_byteLen) + "}";
    if (normals) {
        json += ",{\"buffer\":0,\"byteOffset\":" + std::to_string(nrm_offset) +
                ",\"byteLength\":" + std::to_string(nrm_byteLen) + "}";
    }
    json += "],";

    // accessors
    json += "\"accessors\":[";
    json += "{\"bufferView\":0,\"byteOffset\":0,\"componentType\":5126,\"count\":" +
            std::to_string(nv) + ",\"type\":\"VEC3\"}";
    json += ",{\"bufferView\":1,\"byteOffset\":0,\"componentType\":5123,\"count\":" +
            std::to_string(ni) + ",\"type\":\"SCALAR\"}";
    if (normals) {
        json += ",{\"bufferView\":2,\"byteOffset\":0,\"componentType\":5126,\"count\":" +
                std::to_string(nv) + ",\"type\":\"VEC3\"}";
    }
    json += "],";

    // meshes / primitives
    std::string attrs = "\"POSITION\":0";
    if (normals) attrs += ",\"NORMAL\":2";
    json += "\"meshes\":[{\"primitives\":[{\"attributes\":{" + attrs + "},\"indices\":1}]}]";
    json += "}";

    // Pad JSON to 4-byte boundary
    while (json.size() % 4 != 0) json += ' ';

    // Assemble GLB
    std::vector<uint8_t> glb;
    uint32_t json_chunk_len = (uint32_t)json.size();
    uint32_t bin_chunk_len  = (uint32_t)bin.size();
    uint32_t total = 12 + 8 + json_chunk_len + 8 + bin_chunk_len;

    // Header
    write_u32le(glb, 0x46546C67u); // magic "glTF"
    write_u32le(glb, 2u);           // version
    write_u32le(glb, total);

    // Chunk 0: JSON
    write_u32le(glb, json_chunk_len);
    write_u32le(glb, 0x4E4F534Au); // "JSON"
    for (char c : json) glb.push_back((uint8_t)c);

    // Chunk 1: BIN
    write_u32le(glb, bin_chunk_len);
    write_u32le(glb, 0x004E4942u); // "BIN\0"
    for (uint8_t b : bin) glb.push_back(b);

    return glb;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

TEST_CASE("invalid magic -> empty Mesh + last_error set", "[gltf][reject]") {
    std::vector<uint8_t> bad(12, 0);
    bad[0] = 0xDE; bad[1] = 0xAD; bad[2] = 0xBE; bad[3] = 0xEF;
    // version=2, total=12
    bad[4]=2; bad[8]=12;

    auto m = fn::gfx::gltf_load_mesh_from_memory(bad.data(), bad.size());
    REQUIRE(m.positions.empty());
    REQUIRE(m.indices.empty());
    std::string err = fn::gfx::gltf_load_last_error();
    REQUIRE(!err.empty());
    INFO("last_error: " << err);
    // Should mention magic or "not a GLB"
    REQUIRE((err.find("magic") != std::string::npos ||
             err.find("GLB")   != std::string::npos));
}

TEST_CASE("too-small buffer -> empty Mesh + last_error set", "[gltf][reject]") {
    std::vector<uint8_t> tiny = {0x67, 0x6C, 0x54, 0x46}; // only 4 bytes
    auto m = fn::gfx::gltf_load_mesh_from_memory(tiny.data(), tiny.size());
    REQUIRE(m.positions.empty());
    std::string err = fn::gfx::gltf_load_last_error();
    REQUIRE(!err.empty());
}

TEST_CASE("triangle without NORMAL -> normals generated, correct count", "[gltf][triangle][normals]") {
    // One triangle in XY plane (z=0): (0,0,0), (1,0,0), (0,1,0)
    // Face normal = (0,0,1) → all vertices should get approx (0,0,1)
    float pos[] = { 0,0,0,  1,0,0,  0,1,0 };
    uint16_t idx[] = { 0, 1, 2 };

    auto glb = build_glb(pos, 3, idx, 3, /*normals=*/nullptr);
    auto m = fn::gfx::gltf_load_mesh_from_memory(glb.data(), glb.size());

    REQUIRE(m.positions.size() == 9);   // 3 vertices * 3 floats
    REQUIRE(m.indices.size()   == 3);
    REQUIRE(m.normals.size()   == m.positions.size());

    // Check positions
    REQUIRE(m.positions[0] == Catch::Approx(0.0f));
    REQUIRE(m.positions[1] == Catch::Approx(0.0f));
    REQUIRE(m.positions[2] == Catch::Approx(0.0f));
    REQUIRE(m.positions[3] == Catch::Approx(1.0f));
    REQUIRE(m.positions[6] == Catch::Approx(0.0f));
    REQUIRE(m.positions[7] == Catch::Approx(1.0f));

    // Check indices
    REQUIRE(m.indices[0] == 0u);
    REQUIRE(m.indices[1] == 1u);
    REQUIRE(m.indices[2] == 2u);

    // Generated normals should point toward +Z for all 3 vertices
    for (int v = 0; v < 3; ++v) {
        REQUIRE(m.normals[v*3+0] == Catch::Approx(0.0f).margin(1e-5f));
        REQUIRE(m.normals[v*3+1] == Catch::Approx(0.0f).margin(1e-5f));
        REQUIRE(m.normals[v*3+2] == Catch::Approx(1.0f).margin(1e-5f));
    }
}

TEST_CASE("quad (2 triangles) -> positions.size()==12, indices.size()==6", "[gltf][quad]") {
    // Quad in XY: (0,0,0),(1,0,0),(1,1,0),(0,1,0) split into 2 tris
    float pos[] = { 0,0,0,  1,0,0,  1,1,0,  0,1,0 };
    uint16_t idx[] = { 0,1,2,  0,2,3 };

    auto glb = build_glb(pos, 4, idx, 6, nullptr);
    auto m = fn::gfx::gltf_load_mesh_from_memory(glb.data(), glb.size());

    REQUIRE(m.positions.size() == 12); // 4 * 3
    REQUIRE(m.normals.size()   == 12);
    REQUIRE(m.indices.size()   == 6);

    // All normals should be (0,0,1) — flat XY plane
    for (int v = 0; v < 4; ++v) {
        REQUIRE(m.normals[v*3+2] == Catch::Approx(1.0f).margin(1e-5f));
    }
}

TEST_CASE("explicit normals -> passed through unchanged", "[gltf][normals]") {
    float pos[] = { 0,0,0,  1,0,0,  0,1,0 };
    uint16_t idx[] = { 0, 1, 2 };
    // Provide normals pointing in -Z (unusual, but should be respected)
    float nrm[] = { 0,0,-1,  0,0,-1,  0,0,-1 };

    auto glb = build_glb(pos, 3, idx, 3, nrm);
    auto m = fn::gfx::gltf_load_mesh_from_memory(glb.data(), glb.size());

    REQUIRE(m.positions.size() == 9);
    REQUIRE(m.normals.size()   == 9);

    for (int v = 0; v < 3; ++v) {
        REQUIRE(m.normals[v*3+0] == Catch::Approx(0.0f).margin(1e-5f));
        REQUIRE(m.normals[v*3+1] == Catch::Approx(0.0f).margin(1e-5f));
        REQUIRE(m.normals[v*3+2] == Catch::Approx(-1.0f).margin(1e-5f));
    }
}

TEST_CASE("nonexistent file -> empty Mesh + last_error set", "[gltf][file]") {
    auto m = fn::gfx::gltf_load_mesh_from_file("/tmp/does_not_exist_gltf_test_abc123.glb");
    REQUIRE(m.positions.empty());
    std::string err = fn::gfx::gltf_load_last_error();
    REQUIRE(!err.empty());
}
