#
# LDASTools frameCPP - A library implementing the LIGO/Virgo frame specification
#
# Copyright (C) 2018 California Institute of Technology
#
# LDASTools frameCPP is free software; you may redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 (GPLv2) of the
# License or at your discretion, any later version.
#
# LDASTools frameCPP is distributed in the hope that it will be useful, but
# without any warranty or even the implied warranty of merchantability
# or fitness for a particular purpose. See the GNU General Public
# License (GPLv2) for more details.
#
# Neither the names of the California Institute of Technology (Caltech),
# The Massachusetts Institute of Technology (M.I.T), The Laser
# Interferometer Gravitational-Wave Observatory (LIGO), nor the names
# of its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# You should have received a copy of the licensing terms for this
# software included in the file LICENSE located in the top-level
# directory of this package. If you did not, you can view a copy at
# http://dcc.ligo.org/M1500244/LICENSE
#

#========================================================================
# List of the frame versions
set(FRAME_VERSIONS 3 4 5 6 7 8 9)

##
# @brief Creates an executable target for frameCPP testing
#
# This function creates an executable target with the specified sources and dependencies.
# It handles cross-compilation scenarios and applies standard build configurations.
#
# @param target The name of the executable target to create
# @param INCLUDES (optional) Additional include directories. Defaults to AM_CPPFLAGS if not specified
# @param LDADD (optional) Additional libraries to link. Defaults to LDADD if not specified
# @param SOURCES List of source files for the executable
#
# @note The function returns early if BUILD_TESTING is disabled or if cross-compiling
#       without an emulator
#
function( framecpp_target_executable target )
  if ( NOT BUILD_TESTING OR ( CMAKE_CROSSCOMPILING AND NOT CMAKE_CROSSCOMPILING_EMULATOR ) )
    return( )
  endif( )

  set(options
    NOINST
    )
  set(oneValueArgs
    )
  set(multiValueArgs
    INCLUDES
    LDADD
    SOURCES
    )

  cmake_parse_arguments( ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )

  if ( NOT ARG_INCLUDES)
    set( ARG_INCLUDES ${AM_CPPFLAGS} )
  endif( )
  if ( NOT ARG_LDADD )
    set( ARG_LDADD ${LDADD} )
  endif( )

  # Set NOINST flag if requested
  if ( ARG_NOINST )
    set( NOINST_FLAG NOINST )
  else( )
    set( NOINST_FLAG )
  endif( )

  cx_target_executable( ${target}
    ${NOINST_FLAG}
    SOURCES ${ARG_SOURCES}
    LDADD ${ARG_LDADD}
    INCLUDE_DIRECTORIES BEFORE PRIVATE
      ${ARG_INCLUDES}
  )
endfunction( )

##
# @brief Creates a test executable and registers it as a CTest test
#
# This function creates a test executable and automatically registers it with CTest.
# The executable is marked as NOINST (not installed) and is made dependent on the
# framecpp target.
#
# @param target The name of the test executable to create
# @param SOURCES List of source files for the test executable
# @param LDADD (optional) Additional libraries to link. Defaults to LDADD if not specified
# @param COMMAND_ARGS (optional) Additional command line arguments to pass when running the test
# @param TIMEOUT (optional) Per-test timeout in seconds. Defaults to 180.
# @param REDIRECT_OUTPUT (optional flag) When set, captures the test's stdout/stderr to
#        ${target}.output. On success the file is removed (CI log stays small); on failure
#        the file is preserved and its last 200 lines are echoed to stderr so a reviewer
#        has diagnostic context without scrolling through verbose Boost.Test output. Pass/
#        fail status is always reported by ctest via the wrapper's exit code.
#
# @note The test is only registered with CTest if BUILD_TESTING is enabled
# @note The test environment is set to TESTS_ENVIRONMENT
#
function( framecpp_target_test target )
  set(options
    REDIRECT_OUTPUT
    )
  set(oneValueArgs
    TIMEOUT
    )
  set(multiValueArgs
    SOURCES
    LDADD
    COMMAND_ARGS
    )

  cmake_parse_arguments( ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )

  if ( NOT ARG_LDADD )
    set( ARG_LDADD ${LDADD} )
  endif( )
  if ( NOT ARG_INCLUDES )
    set( ARG_INCLUDES ${AM_CPPFLAGS} )
  endif( )

  cx_target_executable(
    ${target}
    NOINST
    SOURCES ${ARG_SOURCES}
    LDADD ${ARG_LDADD}
    INCLUDE_DIRECTORIES BEFORE PRIVATE
      ${ARG_INCLUDES}
      )
  add_dependencies( ${target} framecpp )
  if ( BUILD_TESTING )
    # Default resource limits for all tests
    set(MEMORY_LIMIT_MB 12288)  # 12GB (required for ASAN on ARM64)
    # Use provided timeout or default to 180 seconds
    if ( ARG_TIMEOUT )
      set(TIMEOUT_SEC ${ARG_TIMEOUT})
    else( )
      set(TIMEOUT_SEC 180)        # 3 minutes (180 seconds)
    endif( )

    # Find Python3 for test wrapper (required)
    find_package(Python3 COMPONENTS Interpreter REQUIRED)

    if ( ARG_REDIRECT_OUTPUT )
      # Wrap output: silent on success, tail-on-failure for CI diagnostics.
      # ctest still receives the wrapper's exit code so pass/fail status is reported.
      # COMMAND_ARGS are interpolated unquoted; values containing whitespace will split.
      cx_target_test( ${target}
        COMMAND sh -c "out=${target}.output; ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_with_memory_limit.py ${MEMORY_LIMIT_MB} ${TIMEOUT_SEC} $<TARGET_FILE:${target}> ${ARG_COMMAND_ARGS} > \"$out\" 2>&1; rc=$?; if [ $rc -eq 0 ]; then rm -f \"$out\"; else echo \"==== last 200 lines of $out (exit=$rc) ====\" >&2; tail -n 200 \"$out\" >&2; fi; exit $rc"
        ENVIRONMENT ${TESTS_ENVIRONMENT}
      )
    else( )
      # Wrap test execution with memory/timeout limiter
      cx_target_test( ${target}
        COMMAND
          ${Python3_EXECUTABLE}
          ${CMAKE_CURRENT_SOURCE_DIR}/run_with_memory_limit.py
          ${MEMORY_LIMIT_MB}
          ${TIMEOUT_SEC}
          $<TARGET_FILE:${target}> ${ARG_COMMAND_ARGS}
        ENVIRONMENT ${TESTS_ENVIRONMENT}
      )
    endif( )
  endif( )
endfunction( )

##
# @brief Creates tests for frame format downconversion
#
# This function sets up tests to verify frame format downconversion functionality.
# It creates a sample frame file using framecpp_sample with specified parameters
# and then verifies the downconverted frame using framecpp_verify.
#
# @param version The frame format version to test downconversion for
#
# @details The function creates two tests:
# - setup_downconvert_framecpp_sample_${version}: Creates a sample frame file with
#   various data types and structures (detector LIGO_India, with events, messages, etc.)
# - test_downconvert_framecpp_sample_${version}: Verifies the created frame file
#   using framecpp_verify with verbose output
#
# The tests are linked using CTest fixtures to ensure proper execution order.
#
# @note Tests are only created if BUILD_TESTING is enabled
# @note Frame files are created with GPS time 600000000 and duration 1 second
#
function( framecpp_target_test_downconvert version )
  set(options
    )
  set(oneValueArgs
    )
  set(multiValueArgs
    )

  cmake_parse_arguments( ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )

  set(_frame_description "framecpp_sample_downconvert_ver_${version}")
  set(_frame_start_time 600000000)
  set(_frame_duration 1)
  set(_frame_filename "Z-${_frame_description}-${_frame_start_time}-${_frame_duration}.gwf")
  set(_frame_filename_full "${CMAKE_CURRENT_BINARY_DIR}/${_frame_filename}")

  if ( BUILD_TESTING )
    #--------------------------------------------------------------------
    # Setup test
    #--------------------------------------------------------------------
    add_test(
      NAME "setup_downconvert_framecpp_sample_${version}"
      COMMAND ${CMAKE_COMMAND} -E env
      $<TARGET_FILE:framecpp_sample>
        --enable-detector
        --with-detector-name LIGO_India
        --description ${_frame_description}
        --version ${version}
        --channel-proc-type int_2u
        --channel-ser-data-type real_8
        --channel-sim-data-type int_4s
        --enable-history
        --with-history-msg "Downconverted ${_frame_description}"
        --enable-fr-event
        --enable-fr-msg
        --enable-fr-sim-event
        --enable-fr-summary
        --enable-fr-table
    )
    #--------------------------------------------------------------------
    # Testing of down conversion
    #--------------------------------------------------------------------
    add_test(
      NAME "test_downconvert_framecpp_sample_${version}"
      COMMAND ${CMAKE_COMMAND} -E env
      $<TARGET_FILE:framecpp_verify> --verbose 2 --force
      ${CMAKE_CURRENT_BINARY_DIR}/${_frame_filename}
    )

    #--------------------------------------------------------------------
    # Establish test dependencies using fixtures
    #--------------------------------------------------------------------
    set_tests_properties( "setup_downconvert_framecpp_sample_${version}"
      PROPERTIES
      FIXTURES_SETUP "fixture_downconvert_framecpp_sample_${version}"
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )
    set_tests_properties( "test_downconvert_framecpp_sample_${version}"
      PROPERTIES
      FIXTURES_REQUIRES "fixture_downconvert_framecpp_sample_${version}"
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )
  endif( )
endfunction( )

##
# @brief Validates a frame file with FrCheck (conditional on FrCheck availability)
#
# This function creates a test that validates a frame file using the FrCheck utility,
# but only if FrCheck is available in the system. This ensures proper conditional
# testing based on FrameL library availability.
#
# @param VERSION (required) Frame specification version number
# @param FRAME_FILE (required) Path to the frame file to validate
# @param TEST_NAME (optional) Custom test name. Defaults to "test_frcheck_validate_v${VERSION}"
#
# @details The function only registers the test if FR_CHECK variable is set (FrCheck found).
# The test runs: FrCheck -i ${FRAME_FILE}
# and expects exit code 0 for success.
#
# @note Tests are only created if BUILD_TESTING is enabled and FR_CHECK is found
#
function( framecpp_target_test_frcheck_validate )
  set(options
    )
  set(oneValueArgs
    VERSION
    FRAME_FILE
    TEST_NAME
    )
  set(multiValueArgs
    )

  cmake_parse_arguments( ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )

  # VERSION and FRAME_FILE are required
  if ( NOT ARG_VERSION )
    message( FATAL_ERROR "VERSION parameter is required for framecpp_target_test_frcheck_validate" )
  endif( )
  if ( NOT ARG_FRAME_FILE )
    message( FATAL_ERROR "FRAME_FILE parameter is required for framecpp_target_test_frcheck_validate" )
  endif( )

  # Set default test name
  if ( NOT ARG_TEST_NAME )
    set( ARG_TEST_NAME "test_frcheck_validate_v${ARG_VERSION}" )
  endif( )

  # Only create test if FrCheck is available
  if ( BUILD_TESTING AND FR_CHECK )
    add_test(
      NAME "${ARG_TEST_NAME}"
      COMMAND ${FR_CHECK} -i ${ARG_FRAME_FILE}
    )

    # Set test properties to capture FRCHECK_NFRAME environment variable
    set_tests_properties( "${ARG_TEST_NAME}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    cm_msg_notice( "FrCheck validation test created: ${ARG_TEST_NAME}" )
  else( )
    if ( BUILD_TESTING )
      cm_msg_notice( "FrCheck not available - skipping validation test: ${ARG_TEST_NAME}" )
    endif( )
  endif( )
endfunction( )

##
# @brief Tests the integrity of a frame file
#
# This function creates tests to verify the integrity of a frame file by running
# two validation tools: framecpp_dump_objects and framecpp_verify.
#
# @param SOURCE (optional) Source identifier for the frame file. Defaults to "Z"
# @param DESCRIPTION (required) Description identifier for the frame file
# @param GPS_START_SEC (optional) GPS start time in seconds. Defaults to "600000000"
# @param TIME_INTERVAL (optional) Time interval/duration in seconds. Defaults to "1"
#
# @details The function generates a frame filename using the pattern:
# ${SOURCE}-${DESCRIPTION}-${GPS_START_SEC}-${TIME_INTERVAL}.gwf
#
# Two tests are created:
# - test_file_integrity_dump_objects_${DESCRIPTION}: Runs framecpp_dump_objects --silent-data
# - test_file_integrity_verify_${DESCRIPTION}: Runs framecpp_verify --verbose 2
#
# Both commands are expected to exit with code 0 to pass the test.
#
# @note Tests are only created if BUILD_TESTING is enabled
# @note DESCRIPTION is the only required parameter; it will cause a fatal error if not provided
#
function( framecpp_target_test_file_integrity )
  set(options
    )
  set(oneValueArgs
    SOURCE
    DESCRIPTION
    GPS_START_SEC
    TIME_INTERVAL
    )
  set(multiValueArgs
    )

  cmake_parse_arguments( ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )

  # Set defaults for optional parameters
  if ( NOT ARG_SOURCE )
    set( ARG_SOURCE "Z" )
  endif( )
  if ( NOT ARG_GPS_START_SEC )
    set( ARG_GPS_START_SEC "600000000" )
  endif( )
  if ( NOT ARG_TIME_INTERVAL )
    set( ARG_TIME_INTERVAL "1" )
  endif( )

  # DESCRIPTION is required
  if ( NOT ARG_DESCRIPTION )
    message( FATAL_ERROR "DESCRIPTION parameter is required for framecpp_target_test_file_integrity" )
  endif( )

  # Generate filename
  set( _filename "${ARG_SOURCE}-${ARG_DESCRIPTION}-${ARG_GPS_START_SEC}-${ARG_TIME_INTERVAL}.gwf" )

  if ( BUILD_TESTING )
    # Test 1: framecpp_dump_objects --silent-data
    # Output redirected to file to keep build logs manageable.
    # On success: file removed (test reports only exit status).
    # On failure: file kept and tail printed to stderr for CI visibility.
    set( _out "test_file_integrity_dump_objects_${ARG_DESCRIPTION}.output" )
    add_test(
      NAME "test_file_integrity_dump_objects_${ARG_DESCRIPTION}"
      COMMAND sh -c "out='${_out}'; $<TARGET_FILE:framecpp_dump_objects> --silent-data ${_filename} > \"$out\" 2>&1; rc=$?; if [ $rc -eq 0 ]; then rm -f \"$out\"; else echo \"==== last 200 lines of $out (exit=$rc) ====\" >&2; tail -n 200 \"$out\" >&2; fi; exit $rc"
    )

    # Test 2: framecpp_verify --verbose 2
    #add_test(
    #  NAME "test_file_integrity_verify_${ARG_DESCRIPTION}"
    #  COMMAND $<TARGET_FILE:framecpp_verify> --verbose 2 ${_filename}
    #)
  endif( )
endfunction( )

#------------------------------------------------------------------------
# Tools to cross check against FrameL implementation
#------------------------------------------------------------------------
cm_check_progs(FR_CHECK FrCheck)
cm_pkg_check_modules(
  PREFIX FRAMEL
  MODULES framel
  )

cm_msg_notice( "FRAMEL_VERSION: ${FRAMEL_VERSION}" )

set( LDAS_DIRECTORY_BUILD_LIB_FRAMECPP ${CMAKE_BINARY_DIR} )

set( AM_CFLAGS -std=c99 )

set( AM_CPPFLAGS
  ${CMAKE_SOURCE_DIR}/src/Utilities
	${CMAKE_BINARY_DIR}/include
	${CMAKE_BINARY_DIR}
  ${LDASTOOLS_INCLUDE_DIRS}
	# $(BOOST_CPPFLAGS)
  )

#COMPRESSION_FRAME_PREFIX = /scratch/frames/compression
#
set( LDADD
  framecpp Threads::Threads
  # Note: Boost::filesystem and Boost::system are now PUBLIC in libframecpp
  # Note: Boost::serialization and Boost::log are PUBLIC in libframecppcmn
  )

set( EXTRA_DIST
	test.hh
	.ccmalloc
  )

set( TESTS_ENVIRONMENT
  BOOST_TEST_LOG_LEVEL=all
  COMPRESSION_FRAME_PREFIX=${COMPRESSION_FRAME_PREFIX}
  BIN_PROGRAMS=${bin_PROGRAMS}
  TEST_DIR=${CMAKE_BINARY_DIR}/src/Utilities
  )

# Add ASAN suppressions to test environment if AddressSanitizer is enabled
# Suppressions handle ODR violations (piecewise_construct, boost::serialization::singleton)
# These work reliably for test executables
if(CX_SCHEME_SANITIZERS_ACTIVE MATCHES "address")
  if(EXISTS ${CMAKE_SOURCE_DIR}/asan_suppressions.txt)
    list(APPEND TESTS_ENVIRONMENT "ASAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/asan_suppressions.txt")
    message(STATUS "ASAN suppressions will be used in tests: ${CMAKE_SOURCE_DIR}/asan_suppressions.txt")
  else()
    message(WARNING "ASAN enabled but no suppressions file found at ${CMAKE_SOURCE_DIR}/asan_suppressions.txt")
  endif()
endif()

# Add LSAN suppressions to test environment if file exists AND LeakSanitizer is enabled
# LSAN (LeakSanitizer) is available when built with -fsanitize=address or -fsanitize=leak
if(EXISTS ${CMAKE_SOURCE_DIR}/lsan_suppressions.txt AND (CX_SCHEME_SANITIZERS_ACTIVE MATCHES "address" OR CX_SCHEME_SANITIZERS_ACTIVE MATCHES "leak"))
  list(APPEND TESTS_ENVIRONMENT "LSAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/lsan_suppressions.txt")
  message(STATUS "LSAN suppressions will be used in tests: ${CMAKE_SOURCE_DIR}/lsan_suppressions.txt")
endif()

#------------------------------------------------------------------------
# Make test memory limiter script executable (Unix-like systems only)
#------------------------------------------------------------------------
if(BUILD_TESTING AND UNIX)
  file(CHMOD ${CMAKE_CURRENT_SOURCE_DIR}/run_with_memory_limit.py
    PERMISSIONS
      OWNER_READ OWNER_WRITE OWNER_EXECUTE
      GROUP_READ GROUP_EXECUTE
      WORLD_READ WORLD_EXECUTE
  )
endif()


#========================================================================
# Programs based on the latest supported frame spec
#========================================================================

set( noinst_HEADERS
	DumpObjects6.cc
	DumpObjects7.tcc
	DumpObjects8.tcc
	DumpObjects9.tcc
	FrameQuery.tcc
	FrStruct.hh
	FrStruct3.tcc
	FrStruct4.tcc
  FrStruct6.tcc
	FrStruct7.tcc
	FrStruct8.tcc
	StandardOptions.hh
	toc.hh.legacy
	toc4.tcc.legacy
  toc6.tcc.legacy
	toc8.tcc.legacy
	toc9.tcc.legacy
	LegacyTOCPrinter.hh
	ModernTOCPrinter.hh
)

list( APPEND bin_PROGRAMS
	framecpp_checksum
	framecpp_compressor
	framecpp_dump_channel
	framecpp_dump_objects
	framecpp_dump_toc
	framecpp_fix_metadata
	framecpp_query
	framecpp_transform
	framecpp_verify
  )

list( APPEND noinst_PROGRAMS
  framecpp_fracfg
  )

framecpp_target_executable( framecpp_query
  SOURCES FrQuery.cc
  LDADD ${LDADD} Boost::program_options Threads::Threads
  )
#
## OLD: FrQuery_SOURCES =
##	IFrameStream.cc
#
list( APPEND EXTRA_DIST  IFrameStream.hh )
#
framecpp_target_executable( framecpp_compressor
  SOURCES Compressor.cc
  )
framecpp_target_executable( framecpp_dump_channel
  SOURCES FrDumpChannel.cc
  )
framecpp_target_executable( framecpp_fix_metadata
  SOURCES FixMetadata.cc
  )
framecpp_target_executable( framecpp_fracfg
  SOURCES framecpp_fracfg.cc
  )
framecpp_target_executable( framecpp_transform
  SOURCES FrTransform.cc
  # Boost::filesystem now PUBLIC in framecpp, using default LDADD
  )
framecpp_target_executable( framecpp_verify
  SOURCES FrVerify.cc
  LDADD ${LDADD} Boost::program_options
  )
framecpp_target_executable( framecpp_checksum
  SOURCES FrVerifyChecksums.cc
  )
framecpp_target_executable( test_verify_options
  NOINST
  SOURCES test_verify_options.cc
  LDADD ${LDADD} Boost::program_options
  )

# TEMPORARY: Diagnostic test for ArchiverTest.FrameHWithNestedStructures debugging
framecpp_target_executable( test_frameh_debug
  NOINST
  SOURCES ../test_frameh_debug.cc
  )

##-----------------------------------------------------------------------
## Generate minimal frame files for systematic Verify class testing
##-----------------------------------------------------------------------
# These minimal frames are used for Phase 0 and Phase 1 testing of Verify options
# Generated with --disable-toc to eliminate TOC-related complexity

if(BUILD_TESTING)
  set(MINIMAL_FRAME_VERSIONS 3 4 5 6 7 8 9)
  set(MINIMAL_FRAME_FILES "")

  # Set up sanitizer environment for build-time frame generation
  cx_scheme_sanitizer_set_environment(
    OUTPUT minimal_frame_sanitizer_env
  )
  if(EXISTS ${CMAKE_SOURCE_DIR}/asan_suppressions.txt AND CX_SCHEME_SANITIZERS_ACTIVE MATCHES "address")
    # Disable ODR violation detection for build-time frame generation
    # Same rationale as src/Utilities/CMakeLists.txt
    list(APPEND minimal_frame_sanitizer_env "ASAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/asan_suppressions.txt:detect_odr_violation=0")
  endif()

  foreach(ver IN LISTS MINIMAL_FRAME_VERSIONS)
    set(_minimal_frame_file "Z-minimal_framecpp_v${ver}_notoc-600000000-1.gwf")
    list(APPEND MINIMAL_FRAME_FILES "${CMAKE_CURRENT_BINARY_DIR}/${_minimal_frame_file}")

    # Generate minimal framecpp frame (FrHeader + FrEndOfFile only)
    add_custom_command(
      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_minimal_frame_file}
      COMMAND ${CMAKE_COMMAND} -E env ${minimal_frame_sanitizer_env}
        $<TARGET_FILE:framecpp_sample>
        --minimal-framecpp-frame
        --disable-toc
        --version ${ver}
        --description "minimal_framecpp_v${ver}"
      DEPENDS framecpp_sample
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
      COMMENT "Generating minimal framecpp v${ver} test frame (no TOC)"
    )
  endforeach()

  # Create a custom target that depends on all minimal frames
  add_custom_target(minimal_test_frames ALL
    DEPENDS ${MINIMAL_FRAME_FILES}
    COMMENT "Generating all minimal test frames for Verify class testing"
  )
endif()

##-----------------------------------------------------------------------
## Systematic Verify Class Testing
##-----------------------------------------------------------------------
# Phase 0: Baseline test with all Verify options disabled
# This establishes that minimal frames are valid and pass with no validation
if(BUILD_TESTING)
  add_test(
    NAME verify_phase0_baseline_all_disabled
    COMMAND $<TARGET_FILE:test_verify_options>
      ${MINIMAL_FRAME_FILES}
  )
  set_tests_properties(verify_phase0_baseline_all_disabled PROPERTIES
    LABELS "verify;baseline"
    FIXTURES_REQUIRED minimal_test_frames_fixture
  )
endif()

# Phase 1: Individual option testing
# Test each Verify option individually to isolate which options cause false positives
set(VERIFY_OPTIONS
  CheckDataValid
  CheckDataValidAll
  CheckFast
  CheckFileChecksum
  CheckFileChecksumOnly
  CheckForDuplicateChannelNames
  CheckFrameChecksum
  CheckMD5Sum
  Expandability
  MustHaveEOFChecksum
  Strict
  UseMemoryMappedIO
  ValidateMetadata
  CollectAllErrors
)

if(BUILD_TESTING)
  foreach(option IN LISTS VERIFY_OPTIONS)
    add_test(
      NAME verify_phase1_${option}
      COMMAND $<TARGET_FILE:test_verify_options>
        --option ${option}
        ${MINIMAL_FRAME_FILES}
    )
    set_tests_properties(verify_phase1_${option} PROPERTIES
      LABELS "verify;phase1"
      FIXTURES_REQUIRED minimal_test_frames_fixture
    )
  endforeach()

  # Create fixture to ensure minimal frames are generated before tests run
  add_test(
    NAME minimal_frames_setup
    COMMAND ${CMAKE_COMMAND} -E echo "Minimal test frames ready"
  )
  set_tests_properties(minimal_frames_setup PROPERTIES
    FIXTURES_SETUP minimal_test_frames_fixture
    DEPENDS minimal_test_frames
  )
endif()

##-----------------------------------------------------------------------
#
#if LDAS_SHARED_BUILD
#noinst_PROGRAMS +=
#	CIDump
#
#CIDump_LDADD= $(top_builddir)/src/CInterface/libframecppc.la
#CIDump_LDADD+= $(LDADD)
#
#TESTS += test_frameCPP_c
#
#test_frameCPP_c_LDADD= $(top_builddir)/src/CInterface/libframecppc.la
#test_frameCPP_c_LDADD+= $(LDADD)
#
#endif # LDAS_SHARED_BUILD
framecpp_target_test( test_frameCPP_c
  SOURCES test_frameCPP_c.c
  LDADD framecppc Threads::Threads
  # Boost::system now PUBLIC in framecpp (via framecppc)
  )
#
##========================================================================
## Standard testing of core api via C++
##========================================================================

framecpp_target_test( test_frameCPP_cpp SOURCES test_frameCPP_cpp.cc )
##========================================================================
## Issue #251 reproducer (LALPulsar test failure under LAL_FRAME_LIBRARY=FrameC)
##
## Walks an external 1800-second .gwf via the OOInterface and the C-API
## (both TOC and per-frame) and prints span. Skipped (CTest 77) when the
## ISSUE_251_GWF env var is unset, so it builds in CI without a fixture.
##========================================================================
framecpp_target_test( test_issue_251_duration_walk
  SOURCES test_issue_251_duration_walk.cc
  LDADD framecpp framecppc Threads::Threads
)
framecpp_target_test( test_default_frame_version SOURCES test_default_frame_version.cc )
framecpp_target_test( test_backward_compat_headers
  SOURCES test_backward_compat_headers.cc
  TIMEOUT 30
)
framecpp_target_test( TestCompression SOURCES TestCompression.cc )
framecpp_target_test( test_CheckSum
  SOURCES test_CheckSum.cc
  LDADD
    PUBLIC ${LDADD}
    PUBLIC Boost::chrono)
# framecpp_target_test( framecpp_compression_beta SOURCES compression_beta.cc )

##-----------------------------------------------------------------------
#
#if LDAS_NEVER_BUILD
#TESTS += Ramp cds_1
#endif
#
#Ramp_SOURCES = Ramp.cc
#cds_1_SOURCES = cds_1.cc
#
##========================================================================
#
#
##------------------------------------------------------------------------
framecpp_target_test(
  test_static_frdetector
  SOURCES StaticFrDetector.cc
  )

##========================================================================
## C++ test to validate FrVect constructor before debugging SWIG wrapper
##========================================================================
framecpp_target_test(
  test_frvect_cpp
  SOURCES test_frvect_cpp.cc
  )

framecpp_target_test(
  test_detector_consistency
  SOURCES test_detector_consistency.cc
  )

##========================================================================
#
#
##------------------------------------------------------------------------
#
#if LDAS_NEVER_BUILD
#TESTS += DeepCopyTest
#DeepCopyTest_SOURCES = DeepCopy.cc
#endif
#
#
framecpp_target_test( ChecksumValidator SOURCES ChecksumValidator.cc )
framecpp_target_test( FrameFilenameTest SOURCES FrameFilename.cc )
framecpp_target_test( FrameStreamTest
  SOURCES FrameStream.cc
  COMMAND_ARGS --log_level=all
  TIMEOUT 300
  )

##========================================================================
## FrCheck validation of smallest/minimal frames
##========================================================================
# FrameStreamTest creates minimal frame files (Z-SmallestFrame_ver*-1000000000-1.gwf)
# with valid GPS time after January 1, 2009 (GPS 1000000000 ≈ May 2011)
# Set it up as a fixture so FrCheck validation can depend on it
#
# NOTE: frameCPP tests all versions (3-9), but FrCheck validation is limited:
# - v3/v4: FrameL "missing dictionary" error in FrCheck (deprecated specifications)
# - v5: frameCPP-only (FrameL never implemented v5)
# - v6-v8: Both frameCPP and FrameL validated with FrCheck
# - v9: frameCPP-only (FrameL only supports v3-v8)
if(BUILD_TESTING)
  set_tests_properties(FrameStreamTest
    PROPERTIES
    FIXTURES_SETUP fixture_smallest_frames
  )

  # Validate v6 smallest frame with FrCheck (if available)
  framecpp_target_test_frcheck_validate(
    VERSION 6
    FRAME_FILE "${CMAKE_CURRENT_BINARY_DIR}/Z-SmallestFrame_ver6-1000000000-1.gwf"
    TEST_NAME "verify_smallest_frame_v6_via_fr_check"
  )

  # Validate v7 smallest frame with FrCheck (if available)
  framecpp_target_test_frcheck_validate(
    VERSION 7
    FRAME_FILE "${CMAKE_CURRENT_BINARY_DIR}/Z-SmallestFrame_ver7-1000000000-1.gwf"
    TEST_NAME "verify_smallest_frame_v7_via_fr_check"
  )

  # Validate v8 smallest frame with FrCheck (if available)
  framecpp_target_test_frcheck_validate(
    VERSION 8
    FRAME_FILE "${CMAKE_CURRENT_BINARY_DIR}/Z-SmallestFrame_ver8-1000000000-1.gwf"
    TEST_NAME "verify_smallest_frame_v8_via_fr_check"
  )

  # Ensure FrCheck validation runs after FrameStreamTest creates the files
  if(FR_CHECK)
    set_tests_properties(verify_smallest_frame_v6_via_fr_check
      PROPERTIES
      FIXTURES_REQUIRES fixture_smallest_frames
    )
    set_tests_properties(verify_smallest_frame_v7_via_fr_check
      PROPERTIES
      FIXTURES_REQUIRES fixture_smallest_frames
    )
    set_tests_properties(verify_smallest_frame_v8_via_fr_check
      PROPERTIES
      FIXTURES_REQUIRES fixture_smallest_frames
    )
  endif()
endif()

framecpp_target_test( FrameStreamPlanTest SOURCES FrameStreamPlanTest.cc )
framecpp_target_test( CrossVersionReadingTest SOURCES test_cross_version_reading.cc )
framecpp_target_test( ChannelTest SOURCES ChannelTest.cc )
# DISABLED: FrameH serialization fails in test frameworks (works in standalone)
# See test_frameh_debug.cc for working standalone version
# framecpp_target_test( FrameHSerializationBoostTest SOURCES test_frameh_serialization_boost.cc )
framecpp_target_test( CompressionTest SOURCES compression.cc REDIRECT_OUTPUT )
framecpp_target_test( DetectorTest SOURCES DetectorTest.cc )
framecpp_target_test( FrAdcDataTest SOURCES test_fr_adc_data.cc )
framecpp_target_test( ChannelDimensionalityTest SOURCES test_channel_dimensionality_cpp.cc )
framecpp_target_test( DimensionTest SOURCES test_dimension.cc )

##========================================================================
## Version I/O compatibility tests (NEW: replaces promote/demote testing)
##========================================================================
# VersionIOCompatibilityTest requires significant memory to compile
# Disable during package builds due to memory constraints in build environments
if(NOT DEFINED BUILD_VERSION_IO_COMPAT_TEST)
  # Check if we're building packages (Debian or RPM)
  if(DEFINED ENV{DEB_BUILD_OPTIONS} OR DEFINED ENV{RPM_BUILD_ROOT} OR DEFINED ENV{CPACK_PACKAGE_NAME})
    set(BUILD_VERSION_IO_COMPAT_TEST_DEFAULT OFF)
    message(STATUS "VersionIOCompatibilityTest: Disabled (package build detected)")
  else()
    set(BUILD_VERSION_IO_COMPAT_TEST_DEFAULT ON)
    message(STATUS "VersionIOCompatibilityTest: Enabled (development build)")
  endif()
else()
  set(BUILD_VERSION_IO_COMPAT_TEST_DEFAULT ${BUILD_VERSION_IO_COMPAT_TEST})
endif()

option(BUILD_VERSION_IO_COMPAT_TEST "Build VersionIOCompatibilityTest (memory-intensive)" ${BUILD_VERSION_IO_COMPAT_TEST_DEFAULT})

if(BUILD_VERSION_IO_COMPAT_TEST)
  framecpp_target_test( VersionIOCompatibilityTest
    SOURCES test_version_io_compatibility.cc
  )
endif()

##========================================================================
## FrVect compression mode conversion tests
##========================================================================
framecpp_target_test( FrVectCompressionConversionTest
  SOURCES test_frvect_compression_conversion.cc
)

##========================================================================
## FrVect dataValid preservation during compression tests
##========================================================================
framecpp_target_test( FrVectDataValidCompressionTest
  SOURCES test_frvect_datavalid_compression.cc
)

##========================================================================
## C++ test to validate FrVect types - mimics Python test_frameCPP_frvect_types.py
##========================================================================
framecpp_target_test( FrVectTypesTest 
  SOURCES test_frvect_types_cpp.cc 
)

# Test FrVect types for each frame version, similar to Python tests
foreach(ver IN LISTS FRAME_VERSIONS)
  # Only run for the specific fr_vect_types test frames
  if(BUILD_TESTING)
    add_test(
      NAME "test_framecpp_frvect_types_ver${ver}"
      COMMAND $<TARGET_FILE:FrVectTypesTest>
        "${CMAKE_BINARY_DIR}/src/Utilities/Z-std_fr_vect_types_ver${ver}-700000000-1.gwf"
    )
    set_tests_properties("test_framecpp_frvect_types_ver${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )
  endif()
endforeach()
framecpp_target_test( FrStatDataTest SOURCES FrStatData.cc )
framecpp_target_test( FrStructsTest SOURCES FrStructs.cc )
framecpp_target_test( IOStreamTest SOURCES IOStream.cc )
framecpp_target_test( SixtyKTest SOURCES SixtyK.cc )
if( NOT SANITIZER_ENABLED )
  framecpp_target_test( LargeFrameTest SOURCES LargeFrame.cc TIMEOUT 360 )
endif( )

##========================================================================
## Setup testing of utilities
##========================================================================
framecpp_target_test( utilities SOURCES test_utilities.cc )

##========================================================================
## Setup testing of Frame debug printing functionality
##========================================================================
framecpp_target_test( test_frame_debug_print
  SOURCES test_frame_debug_print.cc
  LDADD ${LDADD}
  # All Boost libraries now PUBLIC in framecpp/framecppcmn
)

if ( BUILD_TESTING )
  cx_target_test( framecpp_fracfg
    COMMAND framecpp_fracfg
      --channels Z0:RAMPED_INT_2U_1
      --output fracfg.gwf
      ${CMAKE_BINARY_DIR}/src/Utilities/Z-R_std_test_frame_ver9-600000000-1.gwf
  )
endif( )

#------------------------------------------------------------------------
# Test SequentialFrameReader without TOC (in-memory)
#------------------------------------------------------------------------
framecpp_target_test( test_serial_reader_no_toc
  SOURCES test_serial_reader_no_toc.cc
  TIMEOUT 240
)

# Create version-specific tests for each frame version
# Note: Version 3 is skipped because it has a static destructor ordering issue
# when run in isolation (mutex cleanup error), but it works fine when run as
# part of the full test suite (test_serial_reader_no_toc without version specified)
foreach(ver IN LISTS FRAME_VERSIONS)
  if(BUILD_TESTING AND NOT ver EQUAL 3)
    add_test(
      NAME "test_serial_reader_no_toc_ver${ver}"
      COMMAND $<TARGET_FILE:test_serial_reader_no_toc> --catch_system_errors=no
    )
    set_tests_properties("test_serial_reader_no_toc_ver${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT};FRAMECPP_TEST_VERSION=${ver}"
    )
  endif()
endforeach()

#------------------------------------------------------------------------
# FrCheck validation for serial reader no-TOC frames (v6-v8 only)
# Validates that frames generated without TOC pass external FrCheck validation
#------------------------------------------------------------------------
if(FR_CHECK)
  framecpp_target_test( test_serial_reader_frcheck
    SOURCES test_serial_reader_frcheck.cc
  )
  # Note: This test validates v6, v7, and v8 frames using FrCheck -s (sequential only)
  # The -s flag is required because we intentionally create files without TOC
  target_compile_definitions(test_serial_reader_frcheck
    PRIVATE FRCHECK_PATH="${FR_CHECK}"
  )
  set_tests_properties(test_serial_reader_frcheck
    PROPERTIES
    LABELS "frcheck;serial_reader"
    TIMEOUT 180
  )
endif()

#------------------------------------------------------------------------
# Test each frame version for:
# - Dump objects from standard test frames
# - File integrity verification
# - Frame format downconversion
#------------------------------------------------------------------------
foreach(ver IN LISTS FRAME_VERSIONS)
  # Test dumping objects from standard test frame
  # Output redirected to file; on success file deleted, on failure file kept
  # and tail printed to stderr so CI logs show useful diagnostics without bloat.
  cx_target_test(
    framecpp_dump_objects_ver${ver}
    COMMAND sh -c "out=framecpp_dump_objects_ver${ver}.output; $<TARGET_FILE:framecpp_dump_objects> ${CMAKE_BINARY_DIR}/src/Utilities/Z-R_std_test_frame_ver${ver}-600000000-1.gwf > \"$out\" 2>&1; rc=$?; if [ $rc -eq 0 ]; then rm -f \"$out\"; else echo \"==== last 200 lines of $out (exit=$rc) ====\" >&2; tail -n 200 \"$out\" >&2; fi; exit $rc"
  )
  set_tests_properties(framecpp_dump_objects_ver${ver}
    PROPERTIES
    ENVIRONMENT "${TESTS_ENVIRONMENT}"
    TIMEOUT 600
  )

  # Test file integrity
  framecpp_target_test_file_integrity(
    DESCRIPTION ReadingFrame_ver${ver}
  )

  # Test downconversion
  framecpp_target_test_downconvert( ${ver} )
endforeach()

#------------------------------------------------------------------------
## FrameL <-> FrameCPP Multi-Version I/O Compatibility Test
##
## Tests bidirectional I/O compatibility across all frame specification versions
## by having FrameL write in each historic version and FrameCPP read them back,
## and vice versa, to ensure complete bidirectional compatibility.
##
## Only built if FrameL library is available
#------------------------------------------------------------------------
if(FRAMEL_FOUND)
  framecpp_target_test( test_framel_framecpp_compat
    SOURCES test_framel_framecpp_compat.cc
    LDADD ${LDADD} ${FRAMEL_LIBRARIES}
  )

  # Add FrameL include directories to this specific test
  if(TARGET test_framel_framecpp_compat)
    target_include_directories(test_framel_framecpp_compat
      PRIVATE ${FRAMEL_INCLUDE_DIRS}
    )
  endif()

  #----------------------------------------------------------------------
  ## test_framel_debug - Debug tool for FrameL cross-library issues
  ##
  ## Simple diagnostic executable to understand why ReadFrAdcData()
  ## doesn't load nested FrVect data from FrameL files
  ##
  ## Depends on test_framel_framecpp_compat to generate test input files
  #----------------------------------------------------------------------
  framecpp_target_test( test_framel_debug
    SOURCES test_framel_debug.cc
    LDADD ${LDADD}
  )

  # Ensure test_framel_framecpp_compat runs first to generate test_framel_v6.gwf
  # Using FIXTURES to establish test execution order
  if(TARGET test_framel_framecpp_compat AND TARGET test_framel_debug)
    set_tests_properties(test_framel_framecpp_compat
      PROPERTIES FIXTURES_SETUP FrameLTestFiles
    )
    set_tests_properties(test_framel_debug
      PROPERTIES FIXTURES_REQUIRED FrameLTestFiles
    )
  endif()

  #----------------------------------------------------------------------
  ## framel_sample - Minimal Frame Creator using FrameL
  ##
  ## Creates a minimal v8 frame with FrameL library for comparison
  ## with frameCPP's output to identify FrTOC structure differences
  #----------------------------------------------------------------------
  framecpp_target_executable( framel_sample
    SOURCES framel_sample.cc
    LDADD ${LDADD} ${FRAMEL_LIBRARIES} Boost::program_options
  )

  # Add FrameL include directories
  if(TARGET framel_sample)
    target_include_directories(framel_sample
      PRIVATE ${FRAMEL_INCLUDE_DIRS}
    )
  endif()
endif()

#------------------------------------------------------------------------
## FrCheck Conditional Usage Test
##
## Tests that FrCheck validation is used if and only if FrameL library is
## available. This ensures proper conditional compilation and runtime behavior.
##
## The test will:
## - Build with FRAMEL_FOUND defined if FrameL is available
## - Execute FrCheck validation only when FrameL is present
## - Pass gracefully when FrameL is not available
#------------------------------------------------------------------------
framecpp_target_test( test_frcheck_conditional
  SOURCES test_frcheck_conditional.cc
  LDADD ${LDADD}
)

# Add conditional compilation flag if FrameL is available
if(TARGET test_frcheck_conditional)
  if(FRAMEL_FOUND)
    target_compile_definitions(test_frcheck_conditional
      PRIVATE FRAMEL_FOUND
    )
    target_include_directories(test_frcheck_conditional
      PRIVATE ${FRAMEL_INCLUDE_DIRS}
    )
    target_link_libraries(test_frcheck_conditional
      ${FRAMEL_LIBRARIES}
    )
  endif()
endif()

#------------------------------------------------------------------------
## FrameL Version Support Detection Test
##
## Programmatically detects which frame specification versions are supported
## by FrameL library by attempting to write/read frames in each version.
## Generates a comprehensive compatibility matrix.
##
## The test will:
## - Test all frame versions (3, 4, 6, 7, 8, 9) with both FrameCPP and FrameL
## - Report which versions are supported by each library
## - Validate against expected support patterns
#------------------------------------------------------------------------
framecpp_target_test( test_framel_version_support
  SOURCES test_framel_version_support.cc
  LDADD ${LDADD}
)

# Add conditional compilation flag if FrameL is available
if(TARGET test_framel_version_support)
  if(FRAMEL_FOUND)
    target_compile_definitions(test_framel_version_support
      PRIVATE FRAMEL_FOUND
    )
    target_include_directories(test_framel_version_support
      PRIVATE ${FRAMEL_INCLUDE_DIRS}
    )
    target_link_libraries(test_framel_version_support
      ${FRAMEL_LIBRARIES}
    )
  endif()
endif()

#------------------------------------------------------------------------
## Definition Consistency Test
##
## Compares structure definitions against PDF specification (GROUND TRUTH).
## Both FrameL and frameCPP implementations are validated against the official
## LIGO frame specification documents extracted from PDFs.
##
## The test will:
## - Compare all frame object types (FrameH, FrDetector, FrVect, etc.)
## - Test all frame versions (3, 4, 6, 7, 8, 9)
## - Use PDF definitions as definitive ground truth
## - Report any deviations from PDF spec as ERRORS
## - Verify field names, types, and counts match specification
## - Validate version evolution (e.g., FrDetector v3 vs v7)
##
## Only built if FrameL library is available
#------------------------------------------------------------------------
if(FRAMEL_FOUND)
  framecpp_target_test( test_definition_consistency
    SOURCES
      test_definition_consistency.cc
      ../src/Utilities/PDFDefinitions.cc
    LDADD
      PUBLIC ${LDADD}
      PUBLIC ${FRAMEL_LIBRARIES}
  )

  # Add conditional compilation flag and include directories
  if(TARGET test_definition_consistency)
    target_compile_definitions(test_definition_consistency
      PRIVATE FRAMEL_FOUND
    )
    target_include_directories(test_definition_consistency
      PRIVATE ${FRAMEL_INCLUDE_DIRS}
    )
  endif()
endif()

#========================================================================
## Granular Frame Structure Tests
##
## Tests each --enable-* flag individually across all frame versions.
## This validates that each frame structure type works correctly in isolation
## and is properly supported in each frame specification version.
##
## Structure version support matrix (verified from PDFDefinitions.cc):
## - detector:      v3, v4, v6, v7, v8, v9 (added in v3)
## - history:       v3, v4, v6, v7, v8, v9 (added in v3)
## - fr-event:      v3, v4, v6, v7, v8, v9 (added in v3 as FrTrigData, renamed to FrEvent in v5)
## - fr-msg:        v3, v4, v6, v7, v8, v9 (added in v3)
## - fr-sim-event:  v4, v6, v7, v8, v9     (added in v4)
## - fr-summary:    v3, v4, v6, v7, v8, v9 (added in v3)
## - fr-table:      v4, v6, v7, v8, v9     (added in v4)
#========================================================================

if(BUILD_TESTING)
  # Define frame structures to test with their minimum supported version
  # Version history based on PDFDefinitions.cc (ground truth from PDF specs):
  # - v3: FrDetector, FrHistory, FrMsg, FrSummary, FrTrigData (original name for FrEvent)
  # - v4: FrTable, FrSimEvent
  # - v5: FrTrigData renamed to FrEvent (FrameH field changed from "trigData" to "event")
  #
  # Use two parallel lists to avoid CMake semicolon parsing issues
  set(FRAME_STRUCTURE_NAMES
    detector
    history
    fr-adc
    fr-event
    fr-msg
    fr-proc-data
    fr-rawdata
    fr-ser-data
    fr-sim-data
    fr-sim-event
    fr-stat-data
    fr-summary
    fr-table
  )
  set(FRAME_STRUCTURE_MIN_VERSIONS
    3  # detector
    3  # history
    3  # fr-adc
    3  # fr-event
    3  # fr-msg
    3  # fr-proc-data
    3  # fr-rawdata
    3  # fr-ser-data
    3  # fr-sim-data
    4  # fr-sim-event
    3  # fr-stat-data (v3-v8 only, deprecated in v9)
    3  # fr-summary
    4  # fr-table
  )

  #----------------------------------------------------------------------
  # Helper function to test a single structure with framecpp_sample
  #----------------------------------------------------------------------
  function(test_framecpp_structure structure min_version version)
    # Only test if version >= minimum supported version
    if(version LESS min_version)
      return()
    endif()

    # FrStatData is deprecated in v9, skip testing
    if(structure STREQUAL "fr-stat-data" AND version GREATER_EQUAL 9)
      return()
    endif()

    set(_test_name "test_framecpp_${structure}_v${version}")
    set(_frame_file "Z-test_${structure}_v${version}-600000000-1.gwf")
    set(_frame_target "framecpp_${structure}_v${version}_frame")

    # Set up sanitizer environment for build-time frame generation
    cx_scheme_sanitizer_set_environment(OUTPUT _framecpp_sanitizer_env)
    if(EXISTS ${CMAKE_SOURCE_DIR}/asan_suppressions.txt AND CX_SCHEME_SANITIZERS_ACTIVE MATCHES "address")
      list(APPEND _framecpp_sanitizer_env "ASAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/asan_suppressions.txt:detect_odr_violation=0")
    endif()

    # Generate frame file at BUILD time with dependency on framecpp_sample
    # This ensures frames are regenerated when framecpp_sample is rebuilt
    add_custom_command(
      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_frame_file}
      COMMAND ${CMAKE_COMMAND} -E env ${_framecpp_sanitizer_env}
        $<TARGET_FILE:framecpp_sample>
        --version ${version}
        --description test_${structure}_v${version}
        --enable-${structure}
      DEPENDS framecpp_sample
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
      COMMENT "Generating ${_frame_file} with framecpp_sample"
    )

    # Create custom target to trigger frame file generation
    add_custom_target(${_frame_target}
      DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${_frame_file}
    )

    # Verify the frame file was created (existence + readability smoke test).
    # md5sum exits 0 iff the file is readable end-to-end and prints a single
    # short line; cat would dump the entire binary fixture (megabytes per test
    # across 25+ instances) into the CI build log. Structural validation via
    # framecpp_verify is tracked separately in #262 for the 3.3.0 milestone.
    add_test(
      NAME "${_test_name}_verify"
      COMMAND ${CMAKE_COMMAND} -E md5sum ${_frame_file}
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )
    set_tests_properties("${_test_name}_verify"
      PROPERTIES
      FIXTURES_REQUIRED ${_frame_target}
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Setup fixture to ensure frame is built before tests run
    add_test(
      NAME ${_frame_target}_setup
      COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target ${_frame_target}
    )
    set_tests_properties(${_frame_target}_setup
      PROPERTIES
      FIXTURES_SETUP ${_frame_target}
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Validate with FrCheck for v6-v8 (FrameL-compatible versions)
    if(FR_CHECK AND version GREATER_EQUAL 6 AND version LESS_EQUAL 8)
      add_test(
        NAME "${_test_name}_frcheck"
        COMMAND ${FR_CHECK} -i ${CMAKE_CURRENT_BINARY_DIR}/${_frame_file}
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
      )
      set_tests_properties("${_test_name}_frcheck"
        PROPERTIES
        FIXTURES_REQUIRED ${_frame_target}
        ENVIRONMENT "${TESTS_ENVIRONMENT}"
      )
    endif()
  endfunction()

  #----------------------------------------------------------------------
  # Helper function to test a single structure with framel_sample
  #----------------------------------------------------------------------
  function(test_framel_structure structure min_version version)
    # Only test if FrameL is available and version is in supported range (3-8)
    if(NOT FRAMEL_FOUND OR version GREATER 8)
      return()
    endif()

    # Only test if version >= minimum supported version
    if(version LESS min_version)
      return()
    endif()

    # FrStatData is deprecated in v9, skip testing
    if(structure STREQUAL "fr-stat-data" AND version GREATER_EQUAL 9)
      return()
    endif()

    # framel_sample does not support fr-event, fr-msg, fr-sim-event, fr-summary, fr-table
    # But it DOES support fr-proc-data and fr-sim-data
    if(structure MATCHES "^fr-(event|msg|sim-event|summary|table)$")
      return()
    endif()

    set(_test_name "test_framel_${structure}_v${version}")
    set(_frame_file "Z-test_framel_${structure}_v${version}l-1000000000-1.gwf")
    set(_frame_target "framel_${structure}_v${version}_frame")

    # Generate frame file at BUILD time with dependency on framel_sample
    # This ensures frames are regenerated when framel_sample is rebuilt
    # Set up sanitizer environment for build-time frame generation
    cx_scheme_sanitizer_set_environment(OUTPUT _framel_sanitizer_env)
    if(EXISTS ${CMAKE_SOURCE_DIR}/asan_suppressions.txt AND CX_SCHEME_SANITIZERS_ACTIVE MATCHES "address")
      list(APPEND _framel_sanitizer_env "ASAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/asan_suppressions.txt:detect_odr_violation=0")
    endif()
    if(EXISTS ${CMAKE_SOURCE_DIR}/lsan_suppressions.txt AND (CX_SCHEME_SANITIZERS_ACTIVE MATCHES "address" OR CX_SCHEME_SANITIZERS_ACTIVE MATCHES "leak"))
      list(APPEND _framel_sanitizer_env "LSAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/lsan_suppressions.txt")
    endif()
    add_custom_command(
      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_frame_file}
      COMMAND ${CMAKE_COMMAND} -E env ${_framel_sanitizer_env}
        $<TARGET_FILE:framel_sample>
        --version ${version}
        --description test_framel_${structure}_v${version}
        --start-time 1000000000
        --enable-${structure}
      DEPENDS framel_sample
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
      COMMENT "Generating ${_frame_file} with framel_sample"
    )

    # Create custom target to trigger frame file generation
    add_custom_target(${_frame_target}
      DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${_frame_file}
    )

    # Setup fixture to ensure frame is built before tests run
    add_test(
      NAME ${_frame_target}_setup
      COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target ${_frame_target}
    )
    set_tests_properties(${_frame_target}_setup
      PROPERTIES
      FIXTURES_SETUP ${_frame_target}
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Validate with FrCheck for v6-v8 (FrameL-compatible versions)
    if(FR_CHECK AND version GREATER_EQUAL 6 AND version LESS_EQUAL 8)
      add_test(
        NAME "${_test_name}_frcheck"
        COMMAND ${FR_CHECK} -i ${CMAKE_CURRENT_BINARY_DIR}/${_frame_file}
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
      )
      set_tests_properties("${_test_name}_frcheck"
        PROPERTIES
        FIXTURES_REQUIRED ${_frame_target}
        ENVIRONMENT "${TESTS_ENVIRONMENT}"
      )
    endif()
  endfunction()

  #----------------------------------------------------------------------
  # Create granular tests for all structures across all versions
  #----------------------------------------------------------------------
  list(LENGTH FRAME_STRUCTURE_NAMES num_structures)
  math(EXPR last_index "${num_structures} - 1")

  foreach(idx RANGE ${last_index})
    list(GET FRAME_STRUCTURE_NAMES ${idx} structure)
    list(GET FRAME_STRUCTURE_MIN_VERSIONS ${idx} min_version)

    # Test with framecpp_sample across all supported versions
    foreach(ver IN LISTS FRAME_VERSIONS)
      test_framecpp_structure(${structure} ${min_version} ${ver})
    endforeach()

    # Test with framel_sample across all supported versions
    foreach(ver IN LISTS FRAME_VERSIONS)
      test_framel_structure(${structure} ${min_version} ${ver})
    endforeach()
  endforeach()

  #----------------------------------------------------------------------
  # Test custom detector names and history messages
  #----------------------------------------------------------------------
  foreach(ver IN LISTS FRAME_VERSIONS)
    # Test custom detector name
    add_test(
      NAME "test_framecpp_detector_custom_name_v${ver}"
      COMMAND $<TARGET_FILE:framecpp_sample>
        --version ${ver}
        --enable-detector
        --with-detector-name "LIGO_India"
    )
    set_tests_properties("test_framecpp_detector_custom_name_v${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Test custom history message
    add_test(
      NAME "test_framecpp_history_custom_msg_v${ver}"
      COMMAND $<TARGET_FILE:framecpp_sample>
        --version ${ver}
        --enable-history
        --with-history-msg "Custom test message for version ${ver}"
    )
    set_tests_properties("test_framecpp_history_custom_msg_v${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )
  endforeach()

  # Test framel_sample custom names for v3-v8
  if(FRAMEL_FOUND)
    foreach(ver IN LISTS FRAME_VERSIONS)
      if(ver LESS_EQUAL 8)
        # Test custom detector name
        add_test(
          NAME "test_framel_detector_custom_name_v${ver}"
          COMMAND $<TARGET_FILE:framel_sample>
            --version ${ver}
            --enable-detector
            --with-detector-name "LIGO_Hanford"
        )
        set_tests_properties("test_framel_detector_custom_name_v${ver}"
          PROPERTIES
          ENVIRONMENT "${TESTS_ENVIRONMENT}"
        )

        # Test custom history message
        add_test(
          NAME "test_framel_history_custom_msg_v${ver}"
          COMMAND $<TARGET_FILE:framel_sample>
            --version ${ver}
            --enable-history
            --with-history-msg "FrameL test for v${ver}"
        )
        set_tests_properties("test_framel_history_custom_msg_v${ver}"
          PROPERTIES
          ENVIRONMENT "${TESTS_ENVIRONMENT}"
        )
      endif()
    endforeach()
  endif()

  #----------------------------------------------------------------------
  # Phase 5: TOC Validation Tests (Level 7: Frame Markers)
  #----------------------------------------------------------------------
  # Validates Frame Table of Contents (FrTOC) generation across all versions (v3-v9)
  # using cross-platform CMake script with dual validation:
  #   - Primary: framecpp_dump_toc (all versions)
  #   - Secondary: FrCheck (v6-v8, optional)
  #
  # Test types (5 per version × 7 versions = 35 tests):
  #   1. detector: Validate nDetector=1
  #   2. adc: Validate nADC=1
  #   3. multi: Validate multiple counts (nDetector=1, nADC=1, nProc=1)
  #   4. disabled: Validate --disable-toc works (no TOC present)
  #   5. empty: Validate empty frame (all counts=0)

  foreach(ver IN LISTS FRAME_VERSIONS)
    # Test 1: TOC with detector (nDetector=1)
    add_test(
      NAME "test_toc_detector_v${ver}"
      COMMAND ${CMAKE_COMMAND}
        -DVERSION=${ver}
        -DTEST_TYPE=detector
        -DFRAMECPP_SAMPLE_EXE=$<TARGET_FILE:framecpp_sample>
        -DFRAMECPP_DUMP_TOC_EXE=$<TARGET_FILE:framecpp_dump_toc>
        -DFR_CHECK=${FR_CHECK}
        -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/verify_toc.cmake
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )
    set_tests_properties("test_toc_detector_v${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Test 2: TOC with ADC data (nADC=1)
    add_test(
      NAME "test_toc_adc_v${ver}"
      COMMAND ${CMAKE_COMMAND}
        -DVERSION=${ver}
        -DTEST_TYPE=adc
        -DFRAMECPP_SAMPLE_EXE=$<TARGET_FILE:framecpp_sample>
        -DFRAMECPP_DUMP_TOC_EXE=$<TARGET_FILE:framecpp_dump_toc>
        -DFR_CHECK=${FR_CHECK}
        -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/verify_toc.cmake
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )
    set_tests_properties("test_toc_adc_v${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Test 3: TOC with multiple objects (nDetector=1, nADC=1, nProc=1)
    add_test(
      NAME "test_toc_multi_v${ver}"
      COMMAND ${CMAKE_COMMAND}
        -DVERSION=${ver}
        -DTEST_TYPE=multi
        -DFRAMECPP_SAMPLE_EXE=$<TARGET_FILE:framecpp_sample>
        -DFRAMECPP_DUMP_TOC_EXE=$<TARGET_FILE:framecpp_dump_toc>
        -DFR_CHECK=${FR_CHECK}
        -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/verify_toc.cmake
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )
    set_tests_properties("test_toc_multi_v${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Test 4: TOC disabled (--disable-toc flag)
    add_test(
      NAME "test_toc_disabled_v${ver}"
      COMMAND ${CMAKE_COMMAND}
        -DVERSION=${ver}
        -DTEST_TYPE=disabled
        -DFRAMECPP_SAMPLE_EXE=$<TARGET_FILE:framecpp_sample>
        -DFRAMECPP_DUMP_TOC_EXE=$<TARGET_FILE:framecpp_dump_toc>
        -DFR_CHECK=${FR_CHECK}
        -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/verify_toc.cmake
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )
    set_tests_properties("test_toc_disabled_v${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )

    # Test 5: TOC with empty frame (all counts=0)
    add_test(
      NAME "test_toc_empty_v${ver}"
      COMMAND ${CMAKE_COMMAND}
        -DVERSION=${ver}
        -DTEST_TYPE=empty
        -DFRAMECPP_SAMPLE_EXE=$<TARGET_FILE:framecpp_sample>
        -DFRAMECPP_DUMP_TOC_EXE=$<TARGET_FILE:framecpp_dump_toc>
        -DFR_CHECK=${FR_CHECK}
        -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/verify_toc.cmake
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )
    set_tests_properties("test_toc_empty_v${ver}"
      PROPERTIES
      ENVIRONMENT "${TESTS_ENVIRONMENT}"
    )
  endforeach()

  cm_msg_notice("Granular frame structure tests configured:")
  cm_msg_notice("  - Individual structure tests: ${FRAME_STRUCTURE_NAMES}")
  cm_msg_notice("  - Testing across versions: ${FRAME_VERSIONS}")
  cm_msg_notice("  - FrCheck validation: ${FR_CHECK}")
  cm_msg_notice("  - FrameL tests: ${FRAMEL_FOUND}")
endif()

#------------------------------------------------------------------------
# Set 5-minute timeout for all tests in this directory
if(BUILD_TESTING)
  get_property(all_tests DIRECTORY PROPERTY TESTS)
  if(all_tests)
    set_tests_properties(${all_tests} PROPERTIES TIMEOUT 600)
  endif()
  # LargeFrameTest needs extended CTest timeout to match its Python wrapper timeout
  if(TARGET LargeFrameTest)
    set_tests_properties(LargeFrameTest PROPERTIES TIMEOUT 360)
  endif()
endif()

cx_scheme_sanitizer_set_environment( )
