# native/tests — CTest smoke tests proving the M1-01 pipeline links and runs.
# P7-02: windowed tests need a video backend. Linux CI has no display,
# so force the SDL offscreen driver via plain ENVIRONMENT; on Win32/macOS
# the inherited variable must NOT reach the test (offscreen cannot create
# Vulkan windows — real Win32 window / Cocoa so Lavapipe/MoltenVK can
# create a real surface + swapchain). ENVIRONMENT_MODIFICATION unset:
# drops the inherited value on those platforms (a plain empty ENVIRONMENT
# still inherits it). Plain CMake branch (no $<IF> genex:
# ENVIRONMENT_MODIFICATION splits its value on commas, so a comma-bearing
# genex never survives evaluation).
# P7-01: the Windows GPU lane needs a real window; the Linux lane keeps
# offscreen + headless-surface rules.
if(WIN32 OR APPLE)
  set(ENGINE_TEST_VIDEO_ENV "ENVIRONMENT_MODIFICATION" "SDL_VIDEO_DRIVER=unset:")
else()
  set(ENGINE_TEST_VIDEO_ENV "ENVIRONMENT" "SDL_VIDEO_DRIVER=offscreen")
endif()
# NOTE: tests use base flags (exceptions/RTTI on) — they are host tooling,
# not frame-path code.
add_executable(engine_core_smoke test_engine_core_smoke.cpp)

target_link_libraries(engine_core_smoke PRIVATE engine_core)

engine_apply_base_flags(engine_core_smoke)

add_test(NAME engine_core_smoke COMMAND engine_core_smoke)
set_tests_properties(engine_core_smoke PROPERTIES TIMEOUT 60)

# Zero-dependency control: if this passes and engine_core_smoke hangs,
# the fault is in engine_core load/static init, not the runner.
add_executable(bootstrap_smoke test_bootstrap_smoke.cpp)
add_test(NAME bootstrap_smoke COMMAND bootstrap_smoke)
set_tests_properties(bootstrap_smoke PROPERTIES TIMEOUT 60)

# M1-02: allocator + aligned-hook acceptance. Links engine_core (which pulls
# the shared flecs transitively) for ecs_os_api + world/bulk/query APIs.
add_executable(engine_memory_arenas test_memory_arenas.cpp)

target_link_libraries(engine_memory_arenas PRIVATE engine_core)

engine_apply_base_flags(engine_memory_arenas)

add_test(NAME engine_memory_arenas COMMAND engine_memory_arenas)
# The 1M-row sweep needs a real arena; give CTest a generous timeout so
# loaded CI runners don't flake.
set_tests_properties(engine_memory_arenas PROPERTIES TIMEOUT 300)

# M1-03: scheduler + hierarchy acceptance. Links engine_core (Flecs task
# hooks, EcsWorld, SIMD transforms). Owns OS-API setup in-process.
add_executable(engine_job_scheduler test_job_scheduler.cpp)

target_link_libraries(engine_job_scheduler PRIVATE engine_core)

engine_apply_base_flags(engine_job_scheduler)

add_test(NAME engine_job_scheduler COMMAND engine_job_scheduler)
# 1M-row propagate + task-thread staging; generous timeout for CI.
set_tests_properties(engine_job_scheduler PROPERTIES TIMEOUT 300)

# P0-05: Flecs column storage honors alignas(64) (aligned heap hooks are
# installed by EcsWorld::construct before ecs_init).
add_executable(engine_ecs_column_alignment test_ecs_column_alignment.cpp)
target_link_libraries(engine_ecs_column_alignment PRIVATE engine_core)
engine_apply_base_flags(engine_ecs_column_alignment)
add_test(NAME engine_ecs_column_alignment COMMAND engine_ecs_column_alignment)
set_tests_properties(engine_ecs_column_alignment PROPERTIES TIMEOUT 120)

# P7-01/P7-02: windowed tests need a video backend. Linux CI has no
# display, so force the SDL offscreen driver there; on Win32/macOS the
# variable is left empty (real Win32 window / Cocoa, so Lavapipe/MoltenVK
# can create a real surface + swapchain). Plain CMake branch (no $<IF>
# genex: ENVIRONMENT splits its value on semicolons/commas, so a
# comma-bearing genex never survives evaluation).

# M3-01: renderer bootstrap acceptance. Links engine_core (which pulls the
# renderer OBJECT lib). Headless-safe: SDL window + Vulkan init are probed
# and skipped when no display/ICD is present (Lavapipe still exercises the
# real init path where mesa-vulkan-drivers is installed).
add_executable(engine_renderer_bootstrap test_renderer_bootstrap.cpp)

target_link_libraries(engine_renderer_bootstrap PRIVATE engine_core)

engine_apply_base_flags(engine_renderer_bootstrap)

add_test(NAME engine_renderer_bootstrap COMMAND engine_renderer_bootstrap)
set_tests_properties(engine_renderer_bootstrap PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# M3-02: GPU-driven indirect acceptance. Links engine_core (renderer OBJECT
# lib). Lavapipe exercises the real indirect-count draw path where an ICD
# advertises a device; otherwise the GPU section skips cleanly.
# NOTE: this TU includes <volk.h> for vk* entry points but must NOT link
# volk's objects into the exe: volk is compiled into libengine_core.so
# (renderer OBJECT lib) and initialized there; a second copy in the exe
# would be a separate never-initialized .bss (all vk* null in the .so).
# The exe's volk refs stay UNDEFINED, satisfied by the .so at runtime.
add_executable(engine_gpu_indirect test_gpu_indirect.cpp)

target_link_libraries(engine_gpu_indirect PRIVATE engine_core)

engine_apply_base_flags(engine_gpu_indirect)

add_test(NAME engine_gpu_indirect COMMAND engine_gpu_indirect)
set_tests_properties(engine_gpu_indirect PROPERTIES TIMEOUT 300)

# P1-04: swapchain + 2-frame loop + screenshot acceptance. Links
# engine_core (renderer OBJECT lib). Lavapipe exercises the real
# swapchain path where an ICD advertises a device; otherwise the GPU
# section fails closed under ENGINE_REQUIRE_GPU=1 (gpu_required.hpp).
# P1-05: golden_compare.hpp + golden_stb.cpp (PNG IO via the assets
# module's stb) + the checked-in golden clear_orange_320x240.png.
add_executable(engine_swapchain test_swapchain.cpp golden_stb.cpp)

target_link_libraries(engine_swapchain PRIVATE engine_core)

engine_apply_base_flags(engine_swapchain)

# stb_image_write's own TU trips -Wmissing-field-initializers under our
# -Werror (upstream `{ 0 }` on a multi-field struct). Same -w treatment as
# the third-party TUs in native/assets (meshopt/bc7enc/stb decode): our
# code stays -Werror-clean, upstream's does not need to be.
if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

# Golden dir (source tree) + golden output dir (build tree) for
# golden_compare.hpp (P1-05).
target_compile_definitions(engine_swapchain PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_swapchain COMMAND engine_swapchain)
set_tests_properties(engine_swapchain PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P1-06: first triangle through the forward renderer (ForwardRenderer +
# tri.vert/tri.frag). Same golden harness wiring as engine_swapchain
# (golden_compare.hpp + golden_stb.cpp + ENGINE_GOLDEN_* dirs).
add_executable(engine_forward_triangle test_forward_triangle.cpp golden_stb.cpp)

target_link_libraries(engine_forward_triangle PRIVATE engine_core)

engine_apply_base_flags(engine_forward_triangle)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_forward_triangle PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_forward_triangle COMMAND engine_forward_triangle)
set_tests_properties(engine_forward_triangle PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P1-07: lit cube through the mesh pipeline (GpuMesh + D32_SFLOAT depth +
# mesh.vert/mesh.frag, 160-byte push). Same golden harness wiring as
# engine_forward_triangle.
add_executable(engine_forward_mesh test_forward_mesh.cpp golden_stb.cpp)

target_link_libraries(engine_forward_mesh PRIVATE engine_core)

engine_apply_base_flags(engine_forward_mesh)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_forward_mesh PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_forward_mesh COMMAND engine_forward_mesh)
set_tests_properties(engine_forward_mesh PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P3-13: shareable viewport image (exportable external memory, fd share)
# + with-copy vs without-copy timing at 1920x1080. Lavapipe exercises
# the real fd-export path where the ICD advertises it; otherwise the
# GPU section fails closed under ENGINE_REQUIRE_GPU=1 (gpu_required.hpp).
# CPU readback (capture_rgba8) is untouched — still the default fallback.
add_executable(engine_viewport_share test_viewport_share.cpp)

target_link_libraries(engine_viewport_share PRIVATE engine_core)

engine_apply_base_flags(engine_viewport_share)

add_test(NAME engine_viewport_share COMMAND engine_viewport_share)
set_tests_properties(engine_viewport_share PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P1-08: ECS entity draw (Camera/MeshRenderer/DirectionalLight via
# EcsWorld::register_render, MeshRegistry, ForwardRenderer::render_world).
# 10x10x10 grid = 1000 draws + golden + transform-change visibility.
add_executable(engine_ecs_render test_ecs_render.cpp golden_stb.cpp)

target_link_libraries(engine_ecs_render PRIVATE engine_core)

engine_apply_base_flags(engine_ecs_render)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_ecs_render PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_ecs_render COMMAND engine_ecs_render)
set_tests_properties(engine_ecs_render PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P4-07 part 2: GiBaker CPU contract (SDF-brick probe bake, no golden).
add_executable(engine_gi_baker test_gi_baker.cpp)

target_link_libraries(engine_gi_baker PRIVATE engine_core)

engine_apply_base_flags(engine_gi_baker)

add_test(NAME engine_gi_baker COMMAND engine_gi_baker)
set_tests_properties(engine_gi_baker PROPERTIES TIMEOUT 300)

# P6-06: batched 2D sprites + orthographic camera (10k sprites over
# 4 atlases -> 4 batches / one draw call per atlas + golden).
add_executable(engine_sprite_batch test_sprite_batch.cpp golden_stb.cpp)

target_link_libraries(engine_sprite_batch PRIVATE engine_core)

engine_apply_base_flags(engine_sprite_batch)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_sprite_batch PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_sprite_batch COMMAND engine_sprite_batch)
set_tests_properties(engine_sprite_batch PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P6-08: grid tilemap + palette + merged collider (greedy rect merge,
# exact bounds/area/count + CPU raster golden, no GPU needed).
add_executable(engine_tilemap test_tilemap.cpp golden_stb.cpp)

target_link_libraries(engine_tilemap PRIVATE engine_core)

engine_apply_base_flags(engine_tilemap)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_tilemap PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_tilemap COMMAND engine_tilemap)
set_tests_properties(engine_tilemap PROPERTIES TIMEOUT 120)

# M3-03: slang shader pipeline acceptance. Links engine_core (renderer
# OBJECT lib). SHADER_SOURCE_DIR points at the checked-in .slang sources
# for the hot-reload watcher test (read-only); ENGINE_TEST_SLANGC carries
# the slangc path when available (CI: absent -> swap section skips, magic
# checks still prove the embedded blobs).
add_executable(engine_slang_pbr test_slang_pbr.cpp)

target_link_libraries(engine_slang_pbr PRIVATE engine_core)

target_compile_definitions(engine_slang_pbr PRIVATE
    SHADER_SOURCE_DIR="${CMAKE_SOURCE_DIR}/native/renderer/shaders")

find_program(ENGINE_SLANGC_EXE slangc)
engine_apply_base_flags(engine_slang_pbr)

add_test(NAME engine_slang_pbr COMMAND engine_slang_pbr)

# P3-06: object-id picking (ForwardRenderer pick pipeline + PickIdBuffer
# R32_UINT target + one-texel readback). Headless (no window at all);
# ENGINE_REQUIRE_GPU=1 fails closed without a device via gpu_missing.
add_executable(engine_pick_ids test_pick_ids.cpp)

target_link_libraries(engine_pick_ids PRIVATE engine_core)

engine_apply_base_flags(engine_pick_ids)

add_test(NAME engine_pick_ids COMMAND engine_pick_ids)
set_tests_properties(engine_pick_ids PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# M3-03: managed compositor live round-trip. Runs Engine.Tests from a
# staging dir where libengine_core's SONAME symlink (libengine_core.so,
# what dlopen("engine_core") resolves) sits next to the managed dlls, so
# ApiLoader.Load() takes the live path: GraphicsCompositor create/add/
# compile/run against the real ABI 3 table. A sentinel file written by
# the live path fails the test closed when the live path skips.
# The live probe stages the managed output dir (copy_directory so the
# AnalyzerTests FindBin layout + deps.json closure resolve exactly as
# `dotnet run` sees them) alongside the native library; the test itself
# runs from the STAGED dir with cwd == staged dir. Depends on the hostfxr
# fixture build (same `dotnet build Engine.slnx` the smoke test uses), so
# the output dir always exists first (fixes the CI "no such file"
# symlink/workdir failures). The probe flag fails closed when engine_core
# does not load (no silent SKIP on the live path).
add_test(NAME engine_compositor_live_stage
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_SOURCE_DIR}/managed/Engine.Tests/bin/Release/net10.0"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live")
# Stage the runtime closure: native lib under its SONAME + the flecs
# sibling (DT_NEEDED) next to the managed app. The probe prefers these
# CWD/appdir siblings before the build tree (macOS .dylib / Win .dll /
# Linux .so all covered); the build-tree walk stays as fallback.
# NOTE: $<TARGET_FILE_SUFFIX> on a versioned .so yields ".so.0.1.0" while
# the base name drops the "lib" prefix ("engine_core"), producing the
# wrong "engine_core.so.0.1.0". Stage under the explicit link names the
# loader probes: lib<name>.so (Linux), lib<name>.dylib (macOS),
# <name>.dll (Windows). $<TARGET_FILE_NAME> keeps the real filename.
add_test(NAME engine_compositor_live_stage_native
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "$<TARGET_FILE:engine_core>"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/$<TARGET_FILE_NAME:engine_core>")
set_tests_properties(engine_compositor_live_stage_native PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_native_link
    COMMAND ${CMAKE_COMMAND} -E create_symlink
        "$<TARGET_FILE_NAME:engine_core>"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/libengine_core$<TARGET_FILE_SUFFIX:engine_core>")
set_tests_properties(engine_compositor_live_stage_native_link PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_flecs
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "$<TARGET_FILE:flecs>"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/$<TARGET_FILE_NAME:flecs>")
set_tests_properties(engine_compositor_live_stage_flecs PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_analyzers
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_SOURCE_DIR}/managed/Engine.Analyzers/bin/Release/netstandard2.0"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/managed/Engine.Analyzers/bin/Release/netstandard2.0")
set_tests_properties(engine_compositor_live_stage_analyzers PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_codefixes
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_SOURCE_DIR}/managed/Engine.CodeFixes/bin/Release/net8.0"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/managed/Engine.CodeFixes/bin/Release/net8.0")
set_tests_properties(engine_compositor_live_stage_codefixes PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
# M5-03: stage the engine-cli apphost + closure next to the live probe so
# CliApiTests can drive the REAL binary over the test socket (same dir =
# same runtimeconfig/deps resolution as the apphost it probes).
if(WIN32)
    set(_CLI_APPHOST "engine-cli.exe")
else()
    set(_CLI_APPHOST "engine-cli")
endif()
add_test(NAME engine_compositor_live_stage_cli
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_SOURCE_DIR}/managed/Engine.Cli/bin/Release/net10.0/${_CLI_APPHOST}"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/${_CLI_APPHOST}")
set_tests_properties(engine_compositor_live_stage_cli PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_cli_config
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_SOURCE_DIR}/managed/Engine.Cli/bin/Release/net10.0/engine-cli.runtimeconfig.json"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/engine-cli.runtimeconfig.json")
set_tests_properties(engine_compositor_live_stage_cli_config PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_cli_dll
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_SOURCE_DIR}/managed/Engine.Cli/bin/Release/net10.0/engine-cli.dll"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/engine-cli.dll")
set_tests_properties(engine_compositor_live_stage_cli_dll PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
set_tests_properties(engine_compositor_live_stage PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
# The apphost (Engine.Tests exe, NOT the .dll) boots the staged
# deps.json/runtimeconfig closure; --live-native-probe fails closed when
# engine_core does not load. LD_LIBRARY_PATH covers libflecs resolution.
if(WIN32)
    set(_COMPOSITOR_EXE "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/Engine.Tests.exe")
else()
    set(_COMPOSITOR_EXE "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/Engine.Tests")
endif()
add_test(NAME engine_compositor_live
    COMMAND "${_COMPOSITOR_EXE}" --live-native-probe
    WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compositor_live")
# M5-04: stage engine-pak next to the live probe so ExportPipeline +
# ExportApiTests drive the REAL packer binary (same-dir resolution, no
# PATH dependency). engine-pak is a framework-dependent apphost (needs
# its .dll + runtimeconfig beside the exe, same as engine-cli above).
if(WIN32)
    set(_PAK_APPHOST "engine-pak.exe")
else()
    set(_PAK_APPHOST "engine-pak")
endif()
add_test(NAME engine_compositor_live_stage_pak
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_SOURCE_DIR}/tools/engine-pak/bin/Release/net10.0/${_PAK_APPHOST}"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/${_PAK_APPHOST}")
set_tests_properties(engine_compositor_live_stage_pak PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_pak_config
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_SOURCE_DIR}/tools/engine-pak/bin/Release/net10.0/engine-pak.runtimeconfig.json"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/engine-pak.runtimeconfig.json")
set_tests_properties(engine_compositor_live_stage_pak_config PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_compositor_live_stage_pak_dll
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_SOURCE_DIR}/tools/engine-pak/bin/Release/net10.0/engine-pak.dll"
        "${CMAKE_CURRENT_BINARY_DIR}/compositor_live/engine-pak.dll")
set_tests_properties(engine_compositor_live_stage_pak_dll PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP compositor_live_staged
    FIXTURES_REQUIRED benchmark1m_fixtures)
# M5-04: golden-vector — managed engine-pak packs a fixture corpus, the
# native golden reader opens it through PakReader and asserts byte-exact
# content + CRC (proves the managed writer layout matches the native
# reader: magic, GUID sort, LZ4 wire format).
#
# Fixtures are deterministic bytes generated at CONFIGURE time into the
# build tree (never in the repo): text = repeated sentence (LZ4 path),
# noise = seeded PRNG bytes (stored path). The run script packs them with
# the staged managed engine-pak, then the native reader verifies.
execute_process(
    COMMAND python3 -c "open('${CMAKE_CURRENT_BINARY_DIR}/pak_golden_text.bin','wb').write(b'the quick brown fox jumps over the lazy dog. pack my box with five dozen liquor jugs! '*2000); import random; random.seed(42); open('${CMAKE_CURRENT_BINARY_DIR}/pak_golden_noise.bin','wb').write(bytes(random.getrandbits(8) for _ in range(20000)))"
)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/run_pak_golden.sh"
"#!/bin/sh\n"
"set -e\n"
"STAGE=\"${CMAKE_CURRENT_BINARY_DIR}/compositor_live\"\n"
"GOLD=\"$STAGE/pak_golden\"\n"
"rm -rf \"$GOLD\"\n"
"mkdir -p \"$GOLD\"\n"
"cp \"${CMAKE_CURRENT_BINARY_DIR}/pak_golden_text.bin\" \"$GOLD/text.bin\"\n"
"cp \"${CMAKE_CURRENT_BINARY_DIR}/pak_golden_noise.bin\" \"$GOLD/noise.bin\"\n"
"\"$STAGE/engine-pak\" pack --out \"$GOLD/golden.pak\" --compress \\\n"
"  \"01234567-89ab-cdef-0123-456789abcdef:model:$GOLD/text.bin\" \\\n"
"  \"11111111-2222-4333-8555-666666666666:texture:$GOLD/noise.bin\"\n"
"\"${CMAKE_CURRENT_BINARY_DIR}/engine_pak_golden\" \"$GOLD/golden.pak\" \"$GOLD/text.bin\" \"$GOLD/noise.bin\"\n")
add_executable(engine_pak_golden test_pak_golden.cpp)
target_link_libraries(engine_pak_golden PRIVATE engine_core)
engine_apply_base_flags(engine_pak_golden)
add_test(NAME engine_pak_managed_golden
    COMMAND sh "${CMAKE_CURRENT_BINARY_DIR}/run_pak_golden.sh")
set_tests_properties(engine_pak_managed_golden PROPERTIES
    TIMEOUT 120
    FIXTURES_REQUIRED compositor_live_staged)
set_tests_properties(engine_compositor_live PROPERTIES
    TIMEOUT 300
    FIXTURES_REQUIRED compositor_live_staged
    ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/native:${CMAKE_BINARY_DIR}/deps/flecs:$ENV{LD_LIBRARY_PATH};DOTNET_ROOT=$ENV{DOTNET_ROOT};ENGINE_UI_FONT_DIR=${CMAKE_BINARY_DIR}/ui_fonts")

set_tests_properties(engine_slang_pbr PROPERTIES TIMEOUT 300)
if(ENGINE_SLANGC_EXE)
    set_tests_properties(engine_slang_pbr PROPERTIES
        ENVIRONMENT "ENGINE_TEST_SLANGC=${ENGINE_SLANGC_EXE}")
    message(STATUS "slang_pbr: slangc at ${ENGINE_SLANGC_EXE} (hot-swap proven)")
else()
    message(STATUS "slang_pbr: no slangc (hot-swap section skips; blobs still verified)")
endif()

# M3-04: zero-bake GI acceptance. Links engine_core (renderer OBJECT lib).
# CPU-side contract (SDF bricks, cascades, SDSM sizing, SPIR-V blobs)
# runs everywhere; the Lavapipe vkCreateShaderModule section skips cleanly
# with no physical device. GI resolve + shadow trace blobs live in the
# header mirror dir (same shape as the M3-03 pbr_*_spv.inc entries).
add_executable(engine_gi_zero_bake test_gi_zero_bake.cpp)

target_link_libraries(engine_gi_zero_bake PRIVATE engine_core)

engine_apply_base_flags(engine_gi_zero_bake)

add_test(NAME engine_gi_zero_bake COMMAND engine_gi_zero_bake)
set_tests_properties(engine_gi_zero_bake PROPERTIES TIMEOUT 300)

# P4-01: PBR metallic-roughness materials (asset + component + cgltf
# import + bindless SSBO layout + Lavapipe shade/post pipeline proof).
# golden_stb.cpp carries the stb_image_write encoder TU for the golden
# harness (decode reuses the assets TU through the engine_core link).
add_executable(engine_pbr_materials test_pbr_materials.cpp golden_stb.cpp)

target_link_libraries(engine_pbr_materials PRIVATE engine_core)

# stb_image_write's own TU trips -Wmissing-field-initializers under our
# -Werror (upstream `{ 0 }` on a two-field struct). Same -w treatment as
# the third-party TUs in native/assets (meshopt/bc7enc/stb decode): our
# code stays -Werror-clean, upstream's does not need to be.
if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

# Golden dir (source tree) + golden output dir (build tree) for
# golden_compare.hpp (P1-05 contract, first landed by P4-01).
target_compile_definitions(engine_pbr_materials PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_pbr_materials)

add_test(NAME engine_pbr_materials COMMAND engine_pbr_materials)
set_tests_properties(engine_pbr_materials PROPERTIES TIMEOUT 300)

# P4-01 sphere-grid golden: shade+post offscreen render + readback vs the
# checked-in golden (shares golden_stb.cpp for PNG IO).
add_executable(engine_pbr_golden test_pbr_golden.cpp golden_stb.cpp)

target_link_libraries(engine_pbr_golden PRIVATE engine_core)

target_compile_definitions(engine_pbr_golden PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_pbr_golden)

add_test(NAME engine_pbr_golden COMMAND engine_pbr_golden)
set_tests_properties(engine_pbr_golden PROPERTIES TIMEOUT 300)

# P4-06 GPU-driven statics: SceneSsbo stream + phase A/B cull dispatches +
# count-draw front half + P4-01 shade/post back half vs p4_06_statics golden.
add_executable(engine_gpu_driven_statics test_gpu_driven_statics.cpp golden_stb.cpp)

target_link_libraries(engine_gpu_driven_statics PRIVATE engine_core)

target_compile_definitions(engine_gpu_driven_statics PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_gpu_driven_statics)

add_test(NAME engine_gpu_driven_statics COMMAND engine_gpu_driven_statics)
set_tests_properties(engine_gpu_driven_statics PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P4-06 250k acceptance: GPU-driven cull front half at production scale
# (250k instances, 977x256 dispatches) — CPU record time + GPU exec time +
# DRAW-counter proof. Real GPU only; FAIL (not SKIP) under
# ENGINE_REQUIRE_GPU=1.
add_executable(engine_gpu_driven_250k test_gpu_driven_250k.cpp)

target_link_libraries(engine_gpu_driven_250k PRIVATE engine_core)

engine_apply_base_flags(engine_gpu_driven_250k)

add_test(NAME engine_gpu_driven_250k COMMAND engine_gpu_driven_250k)
set_tests_properties(engine_gpu_driven_250k PROPERTIES
    TIMEOUT 600
    ${ENGINE_TEST_VIDEO_ENV})

# P4-02: HDR emissive + exposure tonemap golden (shade emissive term +
# HdrCameraComponent EV stops + PostPush exposure; shares golden_stb.cpp).
add_executable(engine_hdr_tonemap test_hdr_tonemap.cpp golden_stb.cpp)

target_link_libraries(engine_hdr_tonemap PRIVATE engine_core)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_hdr_tonemap PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_hdr_tonemap)

add_test(NAME engine_hdr_tonemap COMMAND engine_hdr_tonemap)
set_tests_properties(engine_hdr_tonemap PROPERTIES TIMEOUT 300)

# P4-10: TAA resolve + FXAA fallback golden (jittered static edge,
# 16-frame history + variance probe; shares golden_stb.cpp).
add_executable(engine_taa_fxaa test_taa_fxaa.cpp golden_stb.cpp)

target_link_libraries(engine_taa_fxaa PRIVATE engine_core)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_taa_fxaa PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_taa_fxaa)

add_test(NAME engine_taa_fxaa COMMAND engine_taa_fxaa)
set_tests_properties(engine_taa_fxaa PROPERTIES TIMEOUT 300)

# P4-04: clustered forward lights (16x9x24 CPU binning + range/spot shade
# golden with 64 colored point lights; shares golden_stb.cpp).
add_executable(engine_clustered_lights test_clustered_lights.cpp golden_stb.cpp)

target_link_libraries(engine_clustered_lights PRIVATE engine_core)

target_compile_definitions(engine_clustered_lights PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_clustered_lights)

add_test(NAME engine_clustered_lights COMMAND engine_clustered_lights)
set_tests_properties(engine_clustered_lights PROPERTIES TIMEOUT 300)

# P4-05: equirect HDR import + GPU cubemap conversion (CPU face-dir
# contract + stb stbi_loadf probe + llvmpipe compute conversion vs the
# CPU reference; no golden PNG — the cubemap pixels are compared
# in-memory). test_skybox_import.cpp carries its own compute entry
# points (GoldenVkProcs covers graphics-only goldens).
add_executable(engine_skybox_import test_skybox_import.cpp golden_stb.cpp)

target_link_libraries(engine_skybox_import PRIVATE engine_core)

target_compile_definitions(engine_skybox_import PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_skybox_import)

add_test(NAME engine_skybox_import COMMAND engine_skybox_import)
set_tests_properties(engine_skybox_import PROPERTIES TIMEOUT 300)

# P4-05: split-sum BRDF LUT (Karis) — CPU reference curve + llvmpipe
# compute generation vs the CPU reference; no golden PNG — the LUT pixels
# are compared in-memory. Same golden-TU pattern as engine_skybox_import
# (own compute entry points; GoldenVkProcs covers graphics-only goldens).
add_executable(engine_brdf_lut test_brdf_lut.cpp golden_stb.cpp)

target_link_libraries(engine_brdf_lut PRIVATE engine_core)

target_compile_definitions(engine_brdf_lut PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_brdf_lut)

add_test(NAME engine_brdf_lut COMMAND engine_brdf_lut)
set_tests_properties(engine_brdf_lut PROPERTIES TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P4-05: irradiance + prefilter + skybox/IBL integration (CPU import
# plans + basis/evaluate contract, llvmpipe irradiance/prefilter compute
# vs the CPU references, and the ibl_shade split-sphere render proving
# chrome shows sharp HDR reflections while rough shows diffuse/specular).
# Same golden-TU pattern (own compute entry points; GoldenBuildPipe for
# the graphics pair; no golden PNG — pixels compared in-memory).
add_executable(engine_skybox_ibl test_skybox_ibl.cpp golden_stb.cpp)

target_link_libraries(engine_skybox_ibl PRIVATE engine_core)

target_compile_definitions(engine_skybox_ibl PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_skybox_ibl)

add_test(NAME engine_skybox_ibl COMMAND engine_skybox_ibl)
set_tests_properties(engine_skybox_ibl PROPERTIES TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P4-12: mesh LOD chain (meshopt_simplify at import, screen-size select
# at draw, crossfade factor) — CPU contract + 3-distance goldens.
add_executable(engine_mesh_lod test_mesh_lod.cpp golden_stb.cpp)

target_link_libraries(engine_mesh_lod PRIVATE engine_core)

engine_apply_base_flags(engine_mesh_lod)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_mesh_lod PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_mesh_lod COMMAND engine_mesh_lod)
set_tests_properties(engine_mesh_lod PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P4-03: cascaded shadow maps (4 cascades, PCF 3x3, texel-snapped
# stability, per-light toggle; csm goldens + CPU contract).
add_executable(engine_cascade_shadows test_cascade_shadows.cpp golden_stb.cpp)

target_link_libraries(engine_cascade_shadows PRIVATE engine_core)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_cascade_shadows PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_cascade_shadows)

add_test(NAME engine_cascade_shadows COMMAND engine_cascade_shadows)
set_tests_properties(engine_cascade_shadows PROPERTIES TIMEOUT 300)

# P4-08: dual-Kawase bloom (prefilter/downsample/upsample/composite) +
# PostProcess threshold/intensity golden + intensity-0 toggle-off check
# (shares golden_stb.cpp for PNG IO).
add_executable(engine_bloom test_bloom.cpp golden_stb.cpp)

target_link_libraries(engine_bloom PRIVATE engine_core)

target_compile_definitions(engine_bloom PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_bloom)

add_test(NAME engine_bloom COMMAND engine_bloom)
set_tests_properties(engine_bloom PROPERTIES TIMEOUT 300)


# P4-09: half-res GTAO occlusion pass proof (CPU reference + GPU
# half-res texture vs reference; no golden — goldens belong to the
# follow-up card). Shares golden_stb.cpp for PNG IO.
add_executable(engine_ssao_gtao test_ssao_gtao.cpp golden_stb.cpp)

target_link_libraries(engine_ssao_gtao PRIVATE engine_core)

target_compile_definitions(engine_ssao_gtao PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_ssao_gtao)

add_test(NAME engine_ssao_gtao COMMAND engine_ssao_gtao)
set_tests_properties(engine_ssao_gtao PROPERTIES TIMEOUT 300)


# P4-09: SSAO bilateral blur + composite (CPU contract + llvmpipe frame:
# P4-05 IBL shade -> blur H/V -> composite ON vs new golden +
# composite OFF vs the P4-05 golden). Shares golden_stb.cpp for PNG IO.
add_executable(engine_ssao test_ssao.cpp golden_stb.cpp)

target_link_libraries(engine_ssao PRIVATE engine_core)

target_compile_definitions(engine_ssao PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_ssao)

add_test(NAME engine_ssao COMMAND engine_ssao)
set_tests_properties(engine_ssao PROPERTIES TIMEOUT 300)


# M5-01: asset pipeline acceptance (GUID/.meta, mmap fast preview,
# watcher, background meshopt+bc7enc worker). Links engine_core (assets
# OBJECT lib). Self-generates GLB/PNG fixtures — no network, no binaries.
add_executable(engine_asset_pipeline test_asset_pipeline.cpp)

# Asset API comes from engine_core (shared .so/.dll). No extra OBJECT link:
# the asset TUs export ENGINE_ASSETS_API from the DLL on Windows and default
# visibility on Linux/macOS.
target_link_libraries(engine_asset_pipeline PRIVATE engine_core)

engine_apply_base_flags(engine_asset_pipeline)

add_test(NAME engine_asset_pipeline COMMAND engine_asset_pipeline)
set_tests_properties(engine_asset_pipeline PROPERTIES TIMEOUT 300)

# P5-01: skeletal mesh + animation clip import (skins, joints, inverse bind
# matrices, clips into .skel/.anim). Self-generates a two-bone .glb — no
# network, no binaries. Links engine_core (assets OBJECT lib).
add_executable(engine_skeletal_import test_skeletal_import.cpp)

target_link_libraries(engine_skeletal_import PRIVATE engine_core)

engine_apply_base_flags(engine_skeletal_import)

add_test(NAME engine_skeletal_import COMMAND engine_skeletal_import)
set_tests_properties(engine_skeletal_import PROPERTIES TIMEOUT 300)

# P5-02: clip sampling acceptance (linear/step/cubic-spline CPU sampling
# into caller-owned poses + P5-01 fixture cross-check). Links engine_core
# (assets OBJECT lib). Pure CPU math, no GPU needed.
add_executable(engine_anim_sampling test_anim_sampling.cpp)

target_link_libraries(engine_anim_sampling PRIVATE engine_core)

engine_apply_base_flags(engine_anim_sampling)

add_test(NAME engine_anim_sampling COMMAND engine_anim_sampling)
set_tests_properties(engine_anim_sampling PROPERTIES TIMEOUT 120)

# P5-02: two-clip blending + bent-arm golden (lerp/slerp blend by weight,
# joint-matrix flatten, deterministic t=0.5s golden). Links engine_core
# (assets OBJECT lib). Pure CPU math, no GPU needed.
add_executable(engine_anim_blending test_anim_blending.cpp)

target_link_libraries(engine_anim_blending PRIVATE engine_core)

engine_apply_base_flags(engine_anim_blending)

add_test(NAME engine_anim_blending COMMAND engine_anim_blending)
set_tests_properties(engine_anim_blending PROPERTIES TIMEOUT 120)

# P5-03: JSON state-machine graph + runtime (float/bool/trigger conditions,
# cross-fade blending, fail-closed loads). Links engine_core (assets OBJECT
# lib). Pure CPU math, no GPU needed.
add_executable(engine_animator_state_machine test_animator_state_machine.cpp)

target_link_libraries(engine_animator_state_machine PRIVATE engine_core)

engine_apply_base_flags(engine_animator_state_machine)

add_test(NAME engine_animator_state_machine
         COMMAND engine_animator_state_machine)
set_tests_properties(engine_animator_state_machine PROPERTIES TIMEOUT 120)

# P3-08: project browser drag-in acceptance (drop assets/cube.glb ->
# one entity + valid mesh_id in one call; failure matrix; undo delete).
# Links engine_core (editor session + fast-lane import).
add_executable(engine_project_drop test_project_drop.cpp)

target_link_libraries(engine_project_drop PRIVATE engine_core)

engine_apply_base_flags(engine_project_drop)

add_test(NAME engine_project_drop COMMAND engine_project_drop)
set_tests_properties(engine_project_drop PROPERTIES TIMEOUT 300)

# P3-03: hierarchy native acceptance (duplicate under a parent keeps the
# tree walkable; subtree snapshot restores original ids).
add_executable(engine_editor_hierarchy test_editor_hierarchy.cpp)

target_link_libraries(engine_editor_hierarchy PRIVATE engine_core)

engine_apply_base_flags(engine_editor_hierarchy)

add_test(NAME engine_editor_hierarchy COMMAND engine_editor_hierarchy)
set_tests_properties(engine_editor_hierarchy PROPERTIES TIMEOUT 300)

# M5-04: .pak archive acceptance (LZ4 codec, GUID-keyed builder, mmap
# reader, corrupt-input gates). Links engine_core (assets OBJECT lib).
add_executable(engine_asset_pak test_asset_pak.cpp)

target_link_libraries(engine_asset_pak PRIVATE engine_core)

engine_apply_base_flags(engine_asset_pak)

add_test(NAME engine_asset_pak COMMAND engine_asset_pak)
set_tests_properties(engine_asset_pak PROPERTIES TIMEOUT 300)

# Windows: the loader only searches the exe dir + PATH, so a test in
# next to every linked test binary (Linux/macOS resolve via rpath instead).
# NOTE: bootstrap_smoke links nothing — an empty TARGET_RUNTIME_DLLS
# expansion would make cmake -E fail the build, so it is excluded here.
# M1-04: ABI contract acceptance.
# (Declared before the WIN32 DLL-copy block: every add_custom_command
# below must reference an already-created target.) Links engine_core (Flecs universe +
# engine_get_api_table). Owns no OS-API setup (uses default malloc hooks).
add_executable(engine_abi_layout test_abi_layout.cpp)

target_link_libraries(engine_abi_layout PRIVATE engine_core)

engine_apply_base_flags(engine_abi_layout)

add_test(NAME engine_abi_layout COMMAND engine_abi_layout)
set_tests_properties(engine_abi_layout PROPERTIES TIMEOUT 120)

# ABI 4.25 (P3-05): panel-edit round-trip (entity_get/set float lane +
# undo replay + entity_remove). Links the ecs render components for
# MeshRenderer (same include the ecs_world tests use).
add_executable(engine_inspector_edits test_inspector_edits.cpp)
target_link_libraries(engine_inspector_edits PRIVATE engine_core)
target_include_directories(engine_inspector_edits PRIVATE "${CMAKE_SOURCE_DIR}/native/ecs/include")
engine_apply_base_flags(engine_inspector_edits)
add_test(NAME engine_inspector_edits COMMAND engine_inspector_edits)
set_tests_properties(engine_inspector_edits PROPERTIES TIMEOUT 120)

# ABI 4.2: generic component/system tail (Illusion Layer native side).
add_executable(engine_generic_systems test_generic_systems.cpp)
target_link_libraries(engine_generic_systems PRIVATE engine_core)
# name_copy.hpp is a private header-only helper of the bridge.
target_include_directories(engine_generic_systems PRIVATE "${CMAKE_SOURCE_DIR}/native/bridge/src")
engine_apply_base_flags(engine_generic_systems)
add_test(NAME engine_generic_systems COMMAND engine_generic_systems)
set_tests_properties(engine_generic_systems PROPERTIES TIMEOUT 120)

# P2-03: world_frame phase routing + world clock through the ABI
# (FixedUpdate once per fixed step, fake clock [0.016, 0.016, 0.2, 0.001]).
add_executable(engine_world_frame test_world_frame.cpp)
target_link_libraries(engine_world_frame PRIVATE engine_core)
engine_apply_base_flags(engine_world_frame)
add_test(NAME engine_world_frame COMMAND engine_world_frame)
set_tests_properties(engine_world_frame PROPERTIES TIMEOUT 120)

# P6-03: Recast bake + DetourCrowd agent acceptance (20x20 floor, center
# box, agent routes around it within 600 frames at 60 Hz).
add_executable(engine_navigation_p6_03 test_navigation_p6_03.cpp)
target_link_libraries(engine_navigation_p6_03 PRIVATE engine_core)
engine_apply_base_flags(engine_navigation_p6_03)
add_test(NAME engine_navigation_p6_03 COMMAND engine_navigation_p6_03)
set_tests_properties(engine_navigation_p6_03 PROPERTIES TIMEOUT 300)

# M1-05: hostfxr embedding acceptance. The managed fixture
# (Benchmark1M.Systems.dll + Benchmark1M.runtimeconfig.json) is built by
# `dotnet build` of Engine.slnx; the native smoke test boots CoreCLR
# through DotNetHost and runs the 1M-row milestone through the UCO
# callback. dotnet is REQUIRED: the M1-05 gate cannot run without managed
# fixtures (CI installs .NET 10 via setup-dotnet before CMake configures).
#
# Fixture paths point at the SOURCE tree outputs (dotnet builds in-source
# under managed/ + samples/), so configure needs no build ordering: the
# ctest fixture step below rebuilds them if stale.
find_program(DOTNET_EXE dotnet REQUIRED)
set(BENCHMARK1M_SYSTEMS_DLL
    "${CMAKE_SOURCE_DIR}/samples/Benchmark1M/bin/Release/net10.0/Benchmark1M.Systems.dll")
set(BENCHMARK1M_CONFIG
    "${CMAKE_SOURCE_DIR}/samples/Benchmark1M/bin/Release/net10.0/Benchmark1M.runtimeconfig.json")

add_executable(engine_hostfxr_smoke test_hostfxr_smoke.cpp)

target_link_libraries(engine_hostfxr_smoke PRIVATE engine_core)

target_compile_definitions(engine_hostfxr_smoke PRIVATE
    BENCHMARK1M_SYSTEMS_DLL="${BENCHMARK1M_SYSTEMS_DLL}"
    BENCHMARK1M_CONFIG="${BENCHMARK1M_CONFIG}")

engine_apply_base_flags(engine_hostfxr_smoke)

# Rebuild managed fixtures before the smoke test runs (no-op when fresh).
# Runs from the repo root so dotnet resolves Engine.slnx's relative project
# paths; no --project path arithmetic needed regardless of preset binary dir.
# -p:Platform="Any CPU": the slnx declares no platform configurations, and
# the Windows job's msvc-dev-cmd environment exports Platform=x64, which
# MSBuild would otherwise demand as "Release|x64" (MSB4126). An explicit
# Any CPU is valid on all three OSes (the projects' default platform).
add_test(NAME engine_hostfxr_smoke_build
    COMMAND ${DOTNET_EXE} build Engine.slnx -c Release --nologo "-p:Platform=Any CPU")
set_tests_properties(engine_hostfxr_smoke_build PROPERTIES
    TIMEOUT 600
    FIXTURES_SETUP benchmark1m_fixtures
    WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")

add_test(NAME engine_hostfxr_smoke COMMAND engine_hostfxr_smoke)
set_tests_properties(engine_hostfxr_smoke PROPERTIES
    TIMEOUT 300
    FIXTURES_REQUIRED benchmark1m_fixtures)

# ABI 4.2: Illusion Layer end-to-end gate. samples/IllusionSample is built
# by the same `dotnet build Engine.slnx` fixture; it loads engine_core by
# absolute path, runs Engine.Analyzers-generated systems on native Flecs
# columns, and prints ILLUSION-SAMPLE-OK (exit 0) when every check passes.
add_test(NAME engine_illusion_sample
    COMMAND ${DOTNET_EXE}
        "${CMAKE_SOURCE_DIR}/samples/IllusionSample/bin/Release/net10.0/IllusionSample.dll"
        "$<TARGET_FILE:engine_core>")
set_tests_properties(engine_illusion_sample PROPERTIES
    TIMEOUT 300
    FIXTURES_REQUIRED benchmark1m_fixtures
    PASS_REGULAR_EXPRESSION "ILLUSION-SAMPLE-OK")

# P1-09: Rendering from C# acceptance. Runs the HelloCube sample (built by
# the same `dotnet build Engine.slnx` fixture) offscreen: 10 frames over a
# borrowed world through the ABI 4.3 renderer tail, then screenshots.
# Prints HELLO-CUBE-OK (exit 0) when every check passes; the screenshot
# lands at <build>/hello_cube.png for the golden compare below.
add_test(NAME engine_hello_cube
    COMMAND ${DOTNET_EXE}
        "${CMAKE_SOURCE_DIR}/samples/HelloCube/bin/Release/net10.0/HelloCube.dll"
        "$<TARGET_FILE:engine_core>"
        --frames 10 --screenshot "${CMAKE_CURRENT_BINARY_DIR}/hello_cube.png")
set_tests_properties(engine_hello_cube PROPERTIES
    TIMEOUT 300
    FIXTURES_REQUIRED benchmark1m_fixtures
    ${ENGINE_TEST_VIDEO_ENV}
    PASS_REGULAR_EXPRESSION "HELLO-CUBE-OK|\\[SKIP\\] hello-cube")

# Golden compare for the HelloCube screenshot: loads the PNG the sample
# wrote and diffs it against golden/hello_cube_10f_640x360.png (P1-05
# tolerance). Runs after engine_hello_cube (DEPENDS on it through the
# hello_cube_screenshot fixture); HELLO_CUBE_PNG points at the sample's
# output. Missing PNG fails closed (never silently passes).
add_executable(engine_hello_cube_compare test_hello_cube_compare.cpp golden_stb.cpp)
target_link_libraries(engine_hello_cube_compare PRIVATE engine_core)
engine_apply_base_flags(engine_hello_cube_compare)
if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()
target_compile_definitions(engine_hello_cube_compare PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")
add_test(NAME engine_hello_cube_compare
    COMMAND engine_hello_cube_compare)
set_tests_properties(engine_hello_cube_compare PROPERTIES
    TIMEOUT 120
    FIXTURES_REQUIRED "benchmark1m_fixtures;hello_cube_screenshot"
    ENVIRONMENT "HELLO_CUBE_PNG=${CMAKE_CURRENT_BINARY_DIR}/hello_cube.png"
    PASS_REGULAR_EXPRESSION "golden: hello_cube_10f_640x360")
set_tests_properties(engine_hello_cube PROPERTIES
    FIXTURES_SETUP hello_cube_screenshot
    FIXTURES_REQUIRED benchmark1m_fixtures)

# P2-07: engine-player acceptance. Runs the player binary against
# samples/HelloCube (demo mode: the sample ships no project.json and no
# bootstrap, so the player builds the HelloCube-equivalent frame
# natively): 10 frames offscreen, screenshot, PLAYER-OK exit 0.
# GPU-less skips exit 0 without PLAYER-OK (and without a screenshot),
# so a skipped run fails closed instead of passing the compare on a
# stale PNG.
add_test(NAME engine_player_hello
    COMMAND engine-player "${CMAKE_SOURCE_DIR}/samples/HelloCube"
        --frames 10 --screenshot "${CMAKE_CURRENT_BINARY_DIR}/player.png"
        --hidden)
set_tests_properties(engine_player_hello PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV}
    FIXTURES_SETUP player_hello_screenshot
    PASS_REGULAR_EXPRESSION "PLAYER-OK")

# Golden compare for the player screenshot: reuse the hello-cube compare
# binary (same golden, same P1-05 tolerance) with PLAYER-equivalent input.
# One TU, no duplicated compare main: HELLO_CUBE_PNG points at the player
# screenshot. Missing PNG fails closed.
add_test(NAME engine_player_hello_compare
    COMMAND engine_hello_cube_compare)
set_tests_properties(engine_player_hello_compare PROPERTIES
    TIMEOUT 120
    FIXTURES_REQUIRED player_hello_screenshot
    ENVIRONMENT "HELLO_CUBE_PNG=${CMAKE_CURRENT_BINARY_DIR}/player.png"
    PASS_REGULAR_EXPRESSION "\\[PASS\\] golden: hello_cube_10f_640x360")

# P2-08 slice (autoplay flag): unit coverage for the --autoplay path that
# does not need a GPU. Replays a 3-frame W-hold script against the
# HelloCube demo project offscreen and asserts the autoplay-complete line
# (deterministic replay through the event-fed input path). The marble
# course + `Finished in` acceptance (engine_marble_autoplay) lands with
# the MarbleRun sample in the follow-up slice.
add_test(NAME engine_player_autoplay
    COMMAND engine-player "${CMAKE_SOURCE_DIR}/samples/HelloCube"
        --frames 3 --autoplay "${CMAKE_SOURCE_DIR}/samples/HelloCube/test/autoplay_smoke.json"
        --hidden)
set_tests_properties(engine_player_autoplay PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV}
    PASS_REGULAR_EXPRESSION "autoplay complete \\(3 frames\\)")

# Headless runtime acceptance: the player boots MarbleRun headless with
# --frames 10 --screenshot, prints COLD_BOOT_S (engine init to first
# frame), and exits PLAYER-OK. The hostfxr fixture builds Engine.slnx
# (which includes samples/MarbleRun), so the game bootstrap binds and the
# log carries "game bootstrap ran" — a demo-frame fallback would mean the
# export is broken, so the regex requires the bootstrap line. GPU-less
# runners [SKIP] (no screenshot) through the player's RequireGpu path, so
# the regex accepts the skip.
add_test(NAME engine_player_headless
    COMMAND engine-player "${CMAKE_SOURCE_DIR}/samples/MarbleRun"
        --frames 10 --screenshot "${CMAKE_CURRENT_BINARY_DIR}/player_headless.png"
        --hidden)
set_tests_properties(engine_player_headless PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV}
    FIXTURES_REQUIRED benchmark1m_fixtures
    PASS_REGULAR_EXPRESSION "game bootstrap ran.*COLD_BOOT_S=[0-9]+\\.[0-9]+.*frames without close.*PLAYER-OK|\\[SKIP\\] player")

# M5-04: one-click export fixtures — NativeAOT game + .pak staging.
# Publishes samples/ExportSample with PublishAot (build --no-build reuses
# the hostfxr fixture build above, so ILCompiler runs once per machine),
# then stages the game binary + native libs + a fixture .pak into
# export_stage/ for the cold-boot acceptance test below. The publish is a
# FIXTURES_SETUP step (not the gate itself): slow ILCompiler failures
# surface here with the SDK log, while the gate measures cold boot.
set(EXPORT_SAMPLE_PUBLISH_DIR
    "${CMAKE_SOURCE_DIR}/samples/ExportSample/bin/Release/net10.0/linux-x64/publish")
if(WIN32)
    set(EXPORT_SAMPLE_RID "win-x64")
    set(EXPORT_SAMPLE_EXE "ExportSample.exe")
elseif(APPLE)
    set(EXPORT_SAMPLE_RID "osx-arm64")
    set(EXPORT_SAMPLE_EXE "ExportSample")
else()
    set(EXPORT_SAMPLE_RID "linux-x64")
    set(EXPORT_SAMPLE_EXE "ExportSample")
endif()
add_test(NAME engine_export_publish
    COMMAND ${DOTNET_EXE} build "${CMAKE_SOURCE_DIR}/samples/ExportSample/ExportSample.csproj"
        -c Release --nologo
    WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(engine_export_publish PROPERTIES
    TIMEOUT 1200
    FIXTURES_SETUP export_stage_built
    FIXTURES_REQUIRED benchmark1m_fixtures)
# Separate publish step: --no-build reuses the build above, so ILCompiler
# runs on bin/Release (never bin/x64/Release) on every OS. Split from the
# build because --no-build publish still evaluates stale per-config
# reference paths (MSB4018 on Windows) when build+publish share one
# invocation through the slnx Platform property.
add_test(NAME engine_export_publish_aot
    COMMAND ${DOTNET_EXE} publish "${CMAKE_SOURCE_DIR}/samples/ExportSample/ExportSample.csproj"
        --no-build -r ${EXPORT_SAMPLE_RID} -c Release -p:PublishAot=true --nologo
    WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(engine_export_publish_aot PROPERTIES
    TIMEOUT 1200
    FIXTURES_SETUP export_stage_built
    FIXTURES_REQUIRED benchmark1m_fixtures)
add_test(NAME engine_export_stage
    COMMAND ${CMAKE_COMMAND} -E make_directory
        "${CMAKE_CURRENT_BINARY_DIR}/export_stage")
set_tests_properties(engine_export_stage PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP export_staged
    FIXTURES_REQUIRED export_stage_built)
# Copy the staged AOT game into export_stage/. The publish output dir
# varies by OS/SDK (bin/Release vs bin/x64/Release when the msvc-dev-cmd
# environment exports Platform=x64): glob for the binary under the sample
# build tree instead of hardcoding one layout. Native libs follow.
add_test(NAME engine_export_stage_game
    COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_BINARY_DIR}/stage_export_game.cmake")
# Stage the AOT game via a real script file (cmake/stage_export_game.cmake.in):
# nesting file(WRITE) quoting inside CMakeLists proved un escapable (the
# ${_src}/${_c} refs kept expanding at configure time). configure_file
# with @ONLY leaves ${} script vars intact for ctest -P time.
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/stage_export_game.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/stage_export_game.cmake" @ONLY)
set_tests_properties(engine_export_stage_game PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP export_staged
    FIXTURES_REQUIRED export_stage_built)
add_test(NAME engine_export_stage_core
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "$<TARGET_FILE:engine_core>"
        "${CMAKE_CURRENT_BINARY_DIR}/export_stage/$<TARGET_FILE_NAME:engine_core>")
set_tests_properties(engine_export_stage_core PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP export_staged
    FIXTURES_REQUIRED export_stage_built)
# Linux note: no SONAME symlink needed here. The unversioned copy below
# (engine_export_stage_core_unversioned) already places the exact probed
# name export_stage/libengine_core.so, so a second writer of that same path
# (previously engine_export_stage_core_link) raced it under ctest -j.
# Unversioned-name copies: ExportSample probes libengine_core.dylib /
# libflecs.dylib (macOS) and libengine_core.so / libflecs.so (Linux) by
# those exact names; TARGET_FILE_NAME is versioned (libfoo.0.1.0.dylib).
# Copy under the probed names on every OS (copy, not symlink — Windows).
add_test(NAME engine_export_stage_core_unversioned
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "$<TARGET_FILE:engine_core>"
        "${CMAKE_CURRENT_BINARY_DIR}/export_stage/libengine_core$<TARGET_FILE_SUFFIX:engine_core>")
set_tests_properties(engine_export_stage_core_unversioned PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP export_staged
    FIXTURES_REQUIRED export_stage_built)
add_test(NAME engine_export_stage_flecs
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "$<TARGET_FILE:flecs>"
        "${CMAKE_CURRENT_BINARY_DIR}/export_stage/$<TARGET_FILE_NAME:flecs>")
set_tests_properties(engine_export_stage_flecs PROPERTIES
    TIMEOUT 120
    FIXTURES_SETUP export_staged
    FIXTURES_REQUIRED export_stage_built)
# Same unversioned-name guarantee for flecs (ExportSample probes
# libflecs.so / libflecs.dylib / flecs.dll exactly).
if(NOT WIN32)
    add_test(NAME engine_export_stage_flecs_unversioned
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "$<TARGET_FILE:flecs>"
            "${CMAKE_CURRENT_BINARY_DIR}/export_stage/libflecs$<TARGET_FILE_SUFFIX:flecs>")
    set_tests_properties(engine_export_stage_flecs_unversioned PROPERTIES
        TIMEOUT 120
        FIXTURES_SETUP export_staged
        FIXTURES_REQUIRED export_stage_built)
endif()

# M5-04: cold-boot gate — the exported NativeAOT game cold-opens beside
# its staged .pak and exits EXPORT-SAMPLE-OK. Wall <1000ms is asserted by
# the test binary itself (per-run Stopwatch + worst-of-5); ctest only
# needs the binary to exit 0. Runs after the export fixtures stage.
add_executable(engine_export_coldboot test_export_coldboot.cpp)
target_link_libraries(engine_export_coldboot PRIVATE engine_core)
engine_apply_base_flags(engine_export_coldboot)
add_test(NAME engine_export_coldboot COMMAND engine_export_coldboot
    WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
set_tests_properties(engine_export_coldboot PROPERTIES
    TIMEOUT 300
    FIXTURES_REQUIRED "export_staged;compositor_live_staged"
    ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/native:${CMAKE_BINARY_DIR}/deps/flecs:$ENV{LD_LIBRARY_PATH}")
# Roofline-relative efficiency gate (not a fixed wall clock): the serial
# 1M STREAM probe measures the machine's own bandwidth for this exact
# traffic pattern (144MB/frame AoS); the parallel best-of-N must reach it
# (gain >= 1/1.5). A fixed 1.5ms demands >= 96GB/s sustained — met at 20%
# on a 256GB/s workstation, unmeetable on a 36GB/s shared CI VM — so the
# fixed number is proven by Benchmark1M on reference hardware while CI
# proves the fan-out delivers the hardware's bandwidth. Absolute ms, GB/s
# and gain stay LOGGED for humans.
# Apple-Silicon CI carve-out: macOS-15 ARM runners hang in the FIRST
# parallel managed fan-out (serial path + serial probe pass on the same
# box; native-only enkiTS fan-out passes in test_job_scheduler — the hang
# is specific to parallel UCO dispatch, root cause under investigation).
# ENGINE_SERIAL_DISPATCH forces the serial fast path there: embedding,
# exactness and no-hang are still proven on macOS while Linux proves the
# parallel path (Ubuntu shows gain 1.3x on 2 real cores, local 3.7x).
# hostfxr discovery is DOTNET_ROOT-first (ScanFxDir): the `dotnet` resolved
# above knows an SDK install, but the SDK dir is NOT necessarily the shared
# runtime install the smoke test must boot (setup-dotnet: SDK in
# $RUNNER_TOOL_CACHE, runtime in /usr/share/dotnet or ~/.dotnet).
# Prefer the SHARED runtime root from `dotnet --list-runtimes`.
# CAREFUL: the bracketed path is the SHARED root
# (<dotnet>/shared/Microsoft.NETCore.App), NOT the dotnet root — hostfxr
# resolves the runtime relative to DOTNET_ROOT, so passing the shared dir
# breaks framework lookup (observed Windows rc=0x80008093). Strip the two
# trailing components (shared/Microsoft.NETCore.App) to recover the real
# dotnet root (carries host/fxr/<ver>). Fall back to the SDK root.
execute_process(
    COMMAND ${DOTNET_EXE} --list-runtimes
    OUTPUT_VARIABLE _DOTNET_RUNTIMES
    OUTPUT_STRIP_TRAILING_WHITESPACE)
if(_DOTNET_RUNTIMES MATCHES "Microsoft\\.NETCore\\.App [0-9.]+ \\[([^]]+)\\]")
    set(_ENGINE_SHARED_ROOT "${CMAKE_MATCH_1}")
    get_filename_component(_ENGINE_DOTNET_ROOT "${_ENGINE_SHARED_ROOT}" DIRECTORY)
    get_filename_component(_ENGINE_DOTNET_ROOT "${_ENGINE_DOTNET_ROOT}" DIRECTORY)
    if(EXISTS "${_ENGINE_DOTNET_ROOT}/host/fxr")
        set(ENGINE_DOTNET_ROOT "${_ENGINE_DOTNET_ROOT}")
    else()
        message(STATUS "dotnet shared root ${_ENGINE_SHARED_ROOT} has no host/fxr sibling; leaving DOTNET_ROOT unset (ScanFxDir well-known prefixes apply)")
    endif()
    unset(_ENGINE_SHARED_ROOT)
    unset(_ENGINE_DOTNET_ROOT)
else()
    # No shared-runtime entry (plain SDK layout): fall back to the SDK root.
    execute_process(
        COMMAND ${DOTNET_EXE} --list-sdks
        OUTPUT_VARIABLE _DOTNET_SDKS
        OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(_DOTNET_SDKS MATCHES "\\[([^]]+)\\]")
        set(ENGINE_DOTNET_ROOT "${CMAKE_MATCH_1}")
    endif()
endif()
if(DEFINED ENGINE_DOTNET_ROOT)
    message(STATUS "dotnet root for hostfxr smoke: ${ENGINE_DOTNET_ROOT}")
    # Windows: pass DOTNET_ROOT with native backslashes. The windows-2025
    # image sets DOTNET_MULTILEVEL_LOOKUP=0, so hostfxr trusts DOTNET_ROOT
    # exclusively for framework resolution — and forward slashes break that
    # strict path (observed rc=0x80008093 with C:/... while the same value
    # works where multilevel lookup stays on). TO_NATIVE_PATH matches what
    # setup-dotnet itself exports (C:\Program Files\dotnet).
    if(WIN32)
        file(TO_NATIVE_PATH "${ENGINE_DOTNET_ROOT}" _ENGINE_DOTNET_ROOT_NATIVE)
        set(_ENGINE_SMOKE_DOTNET_ROOT "${_ENGINE_DOTNET_ROOT_NATIVE}")
        unset(_ENGINE_DOTNET_ROOT_NATIVE)
        # windows-2025 images export DOTNET_MULTILEVEL_LOOKUP=0, under which
        # hostfxr 10.0.14 refuses the fixture runtimeconfig (rc=0x80008093)
        # even with a correct DOTNET_ROOT. Restoring multilevel lookup for
        # this test-only process re-enables the standard search
        # (DOTNET_ROOT + global install) without touching product behavior.
        set(_ENGINE_SMOKE_MLL "DOTNET_MULTILEVEL_LOOKUP=1")
    else()
        set(_ENGINE_SMOKE_DOTNET_ROOT "${ENGINE_DOTNET_ROOT}")
        set(_ENGINE_SMOKE_MLL "ENGINE_MLL_UNUSED=1")
    endif()
    if(APPLE)
        set_tests_properties(engine_hostfxr_smoke PROPERTIES
            ENVIRONMENT "DOTNET_ROOT=${_ENGINE_SMOKE_DOTNET_ROOT};ENGINE_SERIAL_DISPATCH=1")
    elseif(WIN32)
        set_tests_properties(engine_hostfxr_smoke PROPERTIES
            ENVIRONMENT "DOTNET_ROOT=${_ENGINE_SMOKE_DOTNET_ROOT};${_ENGINE_SMOKE_MLL}")
    else()
        set_tests_properties(engine_hostfxr_smoke PROPERTIES
            ENVIRONMENT "DOTNET_ROOT=${_ENGINE_SMOKE_DOTNET_ROOT}")
    endif()
elseif(APPLE)
    # No runtime root resolved, but still force serial dispatch on Apple
    # Silicon (parallel UCO hang — see above).
    set_tests_properties(engine_hostfxr_smoke PROPERTIES
        ENVIRONMENT "ENGINE_SERIAL_DISPATCH=1")
endif()

# M4-01: Jolt physics acceptance (job system, temp allocator, layers,
# components, world step). Uses base flags (host tooling).
add_executable(engine_physics_jolt test_physics_jolt.cpp)

target_link_libraries(engine_physics_jolt PRIVATE engine_core)

engine_apply_base_flags(engine_physics_jolt)

add_test(NAME engine_physics_jolt COMMAND engine_physics_jolt)
set_tests_properties(engine_physics_jolt PROPERTIES TIMEOUT 300)

# M4-02: Flecs <-> Jolt transform sync acceptance (push/pull/events).
# Uses base flags (host tooling). Links engine_core (world + sync).
add_executable(engine_physics_sync test_physics_sync.cpp)

target_link_libraries(engine_physics_sync PRIVATE engine_core)

engine_apply_base_flags(engine_physics_sync)

add_test(NAME engine_physics_sync COMMAND engine_physics_sync)
set_tests_properties(engine_physics_sync PROPERTIES TIMEOUT 300)

# P3-07: play-mode restore acceptance (snapshot 60 falling-sphere frames,
# stop, position equals pre-play exactly). Uses base flags (host tooling);
# links engine_core (world + editor session snapshots).
add_executable(engine_play_mode test_play_mode.cpp)

target_link_libraries(engine_play_mode PRIVATE engine_core)

engine_apply_base_flags(engine_play_mode)

add_test(NAME engine_play_mode COMMAND engine_play_mode)
set_tests_properties(engine_play_mode PROPERTIES TIMEOUT 300)

# M4-03: spatial queries acceptance (raycast / spherecast / overlap /
# parallel batch). Uses base flags (host tooling); TU-local Jolt allocator
# registration handles the Windows DLL-duplication (see TU header).
add_executable(engine_physics_queries test_physics_queries.cpp)

target_link_libraries(engine_physics_queries PRIVATE engine_core)

engine_apply_base_flags(engine_physics_queries)

add_test(NAME engine_physics_queries COMMAND engine_physics_queries)
set_tests_properties(engine_physics_queries PROPERTIES TIMEOUT 300)

# P6-07: Box2D v3 2D physics acceptance (10-box stack settles, contact
# events counted via the shared CollisionEventRing). Uses base flags
# (host tooling). Links engine_core (world2d + ring live in the OBJECT
# lib; TU-local Box2D use needs no extra allocator registration — the C
# API owns its arena internally).
add_executable(engine_physics_2d test_physics_2d.cpp)

target_link_libraries(engine_physics_2d PRIVATE engine_core)

engine_apply_base_flags(engine_physics_2d)

add_test(NAME engine_physics_2d COMMAND engine_physics_2d)
set_tests_properties(engine_physics_2d PROPERTIES TIMEOUT 300)

# P6-01: terrain acceptance (R16 heightmap, chunked LOD mesh, 4-layer
# splat, Jolt heightfield collider + raycast ±0.01, golden hash). Uses base
# flags (host tooling); TU-local Jolt allocator registration handles the
# Windows DLL-duplication (same as the physics tests).
add_executable(engine_terrain_p6_01 test_terrain_p6_01.cpp)

target_link_libraries(engine_terrain_p6_01 PRIVATE engine_core)

engine_apply_base_flags(engine_terrain_p6_01)

add_test(NAME engine_terrain_p6_01 COMMAND engine_terrain_p6_01)
set_tests_properties(engine_terrain_p6_01 PROPERTIES TIMEOUT 300)

# P6-02: terrain editing brushes (raise/lower/smooth/flatten + splat paint
# + stroke undo). Pure CPU grid math; links engine_core like P6-01.
add_executable(engine_terrain_brush_p6_02 test_terrain_brush_p6_02.cpp)

target_link_libraries(engine_terrain_brush_p6_02 PRIVATE engine_core)

engine_apply_base_flags(engine_terrain_brush_p6_02)

add_test(NAME engine_terrain_brush_p6_02 COMMAND engine_terrain_brush_p6_02)
set_tests_properties(engine_terrain_brush_p6_02 PROPERTIES TIMEOUT 300)

# P6-05: physics joints acceptance (fixed / hinge / slider / distance /
# spring components over Jolt constraints; hinged-door angle-limit AC).
add_executable(engine_physics_joints test_physics_joints.cpp)

target_link_libraries(engine_physics_joints PRIVATE engine_core)

engine_apply_base_flags(engine_physics_joints)

add_test(NAME engine_physics_joints COMMAND engine_physics_joints)
set_tests_properties(engine_physics_joints PROPERTIES TIMEOUT 300)

# P6-04: character-controller acceptance (Jolt CharacterVirtual: climbs a
# 0.3 m step, stops at a 60-degree slope).
add_executable(engine_character_p6_04 test_character_p6_04.cpp)

target_link_libraries(engine_character_p6_04 PRIVATE engine_core)

engine_apply_base_flags(engine_character_p6_04)

add_test(NAME engine_character_p6_04 COMMAND engine_character_p6_04)
set_tests_properties(engine_character_p6_04 PROPERTIES TIMEOUT 300)

# Copy runtime DLLs (engine_core + flecs) next to every linked test binary.
# NOTE: engine_audio_spatial is declared AFTER this block (M4-04 section
# below); its own WIN32 copy block lives there — do NOT add it here
# (CMake errors on unknown TARGET at configure time).
if(WIN32)
    foreach(tgt engine_core_smoke engine_memory_arenas engine_job_scheduler engine_abi_layout engine_physics_jolt engine_physics_sync engine_physics_queries engine_play_mode engine_terrain_p6_01 engine_terrain_brush_p6_02 engine_hostfxr_smoke engine_renderer_bootstrap engine_gpu_indirect engine_gpu_driven_statics engine_gpu_driven_250k engine_swapchain engine_forward_triangle engine_slang_pbr engine_gi_zero_bake engine_asset_pipeline engine_project_drop engine_physics_joints engine_character_p6_04)
        add_custom_command(TARGET ${tgt} POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                $<TARGET_RUNTIME_DLLS:${tgt}>
                $<TARGET_FILE_DIR:${tgt}>
            COMMAND_EXPAND_LISTS)
    endforeach()
endif()

# M4-04: miniaudio spatial graph acceptance (streaming decode matrix +
# Flecs spatializer). Headless-safe: the test constructs AudioWorld with
# noDevice=true. Fixtures are CHECKED IN under native/tests/fixtures/
# (3 tiny sine tones, ~68 KB total) so configure never needs ffmpeg or
# network — MSVC/Windows CI included (ffmpeg is absent there).
set(AUDIO_FIXTURE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/fixtures")

add_executable(engine_audio_spatial test_audio_spatial.cpp)

target_link_libraries(engine_audio_spatial PRIVATE engine_core)

target_compile_definitions(engine_audio_spatial PRIVATE
    AUDIO_FIXTURE_DIR="${AUDIO_FIXTURE_DIR}"
)

engine_apply_base_flags(engine_audio_spatial)

add_test(NAME engine_audio_spatial COMMAND engine_audio_spatial)
set_tests_properties(engine_audio_spatial PROPERTIES TIMEOUT 60)

# P5-06: audio mixer acceptance (buses + LPF/delay + dB volume).
# Headless-safe: noDevice=true; the -6 dB sine render peaks at 0.5.
add_executable(engine_audio_mixer test_audio_mixer.cpp)

target_link_libraries(engine_audio_mixer PRIVATE engine_core)

engine_apply_base_flags(engine_audio_mixer)

add_test(NAME engine_audio_mixer COMMAND engine_audio_mixer)
set_tests_properties(engine_audio_mixer PROPERTIES TIMEOUT 300)

# P5-04: game UI acceptance (RmlUi 6.3 + FreeType CPU HUD). Headless-safe:
# no window, no GPU — the frame is CPU bytes (RmlUi structure + FreeType
# glyph runs + deterministic paint). Golden hud_640x360 + exactly-once
# click drain. Golden harness: golden_compare.hpp + golden_stb.cpp +
# ENGINE_GOLDEN_* dirs (same as engine_swapchain); ENGINE_UI_FONT_DIR
# points at the staged LatoLatin face (native/ui/CMakeLists.txt).
add_executable(engine_game_ui test_game_ui.cpp golden_stb.cpp)

target_link_libraries(engine_game_ui PRIVATE engine_core)

engine_apply_base_flags(engine_game_ui)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_game_ui PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out"
    ENGINE_UI_FONT_DIR="${CMAKE_BINARY_DIR}/ui_fonts")

add_test(NAME engine_game_ui COMMAND engine_game_ui)
set_tests_properties(engine_game_ui PROPERTIES TIMEOUT 300)

# P5-05: GPU particle system (compute emit/update + billboard goldens).
# CPU acceptance needs no GPU; the GPU path uses llvmpipe via the shared
# GoldenOpenDevice pattern inside the TU (ENGINE_REQUIRE_GPU=1 fails
# closed when no device is found).
add_executable(engine_particles test_particles.cpp golden_stb.cpp)

target_link_libraries(engine_particles PRIVATE engine_core)

target_compile_definitions(engine_particles PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_CURRENT_SOURCE_DIR}/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_particles)

add_test(NAME engine_particles COMMAND engine_particles)
set_tests_properties(engine_particles PROPERTIES TIMEOUT 300)

# Windows: engine_game_ui + engine_particles are declared after the main
# WIN32 DLL-copy block above (CMake errors on unknown TARGET at
# configure time), so they get their own copy block here.
if(WIN32)
    foreach(tgt engine_game_ui engine_particles)
        add_custom_command(TARGET ${tgt} POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                $<TARGET_RUNTIME_DLLS:${tgt}>
                $<TARGET_FILE_DIR:${tgt}>
            COMMAND_EXPAND_LISTS)
    endforeach()
endif()

# P5-05-followup: particle entities in the ECS frame (60 frames through
# the real RenderOne path + golden + empty-frame differs proof).
add_executable(engine_ecs_particles_frame test_ecs_particles_frame.cpp golden_stb.cpp)

target_link_libraries(engine_ecs_particles_frame PRIVATE engine_core)

engine_apply_base_flags(engine_ecs_particles_frame)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_ecs_particles_frame PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

add_test(NAME engine_ecs_particles_frame COMMAND engine_ecs_particles_frame)
set_tests_properties(engine_ecs_particles_frame PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P5-04-followup: HUD composite in the ECS frame (frames through the real
# RenderOne path with a bound GameUi + hud-on differs from hud-off +
# composite matches the CPU backbuffer + unbind restores clear).
add_executable(engine_hud_composite test_hud_composite.cpp golden_stb.cpp)

target_link_libraries(engine_hud_composite PRIVATE engine_core)

engine_apply_base_flags(engine_hud_composite)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_hud_composite PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out"
    ENGINE_UI_FONT_DIR="${CMAKE_BINARY_DIR}/ui_fonts")

add_test(NAME engine_hud_composite COMMAND engine_hud_composite)
set_tests_properties(engine_hud_composite PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P3-09: console/profiler GPU timer (FrameGpuTimer timestamp round trip).
add_executable(engine_frame_gpu_timer test_frame_gpu_timer.cpp)

target_link_libraries(engine_frame_gpu_timer PRIVATE engine_core)

engine_apply_base_flags(engine_frame_gpu_timer)

add_test(NAME engine_frame_gpu_timer COMMAND engine_frame_gpu_timer)
set_tests_properties(engine_frame_gpu_timer PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV})

# P2-01: native input acceptance. Injects SDL events under the offscreen
# driver via SDL_PushEvent; needs SDL_VIDEO_DRIVER=offscreen where no
# display exists (same shape as the P1 window contract).
add_executable(engine_input test_input.cpp)

target_link_libraries(engine_input PRIVATE engine_core)

engine_apply_base_flags(engine_input)

add_test(NAME engine_input COMMAND engine_input)
set_tests_properties(engine_input PROPERTIES TIMEOUT 120
    ${ENGINE_TEST_VIDEO_ENV})

# P2-03: fixed-timestep acceptance. Pure frame math (fake clock), no GPU
# or window: variable dt clamped to 0.1 s, fixed step 1/50 s with
# accumulator, at most 5 fixed steps per frame.
add_executable(engine_timestep test_timestep.cpp)

target_link_libraries(engine_timestep PRIVATE engine_core)

engine_apply_base_flags(engine_timestep)

add_test(NAME engine_timestep COMMAND engine_timestep)
set_tests_properties(engine_timestep PROPERTIES TIMEOUT 120)

# P2-04: BodySystem acceptance — static box floor (top y=0) + dynamic
# sphere r=0.5 from y=5; 2 simulated s at 50 Hz rests at 0.5 +/- 0.05.
# Raycast down from y=10 hits the floor top; removal drops the body.
add_executable(engine_physics_bodies test_physics_bodies.cpp)

target_link_libraries(engine_physics_bodies PRIVATE engine_core)

engine_apply_base_flags(engine_physics_bodies)

add_test(NAME engine_physics_bodies COMMAND engine_physics_bodies)
set_tests_properties(engine_physics_bodies PROPERTIES TIMEOUT 300)

# P2-06: scene JSON v1 — 10-entity/3-level round-trip byte-identical +
# bad user component skipped with one warning. Scratch files land in the
# test's working directory (argv[1] when CTest passes it).
add_executable(engine_scene_io test_scene_io.cpp)

target_link_libraries(engine_scene_io PRIVATE engine_core)

engine_apply_base_flags(engine_scene_io)

add_test(NAME engine_scene_io COMMAND engine_scene_io ${CMAKE_CURRENT_BINARY_DIR})
set_tests_properties(engine_scene_io PROPERTIES TIMEOUT 120)

if(WIN32)
    foreach(tgt engine_audio_spatial engine_input engine_timestep engine_world_frame engine_physics_bodies engine_scene_io)
        add_custom_command(TARGET ${tgt} POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                $<TARGET_RUNTIME_DLLS:${tgt}>
                $<TARGET_FILE_DIR:${tgt}>
            COMMAND_EXPAND_LISTS)
    endforeach()
endif()

# P4-07: Cornell-box real-time GI golden (SDF bricks + CPU-baked probe
# volume sampled in-shade as the indirect term; shares
# golden_stb.cpp for PNG IO).
add_executable(engine_gi_cornell test_gi_cornell.cpp golden_stb.cpp)

target_link_libraries(engine_gi_cornell PRIVATE engine_core)

if(NOT MSVC)
  set_source_files_properties(golden_stb.cpp PROPERTIES COMPILE_OPTIONS "-w")
endif()

target_compile_definitions(engine_gi_cornell PRIVATE
    ENGINE_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/native/tests/golden"
    ENGINE_GOLDEN_OUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/golden_out")

engine_apply_base_flags(engine_gi_cornell)

add_test(NAME engine_gi_cornell COMMAND engine_gi_cornell)
set_tests_properties(engine_gi_cornell PROPERTIES TIMEOUT 300)

# P5-05-followup: WIN32 DLL-copy (declared after the main copy block).
if(WIN32)
    add_custom_command(TARGET engine_ecs_particles_frame POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            $<TARGET_RUNTIME_DLLS:engine_ecs_particles_frame>
            $<TARGET_FILE_DIR:engine_ecs_particles_frame>
        COMMAND_EXPAND_LISTS)
    add_custom_command(TARGET engine_hud_composite POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            $<TARGET_RUNTIME_DLLS:engine_hud_composite>
            $<TARGET_FILE_DIR:engine_hud_composite>
        COMMAND_EXPAND_LISTS)
endif()

# P2-08: MarbleRun sample acceptance, player-driven. engine-player loads
# samples/MarbleRun (project.json -> gameAssembly MarbleRun.dll),
# Game.Bootstrap.Boot builds the course into the player's live world
# (course slabs + kinematic marble + sensor goal + FixedUpdate drive +
# Update-phase FrameHost), then the player loop replays
# test/autoplay.json (1500 empty-key frames: the event-fed input path is
# exercised while the scripted drive finishes the course unattended) and
# the game prints `Finished in N s` from the real sensor kEnter drain
# (exit 0). Windowed + GPU: needs SDL offscreen on Linux, fails closed
# under ENGINE_REQUIRE_GPU=1 like every GPU test.
add_test(NAME engine_marble_autoplay
    COMMAND "${CMAKE_COMMAND}"
        "-DPLAYER=$<TARGET_FILE:engine-player>"
        "-DPROJECT_DIR=${CMAKE_SOURCE_DIR}/samples/MarbleRun"
        "-DAUTOPLAY=${CMAKE_SOURCE_DIR}/samples/MarbleRun/test/autoplay.json"
        -DFRAMES=1500
        -P "${CMAKE_CURRENT_SOURCE_DIR}/run_marble_autoplay.cmake")
set_tests_properties(engine_marble_autoplay PROPERTIES
    TIMEOUT 300
    ${ENGINE_TEST_VIDEO_ENV}
    FIXTURES_REQUIRED benchmark1m_fixtures)

# P7-04: ENet networking acceptance. Two in-process peers (loopback)
# converge on 100 replicated transforms within 10 ticks at 10%
# simulated loss; reliable RPCs both directions; delta-compression
# gates (0 rows on no-change, 1 row on single-word change).
add_executable(engine_net_replication test_net_replication.cpp)

target_link_libraries(engine_net_replication PRIVATE engine_core)

engine_apply_base_flags(engine_net_replication)

add_test(NAME engine_net_replication COMMAND engine_net_replication)
set_tests_properties(engine_net_replication PROPERTIES TIMEOUT 300)

# P7-04 follow-up: declared after the main WIN32 DLL-copy blocks above
# (CMake errors on unknown TARGET at configure time), so it gets its own
# copy block here like engine_input/engine_timestep.
if(WIN32)
  add_custom_command(TARGET engine_net_replication POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
      $<TARGET_RUNTIME_DLLS:engine_net_replication>
      $<TARGET_FILE_DIR:engine_net_replication>
    COMMAND_EXPAND_LISTS)
endif()
