# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.20)

if (HIP_PLATFORM MATCHES "nvidia")
  if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
    set(CMAKE_CUDA_ARCHITECTURES 75)
  endif()
  set(GPGPU_LANGUAGE CUDA)
  set(GPGPU_LINKER_LIBRARIES hip::host hip::device cuda)
elseif (HIP_PLATFORM MATCHES "amd")
  set(GPGPU_LANGUAGE HIP)
  set(GPGPU_LINKER_LIBRARIES hip::host hip::device)
endif()
project(hiptests LANGUAGES C CXX ${GPGPU_LANGUAGE})
enable_language(${GPGPU_LANGUAGE})

if (HIP_PLATFORM MATCHES "nvidia")
  find_package(CUDAToolkit REQUIRED)
  set(CMAKE_HIP_COMPILER ${CUDAToolkit_NVCC_EXECUTABLE})
endif()

option(ENABLE_ADDRESS_SANITIZER "Option to enable ASAN build" OFF)
option(BUILD_SHARED_LIBS "Option for testing shared libraries" ON)
option(BUILD_UNIT_TESTS "Build unit tests" ON)
option(BUILD_PERF_TESTS "Build perf tests" OFF)
option(BUILD_STRESS_TESTS "Build stress tests" OFF)
option(BUILD_CONTRACT_TESTS "Build semantic contract tests" ON)
option(TEST_CLOCK_CYCLE "Option to use clock64" OFF)
option(RTC_TESTING "Run tests using HIP RTC to compile the kernels" OFF)
option(ENABLE_SPIRV "Build hip-tests for SPIRV" OFF)
option(ENABLE_YAML_TAGS "Enable YAML tags for test cases" ON)
option(STANDALONE_TESTS "Generate standalone executable per source file" OFF)
option(ENABLE_OOB_TESTS "Build OOB tests, deliberately wrong inputs" ON)

# Build tests with -Wall -Werror. Defaults to OFF
set(_hiptests_werror_default OFF)
option(HIP_TESTS_ENABLE_WERROR "Build tests with -Wall -Werror (Linux default ON)" ${_hiptests_werror_default})

if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE "Release")
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_HIP_STANDARD 17)
set(CMAKE_HIP_STANDARD_REQUIRED ON)
set(CMAKE_INSTALL_RPATH "$ORIGIN;$ORIGIN/../../lib")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

find_package(hip REQUIRED)
include_directories(${hip_INCLUDE_DIRS})
find_package(hiprtc)
find_package(Git)
find_package(Python 3 COMPONENTS Interpreter REQUIRED)
find_program(HIPCONFIG_EXEC hipconfig REQUIRED)

# hip root dir is 3 levels down the hip-config.cmake file path
set(HIP_PATH ${hip_DIR}/../../../)
if(NOT DEFINED ROCM_PATH)
  # Derive ROCM_PATH from the HIP location instead of hardcoding to /opt/rocm
  set(ROCM_PATH ${HIP_PATH})
endif()

if(UNIX)
  if(NOT ENABLE_SPIRV)
    find_program(ROCM_AGENT_ENUMERATOR_EXEC rocm_agent_enumerator HINTS ${ROCM_PATH}/bin ${HIP_PATH}/bin)
  endif()
  find_program(GCC_EXEC gcc)
  find_program(GXX_EXEC g++)
else() # Win32
  find_program(LLVM_RC_EXEC NAMES llvm-rc llvm-rc.exe
                 PATHS ${HIP_PATH} ${HIP_PATH}../lc/bin ${ROCM_PATH})
endif()

message(STATUS "HIP Lib path: ${HIP_LIB_INSTALL_DIR}")

# used by clang to find hip
set(HIP_PATH_OPT "--hip-path=${HIP_PATH}")

# Make hipcc path available to tests that need hipcc
if(HIP_HIPCC_EXECUTABLE)
  add_definitions(-DHIP_HIPCC_EXECUTABLE="${HIP_HIPCC_EXECUTABLE}")
endif()

if(TEST_CLOCK_CYCLE)
  add_definitions(-DTEST_CLOCK_CYCLE)
endif()

if(ENABLE_YAML_TAGS)
  add_definitions(-DENABLE_YAML_TAGS)
endif()

if(NOT WIN32)
  set(CPACK_SET_DESTDIR ON CACHE BOOL "Installer package will install hip catch to CMAKE_INSTALL_PREFIX instead of CPACK_PACKAGING_INSTALL_PREFIX")
endif()

# Check if platform is set
if(NOT HIP_PLATFORM STREQUAL "amd" AND NOT HIP_PLATFORM STREQUAL "nvidia")
  message(FATAL_ERROR "Unexpected HIP_PLATFORM: " ${HIP_PLATFORM})
endif()

execute_process(COMMAND ${HIPCONFIG_EXEC} --version
                OUTPUT_VARIABLE HIP_VERSION
                RESULT_VARIABLE result
                OUTPUT_STRIP_TRAILING_WHITESPACE)

if(result AND NOT result EQUAL 0)
  message(FATAL_ERROR "Failure trying to obtain HIP version via command: ${HIPCONFIG_EXEC} --version: ${result}")
endif()

if(NOT WIN32)
  if(NOT BUILD_SHARED_LIBS)
    list(APPEND CMAKE_PREFIX_PATH ${ROCM_PATH} ${ROCM_PATH}/llvm)
    find_package(amd_comgr)
    find_package(hsa-runtime64)
    link_libraries(hsa-runtime64::hsa-runtime64)
    link_libraries(amd_comgr)
    link_libraries(hiprtc-builtins)
  endif() # end BUILD_SHARED_LIBS
endif() # end win32

# Address sanitizer options
if(NOT DEFINED THEROCK_SANITIZER)
  set(THEROCK_SANITIZER)
endif()

set(ASAN_CONFIG_DEF)
set(HIP_TESTS_ASAN_ENABLED OFF)
if(ENABLE_ADDRESS_SANITIZER OR THEROCK_SANITIZER STREQUAL "ASAN" OR THEROCK_SANITIZER STREQUAL "HOST_ASAN")
  set(HIP_TESTS_ASAN_ENABLED ON)
  message(STATUS "Building catch tests with Address Sanitizer options")
  # If user is building hip-tests standalone (without TheRock) and wants to turn on sanitizer
  # We append the flags manually, otherwise we use TheRock's CXX flags which already should have
  # `-fsanitize=address` in it.
  if(ENABLE_ADDRESS_SANITIZER)
    # We support clang for this
    if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR NOT CMAKE_HIP_COMPILER_ID STREQUAL "Clang")
      message(WARNING "CXX and HIP compiler are not Clang, sanitizer build might fail")
    endif()

    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -shared-libasan -g")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -shared-libasan -g")
  endif()
  set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -fsanitize=address -fno-omit-frame-pointer -shared-libasan -g")
  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address -shared-libasan")
  add_compile_definitions(ENABLE_ADDRESS_SANITIZER)
  set(ASAN_CONFIG_DEF "-DENABLE_ADDRESS_SANITIZER")
endif()

string(REPLACE "." ";" VERSION_LIST ${HIP_VERSION})
list(GET VERSION_LIST 0 HIP_VERSION_MAJOR)
list(GET VERSION_LIST 1 HIP_VERSION_MINOR)
list(GET VERSION_LIST 2 HIP_VERSION_PATCH_GITHASH)
string(REPLACE "-" ";" VERSION_LIST ${HIP_VERSION_PATCH_GITHASH})
list(GET VERSION_LIST 0 HIP_VERSION_PATCH)
list(GET VERSION_LIST 1 HIP_VERSION_GITHASH)

if(DEFINED ENV{ROCM_LIBPATCH_VERSION})
  set(HIP_PACKAGING_VERSION_PATCH ${HIP_VERSION_PATCH}.$ENV{ROCM_LIBPATCH_VERSION})
else()
  set(HIP_PACKAGING_VERSION_PATCH ${HIP_VERSION_PATCH}-${HIP_VERSION_GITHASH})
endif()

find_package(Catch2 3.8.1 QUIET)
if(Catch2_FOUND)
  message(STATUS "Catch2 v3.8.1 found: ${Catch2_DIR}")
  # for packaging
  set(CATCH_ADD_TESTS_SCRIPT ${Catch2_DIR}/CatchAddTests.cmake)
  file(GLOB CATCH_LIBS "${Catch2_DIR}/../../*Catch2*")
else()
  # On Windows, ensure Catch2 uses the same MSVC runtime library and build configuration as our tests
  if(WIN32)
    set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static libraries" FORCE)
    set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>" CACHE STRING "MSVC runtime library" FORCE)
    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
      set(win_disable_warnings -fms-runtime-lib=static_dbg)
    endif()
  endif()
  # Setup catch2 manually
  include(FetchContent)
  FetchContent_Declare(
        Catch2
        DOWNLOAD_EXTRACT_TIMESTAMP OFF
        URL "https://rocm-third-party-deps.s3.us-east-2.amazonaws.com/Catch2-3.8.1.tar.gz"
        URL_HASH SHA256=18b3f70ac80fccc340d8c6ff0f339b2ae64944782f8d2fca2bd705cf47cadb79
    )
  FetchContent_MakeAvailable(Catch2)
  # for packaging
  set(CATCH_ADD_TESTS_SCRIPT ${Catch2_SOURCE_DIR}/extras/CatchAddTests.cmake)
  set(CATCH_LIBS $<TARGET_FILE:Catch2>)
endif()

include(cmake/hip-tests.cmake)
include(CTest)

set(CATCH_BUILD_DIR catch_tests)

set(CATCH_INCLUDE_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${CATCH_BUILD_DIR}/include)
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${CATCH_INCLUDE_BINARY_DIR})

include_directories(
    "./include"
    ${CATCH_INCLUDE_BINARY_DIR}
    "./kernels"
    "${CMAKE_CURRENT_LIST_DIR}/external/picojson"
)

if (RTC_TESTING)
  add_definitions(-DRTC_TESTING=ON)
endif()
add_definitions(-DKERNELS_PATH="${CMAKE_CURRENT_SOURCE_DIR}/kernels/")

if (WIN32)
  configure_file(catchProp_in_rc.in ${CMAKE_CURRENT_BINARY_DIR}/catchProp.rc @ONLY)

  # generates the .res files to be used by executables to populate the properties
  # expects LC folder with clang, llvm-rc to be present one level up of HIP
  execute_process(COMMAND ${LLVM_RC_EXEC} ${CMAKE_CURRENT_BINARY_DIR}/catchProp.rc
                  OUTPUT_VARIABLE RC_OUTPUT)
  set(PROP_RC ${CMAKE_CURRENT_BINARY_DIR})
  # When args to linker exceeds max chars.
  # msbuild writes args to a rsp file.
  # This is used to reference the obj file correctly
  SET(CMAKE_C_RESPONSE_FILE_LINK_FLAG "")
  SET(CMAKE_CXX_RESPONSE_FILE_LINK_FLAG "")
endif()

# Add debug and optimization flags for Windows builds
if(WIN32)
    if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:C,CXX>,$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>:-g>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:C,CXX>,$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>:-gcodeview>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:C,CXX>,$<CONFIG:Release>>:-O3>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:C,CXX>,$<CONFIG:RelWithDebInfo>>:-O2>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:C,CXX>,$<CONFIG:Debug>>:-O0>)
    endif()
    if(CMAKE_HIP_COMPILER_ID STREQUAL "Clang" OR HIP_PLATFORM STREQUAL "amd")
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:HIP>,$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>:-g>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:HIP>,$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>:-gcodeview>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:HIP>,$<CONFIG:Release>>:-O3>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:HIP>,$<CONFIG:RelWithDebInfo>>:-O2>)
        add_compile_options($<$<AND:$<COMPILE_LANGUAGE:HIP>,$<CONFIG:Debug>>:-O0>)
    endif()
    if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_HIP_COMPILER_ID STREQUAL "Clang" OR HIP_PLATFORM STREQUAL "amd")
        add_link_options($<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:-Xlinker>)
        add_link_options($<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:/DEBUG:FULL>)
    endif()
endif()

if(HIP_PLATFORM STREQUAL "amd")
  if(WIN32)
    set(win_disable_warnings ${win_disable_warnings} -Wno-ignored-attributes -Wno-microsoft-cast)
    # Windows for some reason does not honor the `CMAKE_CXX_STANDARD` we set initially in the file
    # Which results in errors about missing headers etc
    set(win_cxx_standard -std=c++17)
  endif()
  add_compile_options(${HIP_PATH_OPT} ${win_cxx_standard} -x hip -Wall -Wextra -Wvla -Wno-deprecated ${win_disable_warnings})
  if(HIP_TESTS_ENABLE_WERROR AND UNIX)
    add_compile_options(-Werror)
  endif()
endif()

# Turn off CMAKE_HIP_ARCHITECTURES Feature if cmake version is 3.21+
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.21.0)
  set(CMAKE_HIP_ARCHITECTURES OFF)
endif()
message(STATUS "CMAKE HIP ARCHITECTURES: ${CMAKE_HIP_ARCHITECTURES}")

# Note to pass arch use format like -DOFFLOAD_ARCH_STR="--offload-arch=gfx900  --offload-arch=gfx906"
# having space at the start/end of OFFLOAD_ARCH_STR can cause build failures
# Identify the GPU Targets.
# This is done due to limitation of rocm_agent_enumerator
# While building test parallelly, rocm_agent_enumerator can fail and give out an empty target
# That results in hipcc building the test for gfx803 (the default target)
# preference to pass arch -
# OFFLOAD_ARCH_STR
# rocm_agent_enumerator
if(ENABLE_SPIRV)
  set(OFFLOAD_ARCH_STR "--offload-arch=amdgcnspirv")
elseif(DEFINED OFFLOAD_ARCH_STR)
  string(REPLACE "--offload-arch=" "" HIP_GPU_ARCH_LIST ${OFFLOAD_ARCH_STR})
elseif(DEFINED GPU_TARGETS)
  foreach(_hip_gpu_arch ${GPU_TARGETS})
    set(OFFLOAD_ARCH_STR "--offload-arch=${_hip_gpu_arch} ${OFFLOAD_ARCH_STR}")
  endforeach()
elseif(NOT DEFINED OFFLOAD_ARCH_STR
   AND EXISTS "${ROCM_AGENT_ENUMERATOR_EXEC}"
   AND HIP_PLATFORM STREQUAL "amd" AND UNIX)
  execute_process(COMMAND "${ROCM_AGENT_ENUMERATOR_EXEC}"
         OUTPUT_VARIABLE HIP_GPU_ARCH
         RESULT_VARIABLE ROCM_AGENT_ENUM_RESULT
         OUTPUT_STRIP_TRAILING_WHITESPACE)
  # Trim out gfx000
  string(REPLACE "gfx000\n" "" HIP_GPU_ARCH ${HIP_GPU_ARCH})
  if (NOT HIP_GPU_ARCH STREQUAL "")
    string(REGEX REPLACE "\n" ";" HIP_GPU_ARCH_LIST "${HIP_GPU_ARCH}")
    list(REMOVE_DUPLICATES  HIP_GPU_ARCH_LIST)
    list(LENGTH HIP_GPU_ARCH_LIST HIP_GPU_ARCH_LEN)
    set(OFFLOAD_ARCH_STR "")
    foreach(_hip_gpu_arch ${HIP_GPU_ARCH_LIST})
      set(OFFLOAD_ARCH_STR "--offload-arch=${_hip_gpu_arch} ${OFFLOAD_ARCH_STR}")
    endforeach()
  else()
    message(STATUS "ROCm Agent Enumerator found no valid architectures")
  endif()
endif()

# Trim any trailing spaces, this trips up the cxx function call
string(STRIP "${OFFLOAD_ARCH_STR}" OFFLOAD_ARCH_STR)

message(STATUS "Using offload arch string: ${OFFLOAD_ARCH_STR}")
if(DEFINED OFFLOAD_ARCH_STR)
  set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} ${OFFLOAD_ARCH_STR}")
endif()

string(TOUPPER "${CMAKE_BUILD_TYPE}" _HIP_BUILD_TYPE_UPPER)
separate_arguments(HIP_DEVICE_BUILD_FLAGS_NO_ARCH NATIVE_COMMAND
  "${CMAKE_HIP_FLAGS} ${CMAKE_HIP_FLAGS_${_HIP_BUILD_TYPE_UPPER}}")
list(FILTER HIP_DEVICE_BUILD_FLAGS_NO_ARCH EXCLUDE REGEX "--offload-arch=")

# -fsanitize=address does not work with SPIR-V targets so keep a SPIR-V-specific
# copy of the device build flags.
set(HIP_DEVICE_BUILD_FLAGS_NO_ARCH_SPIRV ${HIP_DEVICE_BUILD_FLAGS_NO_ARCH})
if(ENABLE_ADDRESS_SANITIZER OR THEROCK_SANITIZER STREQUAL "ASAN" OR THEROCK_SANITIZER STREQUAL "HOST_ASAN")
  list(FILTER HIP_DEVICE_BUILD_FLAGS_NO_ARCH_SPIRV EXCLUDE REGEX "-fsanitize=address")
  list(FILTER HIP_DEVICE_BUILD_FLAGS_NO_ARCH_SPIRV EXCLUDE REGEX "-fno-omit-frame-pointer")
  list(FILTER HIP_DEVICE_BUILD_FLAGS_NO_ARCH_SPIRV EXCLUDE REGEX "-shared-libasan")
endif()

# ASan is not supported by SPIR-V, so ASan-related device flags must be dropped in those cases.
if(ENABLE_SPIRV)
  set(HIP_DEVICE_BUILD_FLAGS_NO_ARCH ${HIP_DEVICE_BUILD_FLAGS_NO_ARCH_SPIRV})
endif()

# get hip-tests commit short hash
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  RESULT_VARIABLE git_result
  OUTPUT_VARIABLE git_output
  OUTPUT_STRIP_TRAILING_WHITESPACE)
if(git_result EQUAL 0)
  set(HIP_TESTS_GITHASH ${git_output})
endif()

set(OFFLOAD_ARCH_LIST ${OFFLOAD_ARCH_STR})
separate_arguments(OFFLOAD_ARCH_LIST)
list(REMOVE_DUPLICATES  OFFLOAD_ARCH_LIST)
set(HIP_GPU_ARCH_LIST "")
foreach(__offload_arch ${OFFLOAD_ARCH_LIST})
    string(REPLACE "--offload-arch=" "" targetid ${__offload_arch})
    string(REPLACE ":" ";" target_id_list ${targetid})
    list(GET target_id_list 0 arch)
    string(APPEND HIP_GPU_ARCH_LIST "${arch};")
    list(REMOVE_DUPLICATES  HIP_GPU_ARCH_LIST)
endforeach()

# config generation
add_subdirectory(config)

# prints the catch info to a file
string(TIMESTAMP _timestamp UTC)
set(_autogen "# Auto-generated by cmake on ${_timestamp} UTC\n")
set(_catchInfo ${_autogen} "HIP_VERSION=${HIP_VERSION}\n")
set(_catchInfo ${_catchInfo} "HIP_PLATFORM=${HIP_PLATFORM}\n")
set(_catchInfo ${_catchInfo} "HIP_TESTS_GITHASH=${HIP_TESTS_GITHASH}\n")
set(_catchInfo ${_catchInfo} "ARCHS=${HIP_GPU_ARCH_LIST}\n")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${CATCH_BUILD_DIR}/catchInfo.txt ${_catchInfo})

# Let `ctest` run from catch_tests/. CMake only writes CTestTestfile.cmake and
# DartConfiguration.tcl to the build root, so provide both under catch_tests/:
# write a CTestTestfile.cmake that redirects to the parent, and copy
# DartConfiguration.tcl so ctest's configuration (default timeout, etc.) applies.
set(_subdirs ${_autogen} "subdirs(..)\n")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${CATCH_BUILD_DIR}/CTestTestfile.cmake ${_subdirs})

if(EXISTS "${CMAKE_BINARY_DIR}/DartConfiguration.tcl")
  configure_file(
    "${CMAKE_BINARY_DIR}/DartConfiguration.tcl"
    "${CMAKE_CURRENT_BINARY_DIR}/${CATCH_BUILD_DIR}/DartConfiguration.tcl"
    COPYONLY)
endif()

set(CATCH_SCRIPT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${CATCH_BUILD_DIR}/script)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/cmake/hip-tests.cmake
     DESTINATION ${CATCH_SCRIPT_BINARY_DIR})

# Patch CatchAddTests.cmake in-place so that catch_discover_tests_impl writes
# an empty _tests.cmake when zero tests match the filter, instead of returning
# early without creating the file.  The generated _include.cmake hardcodes the
# path to CatchAddTests.cmake from Catch2's source tree, so we must patch it
# there (not a copy) for the fix to take effect at ctest startup.
# Without this, catch_discover_tests("[disabled]") on a binary with no
# [disabled] tests leaves a dangling _include.cmake whose include() fails:
#   "include could not find requested file: ..._tests.cmake"
# This is a known Catch2 bug fixed upstream in:
#   https://github.com/catchorg/Catch2/pull/3075
# Remove this patch once we update to a Catch2 version that includes that fix.
set(_catch_old_snippet "  if(num_tests STREQUAL \"0\")\n    return()\n  endif()")
set(_catch_new_snippet "  if(num_tests STREQUAL \"0\")\n    file(WRITE \"\${_CTEST_FILE}\" \"# no tests discovered for this spec\\n\")\n    return()\n  endif()")
file(READ "${CATCH_ADD_TESTS_SCRIPT}" _catch_content)
string(FIND "${_catch_content}" "${_catch_new_snippet}" _catch_already_patched)
if(_catch_already_patched EQUAL -1)
  string(REPLACE "${_catch_old_snippet}" "${_catch_new_snippet}" _catch_patched "${_catch_content}")
  if("${_catch_patched}" STREQUAL "${_catch_content}")
    message(FATAL_ERROR
      "Failed to patch CatchAddTests.cmake: expected snippet not found.\n"
      "The Catch2 version may have changed. Check upstream fix:\n"
      "  https://github.com/catchorg/Catch2/pull/3075")
  endif()
  file(WRITE "${CATCH_ADD_TESTS_SCRIPT}" "${_catch_patched}")
endif()
unset(_catch_old_snippet)
unset(_catch_new_snippet)
unset(_catch_content)
unset(_catch_patched)
unset(_catch_already_patched)

# Patch CatchAddTests.cmake in-place so that tests tagged [exclude_amd_wsl] are
# registered as DISABLED when test discovery runs on WSL.  The Linux package is
# shared between native Linux and WSL, so the tag is baked into every binary at
# build time and can only be resolved on the machine that runs ctest.
# Detection requires both a WSL kernel signature and a WSL-specific runtime
# artifact, so a native kernel whose release string happens to contain
# "microsoft" is not mistaken for WSL.  It is cached in _hip_on_wsl so it runs
# once per binary rather than once per test.  list(FIND) is used instead of
# IN_LIST because CMP0057 is not set in the context ctest evaluates this in.
#
# parse_config.py emits [exclude_<reason>] for every disable reason regardless
# of the build platform, so an amd_wsl entry also lands in nvidia binaries.  The
# patch is therefore applied only for AMD builds, and only when the caller has
# not asked for normally-skipped tests to run via HIP_TESTS_RUN_DISABLED.  When
# either condition fails the patch is reversed instead, so reconfiguring an
# existing build directory with changed options leaves Catch2's script in the
# state those options imply.
set(_wsl_old_snippet [==[          LABELS "${tag_list}"
        )
      endif()
    endif(add_tags)]==])
set(_wsl_new_snippet [==[          LABELS "${tag_list}"
        )

        if(NOT DEFINED _hip_on_wsl)
          set(_hip_on_wsl FALSE)
          if(NOT WIN32 AND EXISTS "/proc/sys/kernel/osrelease")
            file(READ "/proc/sys/kernel/osrelease" _hip_osrelease)
            string(TOLOWER "${_hip_osrelease}" _hip_osrelease)
            # WSL2: "<ver>-microsoft-standard-WSL2", WSL1: "<ver>-<build>-Microsoft"
            if(_hip_osrelease MATCHES "-microsoft" OR _hip_osrelease MATCHES "wsl[0-9]*")
              if(EXISTS "/run/WSL"
                 OR EXISTS "/proc/sys/fs/binfmt_misc/WSLInterop"
                 OR EXISTS "/usr/lib/wsl/lib"
                 OR DEFINED ENV{WSL_DISTRO_NAME}
                 OR DEFINED ENV{WSL_INTEROP})
                set(_hip_on_wsl TRUE)
              endif()
            endif()
          endif()
        endif()

        if(_hip_on_wsl)
          list(FIND tag_list "exclude_amd_wsl" _wsl_tag_idx)
          if(NOT _wsl_tag_idx EQUAL -1)
            add_command(set_tests_properties
              "${prefix}${plain_name}${suffix}"
              PROPERTIES
              DISABLED TRUE
            )
          endif()
        endif()
      endif()
    endif(add_tags)]==])
if(HIP_PLATFORM STREQUAL "amd" AND NOT HIP_TESTS_RUN_DISABLED)
  set(_wsl_from "${_wsl_old_snippet}")
  set(_wsl_to "${_wsl_new_snippet}")
else()
  set(_wsl_from "${_wsl_new_snippet}")
  set(_wsl_to "${_wsl_old_snippet}")
endif()
file(READ "${CATCH_ADD_TESTS_SCRIPT}" _wsl_content)
string(FIND "${_wsl_content}" "${_wsl_to}" _wsl_already_patched)
if(_wsl_already_patched EQUAL -1)
  string(REPLACE "${_wsl_from}" "${_wsl_to}" _wsl_patched "${_wsl_content}")
  if("${_wsl_patched}" STREQUAL "${_wsl_content}")
    message(FATAL_ERROR
      "Failed to patch CatchAddTests.cmake for WSL: expected snippet not found.\n"
      "The Catch2 version may have changed.")
  endif()
  file(WRITE "${CATCH_ADD_TESTS_SCRIPT}" "${_wsl_patched}")
endif()
unset(_wsl_old_snippet)
unset(_wsl_new_snippet)
unset(_wsl_from)
unset(_wsl_to)
unset(_wsl_content)
unset(_wsl_patched)
unset(_wsl_already_patched)

file(COPY ${CATCH_ADD_TESTS_SCRIPT}
         DESTINATION ${CATCH_SCRIPT_BINARY_DIR})
set_property(GLOBAL APPEND PROPERTY G_INSTALL_SCRIPT_TARGETS ${CATCH_ADD_TESTS_SCRIPT})

# copy python script and headers to catch test package
file(COPY ./unit/compileAndCaptureOutput.py
            DESTINATION  ${CMAKE_CURRENT_BINARY_DIR}/${CATCH_BUILD_DIR}/unit )

file(COPY ./include/hip_test_common.hh DESTINATION ${CATCH_INCLUDE_BINARY_DIR})
file(COPY ./include/hip_test_context.hh DESTINATION ${CATCH_INCLUDE_BINARY_DIR})

# Enable device lambda on nvidia platforms
if(HIP_PLATFORM STREQUAL "nvidia")
  # SPIRV is not supported on it
  if(ENABLE_SPIRV)
    message(FATAL_ERROR "SPIRV is not supported on this platform")
  endif()

  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --extended-lambda")
  add_compile_options(-Xcompiler=-Wno-deprecated-declarations)
endif()

add_custom_target(build_tests ALL)
add_dependencies(build_tests hip_tests_config)

# Tests folder
# kernels and hipTestMain provide the KERNELS and Main_Object object libraries
# referenced by hip_add_exe_to_target. They are required by every test
# executable built through hip_add_exe_to_target (unit, contract, perf, and
# stress), so add them once when any of those test groups is enabled.
if(BUILD_UNIT_TESTS OR BUILD_CONTRACT_TESTS OR BUILD_PERF_TESTS OR BUILD_STRESS_TESTS)
  add_subdirectory(kernels ${CATCH_BUILD_DIR}/kernels)
  add_subdirectory(hipTestMain ${CATCH_BUILD_DIR}/hipTestMain)
endif()
if(BUILD_UNIT_TESTS)
  add_subdirectory(unit ${CATCH_BUILD_DIR}/unit)
  add_subdirectory(ABM ${CATCH_BUILD_DIR}/ABM)
  add_subdirectory(TypeQualifiers ${CATCH_BUILD_DIR}/TypeQualifiers)
  if(UNIX)
    add_subdirectory(multiproc ${CATCH_BUILD_DIR}/multiproc)
  endif()
endif()
if(BUILD_PERF_TESTS)
  add_subdirectory(performance ${CATCH_BUILD_DIR}/performance)
endif()
if(BUILD_STRESS_TESTS)
  add_subdirectory(stress ${CATCH_BUILD_DIR}/stress)
endif()
if(BUILD_CONTRACT_TESTS)
  add_subdirectory(contract ${CATCH_BUILD_DIR}/contract)
endif()

add_custom_target(gen_coverage
                  COMMAND ${CMAKE_COMMAND} -B build/
                  COMMAND ${CMAKE_COMMAND} --build build/
                  COMMAND ./build/generateHipAPICoverage ${HIP_PATH}/include
                  WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../utils/coverage
                  COMMENT "Generating Test Coverage Report")

# packaging the tests
add_subdirectory(packaging)

# Doxygen documentation
# check if doxygen is installed
find_package(Doxygen)
if(DOXYGEN_FOUND)
  # Configure Doxyfile with proper paths
  set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/DoxyfileTests)
  set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/DoxyfileTests.out)
  configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)

  add_custom_target(build_docs
        COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
        COMMENT "Generating test plan documentation with Doxygen")
else()
  message(STATUS "Doxygen was not found. Building test plan documentation will not be available")
endif()
