cmake_minimum_required(VERSION 3.10)
project(akstdlib LANGUAGES C)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -ggdb -pg")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -g -ggdb -pg")

# Sanitizer build, off by default:
#   cmake -S . -B build-asan -DAKSL_SANITIZE=ON && ctest --test-dir build-asan
# Set before the dependency is added so libakerror is instrumented too --
# several of the defects in TODO.md section 2 (the uninitialised %s in
# aksl_realpath, the unbounded vsprintf in aksl_sprintf, the missing va_end in
# the printf family) only show up under ASan/UBSan.
option(AKSL_SANITIZE "Build the library and its tests with ASan + UBSan" OFF)
if(AKSL_SANITIZE)
  set(AKSL_SANITIZE_FLAGS "-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all")
  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${AKSL_SANITIZE_FLAGS}")
  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${AKSL_SANITIZE_FLAGS}")
  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${AKSL_SANITIZE_FLAGS}")
  message(STATUS "AKSL_SANITIZE=ON: building with ASan + UBSan")
endif()

if(TARGET akerror::akerror)
  message(STATUS "FOUND akerror::akerror")
else()
  message(STATUS "MISSING akerror::akerror")
endif()

include(CTest)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
  # libakerror registers its own CTest entries unconditionally, and because we
  # pull it in EXCLUDE_FROM_ALL its test binaries are never built -- so every
  # one of them used to show up in our suite as "Not Run" and fail. CMake has no
  # way to un-register a test, and set_tests_properties cannot reach across
  # directory scopes, so add_test() is shadowed for the duration of the
  # add_subdirectory() call. The dependency has its own CI; this project's suite
  # should only contain this project's tests.
  set(AKSL_SUPPRESS_ADD_TEST TRUE)
  function(add_test)
    if(NOT AKSL_SUPPRESS_ADD_TEST)
      _add_test(${ARGV})
    endif()
  endfunction()
  # libakerror also marks two of those tests WILL_FAIL, which would now be
  # setting properties on tests that no longer exist, so suppress that too.
  function(set_tests_properties)
    if(NOT AKSL_SUPPRESS_ADD_TEST)
      _set_tests_properties(${ARGV})
    endif()
  endfunction()

  add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)

  set(AKSL_SUPPRESS_ADD_TEST FALSE)
else()
  if(NOT TARGET akerror::akerror)
    find_package(PkgConfig REQUIRED)
    find_package(akerror REQUIRED)
  endif()
endif()

set(akstdlib_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akstdlib")
set(prefix      ${CMAKE_INSTALL_PREFIX})
set(exec_prefix "\${prefix}")
set(libdir      "\${exec_prefix}/lib")
set(includedir  "\${prefix}/include")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akstdlib.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc @ONLY)

add_library(akstdlib SHARED
  src/stdlib.c
)

add_library(akstdlib::akstdlib ALIAS akstdlib)

# Specify include directories for the library's headers (if applicable)
target_include_directories(akstdlib PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
)

target_link_libraries(akstdlib PUBLIC akerror::akerror)

set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
install(TARGETS akstdlib
  EXPORT akstdlibTargets
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

install(FILES "include/akstdlib.h" DESTINATION "include/")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc DESTINATION "lib/pkgconfig/")


install(EXPORT akstdlibTargets
    FILE akstdlibTargets.cmake
    NAMESPACE akstdlib::
    DESTINATION ${akstdlib_install_cmakedir}
)

configure_package_config_file(
    cmake/akstdlib.cmake.in
    "${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
    INSTALL_DESTINATION ${akstdlib_install_cmakedir}
)

install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
    DESTINATION ${akstdlib_install_cmakedir}
)

# Each test is one source file tests/test_<name>.c, built into the executable
# test_<name> and registered as the CTest test <name>. Shared helpers live in
# tests/aksl_capture.h.
#
#   AKSL_TESTS                must exit 0.
#   AKSL_WILL_FAIL_TESTS      expected to abort by design (an unhandled error
#                             reaching FINISH_NORETURN, a deliberate contract
#                             violation), so a non-zero exit is a pass.
#   AKSL_KNOWN_FAILING_TESTS  assert the *correct* behaviour of a confirmed
#                             defect from TODO.md section 2.1. They fail until
#                             the defect is fixed, and are marked WILL_FAIL so
#                             the suite stays green and the gap stays visible.
#                             When one is fixed CTest reports it as failed with
#                             "unexpectedly passed" -- that is the cue to move
#                             it up into AKSL_TESTS.
set(AKSL_TESTS
    linkedlist
    tree
)

set(AKSL_WILL_FAIL_TESTS
)

set(AKSL_KNOWN_FAILING_TESTS
    list_append_chain     # TODO.md 2.1.1 -- append truncates lists of 2+ nodes
    list_iterate_head     # TODO.md 2.1.2 -- iterate starts at the midpoint
    tree_iterate_break    # TODO.md 2.1.3 -- ITERATOR_BREAK does not stop a walk
)

foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
  add_executable(test_${_test} tests/test_${_test}.c)
  target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
  target_link_libraries(test_${_test} PRIVATE akstdlib)
  add_test(NAME ${_test} COMMAND test_${_test})
endforeach()

if(AKSL_WILL_FAIL_TESTS OR AKSL_KNOWN_FAILING_TESTS)
  set_tests_properties(
    ${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
    PROPERTIES WILL_FAIL TRUE
  )
endif()

# Mutation testing: break the library in small ways and confirm the test suite
# notices. This is a meta-check on the tests themselves, so it is a manual
# target (it rebuilds and re-runs the whole suite once per mutant), not a CTest
# test and not yet a CI gate -- the per-function tests in TODO.md sections
# 1.1-1.9 need to exist before a threshold would mean anything.
#   cmake --build build --target mutation
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
  add_custom_target(mutation
    COMMAND ${Python3_EXECUTABLE}
            ${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
            --source-root ${CMAKE_CURRENT_SOURCE_DIR}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    USES_TERMINAL
    COMMENT "Running mutation tests (breaks the library, expects tests to fail)"
  )
endif()


# pkgconfig
