/**
 * @file test_spirv.c
 * @brief SPIR-V cache and translation unit tests
 *
 * Tests:
 *   - mvgal_spirv_hash() determinism and distinction
 *   - mvgal_spirv_cache init / insert / lookup / destroy roundtrip
 *   - mvgal_spirv_opt_create() / destroy (if SPIRV-Tools enabled)
 */

#include "mvgal/mvgal.h"
#include "mvgal/mvgal_log.h"
#include "mvgal_shader_translate.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>

static int test_passed = 0;
static int test_failed = 0;

#define TEST_ASSERT(condition, message) \
    do { if (condition) { test_passed++; } else { test_failed++; \
        MVGAL_LOG_ERROR("TEST FAILED: %s at %s:%d", message, __FILE__, __LINE__); \
    } } while (0)

/* ------------------------------------------------------------------ */
/*  Minimal SPIR-V-like blobs for cache testing                        */
/*  These are NOT valid SPIR-V modules — they're byte patterns that    */
/*  exercise the hash / cache / lookup machinery without needing a     */
/*  real GPU or valid SPIR-V.                                          */
/* ------------------------------------------------------------------ */

static const uint32_t blob_a[] = {
    0x07230203, 0x00010300, 0x00000000, 0x00000005,
    0x00000000, 0x00020011, 0x00000001, 0x00020011,
    0x00000002, 0x0003000e, 0x00000004, 0x00020013,
    0x00000005,
};
static const size_t blob_a_words = sizeof(blob_a) / sizeof(blob_a[0]);

static const uint32_t blob_b[] = {
    0x07230203, 0x00010300, 0x00000000, 0x00000007,
    0x00000000, 0x00020011, 0x00000001, 0x0003000e,
    0x00000004, 0x00020013, 0x00000005, 0x0003000f,
    0x00000005, 0x00000001,
};
static const size_t blob_b_words = sizeof(blob_b) / sizeof(blob_b[0]);

/* ------------------------------------------------------------------ */
/*  Hash tests                                                         */
/* ------------------------------------------------------------------ */

static void test_hash_deterministic(void)
{
    MVGAL_LOG_INFO("TEST: Hash deterministic");
    uint64_t h1 = mvgal_spirv_hash(blob_a, blob_a_words);
    uint64_t h2 = mvgal_spirv_hash(blob_a, blob_a_words);
    TEST_ASSERT(h1 == h2, "Hash of same input must match");
    MVGAL_LOG_INFO("TEST: Hash deterministic - PASSED (%016" PRIx64 ")", h1);
}

static void test_hash_different_inputs(void)
{
    MVGAL_LOG_INFO("TEST: Hash different inputs");
    uint64_t ha = mvgal_spirv_hash(blob_a, blob_a_words);
    uint64_t hb = mvgal_spirv_hash(blob_b, blob_b_words);
    TEST_ASSERT(ha != hb, "Hash of different inputs must differ");
    MVGAL_LOG_INFO("TEST: Hash different inputs - PASSED (a=%016" PRIx64 " b=%016" PRIx64 ")", ha, hb);
}

static void test_hash_empty(void)
{
    MVGAL_LOG_INFO("TEST: Hash empty input");
    uint64_t h = mvgal_spirv_hash(NULL, 0);
    /* FNV-1a of empty input is the offset basis */
    TEST_ASSERT(h == 0xcbf29ce484222325ULL, "Hash of empty input must be FNV offset basis");
    MVGAL_LOG_INFO("TEST: Hash empty input - PASSED (%016" PRIx64 ")", h);
}

static void test_hash_partial_vs_full(void)
{
    MVGAL_LOG_INFO("TEST: Hash partial vs full");
    uint64_t h_full  = mvgal_spirv_hash(blob_a, blob_a_words);
    uint64_t h_part  = mvgal_spirv_hash(blob_a, blob_a_words - 2);
    TEST_ASSERT(h_full != h_part, "Partial hash must differ from full hash");
    MVGAL_LOG_INFO("TEST: Hash partial vs full - PASSED");
}

/* ------------------------------------------------------------------ */
/*  Cache tests                                                        */
/* ------------------------------------------------------------------ */

static void test_cache_init_lookup_empty(void)
{
    MVGAL_LOG_INFO("TEST: Cache init + lookup empty");
    mvgal_spirv_cache_t cache;
    mvgal_spirv_cache_init(&cache);
    TEST_ASSERT(cache.count == 0, "Cache count must be 0 after init");
    VkShaderModule m = mvgal_spirv_cache_lookup(&cache, blob_a, blob_a_words);
    TEST_ASSERT(m == VK_NULL_HANDLE, "Lookup on empty cache must return NULL");
    mvgal_spirv_cache_destroy(&cache);
    TEST_ASSERT(cache.count == 0, "Cache count must be 0 after destroy");
    MVGAL_LOG_INFO("TEST: Cache init + lookup empty - PASSED");
}

static void test_cache_insert_lookup_roundtrip(void)
{
    MVGAL_LOG_INFO("TEST: Cache insert + lookup roundtrip");
    mvgal_spirv_cache_t cache;
    mvgal_spirv_cache_init(&cache);

    VkShaderModule fake_mod_a = (VkShaderModule)(uintptr_t)0x42;
    VkShaderModule fake_mod_b = (VkShaderModule)(uintptr_t)0x99;

    bool ok = mvgal_spirv_cache_insert(&cache, blob_a, blob_a_words, fake_mod_a);
    TEST_ASSERT(ok == true, "Insert must succeed");
    TEST_ASSERT(cache.count == 1, "Cache count must be 1 after insert");

    VkShaderModule found = mvgal_spirv_cache_lookup(&cache, blob_a, blob_a_words);
    TEST_ASSERT(found == fake_mod_a, "Lookup must return inserted module");

    /* Insert a second entry */
    ok = mvgal_spirv_cache_insert(&cache, blob_b, blob_b_words, fake_mod_b);
    TEST_ASSERT(ok == true, "Second insert must succeed");
    TEST_ASSERT(cache.count == 2, "Cache count must be 2");

    found = mvgal_spirv_cache_lookup(&cache, blob_b, blob_b_words);
    TEST_ASSERT(found == fake_mod_b, "Lookup must return second module");

    /* First entry still intact */
    found = mvgal_spirv_cache_lookup(&cache, blob_a, blob_a_words);
    TEST_ASSERT(found == fake_mod_a, "First entry still reachable after second insert");

    mvgal_spirv_cache_destroy(&cache);
    MVGAL_LOG_INFO("TEST: Cache insert + lookup roundtrip - PASSED");
}

static void test_cache_no_match_on_wrong_input(void)
{
    MVGAL_LOG_INFO("TEST: Cache no match on wrong input");
    mvgal_spirv_cache_t cache;
    mvgal_spirv_cache_init(&cache);

    mvgal_spirv_cache_insert(&cache, blob_a, blob_a_words,
                              (VkShaderModule)(uintptr_t)0x42);

    VkShaderModule found = mvgal_spirv_cache_lookup(&cache, blob_b, blob_b_words);
    TEST_ASSERT(found == VK_NULL_HANDLE, "Must not match different input");

    mvgal_spirv_cache_destroy(&cache);
    MVGAL_LOG_INFO("TEST: Cache no match on wrong input - PASSED");
}

static void test_cache_update_same_hash(void)
{
    MVGAL_LOG_INFO("TEST: Cache update same hash");
    mvgal_spirv_cache_t cache;
    mvgal_spirv_cache_init(&cache);

    VkShaderModule mod_old = (VkShaderModule)(uintptr_t)0xAA;
    VkShaderModule mod_new = (VkShaderModule)(uintptr_t)0xBB;

    mvgal_spirv_cache_insert(&cache, blob_a, blob_a_words, mod_old);
    mvgal_spirv_cache_insert(&cache, blob_a, blob_a_words, mod_new);

    VkShaderModule found = mvgal_spirv_cache_lookup(&cache, blob_a, blob_a_words);
    TEST_ASSERT(found == mod_new, "Update must replace module handle");
    /* count should have stayed 1, not grown */
    TEST_ASSERT(cache.count == 1, "Update must not increase cache count");

    mvgal_spirv_cache_destroy(&cache);
    MVGAL_LOG_INFO("TEST: Cache update same hash - PASSED");
}

static void test_cache_full_rejects(void)
{
    MVGAL_LOG_INFO("TEST: Cache full rejects insert");
    mvgal_spirv_cache_t cache;
    mvgal_spirv_cache_init(&cache);

    /* Fill cache to capacity with distinct fake entries */
    uint32_t dummy_word = 0;
    bool capacity_ok = true;
    for (size_t i = 0; i < MVGAL_SPIRV_CAPACITY; i++) {
        dummy_word = (uint32_t)i;
        capacity_ok = mvgal_spirv_cache_insert(&cache, &dummy_word, 1,
                          (VkShaderModule)(uintptr_t)(i + 1));
        if (!capacity_ok) break;
    }

    TEST_ASSERT(cache.count == MVGAL_SPIRV_CAPACITY,
                "Cache must reach capacity");
    TEST_ASSERT(capacity_ok == true,
                "All inserts up to capacity must succeed");

    /* Next insert must fail */
    bool rejected = mvgal_spirv_cache_insert(&cache, &dummy_word, 1,
                        (VkShaderModule)(uintptr_t)0xFF);
    TEST_ASSERT(rejected == false, "Insert at capacity must be rejected");

    mvgal_spirv_cache_destroy(&cache);
    MVGAL_LOG_INFO("TEST: Cache full rejects insert - PASSED");
}

/* ------------------------------------------------------------------ */
/*  SPIRV-Tools optimiser lifecycle tests (if enabled)                 */
/* ------------------------------------------------------------------ */

#ifdef MVGAL_ENABLE_SPIRV_OPT
static void test_opt_create_destroy(void)
{
    MVGAL_LOG_INFO("TEST: SPIRV-Tools opt create/destroy");
    void *ctx = mvgal_spirv_opt_create();
    TEST_ASSERT(ctx != NULL, "Optimiser context must not be NULL");
    mvgal_spirv_opt_destroy(ctx);
    MVGAL_LOG_INFO("TEST: SPIRV-Tools opt create/destroy - PASSED");
}

static void test_opt_optimize_invalid_input(void)
{
    MVGAL_LOG_INFO("TEST: SPIRV-Tools optimize invalid input");
    void *ctx = mvgal_spirv_opt_create();
    TEST_ASSERT(ctx != NULL, "Optimiser context must not be NULL");

    uint32_t garbage[] = { 0xDEAD, 0xBEEF, 0xCAFE, 0xBABE };
    uint32_t *out_spirv = NULL;
    size_t   out_words  = 0;

    int rc = mvgal_spirv_optimize(ctx, garbage, 0, &out_spirv, &out_words);
    TEST_ASSERT(rc != 0, "Optimize of empty/invalid SPIR-V must fail");

    mvgal_spirv_opt_destroy(ctx);
    MVGAL_LOG_INFO("TEST: SPIRV-Tools optimize invalid input - PASSED");
}
#endif /* MVGAL_ENABLE_SPIRV_OPT */

/* ------------------------------------------------------------------ */
/*  main                                                               */
/* ------------------------------------------------------------------ */

int main(void)
{
    printf("\n============================================================\n");
    printf("  MVGAL SPIR-V Unit Tests\n");
    printf("============================================================\n\n");

    test_hash_deterministic();
    test_hash_different_inputs();
    test_hash_empty();
    test_hash_partial_vs_full();

    test_cache_init_lookup_empty();
    test_cache_insert_lookup_roundtrip();
    test_cache_no_match_on_wrong_input();
    test_cache_update_same_hash();
    test_cache_full_rejects();

#ifdef MVGAL_ENABLE_SPIRV_OPT
    test_opt_create_destroy();
    test_opt_optimize_invalid_input();
#else
    printf("  SPIRV-Tools not enabled — skipping optimiser tests\n\n");
#endif

    printf("\n============================================================\n");
    printf("  Test Summary\n");
    printf("============================================================\n");
    printf("  Passed: %d\n", test_passed);
    printf("  Failed: %d\n", test_failed);
    printf("  Total:  %d\n", test_passed + test_failed);
    printf("============================================================\n\n");

    return (test_failed > 0) ? EXIT_FAILURE : EXIT_SUCCESS;
}
