cmake_minimum_required(VERSION 3.10)
# The single source of truth for the version, in the shape libakstdlib and
# libakgl both use. It flows into the library SOVERSION and nothing else spells
# a version number.
#
# 0.x on purpose: the language surface is not being promised yet. TODO.md §6
# records the defects inherited from the Go reference and what became of each;
# the ones still open are in the issue tracker.
project(akbasic VERSION 0.1.0 LANGUAGES C)

# Pre-1.0 the ABI may break on a minor bump, so the soname carries MAJOR.MINOR.
# At 1.0 this becomes ${PROJECT_VERSION_MAJOR} alone.
if(PROJECT_VERSION_MAJOR EQUAL 0)
  set(AKBASIC_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
else()
  set(AKBASIC_SOVERSION "${PROJECT_VERSION_MAJOR}")
endif()

include(CTest)
include(GNUInstallDirs)

option(AKBASIC_WITH_AKGL "Build the libakgl-backed text sink and link SDL3" OFF)
option(AKBASIC_BUILD_EXAMPLES "Build the embedding example in examples/"      ON)
option(AKBASIC_COVERAGE  "Instrument the build with gcov coverage counters"  OFF)
option(AKBASIC_SANITIZE  "Build with ASan + UBSan"                           OFF)

# ---------------------------------------------------------------------------
# Dependencies
#
# libakgl vendors its own copies of libakerror and libakstdlib and guards every
# dependency with if(NOT TARGET ...), so akerror::akerror and akstdlib::akstdlib
# must exist *before* add_subdirectory(deps/libakgl) or the targets are declared
# twice.
#
# All three dependencies register their CTest tests unconditionally. Pulled in
# EXCLUDE_FROM_ALL their test binaries are never built, so each one would land 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() and
# set_tests_properties() are shadowed for the duration of the add_subdirectory()
# calls. Note libakstdlib carries the same shadow but only arms it when *it* is
# top-level, so it does nothing for us -- this one has to wrap all three.
#
# set_property(TEST ...) has to be shadowed alongside them, and shadowed *only*
# in its TEST form. libakgl 0.9.0 moved eight of these calls off
# set_tests_properties because that command parses its arguments as name/value
# pairs and so splits a semicolon-separated list; set_property does not, and is
# the correct fix. But with add_test() suppressed the tests it names do not
# exist, and set_property errors out on an unknown test name where
# set_tests_properties was silent -- eight hard configure failures. Every other
# form (GLOBAL, DIRECTORY, TARGET, SOURCE, INSTALL, CACHE) has to pass straight
# through: the dependencies set target and directory properties that their own
# builds depend on.
#
# All three dependencies now namespace both their `coverage` and their
# `mutation` targets when embedded, so there is no custom-target collision left
# to work around. libakerror was the last holdout -- it namespaced `mutation`
# but not `coverage`, and a coverage build collided on the bare name and failed
# to configure at all. 2.0.2 applies the same CMAKE_SOURCE_DIR test to both
# (deps/libakerror/CMakeLists.txt:429-434), closing libakerror issue #15, and
# the add_custom_target() shadow that renamed it on the way past is gone with
# this comment.
#
# **Only one project in a tree may shadow add_test(), and this is that project.**
# CMake exposes an overridden command as `_name` and chains exactly one level: a
# second override rebinds `_add_test` to the first override and the builtin
# becomes unreachable to everyone, so our own registrations recurse until CMake
# stops at "Maximum recursion depth of 1000 exceeded". libakgl 0.3.0 briefly
# shadowed it unconditionally and this is exactly what happened; the top-level
# guard it used to have is back, and libakstdlib has always had one.
# ---------------------------------------------------------------------------
set(AKBASIC_SUPPRESS_ADD_TEST TRUE)

function(add_test)
  if(NOT AKBASIC_SUPPRESS_ADD_TEST)
    _add_test(${ARGV})
  endif()
endfunction()

function(set_tests_properties)
  if(NOT AKBASIC_SUPPRESS_ADD_TEST)
    _set_tests_properties(${ARGV})
  endif()
endfunction()

function(set_property _scope)
  if(NOT AKBASIC_SUPPRESS_ADD_TEST OR NOT _scope STREQUAL "TEST")
    _set_property(${ARGV})
  endif()
endfunction()

add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
if(AKBASIC_WITH_AKGL)
  # libakgl 0.3.0 adds its own vendored SDL, SDL_image, SDL_mixer, SDL_ttf and
  # jansson whether or not it is top-level, so there is nothing to do here but
  # add it. Before that it went down a find_package path when embedded and
  # required all five installed on the system, and this block declared them by
  # hand first -- filed as libakgl API-gap item 5 and resolved there.
  #
  # akerror::akerror and akstdlib::akstdlib still have to exist first: libakgl
  # guards every dependency with if(NOT TARGET ...), which is what stops its
  # vendored copies of those two being declared a second time.
  add_subdirectory(deps/libakgl EXCLUDE_FROM_ALL)
endif()

set(AKBASIC_SUPPRESS_ADD_TEST FALSE)

# ---------------------------------------------------------------------------
# Instrumentation, applied per target so it never leaks into an exported
# interface.
# ---------------------------------------------------------------------------
function(akbasic_instrument _target)
  if(AKBASIC_COVERAGE)
    target_compile_options(${_target} PRIVATE --coverage -O0 -g)
    target_link_options(${_target} PRIVATE --coverage)
  endif()
  if(AKBASIC_SANITIZE)
    target_compile_options(${_target} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
    target_link_options(${_target} PRIVATE -fsanitize=address,undefined)
  endif()
endfunction()

# ---------------------------------------------------------------------------
# The interpreter library. Everything here must be free of SDL, free of any
# process-terminating call, and buildable with no libakgl present.
# ---------------------------------------------------------------------------
set(AKBASIC_SOURCES
  src/args.c
  src/audio_tables.c
  src/data.c
  src/environment.c
  src/error.c
  src/grammar.c
  src/format.c
  src/graphics_tables.c
  src/parser.c
  src/parser_commands.c
  src/play.c
  src/runtime.c
  src/runtime_audio.c
  src/runtime_commands.c
  src/runtime_console.c
  src/runtime_disk.c
  src/runtime_format.c
  src/runtime_functions.c
  src/runtime_generator.c
  src/runtime_graphics.c
  src/runtime_housekeeping.c
  src/runtime_machine.c
  src/renumber.c
  src/runtime_input.c
  src/runtime_sprite.c
  src/runtime_struct.c
  src/runtime_structure.c
  src/runtime_trap.c
  src/runtime_ui.c
  src/scanner.c
  src/sink_stdio.c
  src/sink_tee.c
  src/sprite_tables.c
  src/host.c
  src/structtype.c
  src/symtab.c
  src/value.c
  src/variable.c
  src/verbs.c
)

add_library(akbasic ${AKBASIC_SOURCES})
add_library(akbasic::akbasic ALIAS akbasic)

target_include_directories(akbasic PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
)

target_compile_options(akbasic PRIVATE -Wall -Wextra)
target_link_libraries(akbasic PUBLIC akstdlib::akstdlib akerror::akerror)
target_link_libraries(akbasic PRIVATE m)

set_target_properties(akbasic PROPERTIES
  VERSION   ${PROJECT_VERSION}
  SOVERSION ${AKBASIC_SOVERSION}
)

akbasic_instrument(akbasic)

# The libakgl-backed text sink, kept in its own target so the core library and
# the whole golden suite build with no SDL present.
if(AKBASIC_WITH_AKGL)
  add_library(akbasic_akgl
    src/audio_akgl.c
    src/graphics_akgl.c
    src/input_akgl.c
    src/sink_akgl.c
    src/sprite_akgl.c
    src/ui_akgl.c
  )
  target_compile_options(akbasic_akgl PRIVATE -Wall -Wextra)
  target_link_libraries(akbasic_akgl PUBLIC akbasic akgl)
  akbasic_instrument(akbasic_akgl)

  # The SDL host, in its own target so the separation goal 3 rests on is visible
  # in the build graph and not only in a comment: akbasic is the interpreter,
  # akbasic_akgl is the adaptors that draw through somebody else's renderer, and
  # this is the one thing in the repository that creates a window. A game
  # embedding the interpreter links the first two and not this.
  add_library(akbasic_frontend src/frontend_akgl.c)
  target_compile_options(akbasic_frontend PRIVATE -Wall -Wextra)
  target_link_libraries(akbasic_frontend PUBLIC akbasic_akgl)
  akbasic_instrument(akbasic_frontend)

  # The documentation's figure generator: a second, much smaller host that draws
  # one program onto an offscreen target and writes a PNG. Not instrumented and
  # not a test -- it produces a build artifact rather than an answer, and folding
  # it into the coverage figure would credit the interpreter for lines only a
  # documentation build runs.
  add_executable(akbasic_screenshot tools/screenshot.c)
  target_compile_options(akbasic_screenshot PRIVATE -Wall -Wextra)
  target_link_libraries(akbasic_screenshot PRIVATE akbasic_akgl SDL3_image::SDL3_image)

  # Regenerating every figure in docs/ is a deliberate act, never part of a
  # build: the PNGs are checked in, and a rebuild that silently rewrote them
  # would put a binary diff in front of anybody who happened to run `make`.
  add_custom_target(docs_screenshots
    COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh"
            --root "${CMAKE_CURRENT_SOURCE_DIR}"
            --tool "$<TARGET_FILE:akbasic_screenshot>"
    DEPENDS akbasic_screenshot
    COMMENT "Regenerating the documentation figures in docs/images"
    VERBATIM)
endif()

# ---------------------------------------------------------------------------
# The standalone driver. This is the only place FINISH_NORETURN may appear.
# ---------------------------------------------------------------------------
add_executable(basic src/main.c)
target_compile_options(basic PRIVATE -Wall -Wextra)
target_link_libraries(basic PRIVATE akbasic)
if(AKBASIC_WITH_AKGL)
  target_link_libraries(basic PRIVATE akbasic_frontend)
  # The reference's own font, at the reference's own size. Its main.go opens
  # "./fonts/C64_Pro_Mono-STYLE.ttf", which only ever resolved from its own
  # source directory; compiled in, it resolves from anywhere, and AKBASIC_FONT
  # overrides it at runtime for an installed copy. Vendored into assets/fonts/
  # rather than reached for in the submodule -- see assets/fonts/PROVENANCE.md,
  # which also carries the licence question that came with it.
  target_compile_definitions(basic PRIVATE
    AKBASIC_HAVE_AKGL=1
    AKBASIC_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
endif()
akbasic_instrument(basic)

# The embedding example. It is built by default and registered as a test so the
# code README.md quotes cannot rot: a signature change breaks the build.
if(AKBASIC_BUILD_EXAMPLES)
  foreach(_example IN ITEMS embed hostvars hoststruct)
    add_executable(akbasic_example_${_example} examples/${_example}.c)
    target_compile_options(akbasic_example_${_example} PRIVATE -Wall -Wextra)
    target_link_libraries(akbasic_example_${_example} PRIVATE akbasic)
    akbasic_instrument(akbasic_example_${_example})
    _add_test(NAME example_${_example} COMMAND akbasic_example_${_example})
  endforeach()
endif()

# The galaga example: a C game on libakgl with the interpreter embedded as its
# enemy-behavior engine. Chapters 20 and 21 build it from an empty file, so it
# is compiled and run by every AKGL build rather than rotting in a document.
# The asset, script and font paths are baked in so the smoke test can launch
# from any working directory; --assets and --script override them at runtime.
if(AKBASIC_BUILD_EXAMPLES AND AKBASIC_WITH_AKGL)
  add_executable(akbasic_example_galaga
    examples/galaga/main.c
    examples/galaga/script.c
    examples/galaga/enemies.c
    examples/galaga/player.c)
  target_compile_options(akbasic_example_galaga PRIVATE -Wall -Wextra)
  target_compile_definitions(akbasic_example_galaga PRIVATE
    GALAGA_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/assets"
    GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas"
    GALAGA_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
  target_link_libraries(akbasic_example_galaga PRIVATE akbasic akgl
    SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image)
  akbasic_instrument(akbasic_example_galaga)
  # Ten seconds of scripted play under the headless drivers: the script boots,
  # a wave enters and forms, the autoplay pilot shoots at it, and the program
  # tears down and exits 0. A tutorial that stops working fails here rather
  # than in front of a reader.
  _add_test(NAME example_galaga COMMAND akbasic_example_galaga --frames 600 --autoplay)
  _set_tests_properties(example_galaga PROPERTIES TIMEOUT 120
    ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")

  # The boundary's round-trip test: links the real script.c and the real
  # galaga.bas, and fails the moment the two sides of the interop disagree.
  add_executable(akbasic_example_galaga_interop
    examples/galaga/interop_test.c
    examples/galaga/script.c)
  target_compile_options(akbasic_example_galaga_interop PRIVATE -Wall -Wextra)
  target_compile_definitions(akbasic_example_galaga_interop PRIVATE
    GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas")
  target_link_libraries(akbasic_example_galaga_interop PRIVATE akbasic akgl
    SDL3::SDL3 m)
  akbasic_instrument(akbasic_example_galaga_interop)
  _add_test(NAME example_galaga_interop COMMAND akbasic_example_galaga_interop)
  _set_tests_properties(example_galaga_interop PROPERTIES TIMEOUT 120)

  # Regenerating the game figures in docs/ is a deliberate act, never part of
  # a build, for the same reason docs_screenshots is: the PNGs are checked in.
  # Wall-clock dt makes each regeneration differ by a few pixels of starfield,
  # so expect a binary diff every time this runs; commit one only when the
  # content changed on purpose. (docs_galaga_figures, not docs_game_figures:
  # the libakgl submodule already owns that target name.)
  add_custom_target(docs_galaga_figures
    COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
            SDL_RENDER_DRIVER=software
            $<TARGET_FILE:akbasic_example_galaga> --frames 40
            --screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-title.png"
            --screenshot-frame 30
    COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
            SDL_RENDER_DRIVER=software
            $<TARGET_FILE:akbasic_example_galaga> --autoplay --frames 370
            --screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-wave.png"
            --screenshot-frame 360
    DEPENDS akbasic_example_galaga
    COMMENT "Regenerating the galaga figures in docs/images"
    VERBATIM)
endif()

# ---------------------------------------------------------------------------
# Tests.
#
# Three lists, and two of them invert the meaning of "Passed", exactly as
# libakstdlib does:
#   AKBASIC_TESTS                must exit 0
#   AKBASIC_WILL_FAIL_TESTS      abort by design
#   AKBASIC_KNOWN_FAILING_TESTS  assert the *correct* contract for a defect
#                                tracked as issues and are expected to fail.
#                                When one starts passing CTest reports
#                                "unexpectedly passed" -- that is the cue to move
#                                it into AKBASIC_TESTS along with the fix.
#
# Test executables are named akbasic_test_<name>, never test_<name>: a
# dependency's targets are created even under EXCLUDE_FROM_ALL, and libakstdlib
# already ships test_version. The CTest name stays bare, so `ctest -R value`
# still selects it.
# ---------------------------------------------------------------------------
set(AKBASIC_TESTS
  audio_verbs
  console_verbs
  devices
  disk_verbs
  environment_scope
  error_codes
  for_next
  format_verbs
  generators
  grammar_leaves
  graphics_verbs
  hoststruct
  hostvars
  housekeeping_verbs
  input_verbs
  interrupts
  machine_verbs
  numeric_contract
  parser_commands
  parser_expressions
  read_data
  renumber
  runtime_evaluate
  runtime_verbs
  scanner_tokens
  sink_stdio
  sink_tee
  sprite_verbs
  structure_verbs
  struct_pointers
  struct_types
  trap_verbs
  symtab
  ui_verbs
  unnumbered
  user_functions
  value_arithmetic
  value_bitwise
  value_compare
  value_pool
  variable_subscript
  verbs_table
  version_check
)

set(AKBASIC_WILL_FAIL_TESTS
)

# Empty, and worth keeping declared rather than deleted -- libakstdlib left its
# AKSL_KNOWN_FAILING_TESTS in place for the same reason when 0.2.0 cleared it.
# The next defect that has to be reproduced before it can be fixed goes here, and
# the machinery to do that should not have to be reinvented.
set(AKBASIC_KNOWN_FAILING_TESTS
  for_semantics
)

foreach(_test IN LISTS AKBASIC_TESTS AKBASIC_WILL_FAIL_TESTS AKBASIC_KNOWN_FAILING_TESTS)
  add_executable(akbasic_test_${_test} tests/${_test}.c)
  target_compile_options(akbasic_test_${_test} PRIVATE -Wall -Wextra)
  target_link_libraries(akbasic_test_${_test} PRIVATE akbasic)
  target_include_directories(akbasic_test_${_test} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
  akbasic_instrument(akbasic_test_${_test})
  _add_test(NAME ${_test} COMMAND akbasic_test_${_test})
endforeach()

# The akgl-backed suite. Its own list because it is the only test here that links
# SDL, and because it can only exist when AKBASIC_WITH_AKGL is on -- the whole
# point of the backend records is that everything else builds without it.
#
# It runs under the dummy video and audio drivers with a software renderer,
# following deps/libakgl/tests/draw.c: a 128x128 target read back with
# SDL_RenderReadPixels, so it needs no display and no offscreen harness.
if(AKBASIC_WITH_AKGL)
  add_executable(akbasic_test_akgl_backends tests/akgl_backends.c)
  target_compile_options(akbasic_test_akgl_backends PRIVATE -Wall -Wextra)
  target_link_libraries(akbasic_test_akgl_backends PRIVATE akbasic_akgl)
  target_include_directories(akbasic_test_akgl_backends PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
  # libakgl's monospaced fixture font, borrowed rather than copied: a second copy
  # of a 10 KB binary is a second thing to keep in step for no benefit.
  target_compile_definitions(akbasic_test_akgl_backends PRIVATE
    AKBASIC_TEST_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests/assets/akgl_test_mono.ttf")
  akbasic_instrument(akbasic_test_akgl_backends)
  _add_test(NAME akgl_backends COMMAND akbasic_test_akgl_backends)
  _set_tests_properties(akgl_backends PROPERTIES
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
    TIMEOUT 60
    ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy")

  # What the collision scan costs against what a frame costs. Labelled `perf` so
  # `ctest -LE perf` leaves it out of the ordinary run: it is the only test here
  # that deliberately spends wall-clock time, and the numbers are meaningless in
  # the Debug trees anyway -- see the file's own header.
  add_executable(akbasic_test_collision_perf tests/collision_perf.c)
  target_compile_options(akbasic_test_collision_perf PRIVATE -Wall -Wextra)
  target_link_libraries(akbasic_test_collision_perf PRIVATE akbasic_akgl)
  # libakgl's benchmark harness, borrowed by include path rather than copied, for
  # the same reason the fixture font is: a second copy is a fork. SYSTEM because
  # benchutil.h is a header of statics and -Wunused-function would fire on every
  # helper this suite happens not to call.
  target_include_directories(akbasic_test_collision_perf SYSTEM PRIVATE
    "${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests")
  target_compile_definitions(akbasic_test_collision_perf PRIVATE
    AKBASIC_TEST_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests/assets/akgl_test_mono.ttf")
  _add_test(NAME collision_perf COMMAND akbasic_test_collision_perf)
  _set_tests_properties(collision_perf PROPERTIES
    LABELS perf
    TIMEOUT 300
    ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software")

  # The frontend suite. Separate from the one above because it tests the *host*
  # rather than the adaptors -- it is the only test that links akbasic_frontend,
  # and the only one that opens a window and pumps events. It uses the
  # reference's own Commodore font, because proving the drawn text is in that
  # font is part of what TODO.md section 3 asks for.
  add_executable(akbasic_test_akgl_frontend tests/akgl_frontend.c)
  target_compile_options(akbasic_test_akgl_frontend PRIVATE -Wall -Wextra)
  target_link_libraries(akbasic_test_akgl_frontend PRIVATE akbasic_frontend)
  target_include_directories(akbasic_test_akgl_frontend PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
  target_compile_definitions(akbasic_test_akgl_frontend PRIVATE
    AKBASIC_TEST_C64_FONT="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
  akbasic_instrument(akbasic_test_akgl_frontend)
  _add_test(NAME akgl_frontend COMMAND akbasic_test_akgl_frontend)
  _set_tests_properties(akgl_frontend PROPERTIES
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
    TIMEOUT 60
    ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")

  # The keyboard test, and the only one in the repository that uses a real X
  # server rather than the dummy driver. Note the deliberate absence of an
  # ENVIRONMENT line: forcing the dummy driver here would defeat the whole point.
  #
  # Everything else synthesises SDL events, which tests the code *downstream* of
  # SDL and cannot test the code upstream of it. That gap shipped a bug -- the
  # frontend never called SDL_StartTextInput(), so SDL emitted no text-input
  # events, the line editor dropped every keystroke, and the suite stayed green
  # because it was pushing those events itself. This one drives xdotool at a
  # focused window, so X11, SDL's composition and the window manager are all in
  # the path.
  #
  # Needs xdotool and script(1), and it **steals keyboard focus** for a few
  # seconds. Skipped rather than failed when it cannot run -- headless, no
  # xdotool, no window manager -- because none of those means the answer is no.
  # AKBASIC_SKIP_INTERACTIVE=1 skips it while you are using the machine.
  find_program(XDOTOOL_EXECUTABLE xdotool)
  if(NOT XDOTOOL_EXECUTABLE)
    message(STATUS "xdotool not found: the akgl_typing test will skip. "
                   "Install it to cover the real keyboard path.")
  endif()
  _add_test(NAME akgl_typing
    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/akgl_typing.sh $<TARGET_FILE:basic>)
  _set_tests_properties(akgl_typing PROPERTIES
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    TIMEOUT 180
    SKIP_RETURN_CODE 77)

  # Every figure in docs/ re-rendered and byte-compared against the checked-in
  # copy. docs_examples only asks whether the file *exists*, which catches a
  # figure that was never generated and not one that stopped being of the code
  # beside it -- and that second failure is the one this whole arrangement is
  # against.
  #
  # **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
  # the language corpus makes about golden output: that the dummy video
  # driver and the software renderer are reproducible. They are, run to run and
  # build to build. What is untested is an SDL upgrade that shifts one pixel of
  # a diagonal, and the answer to that is to regenerate the figures in the same
  # commit as the bump -- not to weaken this to a size check, which would pass
  # for every wrong picture that happened to be 320x200.
  _add_test(NAME docs_screenshots
    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh
            --root "${CMAKE_CURRENT_SOURCE_DIR}"
            --tool $<TARGET_FILE:akbasic_screenshot>
            --check)
  _set_tests_properties(docs_screenshots PROPERTIES
    TIMEOUT 120
    ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
endif()

# ---------------------------------------------------------------------------
# The documentation's own examples.
#
# docs/ and README.md are full of programs, transcripts, C snippets and shell
# commands, and every one of them was checked by hand exactly once -- when it
# was written. Three were already wrong the day this was added, one of them a
# struct that had grown two members hours earlier. Documentation goes stale
# because the *code* moved, so this is an ordinary test case rather than
# something a docs-only CI job runs: it fails when the interpreter changes under
# the chapter, which is the case a path filter would miss.
#
# tests/docs_examples.sh reads the fence info strings; MAINTENANCE.md documents
# them.
#
# The C snippets compile against the real include path, which is transitive
# through akerror, akstdlib and -- in this configuration -- akgl. Writing it out
# for the script to read keeps that one source of truth: a hardcoded -I list
# here would rot exactly the way the documentation does.
set(AKBASIC_DOCS_CFLAGS_FILE "${CMAKE_CURRENT_BINARY_DIR}/docs_cflags.txt")
if(AKBASIC_WITH_AKGL)
  set(AKBASIC_DOCS_CFLAGS_TARGET akbasic_akgl)
else()
  set(AKBASIC_DOCS_CFLAGS_TARGET akbasic)
endif()
file(GENERATE
  OUTPUT  "${AKBASIC_DOCS_CFLAGS_FILE}"
  CONTENT "-I$<JOIN:$<TARGET_PROPERTY:${AKBASIC_DOCS_CFLAGS_TARGET},INCLUDE_DIRECTORIES>,\n-I>\n"
)

# Spelled out with if() rather than $<$<BOOL:...>:--akgl>, because a
# generator expression that evaluates to nothing still contributes an *empty
# argument*. The script read that empty string as the first filename, checked
# no documents at all, and passed -- caught only because it reports what it ran.
set(AKBASIC_DOCS_ARGS
  --root        "${CMAKE_CURRENT_SOURCE_DIR}"
  --basic       $<TARGET_FILE:basic>
  --cflags-file "${AKBASIC_DOCS_CFLAGS_FILE}")
if(AKBASIC_WITH_AKGL)
  list(APPEND AKBASIC_DOCS_ARGS --akgl)
endif()

_add_test(NAME docs_examples
  COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/docs_examples.sh ${AKBASIC_DOCS_ARGS})
_set_tests_properties(docs_examples PROPERTIES
  WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
  TIMEOUT 300)
if(AKBASIC_WITH_AKGL)
  # Same reason as the golden cases: an AKGL `basic` opens a window, and the
  # chapters on graphics, sound and sprites are most of what runs here.
  _set_tests_properties(docs_examples PROPERTIES
    ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
endif()

if(AKBASIC_TESTS OR AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
  _set_tests_properties(
    ${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
    PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
  )
endif()

if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
  _set_tests_properties(
    ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
    PROPERTIES WILL_FAIL TRUE
  )
endif()

# The editable language corpus. One CTest case per .bas so a failure names the
# file. Each program is paired with a sibling .txt expectation; see
# tests/language/README.md for the editing rule.
#
# The corpus includes programs carried over from the deprecated Go implementation
# and cases written for this interpreter. Their provenance is useful when
# investigating a regression, but it does not make any case immutable.
#
# Programs carried over from the deprecated Go implementation and cases written
# for this interpreter use the same editable `.bas`/`.txt` contract. Registered
# under local_ so every failure identifies the program that produced it.
#
# Note what this can and cannot cover. The graphics and sound verbs draw and play
# rather than print, so what a golden file sees of them is their refusals and
# whatever a program can PRINT about the state they changed. The behaviour that
# reaches a device is asserted against tests/mockdevice.h instead.
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
  RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests"
  "${CMAKE_CURRENT_SOURCE_DIR}/tests/language/*.bas"
)

#
# One family of local cases is driver-specific and has to be. A case named
# no_device.bas asserts that a verb needing a device refuses politely when it was
# given none, which is true of the stdio driver and deliberately false of the
# AKGL one -- attaching the three device backends is the whole point of
# AKBASIC_WITH_AKGL, and TODO.md section 8 item 1 asks for exactly that change in
# observable behaviour. Skipping them there is not a coverage hole: the refusal
# path is asserted directly against the backend records in tests/devices.c, which
# both configurations build.
#
set(AKBASIC_LOCAL_NAMES)
foreach(_case IN LISTS AKBASIC_LOCAL_CASES)
  if(AKBASIC_WITH_AKGL AND _case MATCHES "no_device\\.bas$")
    continue()
  endif()
  string(REGEX REPLACE "^language/" "" _name "${_case}")
  string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
  string(REPLACE "/" "_" _name "${_name}")
  _add_test(
    NAME local_${_name}
    COMMAND ${CMAKE_COMMAND}
            -DBASIC=$<TARGET_FILE:basic>
            -DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/${_case}
            -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
  )
  list(APPEND AKBASIC_LOCAL_NAMES local_${_name})
endforeach()

if(AKBASIC_LOCAL_NAMES)
  _set_tests_properties(${AKBASIC_LOCAL_NAMES} PROPERTIES TIMEOUT 30)
  if(AKBASIC_WITH_AKGL)
    _set_tests_properties(${AKBASIC_LOCAL_NAMES} PROPERTIES
      ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
  endif()
endif()

# ---------------------------------------------------------------------------
# Mutation testing.
#
# Coverage says which lines ran; mutation testing says whether anything would
# have noticed if they were wrong. That distinction matters more here than in an
# ordinary C library: the akerror control-flow macros expand at their call sites,
# so gcov attributes ATTEMPT/CATCH/PASS to the caller and cannot really see them.
# Mutation testing is the only thing that checks them at all.
#
# This target runs the whole akbasic-owned src/ tree and is slow -- upwards of an
# hour. CI runs a narrower, faster set with a --threshold gate; see
# .gitea/workflows/ci.yaml.
#
# Namespaced when embedded in another project, for the same reason the coverage
# targets in the dependencies are: a sibling may well ship a `mutation` target.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
  if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
    set(AKBASIC_MUTATION_TARGET mutation)
  else()
    set(AKBASIC_MUTATION_TARGET akbasic_mutation)
  endif()
  add_custom_target(${AKBASIC_MUTATION_TARGET}
    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()

install(TARGETS akbasic basic
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/akbasic DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
