find_program(GCOV_PATH gcov)
find_program(LCOV_PATH lcov)
find_program(GENHTML_PATH genhtml)
find_program(GCOVR_PATH gcovr)
find_program(POWERSHELL_PATH NAMES powershell.exe)
find_program(Python3 python3)
find_program(Simpo NAMES simpo PATHS ${DEPS_DIR}/simpo/bin NO_DEFAULT_PATH)

if (CMAKE_CROSSCOMPILING)
    set(GCOV_PATH "$ENV{GCOV_PATH}")
endif ()

macro(FIND_INCLUDE_DIR result curdir)
    file(GLOB_RECURSE children "${curdir}/*.hpp" "${curdir}/*.h")
    set(dirlist "")
    foreach (child ${children})
        string(REGEX REPLACE "(.*)/.*" "\\1" LIB_NAME ${child})
        if (IS_DIRECTORY ${LIB_NAME})
            list(FIND dirlist ${LIB_NAME} list_index)
            if (${list_index} LESS 0)
                LIST(APPEND dirlist ${LIB_NAME})
            endif ()
        endif ()
    endforeach ()
    set(${result} ${dirlist})
endmacro()

function(setup_coverage)
    if (NOT GCOV_PATH)
        message(FATAL_ERROR "gcov not found! Aborting...")
    else ()
        message(STATUS "gcov found at: ${GCOV_PATH}")
    endif ()

    set(COVERAGE_COMPILER_FLAGS "--coverage")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
    message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}")
    link_libraries(gcov)

    set(COVERAGE_DIR ${CMAKE_BINARY_DIR}/coverage)
    execute_process(COMMAND mkdir -p ${CMAKE_BINARY_DIR}/coverage
            WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    )

    if (LCOV_PATH)
        message(STATUS "lcov found in ${LCOV_PATH}.")
        set(HTML_PATH "${COVERAGE_DIR}/index.html")
        add_custom_target(coverage
                COMMAND bash ${CMAKE_SOURCE_DIR}/scripts/build/coverage.sh ${CMAKE_BINARY_DIR}
                WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
                VERBATIM
        )
        add_custom_command(TARGET coverage POST_BUILD
                COMMAND ${LCOV_PATH} -z -d ${CMAKE_BINARY_DIR} # 删除 *.gcda
                COMMENT "Removing intermediate coverage files"
        )
        add_custom_command(TARGET coverage POST_BUILD
                COMMAND ;
                COMMENT "Open ${HTML_PATH} in your browser to view the coverage report."
        )
        if (ENABLE_HTTP_SERVER)
            add_custom_command(TARGET coverage POST_BUILD
                    COMMAND bash ${CMAKE_SOURCE_DIR}/scripts/build/start_coverage_server.sh ${HTML_PATH}
                    COMMENT "The coverage report has been generated at ${HTML_PATH}."
                    WORKING_DIRECTORY /
                    VERBATIM
            )
        endif ()
    elseif (GCOVR_PATH)
        message(STATUS "gcovr found in ${GCOVR_PATH}.")
        set(HTML_PATH ${CMAKE_BINARY_DIR}/coverage/index.html)
        add_custom_target(coverage
                COMMAND ${GCOVR_PATH} -f src --gcov-exclude '.+log.+' --html-details ${HTML_PATH}
                WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
        )
        add_custom_command(TARGET coverage POST_BUILD
                COMMAND ;
                COMMENT "Open ${HTML_PATH} in your browser to view the coverage report."
        )
    else ()
        message(AUTHOR_WARNING "gcovr not found! replaced by gcov.")
        add_custom_target(coverage
                COMMAND rm -rf *.gcov
                COMMAND find ${CMAKE_BINARY_DIR}/src -name '*.gcda' -exec gcov -pb {} +
                WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/coverage
        )
    endif ()
endfunction()

macro(install_dep dep_name)
    add_custom_target(build_${dep_name}
            COMMAND mkdir -p ${CMAKE_SOURCE_DIR}/deps
            COMMAND rm -rf ${dep_name}/*
            COMMAND tar -xzf ${CMAKE_SOURCE_DIR}/.deps/${dep_name}_aarch64.tar.gz -C ${CMAKE_SOURCE_DIR}/deps
            COMMENT "Install dep ${dep_name} into /deps/${dep_name}."
            WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
    )
endmacro()

function(print_list list)
    foreach (item ${list})
        message(STATUS "${item}")
    endforeach ()
endfunction()

macro(install_lib lib_name)
    file(GLOB ${lib_name}_LIBS "${DEPS_DIR}/${lib_name}/lib/*.so*" "${DEPS_DIR}/${lib_name}/lib64/*.so*" "${DEPS_DIR}/${lib_name}/lib/*.ko")
    file(COPY ${${lib_name}_LIBS} DESTINATION ${CMAKE_BINARY_DIR}/lib/ FILE_PERMISSIONS OWNER_READ OWNER_EXECUTE)
endmacro()

macro(install_lib_cli lib_name)
    file(GLOB ${lib_name}_LIBS "${DEPS_DIR}/${lib_name}/lib/*.so*" "${DEPS_DIR}/${lib_name}/lib/*.ko")
    file(COPY ${${lib_name}_LIBS} DESTINATION ${CMAKE_BINARY_DIR}/cli/lib/ FILE_PERMISSIONS OWNER_READ OWNER_EXECUTE)
endmacro()

macro(install_lib_devel lib_name)
    file(GLOB ${lib_name}_LIBS "${DEPS_DIR}/${lib_name}/lib/*.so*" "${DEPS_DIR}/${lib_name}/lib/*.ko")
    file(COPY ${${lib_name}_LIBS} DESTINATION ${CMAKE_BINARY_DIR}/devel/lib/ FILE_PERMISSIONS OWNER_READ OWNER_EXECUTE)
endmacro()

# 模块序列化头文件生成
macro(build_message module)
    file(GLOB_RECURSE simpo_files *.simpo)
    if (NOT simpo_files)
        message(WARNING "[${module}] No simpo files have been provided!")
    else ()
        message(STATUS "[${module}] Found simpo files:")
        print_list("${simpo_files}")

        set(header_files "")
        foreach (simpo_file IN LISTS simpo_files)
            # 获取文件名(不带路径和扩展名)
            get_filename_component(base_name ${simpo_file} NAME_WE)

            # 生成目标头文件名
            list(APPEND header_files
                    "${CMAKE_BINARY_DIR}/include/${base_name}_builder.h"
                    "${CMAKE_BINARY_DIR}/include/${base_name}_reader.h"
                    "${CMAKE_BINARY_DIR}/include/${base_name}_verifier.h"
            )
        endforeach ()

        # 添加自定义命令来生成头文件
        add_custom_command(
                OUTPUT ${header_files}
                COMMAND mkdir -p ${CMAKE_BINARY_DIR}/include
                COMMAND ${Simpo} -o ${CMAKE_BINARY_DIR}/include --simpo --cpp ${simpo_files}
                COMMENT "[${module}] Generating interface based on simpo files"
                VERBATIM
                DEPENDS ${simpo_files}  # 依赖于 simpo 文件
                WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        )

        # 创建一个自定义目标来生成头文件
        add_custom_target(build_${module}_message ALL DEPENDS ${header_files})
    endif ()
endmacro()

# 添加模块 UT
macro(add_ut module)
    set(UT_BINARY ${CMAKE_PROJECT_NAME}_${module}_ut)
    file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false
            ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
            ${CMAKE_CURRENT_SOURCE_DIR}/*.h
    )
    add_executable(${UT_BINARY} EXCLUDE_FROM_ALL ${TEST_SOURCES} ${CMAKE_SOURCE_DIR}/test/UT/main.cpp)
    target_link_libraries(${UT_BINARY} PUBLIC
            GTest::gmock_main
            mockcpp
            ${module}
    )
    # 打破控制权限
    target_compile_options(${UT_BINARY} PRIVATE -fno-access-control ${DEBUG_FLAGS})
    set_target_properties(${UT_BINARY} PROPERTIES LINK_FLAGS "-Wl,--as-needed")
    set(RUN_TEST "${CMAKE_BINARY_DIR}/bin/${UT_BINARY} \
		--gtest_output=xml:${CMAKE_BINARY_DIR}/coverage/${module}_detail.xml")
    # 处理透传参数
    set(TRANS_PARAMS $ENV{TRANS_PARAMS})
    if (DEFINED TRANS_PARAMS AND NOT "${TRANS_PARAMS}" STREQUAL "")
        set(RUN_TEST "${RUN_TEST} ${TRANS_PARAMS}")
    endif ()
    if (SKIP_RUN_TESTS)
        set(RUN_TEST "echo 'Skip run test, only build binary ${CMAKE_BINARY_DIR}/bin/${UT_BINARY}'")
    endif ()
    add_custom_target(${module}_ut
            COMMAND bash -c "${RUN_TEST}" || true
            COMMENT "Run testing: ${RUN_TEST}"
    )
    add_dependencies(${module}_ut ${UT_BINARY})
    gtest_discover_tests(${UT_BINARY} PROPERTIES LABELS "${module}")
endmacro()

set(UBSE_IT_TEST_TIMEOUT_SECONDS "180" CACHE STRING "Default timeout in seconds for each IT CTest case")
set(UBSE_IT_DISCOVERY_TIMEOUT_SECONDS "30" CACHE STRING "Timeout in seconds for IT GTest discovery")

function(ubse_gtest_filter_to_ctest_regex output_var gtest_filter)
    set(regex "${gtest_filter}")
    string(FIND "${regex}" "-" negative_filter_pos)
    if (NOT negative_filter_pos EQUAL -1)
        string(SUBSTRING "${regex}" 0 ${negative_filter_pos} regex)
    endif ()
    if ("${regex}" STREQUAL "")
        set(regex "*")
    endif ()
    string(REPLACE "." "\\." regex "${regex}")
    string(REPLACE "*" ".*" regex "${regex}")
    string(REPLACE "?" "." regex "${regex}")
    string(REPLACE ":" "|" regex "${regex}")
    set(${output_var} "^(${regex})$" PARENT_SCOPE)
endfunction()

macro(add_it module)
    set(IT_BINARY ${CMAKE_PROJECT_NAME}_${module}_it)
    set(IT_LABEL it_${module})
    set(IT_XML_OUTPUT_DIR ${CMAKE_BINARY_DIR}/coverage/it)
    file(MAKE_DIRECTORY ${IT_XML_OUTPUT_DIR})
    file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false
            ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
            ${CMAKE_CURRENT_SOURCE_DIR}/*.h
    )
    add_executable(${IT_BINARY} EXCLUDE_FROM_ALL
            ${TEST_SOURCES}
            ${CMAKE_SOURCE_DIR}/test/IT/main.cpp
    )
    target_link_libraries(${IT_BINARY} PRIVATE
            GTest::gtest
            ubse_it_infra
    )
    target_compile_options(${IT_BINARY} PRIVATE ${DEBUG_FLAGS})
    add_dependencies(${IT_BINARY} ubse_it_runtime)

    set(IT_CTEST_ARGS
            --test-dir ${CMAKE_BINARY_DIR}/test
            -L "^${IT_LABEL}$"
            --output-on-failure
            --timeout ${UBSE_IT_TEST_TIMEOUT_SECONDS}
            --no-tests=error
    )
    set(TRANS_PARAMS "$ENV{TRANS_PARAMS}")
    if (DEFINED TRANS_PARAMS AND NOT "${TRANS_PARAMS}" STREQUAL "")
        separate_arguments(IT_TRANS_PARAMS UNIX_COMMAND "${TRANS_PARAMS}")
        foreach (IT_TRANS_PARAM IN LISTS IT_TRANS_PARAMS)
            if (IT_TRANS_PARAM MATCHES "^--gtest_filter=(.*)$")
                ubse_gtest_filter_to_ctest_regex(IT_CTEST_FILTER_REGEX "${CMAKE_MATCH_1}")
                list(APPEND IT_CTEST_ARGS -R "${IT_CTEST_FILTER_REGEX}")
            elseif (IT_TRANS_PARAM MATCHES "^--gtest_")
                message(WARNING "Ignoring unsupported GTest argument for CTest-based IT target: ${IT_TRANS_PARAM}")
            else ()
                message(WARNING "Ignoring unsupported IT target argument for CTest-based IT target: ${IT_TRANS_PARAM}")
            endif ()
        endforeach ()
    endif ()

    if (SKIP_RUN_TESTS)
        set(IT_RUN_COMMAND ${CMAKE_COMMAND} -E echo "Skip run IT test, only build binary ${CMAKE_BINARY_DIR}/bin/${IT_BINARY}")
    else ()
        set(IT_RUN_COMMAND ${CMAKE_CTEST_COMMAND} ${IT_CTEST_ARGS})
    endif ()
    add_custom_target(${module}_it
            COMMAND ${IT_RUN_COMMAND}
            COMMENT "Run IT testing via ctest label ${IT_LABEL}"
    )
    add_dependencies(${module}_it ${IT_BINARY})
    set_property(GLOBAL APPEND PROPERTY UBSE_IT_TARGETS ${module}_it)
    set_property(GLOBAL APPEND PROPERTY UBSE_IT_BINARIES ${IT_BINARY})

    # Register the entire IT binary as one ctest entry (not per TEST_F).
    # gtest_discover_tests would spawn a separate process per TEST_F, causing
    # SetUpTestSuite to start/stop the cluster for every single case.
    add_test(
            NAME ${IT_BINARY}
            COMMAND ${IT_BINARY} --gtest_output=xml:${IT_XML_OUTPUT_DIR}/${IT_BINARY}.xml
            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/test
    )
    set_tests_properties(${IT_BINARY}
            PROPERTIES
                    LABELS ${IT_LABEL}
                    RESOURCE_LOCK ubse_it
                    TIMEOUT ${UBSE_IT_TEST_TIMEOUT_SECONDS}
    )
endmacro()

macro(add_it_scene scene)
    cmake_parse_arguments(SCENE "" "" "LINK_LIBS" ${ARGN})

    set(IT_BINARY ${CMAKE_PROJECT_NAME}_${scene}_it)
    set(IT_LABEL it_scene_${scene})
    set(IT_XML_OUTPUT_DIR ${CMAKE_BINARY_DIR}/coverage/it)
    file(MAKE_DIRECTORY ${IT_XML_OUTPUT_DIR})

    # Collect all .cpp/.h in the scenario directory (scenario.h macro expands to SetUp/TearDown)
    file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false
            ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
            ${CMAKE_CURRENT_SOURCE_DIR}/*.h
    )

    add_executable(${IT_BINARY} EXCLUDE_FROM_ALL
            ${TEST_SOURCES}
            ${CMAKE_SOURCE_DIR}/test/IT/main.cpp
    )

    target_link_libraries(${IT_BINARY} PRIVATE
            GTest::gtest
            ubse_it_infra
            ${SCENE_LINK_LIBS}
    )

    target_include_directories(${IT_BINARY} PRIVATE
            ${CMAKE_SOURCE_DIR}/test/IT
    )

    target_compile_options(${IT_BINARY} PRIVATE ${DEBUG_FLAGS})
    add_dependencies(${IT_BINARY} ubse_it_runtime)

    # Generate ctest entry
    set(IT_CTEST_ARGS
            --test-dir ${CMAKE_BINARY_DIR}/test
            -L "^${IT_LABEL}$"
            --output-on-failure
            --timeout ${UBSE_IT_TEST_TIMEOUT_SECONDS}
            --no-tests=error
    )

    set(TRANS_PARAMS "$ENV{TRANS_PARAMS}")
    if (DEFINED TRANS_PARAMS AND NOT "${TRANS_PARAMS}" STREQUAL "")
        separate_arguments(IT_TRANS_PARAMS UNIX_COMMAND "${TRANS_PARAMS}")
        foreach (IT_TRANS_PARAM IN LISTS IT_TRANS_PARAMS)
            if (IT_TRANS_PARAM MATCHES "^--gtest_filter=(.*)$")
                ubse_gtest_filter_to_ctest_regex(IT_CTEST_FILTER_REGEX "${CMAKE_MATCH_1}")
                list(APPEND IT_CTEST_ARGS -R "${IT_CTEST_FILTER_REGEX}")
            elseif (IT_TRANS_PARAM MATCHES "^--gtest_")
                message(WARNING "Ignoring unsupported GTest argument: ${IT_TRANS_PARAM}")
            else ()
                message(WARNING "Ignoring unsupported argument: ${IT_TRANS_PARAM}")
            endif ()
        endforeach ()
    endif ()

    if (SKIP_RUN_TESTS)
        set(IT_RUN_COMMAND ${CMAKE_COMMAND} -E echo
            "Skip run IT test, only build binary ${CMAKE_BINARY_DIR}/bin/${IT_BINARY}")
    else ()
        set(IT_RUN_COMMAND ${CMAKE_CTEST_COMMAND} ${IT_CTEST_ARGS})
    endif ()

    add_custom_target(${scene}_it
            COMMAND ${IT_RUN_COMMAND}
            COMMENT "Run scene IT tests via ctest label ${IT_LABEL}"
    )
    add_dependencies(${scene}_it ${IT_BINARY})

    set_property(GLOBAL APPEND PROPERTY UBSE_IT_SCENE_TARGETS ${scene}_it)
    set_property(GLOBAL APPEND PROPERTY UBSE_IT_SCENE_BINARIES ${IT_BINARY})

    # Register the entire scene binary as one ctest entry (not per TEST_F).
    # gtest_discover_tests would spawn a separate process per TEST_F, causing
    # SetUpTestSuite to start/stop the cluster for every single case.
    # Running the whole binary in one process lets all cases share the cluster.
    add_test(
            NAME ${IT_BINARY}
            COMMAND ${IT_BINARY} --gtest_output=xml:${IT_XML_OUTPUT_DIR}/${IT_BINARY}.xml
            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/test
    )
    set_tests_properties(${IT_BINARY}
            PROPERTIES
                    LABELS ${IT_LABEL}
                    RESOURCE_LOCK ubse_it
                    TIMEOUT ${UBSE_IT_TEST_TIMEOUT_SECONDS}
    )
endmacro()

macro(add_independent_exec_ut executable_name)
    set(GTEST_BRIEF "--gtest_brief=0")
    set(GTEST_OUTPUT "--gtest_output=xml:${CMAKE_BINARY_DIR}/coverage/ut/${executable_name}_detail.xml")

    if (FORCE_COLORED_OUTPUT)
        set(GTEST_COLOR "--gtest_color=yes")
    else ()
        set(GTEST_COLOR "--gtest_color=auto")
    endif ()

    set(RUN_TEST "${CMAKE_BINARY_DIR}/bin/${executable_name} ${GTEST_COLOR} ${GTEST_BRIEF} ${GTEST_OUTPUT}")

    # 处理透传参数
    set(TRANS_PARAMS $ENV{TRANS_PARAMS})
    if (DEFINED TRANS_PARAMS AND NOT "${TRANS_PARAMS}" STREQUAL "")
        set(RUN_TEST "${RUN_TEST} ${TRANS_PARAMS}")
    endif ()

    if (SKIP_RUN_TESTS)
        set(RUN_TEST "echo 'Skip run test, only build binary ${CMAKE_BINARY_DIR}/bin/${executable_name}'")
    endif ()

    set(preface_target "${executable_name}_independent_ut")
    message(STATUS "new target ${preface_target}")
    add_custom_target(${preface_target}
            COMMAND bash -c "${RUN_TEST}"
            COMMENT "Run UT Tests: ${RUN_TEST}"
    )
    add_dependencies(${preface_target} ${executable_name})
    add_dependencies(ut ${preface_target})
endmacro()