/*
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 #59: Device::GetMemoryType used to be a bare
// first-superset match that threw whenever no memory type satisfied the
// requested property flags. Callers that combine a mandatory flag with a
// perf-only one (e.g. HOST_VISIBLE | DEVICE_LOCAL for the descriptor heaps)
// would then crash at allocation time on any device without a host-visible
// device-local heap (no resizable BAR).
//
// GetMemoryType now takes a `preferred` mask distinct from `required`:
//   - prefers a type satisfying required+preferred,
//   - falls back to a required-only match when the preference is unavailable,
//   - still throws only when even `required` cannot be met.
//
// The selection is pure CPU logic over Device::memoryProperties, so this test
// drives it directly with synthetic memory layouts — no Vulkan device needed.

#include <cstdlib>
#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;
}

// Install a synthetic memory layout: one VkMemoryType per entry, in order.
void SetMemoryTypes(std::initializer_list<VkMemoryPropertyFlags> flags) {
    Device::memoryProperties = {};
    Device::memoryProperties.memoryTypeCount = static_cast<std::uint32_t>(flags.size());
    std::uint32_t i = 0;
    for (VkMemoryPropertyFlags f : flags) {
        Device::memoryProperties.memoryTypes[i].propertyFlags = f;
        Device::memoryProperties.memoryTypes[i].heapIndex = 0;
        ++i;
    }
}

constexpr auto DEVICE_LOCAL  = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
constexpr auto HOST_VISIBLE  = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
constexpr auto HOST_CACHED   = VK_MEMORY_PROPERTY_HOST_CACHED_BIT;

// Call GetMemoryType, reporting whether it threw rather than returning.
bool Threw(std::uint32_t typeBits, VkMemoryPropertyFlags required, VkMemoryPropertyFlags preferred) {
    try {
        Device::GetMemoryType(typeBits, required, preferred);
        return false;
    } catch (const std::runtime_error&) {
        return true;
    }
}

} // namespace

int main() {
    constexpr std::uint32_t kAll = ~0u;

    // --- required-only: first superset match wins -------------------------
    {
        SetMemoryTypes({DEVICE_LOCAL, HOST_VISIBLE, HOST_VISIBLE | DEVICE_LOCAL});
        Check(Device::GetMemoryType(kAll, HOST_VISIBLE) == 1,
              "required-only picks the first type satisfying the required bits");
        Check(Device::GetMemoryType(kAll, DEVICE_LOCAL) == 0,
              "required-only honours superset ordering for a different mask");
    }

    // --- typeBits gates which types are eligible --------------------------
    {
        SetMemoryTypes({HOST_VISIBLE, HOST_VISIBLE, HOST_VISIBLE});
        // Only bit 2 is set -> type index 2 is the sole candidate.
        Check(Device::GetMemoryType(0b100, HOST_VISIBLE) == 2,
              "typeBits mask excludes types the allocation can't use");
    }

    // --- preferred bits are honoured when available -----------------------
    {
        SetMemoryTypes({HOST_VISIBLE, HOST_VISIBLE | HOST_COHERENT, HOST_VISIBLE | DEVICE_LOCAL});
        Check(Device::GetMemoryType(kAll, HOST_VISIBLE, DEVICE_LOCAL) == 2,
              "preferred DEVICE_LOCAL selects the host-visible device-local type");
        Check(Device::GetMemoryType(kAll, HOST_VISIBLE, HOST_COHERENT) == 1,
              "preferred HOST_COHERENT selects the host-visible coherent type");
    }

    // --- preferred unavailable: fall back to required-only ----------------
    {
        // No host-visible type is also device-local (the no-resizable-BAR case).
        SetMemoryTypes({DEVICE_LOCAL, HOST_VISIBLE | HOST_COHERENT});
        Check(Device::GetMemoryType(kAll, HOST_VISIBLE, DEVICE_LOCAL) == 1,
              "unsatisfiable preference falls back to a required-only match instead of throwing");
        Check(!Threw(kAll, HOST_VISIBLE, DEVICE_LOCAL),
              "the fallback path does not throw when required is still satisfiable");
    }

    // --- preferred is satisfiable only via the second-best type -----------
    {
        // First host-visible type lacks the preference; a later one has it.
        SetMemoryTypes({HOST_VISIBLE, HOST_VISIBLE | DEVICE_LOCAL});
        Check(Device::GetMemoryType(kAll, HOST_VISIBLE, DEVICE_LOCAL) == 1,
              "preference outranks ordering: skips an earlier required-only type");
    }

    // --- required unsatisfiable still throws ------------------------------
    {
        SetMemoryTypes({DEVICE_LOCAL, HOST_VISIBLE | HOST_COHERENT});
        Check(Threw(kAll, HOST_CACHED, 0),
              "no type satisfies the required bits -> throws (no valid memory)");
        Check(Threw(kAll, HOST_CACHED, DEVICE_LOCAL),
              "unsatisfiable required throws even when a preference is given");
        // typeBits that masks out the only matching type is also unsatisfiable.
        SetMemoryTypes({HOST_VISIBLE});
        Check(Threw(0b10, HOST_VISIBLE, 0),
              "typeBits excluding every match -> throws");
    }

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