function(cartan_add_test TEST_NAME TEST_SOURCE)
    add_executable(${TEST_NAME} ${TEST_SOURCE})
    target_link_libraries(${TEST_NAME}
        PRIVATE
        cartan::cartan
        Catch2::Catch2WithMain
    )
    target_compile_options(${TEST_NAME} PRIVATE ${CARTAN_WARNING_FLAGS})
    if (CARTAN_HAS_WNO_C2Y_EXTENSIONS)
        target_compile_options(${TEST_NAME} PRIVATE -Wno-c2y-extensions)
    endif ()
    add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
endfunction()

cartan_add_test(version_test version_test.cpp)
cartan_add_test(expected_test expected_test.cpp)
cartan_add_test(types_test types_test.cpp)
cartan_add_test(hat_vee_test hat_vee_test.cpp)
cartan_add_test(so2_test so2_test.cpp)
cartan_add_test(se2_test se2_test.cpp)
cartan_add_test(so3_test so3_test.cpp)
cartan_add_test(se3_test se3_test.cpp)
cartan_add_test(se3_left_jacobian_test se3_left_jacobian_test.cpp)
cartan_add_test(se3_q_matrix_sweep_test se3_q_matrix_sweep_test.cpp)
cartan_add_test(analytical_gradient_test analytical_gradient_test.cpp)
cartan_add_test(quaternion_utils_test quaternion_utils_test.cpp)
cartan_add_test(twist_test twist_test.cpp)
cartan_add_test(rotation_test rotation_test.cpp)
cartan_add_test(transform_test transform_test.cpp)
cartan_add_test(framed_twist_test framed_twist_test.cpp)
cartan_add_test(framed_wrench_test framed_wrench_test.cpp)
cartan_add_test(screw_axis_test screw_axis_test.cpp)
cartan_add_test(kinematic_chain_test kinematic_chain_test.cpp)
cartan_add_test(forward_kinematics_test forward_kinematics_test.cpp)
cartan_add_test(jacobian_compile_test jacobian_compile_test.cpp)
cartan_add_test(jacobian_test jacobian_test.cpp)
cartan_add_test(velocity_test velocity_test.cpp)
cartan_add_test(ik_compile_test ik_compile_test.cpp)
cartan_add_test(dls_test dls_test.cpp)
cartan_add_test(lm_test lm_test.cpp)
cartan_add_test(projected_lm_test projected_lm_test.cpp)
cartan_add_test(projected_lm_bit_identity_test projected_lm_bit_identity_test.cpp)
target_compile_definitions(projected_lm_bit_identity_test
    PRIVATE CARTAN_PLM_GOLDEN_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
option(CARTAN_RUN_BITWISE_GOLDEN
    "Run the projected_lm bitwise golden comparison — an opt-in single-build refactor guard" OFF)
if (CARTAN_RUN_BITWISE_GOLDEN)
    target_compile_definitions(projected_lm_bit_identity_test PRIVATE CARTAN_RUN_BITWISE_GOLDEN)
endif ()
cartan_add_test(projected_lm_alloc_free_test projected_lm_alloc_free_test.cpp)
# The global operator new/delete counter and Eigen's process-global no-malloc
# flag are shared per process, so this test must not run concurrently with any
# other test that could perturb them.
set_tests_properties(projected_lm_alloc_free_test PROPERTIES RUN_SERIAL TRUE)
cartan_add_test(basic_ik_solver_compile_test basic_ik_solver_compile_test.cpp)
cartan_add_test(basic_ik_solver_test basic_ik_solver_test.cpp)
cartan_add_test(basic_ik_runner_test basic_ik_runner_test.cpp)
cartan_add_test(bobyqa_solve_policy_test bobyqa_solve_policy_test.cpp)
cartan_add_test(slsqp_solve_policy_test slsqp_solve_policy_test.cpp)
cartan_add_test(lbfgsb_test lbfgsb_test.cpp)
cartan_add_test(restart_wrapper_test restart_wrapper_test.cpp)
cartan_add_test(newton_raphson_solve_policy_test newton_raphson_solve_policy_test.cpp)
cartan_add_test(default_solvers_test default_solvers_test.cpp)
cartan_add_test(solver_semantics_test solver_semantics_test.cpp)

cartan_add_test(variadic_solver_test variadic_solver_test.cpp)
cartan_add_test(solver_factory_test solver_factory_test.cpp)
cartan_add_test(ik_integration_test ik_integration_test.cpp)
cartan_add_test(halton_seed_generator_test halton_seed_generator_test.cpp)
cartan_add_test(exhaustive_ik_runner_test exhaustive_ik_runner_test.cpp)
cartan_add_test(static_chain_test static_chain_test.cpp)
cartan_add_test(static_chain_ik_parity_test static_chain_ik_parity_test.cpp)
cartan_add_test(specialized_fk_jacobian_test specialized_fk_jacobian_test.cpp)

cartan_add_test(paden_kahan_test paden_kahan_test.cpp)
cartan_add_test(angle_unwrap_test angle_unwrap_test.cpp)
cartan_add_test(analytical_solver_2r_test analytical_solver_2r_test.cpp)
cartan_add_test(analytical_solver_3r_test analytical_solver_3r_test.cpp)
cartan_add_test(analytical_solver_6r_test analytical_solver_6r_test.cpp)
cartan_add_test(opw_forward_reconstruction_test opw_forward_reconstruction_test.cpp)
cartan_add_test(analytical_solver_opw_test analytical_solver_opw_test.cpp)
cartan_add_test(unwrapped_solver_test unwrapped_solver_test.cpp)
cartan_add_test(closest_to_seed_test closest_to_seed_test.cpp)
cartan_add_test(abb_irb120_reconciliation_test abb_irb120_reconciliation_test.cpp)

if(CARTAN_BUILD_ARGMIN)
    cartan_add_test(argmin_adapters_compile_test argmin_adapters_compile_test.cpp)
    cartan_add_test(argmin_policies_test argmin_policies_test.cpp)
    cartan_add_test(argmin_slsqp_restart_test argmin_slsqp_restart_test.cpp)
    cartan_add_test(argmin_projected_gn_test argmin_projected_gn_test.cpp)
    cartan_add_test(argmin_projected_gradient_gn_test argmin_projected_gradient_gn_test.cpp)
    cartan_add_test(solver_unit_count_test solver_unit_count_test.cpp)
    target_link_libraries(argmin_projected_gn_test PRIVATE argmin::argmin)
    target_compile_definitions(argmin_projected_gn_test PRIVATE CARTAN_BUILD_ARGMIN)
    target_link_libraries(argmin_projected_gradient_gn_test PRIVATE argmin::argmin)
    target_compile_definitions(argmin_projected_gradient_gn_test PRIVATE CARTAN_BUILD_ARGMIN)
    target_link_libraries(solver_unit_count_test PRIVATE argmin::argmin)
    target_compile_definitions(solver_unit_count_test PRIVATE CARTAN_BUILD_ARGMIN)
    target_link_libraries(exhaustive_ik_runner_test PRIVATE argmin::argmin)
    target_compile_definitions(exhaustive_ik_runner_test PRIVATE CARTAN_BUILD_ARGMIN)
    target_link_libraries(solver_semantics_test PRIVATE argmin::argmin)
    target_compile_definitions(solver_semantics_test PRIVATE CARTAN_BUILD_ARGMIN)
endif()

cartan_add_test(fk_sweep_test fk_sweep_test.cpp)
cartan_add_test(jacobian_sweep_test jacobian_sweep_test.cpp)
cartan_add_test(ik_sweep_test ik_sweep_test.cpp)
cartan_add_test(ik_float_tolerance_sweep_test ik_float_tolerance_sweep_test.cpp)

if(CARTAN_BUILD_NLOPT)
    target_link_libraries(bobyqa_solve_policy_test PRIVATE cartan::nlopt)
    target_link_libraries(slsqp_solve_policy_test PRIVATE cartan::nlopt)
endif()

# The cartan-vs-KDL FK comparison is an external-library comparison and lives in
# benchmarks/, not here: tests/ verify cartan using only cartan.

if (CARTAN_BUILD_URDF)
    cartan_add_test(urdf_parser_test urdf_parser_test.cpp)
    target_link_libraries(urdf_parser_test PRIVATE cartan::urdf)
    target_compile_definitions(urdf_parser_test PRIVATE
        CARTAN_TESTS_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures")

    cartan_add_test(urdf_chain_extractor_test urdf_chain_extractor_test.cpp)
    target_link_libraries(urdf_chain_extractor_test PRIVATE cartan::urdf)
    target_compile_definitions(urdf_chain_extractor_test PRIVATE
        CARTAN_TESTS_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures")

    cartan_add_test(urdf_parity_test urdf_parity_test.cpp)
    target_link_libraries(urdf_parity_test PRIVATE cartan::urdf)
    target_compile_definitions(urdf_parity_test PRIVATE
        CARTAN_TESTS_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures")
    if (CARTAN_BUILD_URDF_EXTENDED_TESTS)
        target_compile_definitions(urdf_parity_test PRIVATE CARTAN_URDF_EXTENDED_TESTS)
    endif ()

    cartan_add_test(solver_continuous_joint_test solver_continuous_joint_test.cpp)
    target_link_libraries(solver_continuous_joint_test PRIVATE cartan::urdf)
    target_compile_definitions(solver_continuous_joint_test PRIVATE
        CARTAN_TESTS_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures")
    if (CARTAN_BUILD_ARGMIN)
        target_link_libraries(solver_continuous_joint_test PRIVATE argmin::argmin)
        target_compile_definitions(solver_continuous_joint_test PRIVATE CARTAN_TEST_HAVE_ARGMIN)
    endif ()

    if (CARTAN_BUILD_ARGMIN)
        cartan_add_test(continuous_joint_fallback_sweep continuous_joint_fallback_sweep.cpp)
        target_link_libraries(continuous_joint_fallback_sweep PRIVATE cartan::urdf argmin::argmin)
        target_compile_definitions(continuous_joint_fallback_sweep PRIVATE
            CARTAN_TESTS_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures")
        # Exclude from the default ctest run; invoke directly via
        #   ctest -R continuous_joint_fallback_sweep
        # or run the binary at tests/unit/continuous_joint_fallback_sweep.
        set_tests_properties(continuous_joint_fallback_sweep
            PROPERTIES LABELS "manual" DISABLED TRUE)
    endif ()
endif ()
