/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/

// Regression test for issue #69: the engine feeds a shared Device::pipelineCache
// to every vkCreate*Pipelines call and persists it across runs. A blob written
// by a different GPU (or a corrupt/short file) must be rejected before it is
// handed to vkCreatePipelineCache, otherwise the driver ignores or rejects it.
// Device::PipelineCacheDataCompatible is that gate: pure logic over the standard
// 32-byte VkPipelineCacheHeaderVersionOne header (headerSize, headerVersion,
// vendorID, deviceID, pipelineCacheUUID) compared against Device::deviceProperties.
// It needs no GPU, so this test stamps synthetic device identities and headers
// and drives it directly, mirroring MemoryTypeFallback / UploadStrategy.

#include <cstdlib>
#include <cstring>
#include "vulkan/vulkan.h"

import Crafter.Graphics;
import std;
using namespace Crafter;

namespace {

int failures = 0;

void Check(bool ok, std::string_view what) {
    std::println("{}  {}", ok ? "PASS" : "FAIL", what);
    if (!ok) ++failures;
}

// Stamp the device identity the validator compares against.
void SetDevice(std::uint32_t vendorID, std::uint32_t deviceID,
               const std::array<std::uint8_t, VK_UUID_SIZE>& uuid) {
    Device::deviceProperties = {};
    Device::deviceProperties.vendorID = vendorID;
    Device::deviceProperties.deviceID = deviceID;
    std::memcpy(Device::deviceProperties.pipelineCacheUUID, uuid.data(), VK_UUID_SIZE);
}

// Build a 32-byte VkPipelineCacheHeaderVersionOne blob, optionally with extra
// trailing payload bytes (the real cache body). Fields are written little-endian
// at fixed offsets, matching how the driver lays the header out on disk.
std::vector<std::byte> MakeHeader(std::uint32_t headerSize,
                                  std::uint32_t headerVersion,
                                  std::uint32_t vendorID,
                                  std::uint32_t deviceID,
                                  const std::array<std::uint8_t, VK_UUID_SIZE>& uuid,
                                  std::size_t trailing = 0) {
    std::vector<std::byte> blob(16 + VK_UUID_SIZE + trailing, std::byte{0xAB});
    auto put = [&](std::size_t off, std::uint32_t v) {
        std::memcpy(blob.data() + off, &v, sizeof(v));
    };
    put(0, headerSize);
    put(4, headerVersion);
    put(8, vendorID);
    put(12, deviceID);
    std::memcpy(blob.data() + 16, uuid.data(), VK_UUID_SIZE);
    return blob;
}

constexpr std::uint32_t kVendor = 0x10DE;   // NVIDIA
constexpr std::uint32_t kDevice = 0x2204;   // some GPU device id
constexpr std::array<std::uint8_t, VK_UUID_SIZE> kUuid = {
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
    0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
};
constexpr std::uint32_t kV1 = VK_PIPELINE_CACHE_HEADER_VERSION_ONE;
constexpr std::uint32_t kHeaderSize = 16 + VK_UUID_SIZE;  // 32

} // namespace

int main() {
    SetDevice(kVendor, kDevice, kUuid);

    // --- the happy path: a header written by this exact device --------------
    {
        auto blob = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid);
        Check(Device::PipelineCacheDataCompatible(blob),
              "matching vendor/device/UUID header is accepted");

        auto withBody = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid, /*trailing*/ 4096);
        Check(Device::PipelineCacheDataCompatible(withBody),
              "matching header followed by a cache body is accepted");
    }

    // --- foreign / stale blobs must be rejected -----------------------------
    {
        auto otherVendor = MakeHeader(kHeaderSize, kV1, 0x1002 /*AMD*/, kDevice, kUuid);
        Check(!Device::PipelineCacheDataCompatible(otherVendor),
              "different vendorID is rejected");

        auto otherDevice = MakeHeader(kHeaderSize, kV1, kVendor, 0x9999, kUuid);
        Check(!Device::PipelineCacheDataCompatible(otherDevice),
              "different deviceID is rejected");

        std::array<std::uint8_t, VK_UUID_SIZE> otherUuid = kUuid;
        otherUuid[15] ^= 0xFF;  // a driver update bumps the UUID
        auto staleUuid = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, otherUuid);
        Check(!Device::PipelineCacheDataCompatible(staleUuid),
              "different pipelineCacheUUID (e.g. driver update) is rejected");
    }

    // --- malformed headers --------------------------------------------------
    {
        auto badVersion = MakeHeader(kHeaderSize, 0xDEAD, kVendor, kDevice, kUuid);
        Check(!Device::PipelineCacheDataCompatible(badVersion),
              "unknown headerVersion is rejected");

        auto smallHeaderSize = MakeHeader(8, kV1, kVendor, kDevice, kUuid);
        Check(!Device::PipelineCacheDataCompatible(smallHeaderSize),
              "headerSize smaller than the 32-byte header is rejected");

        Check(!Device::PipelineCacheDataCompatible({}),
              "empty blob (no file / cold start) is rejected");

        std::vector<std::byte> truncated(20, std::byte{0});
        Check(!Device::PipelineCacheDataCompatible(truncated),
              "blob too short to hold the header is rejected");
    }

    // --- identity follows the active device ---------------------------------
    {
        // Re-stamp as a different device; a blob valid for the old one is now
        // foreign. Guards against the validator caching identity anywhere but
        // Device::deviceProperties.
        auto blob = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid);
        SetDevice(0x8086 /*Intel*/, 0x1234, kUuid);
        Check(!Device::PipelineCacheDataCompatible(blob),
              "a blob from the previous device is rejected after the device changes");
        SetDevice(kVendor, kDevice, kUuid);
        Check(Device::PipelineCacheDataCompatible(blob),
              "...and accepted again once the matching device is restored");
    }

    if (failures != 0) {
        std::println("{} check(s) failed", failures);
        return EXIT_FAILURE;
    }
    std::println("all checks passed");
    return EXIT_SUCCESS;
}
