cmake_minimum_required(VERSION 3.15)
project(MRTKLIB VERSION 0.7.11 LANGUAGES C)

# ── Version header generation ─────────────────────────────────────────────────
set(MRTKLIB_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(MRTKLIB_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(MRTKLIB_VERSION_PATCH ${PROJECT_VERSION_PATCH})
set(MRTKLIB_VERSION_STRING ${PROJECT_VERSION})
configure_file(
    ${CMAKE_SOURCE_DIR}/include/mrtklib/mrtk_version.h.in
    ${CMAKE_BINARY_DIR}/include/mrtklib/mrtk_version.h
    @ONLY
)

# ── Build-time git hash for `mrtk --version` (#120) ──────────────────────────
# Regenerates ${CMAKE_BINARY_DIR}/generated/mrtk_git_version.c on every build.
# The script only rewrites the file when the resolved hash changes, so a
# no-op build does not recompile unrelated translation units.
set(MRTK_GIT_VERSION_C ${CMAKE_BINARY_DIR}/generated/mrtk_git_version.c)
add_custom_target(mrtk_git_version_gen ALL
    COMMAND ${CMAKE_COMMAND}
        -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
        -DBINARY_DIR=${CMAKE_BINARY_DIR}
        -P ${CMAKE_SOURCE_DIR}/cmake/GenerateGitVersion.cmake
    BYPRODUCTS ${MRTK_GIT_VERSION_C}
    COMMENT "Refreshing mrtk git version metadata"
    VERBATIM
)
# Materialise the file at configure time so the first build has something to
# compile; subsequent builds get refreshed by the custom target above.
execute_process(
    COMMAND ${CMAKE_COMMAND}
        -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
        -DBINARY_DIR=${CMAKE_BINARY_DIR}
        -P ${CMAKE_SOURCE_DIR}/cmake/GenerateGitVersion.cmake
)

# Expose POSIX/BSD APIs hidden by -ansi (matches makefile platform detection)
if(APPLE)
    add_compile_definitions(_DARWIN_C_SOURCE)
elseif(UNIX)
    add_compile_definitions(_GNU_SOURCE)
endif()

# Compile options matching existing makefiles
add_compile_options(-Wall -O3 -ansi -pedantic -g)
if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID MATCHES "Clang")
    add_compile_options(-Wno-unused-but-set-variable)
endif()

# ── MRTKLIB modular library (new, C11) ───────────────────────────────────────
# New modular implementation that will gradually replace rtklib.
# Uses C11 (ISO/IEC 9899:2011) and the include/mrtklib/ public headers.
add_library(mrtklib STATIC
    # ── core/ (基盤・数学・ユーティリティ) ─────────────────────────────────────
    src/core/tomlc99/toml.c
    src/core/mrtk_toml.c
    src/core/mrtk_context.c
    src/core/mrtk_core.c
    src/core/mrtk_time.c
    src/core/mrtk_mat.c
    src/core/mrtk_coords.c
    src/core/mrtk_bits.c
    src/core/mrtk_sys.c
    src/core/mrtk_trace.c
    src/core/mrtk_cli.c
    # ── models/ (物理・環境モデル) ─────────────────────────────────────────────
    src/models/mrtk_atmos.c
    src/models/mrtk_astro.c
    src/models/mrtk_antenna.c
    src/models/mrtk_geoid.c
    src/models/mrtk_tides.c
    src/models/mrtk_station.c
    # ── data/ (データ構造・I/Oパース) ──────────────────────────────────────────
    src/data/mrtk_eph.c
    src/data/mrtk_peph.c
    src/data/mrtk_obs.c
    src/data/mrtk_nav.c
    src/data/mrtk_rinex.c
    src/data/mrtk_convrnx.c
    src/data/mrtk_ionex.c
    src/data/mrtk_bias_sinex.c
    src/data/mrtk_fcb.c
    src/data/mrtk_sbas.c
    src/data/mrtk_rcvraw.c
    src/data/rcv/mrtk_rcv_binex.c
    src/data/rcv/mrtk_rcv_crescent.c
    src/data/rcv/mrtk_rcv_javad.c
    src/data/rcv/mrtk_rcv_novatel.c
    src/data/rcv/mrtk_rcv_nvs.c
    src/data/rcv/mrtk_rcv_rt17.c
    src/data/rcv/mrtk_rcv_septentrio.c
    src/data/rcv/mrtk_rcv_skytraq.c
    src/data/rcv/mrtk_rcv_ss2.c
    src/data/rcv/mrtk_rcv_ublox.c
    # ── rtcm/ (RTCM関連) ──────────────────────────────────────────────────────
    src/rtcm/mrtk_rtcm.c
    src/rtcm/mrtk_rtcm2.c
    src/rtcm/mrtk_rtcm3.c
    src/rtcm/mrtk_rtcm3e.c
    src/rtcm/mrtk_rtcm3_local_corr.c
    # ── madoca/ (MADOCA-PPP固有処理) ───────────────────────────────────────────
    src/madoca/mrtk_madoca.c
    src/madoca/mrtk_madoca_iono.c
    src/madoca/mrtk_madoca_local_corr.c
    src/madoca/mrtk_madoca_local_comb.c
    # ── has/ (Galileo HAS処理) ─────────────────────────────────────────────────
    src/has/mrtk_has.c
    src/has/mrtk_has_rs.c
    # ── clas/ (CLAS CSSR処理) ──────────────────────────────────────────────────
    src/clas/mrtk_clas.c
    src/clas/mrtk_clas_grid.c
    src/clas/mrtk_clas_osr.c
    src/clas/mrtk_clas_isb.c

    # ── pos/ (測位アルゴリズム・オプション) ────────────────────────────────────
    src/pos/mrtk_spp.c
    src/pos/mrtk_rtkpos.c
    src/pos/mrtk_ppp_ar.c
    src/pos/mrtk_ppp.c
    src/pos/mrtk_ppp_rtk.c
    src/pos/mrtk_vrs.c
    src/pos/mrtk_ppp_iono.c
    src/pos/mrtk_lambda.c
    src/pos/mrtk_postpos.c
    src/pos/mrtk_sol.c
    src/pos/mrtk_opt.c
    src/pos/mrtk_options.c
    # ── stream/ (リアルタイム基盤) ─────────────────────────────────────────────
    src/stream/mrtk_stream.c
    src/stream/mrtk_rtksvr.c
    src/stream/mrtk_streamsvr.c
    # Generated at every build by mrtk_git_version_gen (see top of file).
    ${MRTK_GIT_VERSION_C}
)
add_dependencies(mrtklib mrtk_git_version_gen)
set_source_files_properties(${MRTK_GIT_VERSION_C} PROPERTIES GENERATED TRUE)
target_include_directories(mrtklib PUBLIC include ${CMAKE_BINARY_DIR}/include)
set_target_properties(mrtklib PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
# Override the global -ansi (= -std=c90) for this target: -std=c11 wins when placed after.
target_compile_options(mrtklib PRIVATE -std=c11)
target_compile_definitions(mrtklib PUBLIC
    ENAGLO ENAGAL ENAQZS ENACMP ENAIRN NFREQ=5 NEXOBS=5 TRACE
)
target_link_libraries(mrtklib PUBLIC m)
if(UNIX AND NOT APPLE)
    target_link_libraries(mrtklib PUBLIC rt)
endif()

# ── System LAPACK/BLAS integration for fast matrix operations ────────────────
# Request LP64 BLAS/LAPACK integer ABI; ILP64 silently breaks our int* wrappers (#108).
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
    if(DEFINED BLA_SIZEOF_INTEGER AND NOT "${BLA_SIZEOF_INTEGER}" STREQUAL "4")
        message(FATAL_ERROR
            "MRTKLIB requires LP64 BLAS/LAPACK (32-bit integer interface). "
            "Set -DBLA_SIZEOF_INTEGER=4 or unset it.")
    endif()
    set(BLA_SIZEOF_INTEGER 4 CACHE STRING
        "BLAS/LAPACK integer size (MRTKLIB requires 4)")
endif()

# Opt-in reproducible-numerics build: pin the BLAS provider to single-threaded
# OpenBLAS so every run is bit-identical. The default (this option OFF) uses
# whatever LAPACK CMake finds (Accelerate on macOS, system LAPACK on Linux),
# which is faster but whose threaded reductions are not run-to-run reproducible.
# Needed for stable benchmarking/regression of AR-margin (storm-day) cases.
option(MRTK_DETERMINISTIC_BLAS
    "Pin BLAS to single-threaded OpenBLAS for bit-reproducible results" OFF)
if(MRTK_DETERMINISTIC_BLAS)
    set(BLA_VENDOR OpenBLAS)
    message(STATUS "MRTK_DETERMINISTIC_BLAS=ON: pinning BLAS provider to OpenBLAS (single-threaded).")
endif()

find_package(LAPACK)
if(LAPACK_FOUND)
    target_compile_definitions(mrtklib PRIVATE LAPACK)
    target_link_libraries(mrtklib PRIVATE LAPACK::LAPACK)
    if(MRTK_DETERMINISTIC_BLAS)
        # BLA_VENDOR is only a hint to FindLAPACK: an explicit LAPACK_LIBRARIES/
        # BLAS_LIBRARIES override can still resolve a non-OpenBLAS provider, and
        # mrtk_mat.c references openblas_set_num_threads(), which would then
        # fail at link time with a confusing error. Verify the resolved
        # libraries actually are OpenBLAS before enabling the pin.
        string(TOLOWER "${LAPACK_LIBRARIES}" _mrtk_lapack_lower)
        if(NOT _mrtk_lapack_lower MATCHES "openblas")
            message(FATAL_ERROR
                "MRTK_DETERMINISTIC_BLAS=ON requires OpenBLAS, but the resolved "
                "LAPACK is: ${LAPACK_LIBRARIES}. Remove LAPACK_LIBRARIES/"
                "BLAS_LIBRARIES overrides or point CMAKE_PREFIX_PATH at an "
                "OpenBLAS install.")
        endif()
        unset(_mrtk_lapack_lower)
        # Force a single thread at load time (mrtk_mat.c constructor) so the
        # reduction order is deterministic; OpenBLAS otherwise threads by default.
        target_compile_definitions(mrtklib PRIVATE MRTK_DETERMINISTIC_BLAS)
        message(STATUS "Reproducible numerics: OpenBLAS forced to a single thread at startup.")
    endif()
    message(STATUS "Matrix backend: LAPACK/BLAS (${LAPACK_LIBRARIES})")
elseif(MRTK_DETERMINISTIC_BLAS)
    message(FATAL_ERROR
        "MRTK_DETERMINISTIC_BLAS=ON but OpenBLAS LAPACK was not found. "
        "Install OpenBLAS (e.g. 'brew install openblas') and/or pass "
        "-DCMAKE_PREFIX_PATH=<openblas-prefix>.")
else()
    message(WARNING "LAPACK not found. Falling back to internal (slower) matrix math.")
    message(STATUS "Matrix backend: internal LU")
endif()

# ── mrtk (unified CLI) ────────────────────────────────────────────────────────
add_executable(mrtk
    apps/mrtk/mrtk_main.c
    apps/rnx2rtkp/rnx2rtkp.c
    apps/rtkrcv/rtkrcv.c
    apps/rtkrcv/vt.c
    apps/str2str/str2str.c
    apps/convbin/convbin.c
    apps/ssr2obs/ssr2obs.c
    apps/ssr2osr/ssr2osr.c
    apps/recvbias/recvbias.c
    apps/dumpcssr/dumpcssr.c
    apps/dumpcssr/cssr_parse.c
    apps/cssr2rtcm3/cssr2rtcm3.c
    apps/l6extract/l6extract.c
)
target_include_directories(mrtk PRIVATE apps/rtkrcv)
target_compile_definitions(mrtk PRIVATE SVR_REUSEADDR)
target_link_libraries(mrtk PRIVATE mrtklib pthread)
target_link_options(mrtk PRIVATE $<$<OR:$<PLATFORM_ID:Linux>,$<PLATFORM_ID:FreeBSD>>:-rdynamic>)

# ── Install ───────────────────────────────────────────────────────────────────
# cmake --install build  →  copies binaries to bin/ (matches make install)
install(TARGETS mrtk RUNTIME DESTINATION bin)

# ── Utility tools (optional) ─────────────────────────────────────────────────
# Build with: cmake --preset default -DBUILD_UTILS=ON
option(BUILD_UTILS "Build utility tools" OFF)
if(BUILD_UTILS)
    # Tools that depend on mrtklib
    add_executable(mrtk_rnx2rtcm  util/rnx2rtcm/rnx2rtcm.c)
    # Excluded (depend on removed/changed internal types):
    #   diffeph, dumplex, convlex, dumpssr, outlexion — lex_t removed
    #   geniono, genstec, estiono, gengrid, rcvdcb    — sstat_t/lam changed
    #   simobs — lam/nav_t.ion/satpos signature changed
    #   margelog — unfinished draft code with many bugs

    set(_UTIL_TARGETS
        mrtk_rnx2rtcm
    )
    foreach(tgt IN LISTS _UTIL_TARGETS)
        target_link_libraries(${tgt} PRIVATE mrtklib)
    endforeach()

    # Standalone tools excluded: gencrc, genmsk, genxor
    #   — need stdint.h but global -ansi flag prevents it
endif()

# ── Doxygen documentation ────────────────────────────────────────────────────
# Build with: cmake --build build --target doc
find_package(Doxygen)
if(DOXYGEN_FOUND)
    configure_file(
        ${CMAKE_SOURCE_DIR}/Doxyfile.in
        ${CMAKE_BINARY_DIR}/Doxyfile
        @ONLY
    )
    add_custom_target(doc
        COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_BINARY_DIR}/Doxyfile
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        COMMENT "Generating API documentation with Doxygen"
        VERBATIM
    )
endif()

# ── Regression tests ──────────────────────────────────────────────────────────
# Run with: ctest --test-dir build --output-on-failure
# Skip slow rtkrcv test: ctest --test-dir build -LE realtime
enable_testing()

# ── Unit tests ───────────────────────────────────────────────────────────────
set(_UTEST_DIR ${CMAKE_SOURCE_DIR}/tests/utest)
set(_UTEST_SOURCES
    t_atmos t_clas_route t_coord t_freqidx t_geoid t_gloeph t_ionex
    t_lambda t_matrix t_misc t_obsdef t_ppp t_preceph
    t_rinex t_rinex4 t_sigcfg_decode t_time t_toml
    # t_tle excluded: tle_pos() implementation not yet in library
)
foreach(test IN LISTS _UTEST_SOURCES)
    add_executable(${test} ${_UTEST_DIR}/${test}.c)
    target_link_libraries(${test} PRIVATE mrtklib)
    add_test(NAME utest_${test} COMMAND ${test}
             WORKING_DIRECTORY ${_UTEST_DIR})
    set_tests_properties(utest_${test} PROPERTIES LABELS "utest")
endforeach()

# Galileo HAS RS(255,32,224) erasure decoder test. The source is named
# utest_has_rs.c (not t_*.c) and the test name must be utest_has_rs, so it is
# registered explicitly rather than via the t_* loop above (which prepends
# utest_). Self-contained, but linking the full library is fine.
add_executable(utest_has_rs ${_UTEST_DIR}/utest_has_rs.c)
# mrtk_has_rs.h is an internal (non-public) header under src/has/.
target_include_directories(utest_has_rs PRIVATE ${CMAKE_SOURCE_DIR}/src/has)
# Override global -ansi (=C90): this test uses C99 for-loop declarations (P-01).
target_compile_options(utest_has_rs PRIVATE -std=c11)
target_link_libraries(utest_has_rs PRIVATE mrtklib)
add_test(NAME utest_has_rs COMMAND utest_has_rs
         WORKING_DIRECTORY ${_UTEST_DIR})
set_tests_properties(utest_has_rs PROPERTIES LABELS "utest")

# Galileo HAS MT1 decoder hardening test. Drives the public has_input_page()
# API with synthetic recovered messages (no real signal data); same explicit
# registration as utest_has_rs (source name is utest_has_mt1.c, not t_*.c).
add_executable(utest_has_mt1 ${_UTEST_DIR}/utest_has_mt1.c)
# Override global -ansi (=C90): this test uses C99 for-loop declarations (P-01).
target_compile_options(utest_has_mt1 PRIVATE -std=c11)
target_link_libraries(utest_has_mt1 PRIVATE mrtklib)
add_test(NAME utest_has_mt1 COMMAND utest_has_mt1
         WORKING_DIRECTORY ${_UTEST_DIR})
set_tests_properties(utest_has_mt1 PROPERTIES LABELS "utest")

# RTCM local-correction lazy-allocation test (issue #295). Drives the public
# rtcm_lclblk()/encode/decode path for RTCM3 types 2001-2016 (no regression
# test feeds these messages); same explicit registration as utest_has_rs
# (source name is utest_lclblk.c, not t_*.c).
add_executable(utest_lclblk ${_UTEST_DIR}/utest_lclblk.c)
# Override global -ansi (=C90): this test uses C11 (P-01).
target_compile_options(utest_lclblk PRIVATE -std=c11)
target_link_libraries(utest_lclblk PRIVATE mrtklib)
add_test(NAME utest_lclblk COMMAND utest_lclblk
         WORKING_DIRECTORY ${_UTEST_DIR})
set_tests_properties(utest_lclblk PROPERTIES LABELS "utest")

# RTCM3 MSM BDS B2b round-trip test (issue #333 / #296)
add_executable(utest_msm_bds_b2b ${_UTEST_DIR}/utest_msm_bds_b2b.c)
# Override global -ansi (=C90): this test uses C11 (P-01).
target_compile_options(utest_msm_bds_b2b PRIVATE -std=c11)
target_link_libraries(utest_msm_bds_b2b PRIVATE mrtklib)
add_test(NAME utest_msm_bds_b2b COMMAND utest_msm_bds_b2b
         WORKING_DIRECTORY ${_UTEST_DIR})
set_tests_properties(utest_msm_bds_b2b PROPERTIES LABELS "utest")

# NTRIP v2 chunked codec test (header-only, no library dependency)
add_executable(t_ntrip ${_UTEST_DIR}/t_ntrip.c)
# Override global -ansi (=C90) for this target, same as mrtklib target.
# Note: set_property(C_STANDARD 11) and target_compile_features(c_std_11) do NOT
# override -ansi from add_compile_options(); explicit -std=c11 is required.
target_compile_options(t_ntrip PRIVATE -std=c11)
add_test(NAME utest_t_ntrip COMMAND t_ntrip
         WORKING_DIRECTORY ${_UTEST_DIR})
set_tests_properties(utest_t_ntrip PROPERTIES LABELS "utest")

# ── RINEX 4 test data fixture (decompress BRD400 NAV) ────────────────────────
set(_RINEX4_DIR ${CMAKE_SOURCE_DIR}/tests/data/rinex4)
set(_BRD400_GZ  ${_RINEX4_DIR}/BRD400DLR_S_20260680000_01D_MN.rnx.gz)
set(_BRD400_RNX ${_RINEX4_DIR}/BRD400DLR_S_20260680000_01D_MN.rnx)
add_test(NAME rinex4_setup
    COMMAND bash -c "gunzip -kf ${_BRD400_GZ}"
)
set_tests_properties(rinex4_setup PROPERTIES
    FIXTURES_SETUP  rinex4_data
    LABELS          "utest"
    TIMEOUT         10
)
add_test(NAME rinex4_cleanup
    COMMAND ${CMAKE_COMMAND} -E rm -f ${_BRD400_RNX}
)
set_tests_properties(rinex4_cleanup PROPERTIES
    FIXTURES_CLEANUP rinex4_data
    LABELS           "utest"
    TIMEOUT          5
)
set_tests_properties(utest_t_rinex4 PROPERTIES
    FIXTURES_REQUIRED rinex4_data
    TIMEOUT           30
)

# ── Python-based comparison tool ─────────────────────────────────────────────
find_package(Python3 COMPONENTS Interpreter)
set(_COMPARE_POS      ${CMAKE_SOURCE_DIR}/scripts/tests/compare_pos.py)
set(_COMPARE_POS_ABS  ${CMAKE_SOURCE_DIR}/scripts/tests/compare_pos_abs.py)
set(_COMPARE_NMEA_ABS ${CMAKE_SOURCE_DIR}/scripts/tests/compare_nmea_abs.py)
set(_CHECK_SCATTER    ${CMAKE_SOURCE_DIR}/scripts/tests/check_pos_scatter.py)

# ── Regression test data ─────────────────────────────────────────────────────
set(_TESTDATA  ${CMAKE_SOURCE_DIR}/tests/data/malib)
set(_REFDATA   ${CMAKE_SOURCE_DIR}/tests/data/malib)
set(_TESTCMAKE ${CMAKE_SOURCE_DIR}/tests/cmake)

# ── Fixture: extract / clean test data archive ───────────────────────────────
# Archive has ./data/ prefix internally; --strip-components=2 removes it.
add_test(NAME regression_setup
    COMMAND bash -c "tar xzf ${_TESTDATA}/MALIB_OSS_data.tar.gz --strip-components=2 -C ${_TESTDATA}"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(regression_setup PROPERTIES
    FIXTURES_SETUP  regression_data
    TIMEOUT         30
)

# ── Fixture: download IONEX TEC file (for recvbias) ──────────────────────────
add_test(NAME regression_download_tec
    COMMAND ${CMAKE_COMMAND}
        -DTESTDATA_DIR=${_TESTDATA}
        -P ${_TESTCMAKE}/download_tec.cmake
)
set_tests_properties(regression_download_tec PROPERTIES
    FIXTURES_SETUP  regression_data
    TIMEOUT         60
)

add_test(NAME regression_cleanup
    COMMAND ${CMAKE_COMMAND}
        -DTESTDATA_DIR=${_TESTDATA}
        -P ${_TESTCMAKE}/cleanup_testdata.cmake
)
set_tests_properties(regression_cleanup PROPERTIES
    FIXTURES_CLEANUP regression_data
)

# ── rnx2rtkp SPP regression ───────────────────────────────────────────────────
add_test(NAME rnx2rtkp_spp
    COMMAND $<TARGET_FILE:mrtk> post
        -p 0 -sys G,R,E,J
        -o ${CMAKE_BINARY_DIR}/out_spp.pos
        ${_TESTDATA}/MALIB_OSS_data_obsnav_240822-1100.obs
        ${_TESTDATA}/MALIB_OSS_data_obsnav_240822-1100.nav
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(rnx2rtkp_spp PROPERTIES
    FIXTURES_REQUIRED regression_data
    TIMEOUT           60
    LABELS            "regression;spp"
)

add_test(NAME rnx2rtkp_spp_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS}
        ${_REFDATA}/MALIB_OSS_data_obsnav_240822-1100.pp.spp.pos
        ${CMAKE_BINARY_DIR}/out_spp.pos
        --tolerance 0.005
)
set_tests_properties(rnx2rtkp_spp_check PROPERTIES
    DEPENDS           rnx2rtkp_spp
    FIXTURES_REQUIRED regression_data
    LABELS            "regression;spp"
)

# ── recvbias regression ──────────────────────────────────────────────────────
# Note: recvbias returns exit code 1 on success (non-standard).
# The bash wrapper normalises this so CTest sees exit code 0.
add_test(NAME recvbias_rb
    COMMAND bash -c "\"$@\"; rc=$?; [ $rc -eq 1 ] && exit 0 || exit $rc" _
        $<TARGET_FILE:mrtk> bias
        -td 2024/8/22
        -nav ${_TESTDATA}/MALIB_OSS_data_obsnav_240822-1100.nav
        -tec ${_TESTDATA}/COD0OPSFIN_20242350000_01D_01H_GIM.INX
        -scb ${_TESTDATA}/2024235L.209.l6
        -pos -3961440.433,3308948.812,3734425.963
        -el 30
        -out ${CMAKE_BINARY_DIR}/out_rb.bia
        ${_TESTDATA}/MALIB_OSS_data_obsnav_240822-1100.obs
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(recvbias_rb PROPERTIES
    FIXTURES_REQUIRED regression_data
    TIMEOUT           120
    LABELS            "regression;recvbias"
)

add_test(NAME recvbias_rb_check
    COMMAND ${CMAKE_COMMAND}
        -DOUTPUT=${CMAKE_BINARY_DIR}/out_rb.bia
        -DREFERENCE=${_REFDATA}/MALIB_OSS_data_obsnav_240822-1100.rb.bia
        -DTHRESHOLD=90
        -P ${_TESTCMAKE}/compare_bia.cmake
)
set_tests_properties(recvbias_rb_check PROPERTIES
    DEPENDS           recvbias_rb
    FIXTURES_REQUIRED regression_data
    LABELS            "regression;recvbias"
)

# ── rtkrcv RT-PPP regression ──────────────────────────────────────────────────
# Slower test (~60-120s): replays SBF files at x10 speed via rtkrcv file stream.
# Uses a line-count threshold (≥90% of reference) rather than exact comparison
# because real-time cycle timing can vary slightly between runs.
add_test(NAME rtkrcv_rt
    COMMAND bash ${_TESTCMAKE}/run_rtkrcv_test.sh
        $<TARGET_FILE:mrtk> run
        ${CMAKE_SOURCE_DIR}
        ${_REFDATA}/MALIB_OSS_data_obsnav_240822-1100.rt.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(rtkrcv_rt PROPERTIES
    FIXTURES_REQUIRED regression_data
    TIMEOUT           300
    LABELS            "regression;realtime"
    RESOURCE_LOCK     rtkrcv_port
)

# ═══════════════════════════════════════════════════════════════════════════════
# MADOCALIB regression tests
# ═══════════════════════════════════════════════════════════════════════════════
# Data: MIZU station, 2025/04/01 Session A (00:00-00:59)
# Source: upstream/madocalib/sample_data
# Test data archive: tests/data/madocalib/madocalib_testdata.tar.gz
#
# Tests:
#   madocalib_ppp         PPP (sample.conf)           L6E mdc-004
#   madocalib_pppar       PPP-AR (sample_pppar.conf)  L6E mdc-004
#   madocalib_pppar_003   PPP-AR (sample_pppar.conf)  L6E mdc-003
#   madocalib_pppar_ion   PPP-AR+iono (sample_pppar_iono.conf) L6E+L6D mdc-004
#   madocalib_cssr2ssr    CSSR→SSR conversion (cssr2ssr, not yet ported)
set(_MDLDATA  ${CMAKE_SOURCE_DIR}/tests/data/madocalib)

# ── Fixture: extract / clean madocalib test data archive ─────────────────────
add_test(NAME madocalib_setup
    COMMAND bash -c "tar xzf ${_MDLDATA}/madocalib_testdata.tar.gz -C ${_MDLDATA}"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_setup PROPERTIES
    FIXTURES_SETUP  madocalib_data
    TIMEOUT         30
    LABELS          "regression;madocalib"
)

add_test(NAME madocalib_cleanup
    COMMAND bash -c "rm -f \
        ${_MDLDATA}/MIZU00JPN_R_20250910000_01D_30S_MO.obs \
        ${_MDLDATA}/BRDM00DLR_S_20250910000_01D_MN.rnx \
        ${_MDLDATA}/2025091A.204.l6 \
        ${_MDLDATA}/2025091A.206.l6 \
        ${_MDLDATA}/2025091A.204.mdc003.l6 \
        ${_MDLDATA}/2025091A.206.mdc003.l6 \
        ${_MDLDATA}/2025091A.200.l6 \
        ${_MDLDATA}/2025091A.201.l6 \
        ${_MDLDATA}/igs20.atx"
)
set_tests_properties(madocalib_cleanup PROPERTIES
    FIXTURES_CLEANUP madocalib_data
    LABELS           "regression;madocalib"
)

# ── rnx2rtkp MADOCA-PPP regression ──────────────────────────────────────────
add_test(NAME madocalib_ppp
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/madocalib/rnx2rtkp.toml
        -ts 2025/04/01 0:00:00 -te 2025/04/01 0:59:30 -ti 30
        -o ${CMAKE_BINARY_DIR}/out_madocalib_ppp.pos
        ${_MDLDATA}/MIZU00JPN_R_20250910000_01D_30S_MO.obs
        ${_MDLDATA}/BRDM00DLR_S_20250910000_01D_MN.rnx
        ${_MDLDATA}/2025091A.204.l6
        ${_MDLDATA}/2025091A.206.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_ppp PROPERTIES
    FIXTURES_REQUIRED madocalib_data
    TIMEOUT           180
    LABELS            "regression;madocalib;ppp"
)

add_test(NAME madocalib_ppp_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS}
        ${_MDLDATA}/madocalib_MIZU_20250401-0000.pp.pos
        ${CMAKE_BINARY_DIR}/out_madocalib_ppp.pos
        --tolerance 0.005
)
set_tests_properties(madocalib_ppp_check PROPERTIES
    DEPENDS           madocalib_ppp
    FIXTURES_REQUIRED madocalib_data
    LABELS            "regression;madocalib;ppp"
)

# ── rnx2rtkp MADOCA PPP-AR regression (mdc-004) ─────────────────────────────
add_test(NAME madocalib_pppar
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/madocalib/rnx2rtkp_pppar.toml
        -ts 2025/04/01 0:00:00 -te 2025/04/01 0:59:30 -ti 30
        -o ${CMAKE_BINARY_DIR}/out_madocalib_pppar.pos
        ${_MDLDATA}/MIZU00JPN_R_20250910000_01D_30S_MO.obs
        ${_MDLDATA}/BRDM00DLR_S_20250910000_01D_MN.rnx
        ${_MDLDATA}/2025091A.204.l6
        ${_MDLDATA}/2025091A.206.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_pppar PROPERTIES
    FIXTURES_REQUIRED madocalib_data
    TIMEOUT           180
    LABELS            "regression;madocalib;pppar"
)

# Upstream-parity tolerances are the same in every build (#302). The references
# come from upstream's internal-LU build; MRTKLIB's matrix backend (internal LU,
# Accelerate, OpenBLAS) shifts results at ULP level, which flips PPP-AR fix
# decisions at marginal epochs of this dataset (one flip is 17-24 cm). Measured
# worst case across the three backends: pppar 1.56 cm, pppar_ion 3.40 cm;
# tolerance = ~1.3x that, rounded up. Accuracy is gated by the *_abs_check tests.
set(_PPPAR_TOL     0.020)
set(_PPPAR_ION_TOL 0.050)
# PPP-AR integer-fix rate (Q=1 only), one-sided: fixing more often than
# upstream passes. Measured worst drop across backends: -3.39 % (4 of 118
# epochs, pppar_ion); 5 % allows 5 epochs.
set(_PPPAR_FIX_DROP 5.0)

add_test(NAME madocalib_pppar_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS}
        ${_MDLDATA}/madocalib_MIZU_20250401-0000.pppar.pos
        ${CMAKE_BINARY_DIR}/out_madocalib_pppar.pos
        --tolerance ${_PPPAR_TOL}
        --fix-q 1 --max-fix-drop ${_PPPAR_FIX_DROP}
)
set_tests_properties(madocalib_pppar_check PROPERTIES
    DEPENDS           madocalib_pppar
    FIXTURES_REQUIRED madocalib_data
    LABELS            "regression;madocalib;pppar"
)

# ── rnx2rtkp MADOCA PPP-AR regression (mdc-003) ─────────────────────────────
add_test(NAME madocalib_pppar_003
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/madocalib/rnx2rtkp_pppar.toml
        -ts 2025/04/01 0:00:00 -te 2025/04/01 0:59:30 -ti 30
        -o ${CMAKE_BINARY_DIR}/out_madocalib_pppar_003.pos
        ${_MDLDATA}/MIZU00JPN_R_20250910000_01D_30S_MO.obs
        ${_MDLDATA}/BRDM00DLR_S_20250910000_01D_MN.rnx
        ${_MDLDATA}/2025091A.204.mdc003.l6
        ${_MDLDATA}/2025091A.206.mdc003.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_pppar_003 PROPERTIES
    FIXTURES_REQUIRED madocalib_data
    TIMEOUT           180
    LABELS            "regression;madocalib;pppar"
)

add_test(NAME madocalib_pppar_003_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS}
        ${_MDLDATA}/madocalib_MIZU_20250401-0000.pppar_003.pos
        ${CMAKE_BINARY_DIR}/out_madocalib_pppar_003.pos
        --tolerance 0.005
        --fix-q 1 --max-fix-drop ${_PPPAR_FIX_DROP}
)
set_tests_properties(madocalib_pppar_003_check PROPERTIES
    DEPENDS           madocalib_pppar_003
    FIXTURES_REQUIRED madocalib_data
    LABELS            "regression;madocalib;pppar"
)

# ── rnx2rtkp MADOCA PPP-AR + ionosphere regression ──────────────────────────
# Uses L6E (.204/.206) + L6D (.200/.201) for ionospheric correction
add_test(NAME madocalib_pppar_ion
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/madocalib/rnx2rtkp_pppar_iono.toml
        -ts 2025/04/01 0:00:00 -te 2025/04/01 0:59:30 -ti 30
        -o ${CMAKE_BINARY_DIR}/out_madocalib_pppar_ion.pos
        ${_MDLDATA}/MIZU00JPN_R_20250910000_01D_30S_MO.obs
        ${_MDLDATA}/BRDM00DLR_S_20250910000_01D_MN.rnx
        ${_MDLDATA}/2025091A.204.l6
        ${_MDLDATA}/2025091A.206.l6
        ${_MDLDATA}/2025091A.200.l6
        ${_MDLDATA}/2025091A.201.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_pppar_ion PROPERTIES
    FIXTURES_REQUIRED madocalib_data
    TIMEOUT           180
    LABELS            "regression;madocalib;pppar"
)

add_test(NAME madocalib_pppar_ion_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS}
        ${_MDLDATA}/madocalib_MIZU_20250401-0000.pppar_ion.pos
        ${CMAKE_BINARY_DIR}/out_madocalib_pppar_ion.pos
        --tolerance ${_PPPAR_ION_TOL}
        --fix-q 1 --max-fix-drop ${_PPPAR_FIX_DROP}
)
set_tests_properties(madocalib_pppar_ion_check PROPERTIES
    DEPENDS           madocalib_pppar_ion
    FIXTURES_REQUIRED madocalib_data
    LABELS            "regression;madocalib;pppar"
)

# ── Absolute accuracy checks: MADOCA-PPP vs SINEX MIZU true coordinate ─────────
# Upstream-parity checks above confirm the port; these measure accuracy, which
# stays meaningful when MRTKLIB diverges from (or improves on) upstream (#302).
# Reference: IGS weekly SINEX, GPS week 2360 (2025/03/30-04/05), which contains
#   the 2025/04/01 data epoch, so no velocity propagation is needed. MIZU-only
#   extract of IGS0OPSSNX_20250890000_07D_07D_SOL.SNX.gz.
# Evaluation: 2D horizontal 1σ and 95th percentile after the first 60 epochs
#   (30 min convergence). PPP vertical is troposphere-limited, so 2D is used.
# Tolerance: ~2x the worst 2D 95th percentile measured across internal LU /
#   Accelerate / OpenBLAS builds, rounded up to the cm:
#     ppp 24.4 cm, pppar 2.66 cm, pppar_003 3.65 cm, pppar_ion 2.75 cm.
set(_SINEX_MIZU ${_MDLDATA}/IGS0OPSSNX_20250890000_07D_07D_SOL_MIZU.snx)
add_test(NAME madocalib_ppp_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --sinex ${_SINEX_MIZU}
        --station MIZU
        --tolerance 0.50
        --skip-epochs 60
        --use-2d
        ${CMAKE_BINARY_DIR}/out_madocalib_ppp.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_ppp_abs_check PROPERTIES
    DEPENDS           madocalib_ppp
    FIXTURES_REQUIRED madocalib_data
    LABELS            "absolute;madocalib;ppp"
)

add_test(NAME madocalib_pppar_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --sinex ${_SINEX_MIZU}
        --station MIZU
        --tolerance 0.06
        --skip-epochs 60
        --use-2d
        ${CMAKE_BINARY_DIR}/out_madocalib_pppar.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_pppar_abs_check PROPERTIES
    DEPENDS           madocalib_pppar
    FIXTURES_REQUIRED madocalib_data
    LABELS            "absolute;madocalib;pppar"
)

add_test(NAME madocalib_pppar_003_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --sinex ${_SINEX_MIZU}
        --station MIZU
        --tolerance 0.08
        --skip-epochs 60
        --use-2d
        ${CMAKE_BINARY_DIR}/out_madocalib_pppar_003.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_pppar_003_abs_check PROPERTIES
    DEPENDS           madocalib_pppar_003
    FIXTURES_REQUIRED madocalib_data
    LABELS            "absolute;madocalib;pppar"
)

add_test(NAME madocalib_pppar_ion_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --sinex ${_SINEX_MIZU}
        --station MIZU
        --tolerance 0.06
        --skip-epochs 60
        --use-2d
        ${CMAKE_BINARY_DIR}/out_madocalib_pppar_ion.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_pppar_ion_abs_check PROPERTIES
    DEPENDS           madocalib_pppar_ion
    FIXTURES_REQUIRED madocalib_data
    LABELS            "absolute;madocalib;pppar"
)

# ── cssr2ssr regression (not yet ported) ─────────────────────────────────────
# TODO: cssr2ssr binary is not yet integrated into MRTKLIB.
#       When ported, add:
#   add_test(NAME madocalib_cssr2ssr
#       COMMAND $<TARGET_FILE:cssr2ssr>
#           ${_MDLDATA}/2025091A.204.l6
#           -o ${CMAKE_BINARY_DIR}/out_cssr2ssr.rtcm3
#           -td 2025/04/01 -d
#       WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
#   )

# ═══════════════════════════════════════════════════════════════════════════════
# CLASLIB PPP-RTK regression tests
# ═══════════════════════════════════════════════════════════════════════════════
# Data: 0627 station, 2019/08/27 Session Q (16:00-17:00) and 2019/12/15
# Source: upstream/claslib/data
# Test data archive: tests/data/claslib/claslib_testdata.tar.gz
#
# Tests (8/8 upstream claslib coverage):
#   claslib_ppp_rtk         PPP-RTK with L6 (RINEX obs, 2019/239)
#   claslib_ppp_rtk_bnx     PPP-RTK with L6 (BINEX obs, 2019/239)
#   claslib_ppp_rtk_2ch     PPP-RTK with 2-channel L6 (2025/157)
#   claslib_ppp_rtk_2ch_merge_diff_check
#                           Sensitivity guard: 2ch merge output must differ from
#                           ch1-only output (catches a dead/no-op merge, #303 A-4)
#   claslib_ppp_rtk_l6mrg_1file
#                           PPP-RTK with l6_merge but only one L6 file (2025/157)
#   claslib_ppp_rtk_st12    PPP-RTK with SubType 12 (BINEX obs, 2019/349)
#   claslib_ppp_rtk_l1cl5   PPP-RTK with L1C+L5 frequency (2025/022)
#   claslib_vrs_rtk         VRS-RTK (2019/239)
#   claslib_vrs_rtk_st12    VRS-RTK with SubType 12 (2019/349)
#   claslib_vrs_rtk_2ch     VRS-RTK with 2-channel (2025/157)
#   rtkrcv_rt_clas          Real-time CLAS PPP-RTK (BINEX+L6, rtkrcv file replay)
#   rtkrcv_rt_clas_2ch      Real-time CLAS PPP-RTK dual-channel L6, single-channel
#                           engine (l6_merge pinned to 0; rtkrcv file replay)
#   rtkrcv_rt_clas_2ch_merge
#                           Same replay with the shipped l6_merge = 1 default (#309)
#   rtkrcv_rt_clas_2ch_merge_nsat_check
#                           Sensitivity guard: the merged run must track more
#                           satellites per fixed epoch than the single-channel run
set(_CLSDATA  ${CMAKE_SOURCE_DIR}/tests/data/claslib)
set(_CLSRTREFDATA ${CMAKE_BINARY_DIR}/tests/data/claslib)
set(_COMPARE_NMEA ${CMAKE_SOURCE_DIR}/scripts/tests/compare_nmea.py)
# GSI F5 daily-coordinate truth for station 0627 (TSUKUBA3), used by the
# *_abs_check tests.  2019 = F3/ITRF2005, 2025 = F5/ITRF2014 (set as _F5_0627
# further down).  All CLAS PPP-RTK/VRS datasets are this station, so each has a
# geodetic-accuracy guard, not only consistency against upstream claslib output.
set(_F5_0627_2019 ${_CLSDATA}/960627.19.pos)

# ── Fixture: extract / clean claslib test data archive ────────────────────────
add_test(NAME claslib_setup
    COMMAND bash -c "tar xzf \"${_CLSDATA}/claslib_testdata.tar.gz\" \
        --exclude=ref_L6_2CH_RT.nmea --exclude=rtkrcv_clas_ref.nmea -C \"${_CLSDATA}\" && \
        mkdir -p \"${_CLSRTREFDATA}\" && \
        tar xzf \"${_CLSDATA}/claslib_testdata.tar.gz\" -C \"${_CLSRTREFDATA}\" \
        ref_L6_2CH_RT.nmea rtkrcv_clas_ref.nmea"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_setup PROPERTIES
    FIXTURES_SETUP  claslib_data
    TIMEOUT         60
    LABELS          "regression;claslib"
)

add_test(NAME claslib_cleanup
    COMMAND bash -c "rm -f \
        ${_CLSDATA}/0627239Q.obs \
        ${_CLSDATA}/0627239Q.bnx \
        ${_CLSDATA}/0627349AB.bnx \
        ${_CLSDATA}/0627157U.bnx \
        ${_CLSDATA}/0627022Q.bnx \
        ${_CLSDATA}/sept_2019239.nav \
        ${_CLSDATA}/sept_2019349.nav \
        ${_CLSDATA}/0627157U.nav \
        ${_CLSDATA}/2019239Q.l6 \
        ${_CLSDATA}/2019349B.l6 \
        ${_CLSDATA}/2025157U_ch1.l6 \
        ${_CLSDATA}/2025157U_ch2.l6 \
        ${_CLSDATA}/2025022Q.l6 \
        ${_CLSDATA}/clas_grid.def \
        ${_CLSDATA}/clas_grid.blq \
        ${_CLSDATA}/igu00p01.erp \
        ${_CLSDATA}/igs14_L5copy.atx \
        ${_CLSDATA}/isb.tbl \
        ${_CLSDATA}/l2csft.tbl \
        ${_CLSDATA}/vrs2019239Q.obs \
        ${_CLSDATA}/vrs2019349B.obs \
        ${_CLSDATA}/2025157U_ch1.obs \
        ${_CLSDATA}/2025157U_ch2.obs \
        ${_CLSDATA}/0627239Q.bnx.tag \
        ${_CLSDATA}/2019239Q.l6.tag \
        ${_CLSDATA}/0627157U.bnx.tag \
        ${_CLSDATA}/2025157U_ch1.l6.tag \
        ${_CLSDATA}/2025157U_ch2.l6.tag \
        \"${_CLSRTREFDATA}/rtkrcv_clas_ref.nmea\" \
        \"${_CLSRTREFDATA}/ref_L6_2CH_RT.nmea\""
)
set_tests_properties(claslib_cleanup PROPERTIES
    FIXTURES_CLEANUP claslib_data
    LABELS           "regression;claslib"
)

# ── rnx2rtkp CLAS PPP-RTK regression (L6, RINEX obs) ─────────────────────────
# Data: 2019/08/27 16:00-17:00, 0627239Q.obs + sept_2019239.nav + 2019239Q.l6
# Reference: ref_L6.nmea (upstream claslib output)
add_test(NAME claslib_ppp_rtk
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk.nmea
        ${_CLSDATA}/0627239Q.obs
        ${_CLSDATA}/sept_2019239.nav
        ${_CLSDATA}/2019239Q.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_L6.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_ppp_rtk_check PROPERTIES
    DEPENDS           claslib_ppp_rtk
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# Absolute accuracy vs GSI F5 (TSUKUBA3, 2019/08/27). The check above only
# guards consistency with upstream claslib; this guards geodetic truth.
# Measured 2D 95% = 4.3 cm; tolerance 0.09 m = measured + ~4 cm cross-platform
# (LAPACK-vs-LU) headroom, tight enough to catch a real accuracy regression.
add_test(NAME claslib_ppp_rtk_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627_2019}
        --date 2019/08/27
        --tolerance 0.090
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_abs_check PROPERTIES
    DEPENDS           claslib_ppp_rtk
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;ppp-rtk"
)

# ── CLAS PPP-RTK periodic filter reset (#264) ────────────────────────────────
# reset_interval overlay forces a full reset every 600 s over the 1 h window
# (5 resets). -x 3 writes the trace next to the output; the checker asserts the
# resets fired, each showed as a Single epoch, and the filter re-converged.
add_test(NAME claslib_ppp_rtk_regularly
    COMMAND $<TARGET_FILE:mrtk> post
        -x 3
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp.toml
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_regularly.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_regularly.nmea
        ${_CLSDATA}/0627239Q.obs
        ${_CLSDATA}/sept_2019239.nav
        ${_CLSDATA}/2019239Q.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_regularly PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_regularly_check
    COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/tests/check_regularly_reset.py
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_regularly.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_regularly.nmea.trace
        --min-resets 4
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_regularly_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_regularly
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# ── rnx2rtkp CLAS PPP-RTK regression (L6, BINEX obs) ─────────────────────────
# Data: 2019/08/27 16:00-17:00, 0627239Q.bnx + sept_2019239.nav + 2019239Q.l6
# Reference: ref_L6_bnx.nmea (upstream claslib output)
add_test(NAME claslib_ppp_rtk_bnx
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_bnx.nmea
        ${_CLSDATA}/0627239Q.bnx
        ${_CLSDATA}/sept_2019239.nav
        ${_CLSDATA}/2019239Q.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_bnx PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_bnx_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_L6_bnx.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_bnx.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_ppp_rtk_bnx_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_bnx
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# Absolute accuracy vs GSI F5 (TSUKUBA3, 2019/08/27). Measured 2D 95% = 4.2 cm;
# tolerance 0.09 m (measured + ~4 cm cross-platform headroom).
add_test(NAME claslib_ppp_rtk_bnx_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627_2019}
        --date 2019/08/27
        --tolerance 0.090
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_bnx.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_bnx_abs_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_bnx
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;ppp-rtk"
)

# ── rnx2rtkp CLAS PPP-RTK single-frequency (nf=1) capability regression ───────
# Same 2019/239 dataset processed with an L1-only signal set
# (conf/claslib/rnx2rtkp_sf.toml, signals=["G1C","E1C","J1C"] -> nf=1). Locks in
# that the v0.7.4+ engine handles single-frequency CLAS PPP-RTK without any
# dedicated SF machinery (the per-satellite iono states are bounded by the
# ionosphere Gauss-Markov decay). See docs/design/sf-ppp-rtk.md.
add_test(NAME claslib_ppp_rtk_sf
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_sf.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_sf.nmea
        ${_CLSDATA}/0627239Q.bnx
        ${_CLSDATA}/sept_2019239.nav
        ${_CLSDATA}/2019239Q.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_sf PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_sf_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_L6_SF.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_sf.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_ppp_rtk_sf_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_sf
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# Absolute accuracy vs GSI F5 (TSUKUBA3, 2019/08/27) — independent truth.
# Measured (v0.7.4 engine): 2D 1σ 2.5 cm / 95% 3.9 cm; tolerance 0.08 m
# (measured + ~4 cm cross-platform headroom).
add_test(NAME claslib_ppp_rtk_sf_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627_2019}
        --date 2019/08/27
        --tolerance 0.080
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_sf.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_sf_abs_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_sf
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;ppp-rtk"
)

# ── Fixture: extract / clean F9P band-mismatch test data ──────────────────────
# Separate small archive (rover obs/nav + L6); the CLAS aux files (grid/blq/erp/
# atx/isb/l2csft) are reused from the main claslib_data fixture.
add_test(NAME claslib_f9p_setup
    COMMAND bash -c "tar xzf ${_CLSDATA}/claslib_f9p_testdata.tar.gz -C ${_CLSDATA}"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_f9p_setup PROPERTIES
    FIXTURES_SETUP  claslib_f9p_data
    TIMEOUT         60
    LABELS          "regression;claslib"
)

add_test(NAME claslib_f9p_cleanup
    COMMAND bash -c "rm -f \
        ${_CLSDATA}/F9P0121a.obs \
        ${_CLSDATA}/F9P0121a.nav \
        ${_CLSDATA}/2026121A.l6"
)
set_tests_properties(claslib_f9p_cleanup PROPERTIES
    FIXTURES_CLEANUP claslib_f9p_data
    LABELS           "regression;claslib"
)

# ── rnx2rtkp CLAS PPP-RTK regression: Galileo E1/E5b band mismatch ────────────
# u-blox ZED-F9P (ECJ02), 2026/05/01, 1 h. The receiver tracks Galileo E1+E5b
# (E7Q) while CLAS corrects only E5a (E5Q): the E5b band has no matching bias,
# is dropped per-frequency in clas_osr_zdres, and the affected Galileo
# satellites run E1-only. Locks in that the v0.7.4+ engine handles this
# mismatch without any dedicated machinery. See docs/design/sf-ppp-rtk.md.
add_test(NAME claslib_ppp_rtk_f9p
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_f9p.toml
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_f9p.nmea
        ${_CLSDATA}/F9P0121a.obs
        ${_CLSDATA}/F9P0121a.nav
        ${_CLSDATA}/2026121A.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_f9p PROPERTIES
    FIXTURES_REQUIRED "claslib_data;claslib_f9p_data"
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_f9p_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_F9P0121a.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_f9p.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_ppp_rtk_f9p_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_f9p
    FIXTURES_REQUIRED "claslib_data;claslib_f9p_data"
    LABELS            "regression;claslib;ppp-rtk"
)

# Absolute accuracy vs the ECJ02 IGS-products coordinate — independent truth.
# Same fixed LLH as igs_iflc_ppp_check (derived from IGS/MGEX final products,
# #142), cross-checked against a 3-hour MADOCA-PPP float solution (agreement
# within the float scatter). Measured (v0.7.4 engine, after 150-epoch
# convergence skip): 3D 1σ 18.5 cm / 95% 20.6 cm — the offset is E/Up-loaded
# (CLAS-vs-products inter-technique difference on this receiver); tolerance
# 0.30 m (measured + ~9 cm headroom).
add_test(NAME claslib_ppp_rtk_f9p_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --llh 35.666334193,139.792201653,59.8539
        --use-3d
        --skip-epochs 150
        --tolerance 0.30
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_f9p.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_f9p_abs_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_f9p
    FIXTURES_REQUIRED "claslib_data;claslib_f9p_data"
    LABELS            "absolute;claslib;ppp-rtk"
)

# ── rnx2rtkp CLAS PPP-RTK regression (dual-channel L6) ────────────────────────
# Data: 2025/06/06 20:00-21:00, 0627157U.bnx + 2025157U_ch1.l6 + ch2.l6
# Reference: ref_L6_2CH.nmea (upstream claslib output with -l6msg 1)
add_test(NAME claslib_ppp_rtk_2ch
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp.toml
        -ts 2025/06/06 20:00:00 -te 2025/06/06 20:59:59 -ti 1
        -l6msg 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_2ch.nmea
        ${_CLSDATA}/0627157U.bnx
        ${_CLSDATA}/0627157U.nav
        ${_CLSDATA}/2025157U_ch1.l6
        ${_CLSDATA}/2025157U_ch2.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_2ch PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

# Tolerance 0.10m: post-#305 dual-channel merge measures 4.543cm 3D RMS / fix
# rate identical to reference (99.86%) -- ~2.2x headroom, matching the
# ch1-only sibling claslib_ppp_rtk_l6mrg_1file_check's 0.10m/4.11cm precedent.
# Fix-rate check is no longer skipped: the merge now reproduces the
# reference's fix rate exactly, so a degradation is a real regression signal.
add_test(NAME claslib_ppp_rtk_2ch_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_L6_2CH.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_2ch.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_ppp_rtk_2ch_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_2ch
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# ── Absolute accuracy check: CLAS PPP-RTK 2CH vs GSI F5 TSUKUBA3 ─────────────
# Reference: GSI F5 station 960627 (TSUKUBA3/0627), 15-day median centred on
# 2025/06/06 (DOY 157).  Pass/fail on 2D horizontal accuracy (1σ, 95%).
# Measured 2D 95% = 5.4 cm; tolerance 0.10 m = measured + ~4 cm cross-platform
# (LAPACK-vs-LU) headroom.  (Tightened from a stale 0.30 m "plausibility"
# threshold whose ~26 cm / 88%-fix premise no longer holds.)
set(_F5_0627 ${_CLSDATA}/960627.25.pos)
add_test(NAME claslib_ppp_rtk_2ch_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627}
        --date 2025/06/06
        --tolerance 0.100
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_2ch.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_2ch_abs_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_2ch
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;ppp-rtk"
)

# ── rnx2rtkp CLAS PPP-RTK with l6_merge but a single L6 source (#303) ─────────
# Same dataset/window as claslib_ppp_rtk_2ch, but only ch1 is supplied while
# -l6msg 1 stays on: the correction fetch must skip the empty channel instead of
# degrading every epoch to Single.  Reference is the upstream 2-channel output,
# which the single-channel solution tracks to ~4 cm 3D RMS.
add_test(NAME claslib_ppp_rtk_l6mrg_1file
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp.toml
        -ts 2025/06/06 20:00:00 -te 2025/06/06 20:59:59 -ti 1
        -l6msg 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_l6mrg_1file.nmea
        ${_CLSDATA}/0627157U.bnx
        ${_CLSDATA}/0627157U.nav
        ${_CLSDATA}/2025157U_ch1.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_l6mrg_1file PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_l6mrg_1file_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_L6_2CH.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_l6mrg_1file.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_ppp_rtk_l6mrg_1file_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_l6mrg_1file
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# ── Sensitivity guard: the 2ch merge must actually change the solution ───────
# claslib_ppp_rtk_2ch_check / claslib_ppp_rtk_l6mrg_1file_check alone cannot
# tell a working 2-channel merge from a dead one that silently degrades to
# ch1-only: both would still pass their tolerance-vs-reference checks. This
# test instead compares the two engine outputs directly and asserts they
# DIFFER by at least 2mm -- comfortably below the actual 1.77cm 3D RMS
# merge-vs-ch1-only delta (median dH 0.319cm / max 3.156cm), comfortably above
# the ~0.2mm single-epoch Accelerate ULP wobble between otherwise-identical
# runs (#303 A-4). Verified sensitive: the same comparison against a run with
# the merge disabled (l6_merge=0, both .l6 files still supplied) measures
# 0.015cm 3D RMS and correctly FAILs this check.
add_test(NAME claslib_ppp_rtk_2ch_merge_diff_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_l6mrg_1file.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_2ch.nmea
        --expect-min-rms 0.002
)
set_tests_properties(claslib_ppp_rtk_2ch_merge_diff_check PROPERTIES
    DEPENDS           "claslib_ppp_rtk_2ch;claslib_ppp_rtk_l6mrg_1file"
    FIXTURES_REQUIRED  claslib_data
    LABELS             "regression;claslib;ppp-rtk"
)

# ── rnx2rtkp CLAS VRS-RTK regression ──────────────────────────────────────────
# Data: 2019/08/27 16:00-17:00, 0627239Q.obs + sept_2019239.nav + vrs2019239Q.obs
# Reference: ref_VRS.nmea (upstream claslib output)
add_test(NAME claslib_vrs_rtk
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_vrs.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk.nmea
        ${_CLSDATA}/0627239Q.obs
        ${_CLSDATA}/sept_2019239.nav
        ${_CLSDATA}/vrs2019239Q.obs
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_vrs_rtk PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;vrs"
)

add_test(NAME claslib_vrs_rtk_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_VRS.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_vrs_rtk_check PROPERTIES
    DEPENDS           claslib_vrs_rtk
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;vrs"
)

# Absolute accuracy vs GSI F5 (TSUKUBA3, 2019/08/27). The VRS rover is the 0627
# station, so its truth is the same F5 coordinate. Measured 2D 95% = 3.5 cm;
# tolerance 0.08 m (measured + ~4 cm headroom).
add_test(NAME claslib_vrs_rtk_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627_2019}
        --date 2019/08/27
        --tolerance 0.080
        ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_vrs_rtk_abs_check PROPERTIES
    DEPENDS           claslib_vrs_rtk
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;vrs"
)

# ── rnx2rtkp CLAS PPP-RTK regression (SubType 12 BINEX) ─────────────────────
# Data: 2019/12/15 01:00-02:00, 0627349AB.bnx + sept_2019349.nav + 2019349B.l6
# Reference: ref_ST12.nmea (upstream claslib output)
add_test(NAME claslib_ppp_rtk_st12
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp.toml
        -ts 2019/12/15 01:00:00 -te 2019/12/15 01:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_st12.nmea
        ${_CLSDATA}/0627349AB.bnx
        ${_CLSDATA}/sept_2019349.nav
        ${_CLSDATA}/2019349B.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_st12 PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_st12_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_ST12.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_st12.nmea
        --tolerance 0.15
)
set_tests_properties(claslib_ppp_rtk_st12_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_st12
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# Absolute accuracy vs GSI F5 (TSUKUBA3, 2019/12/15). Measured 2D 95% = 7.3 cm
# (97% fix, some float epochs); tolerance 0.12 m (measured + ~4 cm headroom).
add_test(NAME claslib_ppp_rtk_st12_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627_2019}
        --date 2019/12/15
        --tolerance 0.120
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_st12.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_st12_abs_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_st12
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;ppp-rtk"
)

# ── rnx2rtkp CLAS PPP-RTK regression (L1C+L5 frequency) ─────────────────────
# Data: 2025/01/22 16:00-17:00, 0627022Q.bnx + 2025022Q.l6 (no separate nav)
# Reference: ref_L6_L1CL5.nmea (upstream claslib output)
add_test(NAME claslib_ppp_rtk_l1cl5
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_L1CL5.toml
        -ts 2025/01/22 16:00:00 -te 2025/01/22 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_l1cl5.nmea
        ${_CLSDATA}/0627022Q.bnx
        ${_CLSDATA}/2025022Q.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_l1cl5 PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;ppp-rtk"
)

add_test(NAME claslib_ppp_rtk_l1cl5_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_L6_L1CL5.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_l1cl5.nmea
        --tolerance 0.12
)
set_tests_properties(claslib_ppp_rtk_l1cl5_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_l1cl5
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;ppp-rtk"
)

# ── Absolute accuracy check: CLAS PPP-RTK L1C+L5 vs GSI F5 TSUKUBA3 ──────────
# Reference: GSI F5 station 960627 (TSUKUBA3/0627), daily coordinate for
# 2025/01/22 (DOY 022).  The consistency check above only compares against
# upstream claslib output; this guards true geodetic accuracy.
# Measured 2D 95% = 4.4 cm (fix 99.3%); tolerance 0.09 m = measured + ~4 cm
# cross-platform (LAPACK-vs-LU) headroom, tight enough to catch a regression.
add_test(NAME claslib_ppp_rtk_l1cl5_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627}
        --date 2025/01/22
        --tolerance 0.090
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_l1cl5.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_l1cl5_abs_check PROPERTIES
    DEPENDS           claslib_ppp_rtk_l1cl5
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;ppp-rtk"
)

# ── rnx2rtkp CLAS VRS-RTK regression (SubType 12 BINEX) ─────────────────────
# Data: 2019/12/15 01:00-02:00, 0627349AB.bnx + sept_2019349.nav + vrs2019349B.obs
# Reference: ref_ST12_VRS.nmea (upstream claslib output)
add_test(NAME claslib_vrs_rtk_st12
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_vrs.toml
        -ts 2019/12/15 01:00:00 -te 2019/12/15 01:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk_st12.nmea
        ${_CLSDATA}/0627349AB.bnx
        ${_CLSDATA}/sept_2019349.nav
        ${_CLSDATA}/vrs2019349B.obs
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_vrs_rtk_st12 PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;vrs"
)

add_test(NAME claslib_vrs_rtk_st12_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_ST12_VRS.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk_st12.nmea
        --tolerance 0.10
)
set_tests_properties(claslib_vrs_rtk_st12_check PROPERTIES
    DEPENDS           claslib_vrs_rtk_st12
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;vrs"
)

# Absolute accuracy vs GSI F5 (TSUKUBA3, 2019/12/15). Measured 2D 95% = 4.4 cm;
# tolerance 0.09 m (measured + ~4 cm headroom).
add_test(NAME claslib_vrs_rtk_st12_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627_2019}
        --date 2019/12/15
        --tolerance 0.090
        ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk_st12.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_vrs_rtk_st12_abs_check PROPERTIES
    DEPENDS           claslib_vrs_rtk_st12
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;vrs"
)

# ── rnx2rtkp CLAS VRS-RTK regression (dual-channel) ─────────────────────────
# Data: 2025/06/06 20:00-21:00, 0627157U.bnx + ch1.obs + ch2.obs (VRS)
# Reference: ref_L6_2CH_VRS.nmea (upstream claslib output)
# Note: No separate .nav — nav embedded in BINEX; no .l6 — VRS obs replace L6
add_test(NAME claslib_vrs_rtk_2ch
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_vrs.toml
        -ts 2025/06/06 20:00:00 -te 2025/06/06 20:59:59 -ti 1
        -l6msg 1
        -o ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk_2ch.nmea
        ${_CLSDATA}/0627157U.bnx
        ${_CLSDATA}/2025157U_ch1.obs
        ${_CLSDATA}/2025157U_ch2.obs
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_vrs_rtk_2ch PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;vrs"
)

add_test(NAME claslib_vrs_rtk_2ch_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_L6_2CH_VRS.nmea
        ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk_2ch.nmea
        --tolerance 0.20
        --skip-fixrate
)
set_tests_properties(claslib_vrs_rtk_2ch_check PROPERTIES
    DEPENDS           claslib_vrs_rtk_2ch
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;vrs"
)

# ── Absolute accuracy check: CLAS VRS-RTK 2CH vs GSI F5 TSUKUBA3 ─────────────
# Reference: GSI F5 station 960627 (TSUKUBA3/0627), daily coordinate for
# 2025/06/06 (DOY 157).  The consistency check above only compares against
# upstream claslib output; this guards true geodetic accuracy.
# Measured 2D 95% = 7.9 cm (fix 99.9%, VRS carries a ~6 cm bias); tolerance
# 0.12 m = measured + ~4 cm cross-platform headroom.
add_test(NAME claslib_vrs_rtk_2ch_abs_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA_ABS}
        --f5 ${_F5_0627}
        --date 2025/06/06
        --tolerance 0.120
        ${CMAKE_BINARY_DIR}/out_claslib_vrs_rtk_2ch.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_vrs_rtk_2ch_abs_check PROPERTIES
    DEPENDS           claslib_vrs_rtk_2ch
    FIXTURES_REQUIRED claslib_data
    LABELS            "absolute;claslib;vrs"
)

# ── ssr2obs regression: generate VRS and verify via VRS-RTK pipeline ─────────
# Step 1: Generate VRS observations from SSR corrections using ssr2obs
add_test(NAME claslib_ssr2obs
    COMMAND $<TARGET_FILE:mrtk> ssr2obs
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/ssr2obs.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -r -o ${CMAKE_BINARY_DIR}/out_ssr2obs_vrs2019239Q.obs
        ${_CLSDATA}/sept_2019239.nav ${_CLSDATA}/2019239Q.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ssr2obs PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;vrs"
)

# Step 2: Feed ssr2obs output into VRS-RTK and compare against reference
add_test(NAME claslib_ssr2obs_vrs_rtk
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/rnx2rtkp_vrs.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_ssr2obs_vrs_rtk.nmea
        ${_CLSDATA}/0627239Q.obs
        ${_CLSDATA}/sept_2019239.nav
        ${CMAKE_BINARY_DIR}/out_ssr2obs_vrs2019239Q.obs
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ssr2obs_vrs_rtk PROPERTIES
    DEPENDS           claslib_ssr2obs
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib;vrs"
)

add_test(NAME claslib_ssr2obs_vrs_rtk_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_NMEA}
        ${_CLSDATA}/ref_VRS.nmea
        ${CMAKE_BINARY_DIR}/out_ssr2obs_vrs_rtk.nmea
        --tolerance 0.20
        --skip-fixrate
)
set_tests_properties(claslib_ssr2obs_vrs_rtk_check PROPERTIES
    DEPENDS           claslib_ssr2obs_vrs_rtk
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;vrs"
)

# ── ssr2osr regression: apply SSR corrections to real obs and verify output ──
# Step 1: Run ssr2osr with real observations + L6 corrections
add_test(NAME claslib_ssr2osr
    COMMAND $<TARGET_FILE:mrtk> ssr2osr
        -k ${CMAKE_SOURCE_DIR}/conf/claslib/ssr2osr.toml
        -ts 2019/08/27 16:00:00 -te 2019/08/27 16:59:59 -ti 1
        -o ${CMAKE_BINARY_DIR}/out_ssr2osr.nmea
        ${_CLSDATA}/0627239Q.obs ${_CLSDATA}/sept_2019239.nav ${_CLSDATA}/2019239Q.l6
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ssr2osr PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           300
    LABELS            "regression;claslib"
)

# Step 2: Verify output is non-empty and positions match config
add_test(NAME claslib_ssr2osr_check
    COMMAND ${Python3_EXECUTABLE} -c "
import sys
lines=[l for l in open(sys.argv[1]) if l.startswith('$GPGGA') or l.startswith('$GNGGA')]
assert len(lines)>=100, f'too few GGA sentences: {len(lines)}'
for l in lines[:5]:
    f=l.split(',')
    lat=float(f[2][:2])+float(f[2][2:])/60.0
    lon=float(f[4][:3])+float(f[4][3:])/60.0
    assert abs(lat-36.1036)<0.01, f'lat {lat}'
    assert abs(lon-140.0863)<0.01, f'lon {lon}'
print(f'OK: {len(lines)} GGA sentences, position verified')
"
        ${CMAKE_BINARY_DIR}/out_ssr2osr.nmea
)
set_tests_properties(claslib_ssr2osr_check PROPERTIES
    DEPENDS           claslib_ssr2osr
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib"
)

# ── rtkrcv RT CLAS PPP-RTK regression ─────────────────────────────────────────
# Replays BINEX obs + CLAS L6 via rtkrcv at x10 speed.
# Uses line-count threshold (≥90% of reference) for pass/fail.
add_test(NAME rtkrcv_rt_clas
    COMMAND bash ${_TESTCMAKE}/run_rtkrcv_test.sh
        $<TARGET_FILE:mrtk> run
        ${CMAKE_SOURCE_DIR}
        ${_CLSRTREFDATA}/rtkrcv_clas_ref.nmea
        10
        conf/claslib/rtkrcv.toml
        52005
        500
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(rtkrcv_rt_clas PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           600
    LABELS            "regression;claslib;realtime"
    RESOURCE_LOCK     rtkrcv_port
)

# ── rtkrcv RT CLAS PPP-RTK dual-channel regression ────────────────────────────
# Replays BINEX obs + 2-channel CLAS L6 via rtkrcv at x10 speed.
# Stream 2 (base slot) carries L6 ch2; stream 3 carries L6 ch1.
# Uses line-count threshold (≥90% of reference) for pass/fail.
#
# The conf ships with l6_merge = 1 (#309); this run pins it back to 0 so the
# single-channel real-time path (engine on ch0 only) keeps its own regression,
# which is also what ref_L6_2CH_RT.nmea was recorded under.  The merge-enabled
# run is rtkrcv_rt_clas_2ch_merge below.
add_test(NAME rtkrcv_rt_clas_2ch
    COMMAND bash ${_TESTCMAKE}/run_rtkrcv_test.sh
        $<TARGET_FILE:mrtk> run
        ${CMAKE_SOURCE_DIR}
        ${_CLSRTREFDATA}/ref_L6_2CH_RT.nmea
        10
        conf/claslib/rtkrcv_2ch.toml
        52006
        500
        ${CMAKE_BINARY_DIR}/out_rtkrcv_rt_clas_2ch_1ch.nmea
        "l6_merge=0"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(rtkrcv_rt_clas_2ch PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           600
    LABELS            "regression;claslib;realtime"
    RESOURCE_LOCK     rtkrcv_port
)

# ── rtkrcv RT CLAS PPP-RTK dual-channel merge regression (#309) ───────────────
# Same replay with the shipped conf as-is, i.e. l6_merge = 1: both L6 transmit
# patterns feed the engine and are merged per satellite.
add_test(NAME rtkrcv_rt_clas_2ch_merge
    COMMAND bash ${_TESTCMAKE}/run_rtkrcv_test.sh
        $<TARGET_FILE:mrtk> run
        ${CMAKE_SOURCE_DIR}
        ${_CLSRTREFDATA}/ref_L6_2CH_RT.nmea
        10
        conf/claslib/rtkrcv_2ch.toml
        52007
        500
        ${CMAKE_BINARY_DIR}/out_rtkrcv_rt_clas_2ch_merge.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(rtkrcv_rt_clas_2ch_merge PROPERTIES
    FIXTURES_REQUIRED claslib_data
    TIMEOUT           600
    LABELS            "regression;claslib;realtime"
    RESOURCE_LOCK     rtkrcv_port
)

# Sensitivity guard: the merged run must use satellites the single-channel run
# cannot see.  Position RMS is useless as the discriminator here — real-time
# replay scatters 3.0 cm 3D RMS between two identical runs, while merge moves
# the solution only 4.5-4.8 cm — so this compares the mean GGA satellite count
# over fixed epochs instead, which is structural rather than timing-dependent:
# measured 14.59 (single-channel) vs 15.56-15.63 (merged), i.e. +0.97, with
# +-0.07 spread across repeats, and it stays valid even in a run that loses
# fix for a while.  Threshold 0.5 keeps ~2x headroom.
add_test(NAME rtkrcv_rt_clas_2ch_merge_nsat_check
    COMMAND ${Python3_EXECUTABLE} -c "
import statistics, sys
def mean_nsat(path):
    ns=[]
    for line in open(path):
        if line.startswith(('$GNGGA','$GPGGA')):
            f=line.split(',')
            if len(f)>=10 and f[6]=='4':
                ns.append(int(f[7]))
    assert len(ns)>=1000, f'{path}: too few fixed epochs ({len(ns)})'
    return statistics.mean(ns)
one=mean_nsat(sys.argv[1]); both=mean_nsat(sys.argv[2])
print(f'mean satellites per fixed epoch: single-channel={one:.3f} merged={both:.3f} delta={both-one:+.3f}')
assert both-one>=0.5, f'merge added only {both-one:+.3f} satellites (expected >= 0.5)'
print('OK: dual-channel merge adds satellites')
"
        ${CMAKE_BINARY_DIR}/out_rtkrcv_rt_clas_2ch_1ch.nmea
        ${CMAKE_BINARY_DIR}/out_rtkrcv_rt_clas_2ch_merge.nmea
)
set_tests_properties(rtkrcv_rt_clas_2ch_merge_nsat_check PROPERTIES
    DEPENDS           "rtkrcv_rt_clas_2ch;rtkrcv_rt_clas_2ch_merge"
    FIXTURES_REQUIRED claslib_data
    LABELS            "regression;claslib;realtime"
)

# ═══════════════════════════════════════════════════════════════════════════════
# Tier 3 tests — position scatter (precision)
# ═══════════════════════════════════════════════════════════════════════════════
# No external reference is required.  The session centroid is computed from
# the solutions themselves, and 1σ (68th pctile) and 95% horizontal scatter
# must stay within the tolerance.  These catch gross regressions that leave
# the engine producing scattered or single-point solutions.
#
# Scatter checks are applied to the outputs already produced by Tier-1 runs:
#
#   madocalib_ppp_scatter
#       MIZU RINEX obs + MADOCA L6E (DOY 091 / 2025-04-01)  → MADOCA-PPP
#       Actual:  1σ=7.4 cm  95%=11.2 cm  (skip-epochs=30, fix-only)
#       Tolerance: 0.150 m  (≈2× actual)
#
#   claslib_ppp_rtk_2ch_scatter
#       0627 BINEX obs + dual-channel CLAS L6D (DOY 157 / 2025-06-06) → CLAS PPP-RTK
#       Actual:  1σ=5.8 cm  95%=25.2 cm  (skip-epochs=20, fix-only)
#       Tolerance: 0.400 m  (≈2× actual 95th; 2CH has ~88% fix rate)
#
# NOTE: Original cross-engine design (0627 obs → MADOCA-PPP; MIZU obs → CLAS
# PPP-RTK) required correction data from the same date as the observations.
# The prepared L6 files (2025157U.l6, 2025091A.{204,206}.l6) use a newer
# QZSS L6 encoding (MADOCA-PPPv2 / port-indexed format) that MRTKLIB does not
# yet decode.  Providing L6 data in the same format as 2025157U_ch{1,2}.l6
# (satellite-PRN-indexed, type-byte 0xA9/0xA3) would unblock those tests.

# ── madocalib_ppp_scatter ─────────────────────────────────────────────────────
add_test(NAME madocalib_ppp_scatter
    COMMAND ${Python3_EXECUTABLE} ${_CHECK_SCATTER}
        --tolerance 0.150
        --skip-epochs 30
        --fix-only
        ${CMAKE_BINARY_DIR}/out_madocalib_ppp.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(madocalib_ppp_scatter PROPERTIES
    DEPENDS           madocalib_ppp
    FIXTURES_REQUIRED madocalib_data
    LABELS            "tier3;scatter;madocalib"
)

# ── claslib_ppp_rtk_2ch_scatter ───────────────────────────────────────────────
add_test(NAME claslib_ppp_rtk_2ch_scatter
    COMMAND ${Python3_EXECUTABLE} ${_CHECK_SCATTER}
        --tolerance 0.400
        --skip-epochs 20
        --fix-only
        ${CMAKE_BINARY_DIR}/out_claslib_ppp_rtk_2ch.nmea
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(claslib_ppp_rtk_2ch_scatter PROPERTIES
    DEPENDS           claslib_ppp_rtk_2ch
    FIXTURES_REQUIRED claslib_data
    LABELS            "tier3;scatter;claslib"
)

# ─────────────────────────────────────────────────────────────────────────────
# IGS-products conventional float PPP  (correction = igs, #130)
# Source: GSI GEONET station 3034 (FUJISAWA), 2026-01-21, 2 h window, GPS+GAL.
# Validated against the published GSI F5 daily coordinate (RTKLIB-independent).
# Test data archive: tests/data/igs/igs_testdata.tar.gz
# ─────────────────────────────────────────────────────────────────────────────
set(_IGSDATA ${CMAKE_SOURCE_DIR}/tests/data/igs)

add_test(NAME igs_setup
    COMMAND bash -c "tar xzf ${_IGSDATA}/igs_testdata.tar.gz -C ${_IGSDATA}"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_setup PROPERTIES
    FIXTURES_SETUP  igs_data
    TIMEOUT         30
    LABELS          "regression;igs;ppp"
)

add_test(NAME igs_cleanup
    COMMAND bash -c "rm -f \
        ${_IGSDATA}/30340210.26o \
        ${_IGSDATA}/BRDC00IGS_2026021_GE.rnx \
        ${_IGSDATA}/COD0MGXFIN_2026021_05M_ORB.SP3 \
        ${_IGSDATA}/COD0MGXFIN_2026021_30S_CLK.CLK \
        ${_IGSDATA}/COD0MGXFIN_2026021_01D_OSB.BIA \
        ${_IGSDATA}/igs20_trim.atx \
        ${_IGSDATA}/93034.26.pos"
)
set_tests_properties(igs_cleanup PROPERTIES
    FIXTURES_CLEANUP igs_data
    LABELS           "regression;igs;ppp"
)

# ── rnx2rtkp conventional IGS float PPP ──────────────────────────────────────
add_test(NAME igs_ppp
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_ppp.toml
        -ts 2026/01/21 0:00:00 -te 2026/01/21 2:00:00
        -o ${CMAKE_BINARY_DIR}/out_igs_ppp.pos
        ${_IGSDATA}/30340210.26o
        ${_IGSDATA}/BRDC00IGS_2026021_GE.rnx
        ${_IGSDATA}/COD0MGXFIN_2026021_05M_ORB.SP3
        ${_IGSDATA}/COD0MGXFIN_2026021_30S_CLK.CLK
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_ppp PROPERTIES
    FIXTURES_REQUIRED igs_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp"
)

# ── absolute-accuracy check vs published GSI F5 coordinate ────────────────────
add_test(NAME igs_ppp_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --f5 ${_IGSDATA}/93034.26.pos
        --date 2026/01/21
        --tolerance 0.15
        --skip-epochs 30
        ${CMAKE_BINARY_DIR}/out_igs_ppp.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_ppp_check PROPERTIES
    DEPENDS           igs_ppp
    FIXTURES_REQUIRED igs_data
    LABELS            "absolute;igs;ppp"
)

# ── IGS-products integer PPP-AR (#142) ───────────────────────────────────────
# Uncombined measurement model + Bias-SINEX OSB phase biases. Two variants:
#   igs_pppar_gps : GPS-only -- guards the uncombined OSB path and the
#                   dual-frequency AR cascade (no EWL ambiguity); accuracy only,
#                   since 6 satellites are too few for reliable narrow-lane fix.
#   igs_pppar_ge  : GPS+Galileo -- asserts a real integer-fix rate (Q=1).
add_test(NAME igs_pppar_gps
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_igspppar_gps.toml
        -ts 2026/01/21 0:00:00 -te 2026/01/21 2:00:00
        -o ${CMAKE_BINARY_DIR}/out_igs_pppar_gps.pos
        ${_IGSDATA}/30340210.26o
        ${_IGSDATA}/BRDC00IGS_2026021_GE.rnx
        ${_IGSDATA}/COD0MGXFIN_2026021_05M_ORB.SP3
        ${_IGSDATA}/COD0MGXFIN_2026021_30S_CLK.CLK
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps PROPERTIES
    FIXTURES_REQUIRED igs_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp;pppar"
)

add_test(NAME igs_pppar_gps_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --f5 ${_IGSDATA}/93034.26.pos
        --date 2026/01/21
        --tolerance 0.15
        --skip-epochs 30
        ${CMAKE_BINARY_DIR}/out_igs_pppar_gps.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps_check PROPERTIES
    DEPENDS           igs_pppar_gps
    FIXTURES_REQUIRED igs_data
    LABELS            "absolute;igs;ppp;pppar"
)

add_test(NAME igs_pppar_ge
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_igspppar.toml
        -ts 2026/01/21 0:00:00 -te 2026/01/21 2:00:00
        -o ${CMAKE_BINARY_DIR}/out_igs_pppar_ge.pos
        ${_IGSDATA}/30340210.26o
        ${_IGSDATA}/BRDC00IGS_2026021_GE.rnx
        ${_IGSDATA}/COD0MGXFIN_2026021_05M_ORB.SP3
        ${_IGSDATA}/COD0MGXFIN_2026021_30S_CLK.CLK
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_ge PROPERTIES
    FIXTURES_REQUIRED igs_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp;pppar"
)

# integer-fix assertion: GPS+GAL resolves narrow-lane (Q=1). Threshold 10% is a
# conservative floor; observed ~31% on this 2 h window (100% once converged).
add_test(NAME igs_pppar_ge_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --f5 ${_IGSDATA}/93034.26.pos
        --date 2026/01/21
        --tolerance 0.15
        --skip-epochs 30
        --min-fix-rate 10
        ${CMAKE_BINARY_DIR}/out_igs_pppar_ge.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_ge_check PROPERTIES
    DEPENDS           igs_pppar_ge
    FIXTURES_REQUIRED igs_data
    LABELS            "absolute;igs;ppp;pppar"
)

# ── receiver code bias coverage (#263) ───────────────────────────────────────
# The COD MGEX bias-sinex product used above has no station-level (receiver)
# OSB records, so nav->osb.rsatcb/rsyscb are never populated by the tests
# above -- the receiver code bias branch in corr_meas() is otherwise dead in
# CI. tests/data/igs/COD0MGXFIN_2026021_01D_OSB_recvbias.BIA is a small,
# permanently-tracked (not tar.gz-extracted) copy of the same product with
# added station-level entries for station "3034" (the RINEX MARKER NAME in
# 30340210.26o, auto-selected as prcopt.staname): a satellite-specific record
# for G03 and a system-wide GPS record covering the rest, exercising both the
# vrsatcb and vrsyscb fallback paths. Receiver OSB is applied, accuracy
# unaffected vs the plain igs_pppar_gps baseline.
add_test(NAME igs_pppar_gps_recvbias
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_igspppar_gps_recvbias.toml
        -ts 2026/01/21 0:00:00 -te 2026/01/21 2:00:00
        -o ${CMAKE_BINARY_DIR}/out_igs_pppar_gps_recvbias.pos
        ${_IGSDATA}/30340210.26o
        ${_IGSDATA}/BRDC00IGS_2026021_GE.rnx
        ${_IGSDATA}/COD0MGXFIN_2026021_05M_ORB.SP3
        ${_IGSDATA}/COD0MGXFIN_2026021_30S_CLK.CLK
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps_recvbias PROPERTIES
    FIXTURES_REQUIRED igs_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp;pppar"
)

add_test(NAME igs_pppar_gps_recvbias_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --f5 ${_IGSDATA}/93034.26.pos
        --date 2026/01/21
        --tolerance 0.15
        --skip-epochs 30
        ${CMAKE_BINARY_DIR}/out_igs_pppar_gps_recvbias.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps_recvbias_check PROPERTIES
    DEPENDS           igs_pppar_gps_recvbias
    FIXTURES_REQUIRED igs_data
    LABELS            "absolute;igs;ppp;pppar"
)

# drop_uncorrected_code = true against the receiver-OSB fixture. The fixture
# supplies the L1/L2 receiver OSBs used by this regression; third-band C5X
# measurements without a satellite OSB are safely discarded without changing
# the selected L1/L2 solution. This confirms that enabling the gate preserves
# a fully usable L1/L2 PPP-AR solution (#263).
add_test(NAME igs_pppar_gps_recvbias_unbias
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_igspppar_gps_unbias.toml
        -ts 2026/01/21 0:00:00 -te 2026/01/21 2:00:00
        -o ${CMAKE_BINARY_DIR}/out_igs_pppar_gps_unbias.pos
        ${_IGSDATA}/30340210.26o
        ${_IGSDATA}/BRDC00IGS_2026021_GE.rnx
        ${_IGSDATA}/COD0MGXFIN_2026021_05M_ORB.SP3
        ${_IGSDATA}/COD0MGXFIN_2026021_30S_CLK.CLK
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps_recvbias_unbias PROPERTIES
    FIXTURES_REQUIRED igs_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp;pppar"
)

add_test(NAME igs_pppar_gps_recvbias_unbias_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --f5 ${_IGSDATA}/93034.26.pos
        --date 2026/01/21
        --tolerance 0.15
        --skip-epochs 30
        ${CMAKE_BINARY_DIR}/out_igs_pppar_gps_unbias.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps_recvbias_unbias_check PROPERTIES
    DEPENDS           igs_pppar_gps_recvbias_unbias
    FIXTURES_REQUIRED igs_data
    LABELS            "absolute;igs;ppp;pppar"
)

# The standard COD product has no receiver OSBs. Layering the small unbias
# overlay onto the ordinary GPS-only PPP-AR config must reject every usable
# pseudorange. The checker asserts both satellite- and receiver-bias miss
# paths in the trace, so the gate cannot silently become a no-op.
add_test(NAME igs_pppar_gps_unbias_missing
    COMMAND $<TARGET_FILE:mrtk> post
        -x 4
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_igspppar_gps.toml
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_igspppar_unbias.toml
        -ts 2026/01/21 0:00:00 -te 2026/01/21 2:00:00
        -o ${CMAKE_BINARY_DIR}/out_igs_pppar_gps_unbias_missing.pos
        ${_IGSDATA}/30340210.26o
        ${_IGSDATA}/BRDC00IGS_2026021_GE.rnx
        ${_IGSDATA}/COD0MGXFIN_2026021_05M_ORB.SP3
        ${_IGSDATA}/COD0MGXFIN_2026021_30S_CLK.CLK
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps_unbias_missing PROPERTIES
    FIXTURES_REQUIRED igs_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp;pppar"
)

add_test(NAME igs_pppar_gps_unbias_missing_check
    COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/tests/check_unbias_drop.py
        ${CMAKE_BINARY_DIR}/out_igs_pppar_gps_unbias_missing.pos
        ${CMAKE_BINARY_DIR}/out_igs_pppar_gps_unbias_missing.pos.trace
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_pppar_gps_unbias_missing_check PROPERTIES
    DEPENDS           igs_pppar_gps_unbias_missing
    FIXTURES_REQUIRED igs_data
    LABELS            "regression;igs;ppp;pppar"
)

# -----------------------------------------------------------------------------
# IGS-products iono-free frequency fallback regression (#135)
# Source: ECJ02 u-blox raw log, 2026-05-01, first 1 h window.
# Reference coordinate: full-day static PPP median from the same raw log.
# Test data archive: tests/data/igs/igs_iflc_testdata.tar.gz
# -----------------------------------------------------------------------------
add_test(NAME igs_iflc_setup
    COMMAND bash -c "tar xzf ${_IGSDATA}/igs_iflc_testdata.tar.gz -C ${_IGSDATA}"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_iflc_setup PROPERTIES
    FIXTURES_SETUP  igs_iflc_data
    TIMEOUT         30
    LABELS          "regression;igs;ppp;iflc"
)

add_test(NAME igs_iflc_cleanup
    COMMAND bash -c "rm -f \
        ${_IGSDATA}/ECJ02_26050100_01H.obs \
        ${_IGSDATA}/BRDM00DLR_S_20261210000_01D_MN.rnx \
        ${_IGSDATA}/igs20_iflc_trim.atx \
        ${_IGSDATA}/COD0MGXFIN_20261210000_01D_01D_OSB.BIA \
        ${_IGSDATA}/COD0MGXFIN_20261210000_01D_05M_ORB.SP3 \
        ${_IGSDATA}/COD0MGXFIN_20261210000_01D_12H_ERP.ERP \
        ${_IGSDATA}/COD0MGXFIN_20261210000_01D_30S_CLK.CLK"
)
set_tests_properties(igs_iflc_cleanup PROPERTIES
    FIXTURES_CLEANUP igs_iflc_data
    LABELS           "regression;igs;ppp;iflc"
)

add_test(NAME igs_iflc_ppp
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_iflc_fallback.toml
        -ts 2026/05/01 0:00:00 -te 2026/05/01 1:00:00
        -o ${CMAKE_BINARY_DIR}/out_igs_iflc_ppp.pos
        ${_IGSDATA}/ECJ02_26050100_01H.obs
        ${_IGSDATA}/BRDM00DLR_S_20261210000_01D_MN.rnx
        ${_IGSDATA}/COD0MGXFIN_20261210000_01D_05M_ORB.SP3
        ${_IGSDATA}/COD0MGXFIN_20261210000_01D_30S_CLK.CLK
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_iflc_ppp PROPERTIES
    FIXTURES_REQUIRED igs_iflc_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp;iflc"
)

add_test(NAME igs_iflc_ppp_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --llh 35.666334193,139.792201653,59.8539
        --tolerance 0.20
        ${CMAKE_BINARY_DIR}/out_igs_iflc_ppp.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igs_iflc_ppp_check PROPERTIES
    DEPENDS           igs_iflc_ppp
    FIXTURES_REQUIRED igs_iflc_data
    LABELS            "absolute;igs;ppp;iflc"
)

# -----------------------------------------------------------------------------
# IGS-RTS float-PPP regression (#138): correction = "igs-rts"
# Source: AIRA00JPN (Aira, Japan, IGS) GPS+GAL, captured live 2026-05-22 from
# the CDDIS NTRIP caster: rover obs (AIRA MSM7), IGS combined RTCM-SSR
# (SSRA02IGS0, code-bias-only) and BKG broadcast eph (BCEP00BKG0); obs at 30 s.
# Reference coordinate: IGS final weekly SINEX (excerpt igsrts_aira.snx, AIRA
# estimate at 2026-05-06, ~2 weeks from the obs epoch), propagated to 2026-05-22.
# Receiver antenna TRM59800.00 SCIS PCV applied (igs20_aira.atx, the AIRA block
# from igs20.atx) — the real-time MSM RINEX carries no antenna name so it is set
# in the config. Result: 3D 1σ ~0.18 m / 95% ~0.33 m float — the expected
# real-time IGS-RTS float-PPP accuracy. Cross-checked against upstream RTKLIB
# 2.4.3 rnx2rtkp on the identical inputs (agreement to the LAPACK-vs-LU level).
# Test data archive: tests/data/igs/igsrts_testdata.tar.gz
# -----------------------------------------------------------------------------
add_test(NAME igsrts_setup
    COMMAND bash -c "tar xzf ${_IGSDATA}/igsrts_testdata.tar.gz -C ${_IGSDATA}"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igsrts_setup PROPERTIES
    FIXTURES_SETUP  igsrts_data
    TIMEOUT         30
    LABELS          "regression;igs;ppp;igsrts"
)

add_test(NAME igsrts_cleanup
    COMMAND bash -c "rm -f \
        ${_IGSDATA}/igsrts_aira.obs \
        ${_IGSDATA}/igsrts_brdc.nav \
        ${_IGSDATA}/igsrts_ssr.rtcm3 \
        ${_IGSDATA}/igsrts_aira.snx \
        ${_IGSDATA}/igs20_aira.atx"
)
set_tests_properties(igsrts_cleanup PROPERTIES
    FIXTURES_CLEANUP igsrts_data
    LABELS           "regression;igs;ppp;igsrts"
)

add_test(NAME igsrts_ppp
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/igs/rnx2rtkp_igsrts_test.toml
        -o ${CMAKE_BINARY_DIR}/out_igsrts_ppp.pos
        ${_IGSDATA}/igsrts_aira.obs
        ${_IGSDATA}/igsrts_brdc.nav
        ${_IGSDATA}/igsrts_ssr.rtcm3
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igsrts_ppp PROPERTIES
    FIXTURES_REQUIRED igsrts_data
    TIMEOUT           180
    LABELS            "regression;igs;ppp;igsrts"
)

add_test(NAME igsrts_ppp_check
    COMMAND ${Python3_EXECUTABLE} ${_COMPARE_POS_ABS}
        --sinex ${_IGSDATA}/igsrts_aira.snx --station AIRA --epoch 2026/05/22
        --tolerance 0.5
        --skip-epochs 20
        ${CMAKE_BINARY_DIR}/out_igsrts_ppp.pos
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(igsrts_ppp_check PROPERTIES
    DEPENDS           igsrts_ppp
    FIXTURES_REQUIRED igsrts_data
    LABELS            "absolute;igs;ppp;igsrts"
)

# ═══════════════════════════════════════════════════════════════════════════════
# Galileo HAS (High Accuracy Service) float-PPP regression tests
# ═══════════════════════════════════════════════════════════════════════════════
# Data: mosaic-G5 capture G5P3162a, 2026/06/10 23:59:31 (15-min window).
# Source: block-filtered + time-trimmed from the full-hour G5P3162a.sbf via
#         scripts/tests/sbf_filter.py (blocks 4027/4017/4023/4024; 15 min @ 1 s).
#         A strict before/after-trim equivalence proof (.has byte-identity,
#         identical obs records, bit-identical PPP solutions over the common
#         window) backs the trim -- see docs/design/gal-has.md §11.
# Archive: tests/data/has/has_testdata.tar.gz  (G5P3162a_15m.sbf, ~2.1 MB gz)
#
# Pipeline: SBF --l6extract--> .has  (HAS pages)
#           SBF --convert----> RINEX obs/nav
#           obs + nav + .has --post (correction=gal-has)--> float PPP
#
# Reference: ITRF current-epoch receiver position from MADOCA-PPP (static +
#            combined PPP-AR) over the full hour, 2026-06-12, ~1-2 cm.
#              XYZ = -3961905.2046 3348992.8024 3698212.4790
#            HAS float sits ~0.56 m horiz / 0.69 m 3D from this reference
#            (HAS Initial Service absolute accuracy; investigation open).
set(_HASDATA      ${CMAKE_SOURCE_DIR}/tests/data/has)
set(_CHECK_HASPPP ${CMAKE_SOURCE_DIR}/scripts/tests/check_has_ppp.py)
set(_HAS_REF_XYZ  -3961905.2046 3348992.8024 3698212.4790)

# ── Fixture: extract / clean HAS test data archive ────────────────────────────
add_test(NAME has_setup
    COMMAND bash -c "tar xzf ${_HASDATA}/has_testdata.tar.gz -C ${_HASDATA}"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(has_setup PROPERTIES
    FIXTURES_SETUP  has_data
    TIMEOUT         30
    LABELS          "regression;has"
)

add_test(NAME has_cleanup
    COMMAND bash -c "rm -f \
        ${_HASDATA}/G5P3162a_15m.sbf \
        ${_HASDATA}/G5P3162a_15m.has \
        ${_HASDATA}/has_obs.obs \
        ${_HASDATA}/has_nav.nav"
)
set_tests_properties(has_cleanup PROPERTIES
    FIXTURES_CLEANUP has_data
    LABELS           "regression;has"
)

# ── l6extract: SBF GALRawCNAV → .has, assert record count ─────────────────────
# The bundled 15-min SBF yields exactly 4834 valid HAS records (8129 GALRawCNAV
# blocks, 3 CRC-failed, 3292 dummy pages). The count is a stable fixture
# constant -- a change signals a regression in the HAS page extractor.
# l6extract writes the .has to <input-basename>.has in the working directory,
# so run it in ${_HASDATA} where has_ppp expects G5P3162a_15m.has.
add_test(NAME has_extract
    COMMAND bash -c "\"$0\" l6extract -in \"$1\" -o \"$2\" 2>&1 | grep -q '4834 valid records'"
        $<TARGET_FILE:mrtk>
        ${_HASDATA}/G5P3162a_15m.sbf
        ${CMAKE_BINARY_DIR}/has_l6
    WORKING_DIRECTORY ${_HASDATA}
)
set_tests_properties(has_extract PROPERTIES
    FIXTURES_REQUIRED has_data
    DEPENDS           has_setup
    TIMEOUT           60
    LABELS            "regression;has"
)

# ── convert: SBF → RINEX obs/nav (test-step, archive stays SBF-only) ──────────
add_test(NAME has_convert
    COMMAND $<TARGET_FILE:mrtk> convert -r sbf
        -o ${_HASDATA}/has_obs.obs -n ${_HASDATA}/has_nav.nav
        ${_HASDATA}/G5P3162a_15m.sbf
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(has_convert PROPERTIES
    FIXTURES_REQUIRED has_data
    DEPENDS           has_setup
    TIMEOUT           120
    LABELS            "regression;has"
)

# ── post: Galileo HAS float PPP ───────────────────────────────────────────────
add_test(NAME has_ppp
    COMMAND $<TARGET_FILE:mrtk> post
        -k ${CMAKE_SOURCE_DIR}/conf/has/ppp_gal_has.toml
        -o ${CMAKE_BINARY_DIR}/out_has_ppp.pos
        ${_HASDATA}/has_obs.obs
        ${_HASDATA}/has_nav.nav
        ${_HASDATA}/G5P3162a_15m.has
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(has_ppp PROPERTIES
    FIXTURES_REQUIRED has_data
    DEPENDS           "has_convert;has_extract"
    TIMEOUT           180
    LABELS            "regression;has;ppp"
)

# ── HAS float-PPP accuracy / quality gate ─────────────────────────────────────
# Thresholds: measured trimmed-window offsets are horiz 0.575 m / 3D 0.634 m;
# gates set to 1.0 m horiz (>= full-hour 0.56 m + 0.3 m floor) and 1.5 m 3D,
# with ~2x margin over the measured offsets. All epochs must be Q=6 (float PPP)
# and >= 95% of the 900 expected.
add_test(NAME has_ppp_check
    COMMAND ${Python3_EXECUTABLE} ${_CHECK_HASPPP}
        ${CMAKE_BINARY_DIR}/out_has_ppp.pos
        --ref-xyz ${_HAS_REF_XYZ}
        --expected-epochs 900
        --min-ratio 0.95
        --interval 1
        --last-min 5
        --max-horiz 1.0
        --max-3d 1.5
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(has_ppp_check PROPERTIES
    DEPENDS           has_ppp
    FIXTURES_REQUIRED has_data
    LABELS            "regression;has;ppp"
)
