cmake_minimum_required(VERSION 3.25)
project(AetherSDR VERSION 26.7.2 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

# Libraries options — opt-in system-package replacements for vendored
# dependencies.  For dependencies enabled in a build, these stay off by
# default so CI, releases, and contributor laptops use the bundled,
# version-pinned third_party/ snapshots.  Distro packagers turn these ON to
# match their dynamic-linking policy.

option(USE_SYSTEM_ZLIB         "Use system zlib"         OFF)
option(USE_SYSTEM_MSPACK       "Use system libmspack"    OFF)
option(USE_SYSTEM_LIBMOSQUITTO "Use system libmosquitto" OFF)
option(USE_SYSTEM_RTMIDI       "Use system RtMidi"       OFF)
option(USE_SYSTEM_LIQUID_DSP   "Use system liquid-dsp"   OFF)

# Build features options

option(REQUIRE_SERIALPORT "Fail if Qt6 SerialPort is not found (use for release builds)" OFF)
option(REQUIRE_KEYCHAIN "Fail if Qt6Keychain is not found (use for release builds)" OFF)
option(ENABLE_LIQUID_DSP "Build with liquid-dsp toolkit support" OFF)
option(ENABLE_RADE "Build with RADE digital voice support (uses vendored Opus snapshot)" ON)
option(ENABLE_DSTAR "Build the local D-STAR waveform helper (vendored smartsdr-dsp/ThumbDV)" ON)
option(AETHER_GPU_SPECTRUM "Enable QRhi GPU spectrum rendering" ON)
option(ENABLE_SPECBLEACH "Enable NR4 spectral bleach noise reduction" ON)
option(ENABLE_DFNR "Enable DFNR DeepFilterNet3 noise reduction" ON)
# ON by default (like the other NR engines) so the pre-compiled release images
# all ship BNR — the runtime is download-on-demand, so this only compiles a small
# dlopen wrapper (no NVIDIA SDK at build time). Compiled on Linux/Windows only
# (see the platform gate below; macOS has no AFX runtime and uses DFNR). Pass
# -DENABLE_NVIDIA_AFX=OFF to opt out.
option(ENABLE_NVIDIA_AFX "Enable the NVIDIA Maxine AFX GPU denoiser (BNR; runtime-loaded, download-on-demand; NVIDIA RTX/GeForce, Linux/Windows)" ON)
option(ENABLE_MQTT "Enable MQTT client support" ON)
option(MQTT_TLS "Enable MQTT TLS via OpenSSL (disable for AppImage)" ON)
option(LOWER_CASE_BINARY_NAME "Make the output binary lower case. Only affects Linux" OFF)
if(WIN32)
    set(AETHER_EMBED_DFNR_MODEL_DEFAULT ON)
else()
    set(AETHER_EMBED_DFNR_MODEL_DEFAULT OFF)
endif()
option(AETHER_EMBED_DFNR_MODEL
       "Embed the DFNR model payload into application resources instead of deploying a loose archive"
       ${AETHER_EMBED_DFNR_MODEL_DEFAULT})

# macOS: ensure Homebrew lib/include paths are in the search path
# (universal builds with CMAKE_OSX_ARCHITECTURES may not search /opt/homebrew by default)
if(APPLE)
    execute_process(COMMAND brew --prefix OUTPUT_VARIABLE HOMEBREW_PREFIX OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(HOMEBREW_PREFIX)
        link_directories("${HOMEBREW_PREFIX}/lib")
        include_directories("${HOMEBREW_PREFIX}/include")
    endif()
endif()

# Build type
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

# Qt6 components
# Qt 6.2 minimum: matches Ubuntu 22.04 LTS (our oldest supported distro)
# and is the floor where QWindow::startSystemMove() — used by the
# frameless title-bar drag in TitleBar / ContainerWidget / PanadapterApplet
# — became available.  Binary distributions bundle Qt 6.7+ for QRhiWidget.
find_package(Qt6 6.2 REQUIRED COMPONENTS
    Core
    Concurrent
    Widgets
    Network
    Multimedia
    Test
)
# zlib is bundled under third_party/zlib (1.3.1) for parity with the
# libmosquitto bundling pattern (#699) and per the constitution's
# Technology Constraint preferring bundled libraries over package
# managers.  The vcpkg dependency on Windows + system-zlib on Linux/macOS
# is gone — single source-of-truth across all platforms. (#2651)
set(ZLIB_BUILD_EXAMPLES OFF CACHE BOOL "Disable zlib examples" FORCE)
set(SKIP_INSTALL_ALL ON CACHE BOOL "Don't install zlib" FORCE)

if (USE_SYSTEM_ZLIB)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(zlib REQUIRED IMPORTED_TARGET zlib)
    endif()
else()
    add_subdirectory(third_party/zlib EXCLUDE_FROM_ALL)
endif()

# Note: on Windows the SerialPortController uses raw Win32 WaitCommEvent for
# DSR/CTS edge detection (FTDI VCP drivers don't refresh
# GetCommModemStatus outside a WaitCommEvent completion).  Qt6::SerialPort
# is still useful on Linux/macOS for pin polling, so the find_package call
# stays platform-agnostic.
if(REQUIRE_SERIALPORT)
    find_package(Qt6 REQUIRED COMPONENTS SerialPort)
else()
    find_package(Qt6 QUIET COMPONENTS SerialPort)
endif()
if(Qt6SerialPort_FOUND)
    message(STATUS "Qt6::SerialPort found — serial PTT/CW support enabled")
else()
    message(STATUS "Qt6::SerialPort not found — serial PTT/CW support disabled")
endif()
find_package(Qt6 QUIET COMPONENTS WebSockets)
if(Qt6WebSockets_FOUND)
    message(STATUS "Qt6::WebSockets found — FreeDV Reporter spot source enabled")
else()
    message(STATUS "Qt6::WebSockets not found — FreeDV Reporter spot source disabled")
endif()
if(UNIX AND NOT APPLE)
    find_package(Qt6 QUIET COMPONENTS DBus)
    if(Qt6DBus_FOUND)
        message(STATUS "Qt6::DBus found — sleep inhibition via D-Bus enabled")
    else()
        message(STATUS "Qt6::DBus not found — sleep inhibition disabled on Linux")
    endif()
endif()
# qtkeychain may be staged into third_party/ by scripts/setup/setup-qtkeychain.*
# (Windows + Linux AppImage release builds build it from source there). Prepend
# so find_package resolves it before any system install. This EXISTS-guarded
# prepend supersedes the earlier WIN32-only one (#3634): Windows stages into the
# same dir, so the general form covers both platforms.
if(EXISTS "${CMAKE_SOURCE_DIR}/third_party/qtkeychain")
    list(PREPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/third_party/qtkeychain")
endif()
# REQUIRE_KEYCHAIN makes a missing Qt6Keychain a hard build failure rather than
# a silent compile-out of SmartLink credential persistence — release/packaged
# builds set it so an artifact can never ship without working persistence (#3639).
if(REQUIRE_KEYCHAIN)
    find_package(Qt6Keychain REQUIRED)
else()
    find_package(Qt6Keychain QUIET)
endif()
if(Qt6Keychain_FOUND)
    message(STATUS "Qt6Keychain found — credential persistence enabled")
else()
    message(STATUS "Qt6Keychain not found — credential persistence disabled")
endif()

# PortAudio (optional fallback for audio)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
    pkg_check_modules(PORTAUDIO portaudio-2.0)
endif()

# FFTW3 (required for NR2 spectral noise reduction)
# Windows: run scripts/setup/setup-fftw.ps1 first to download prebuilt DLLs
# Linux:   apt install libfftw3-dev
# macOS:   brew install fftw
if(WIN32)
    set(FFTW3_ROOT "${CMAKE_SOURCE_DIR}/third_party/fftw3")
    if(EXISTS "${FFTW3_ROOT}/include/fftw3.h")
        set(FFTW3_FOUND TRUE)
        set(FFTW3_INCLUDE_DIRS "${FFTW3_ROOT}/include")
        set(FFTW3_LIBRARIES "${FFTW3_ROOT}/lib/fftw3.lib")
        set(FFTW3_DLL "${FFTW3_ROOT}/bin/libfftw3-3.dll")
    else()
        message(WARNING "FFTW3 not found. Run scripts/setup/setup-fftw.ps1 to download it. NR2 will use fallback FFT.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(FFTW3 fftw3)
    endif()
    if(NOT FFTW3_FOUND)
        find_library(FFTW3_LIB fftw3)
        find_path(FFTW3_INC fftw3.h)
        if(FFTW3_LIB AND FFTW3_INC)
            set(FFTW3_FOUND TRUE)
            set(FFTW3_LIBRARIES ${FFTW3_LIB})
            set(FFTW3_INCLUDE_DIRS ${FFTW3_INC})
        endif()
    endif()
endif()

# Bundled RADE (BSD-2) — FreeDV Radio Autoencoder digital voice codec
# Opus (with FARGAN/LPCNet) is built from a vendored local snapshot via ExternalProject
set(RADE_DIR ${CMAKE_SOURCE_DIR}/third_party/radae)
option(RADE_WAV_TAP "Write diagnostic WAV files at RADE TX tap points (Taps A/B/D/E/F) for offline demod analysis" OFF)
if(ENABLE_RADE AND EXISTS "${RADE_DIR}/src/rade_api.h")
    # macOS universal binary: build Opus for both architectures
    if(APPLE AND CMAKE_OSX_ARCHITECTURES MATCHES "x86_64.*arm64|arm64.*x86_64")
        set(BUILD_OSX_UNIVERSAL ON)
    endif()
    include(${RADE_DIR}/cmake/BuildOpus.cmake)
    set(RADE_SOURCES
        ${RADE_DIR}/src/rade_api_nopy.c
        ${RADE_DIR}/src/rade_dsp.c
        ${RADE_DIR}/src/rade_ofdm.c
        ${RADE_DIR}/src/rade_bpf.c
        ${RADE_DIR}/src/rade_acq.c
        ${RADE_DIR}/src/rade_tx.c
        ${RADE_DIR}/src/rade_rx.c
        ${RADE_DIR}/src/rade_enc.c
        ${RADE_DIR}/src/rade_dec.c
        ${RADE_DIR}/src/rade_enc_data.c
        ${RADE_DIR}/src/rade_dec_data.c
        ${RADE_DIR}/src/kiss_fft.c
        ${RADE_DIR}/src/kiss_fftr.c
        # codec2-derived LDPC + GP-interleaver for EOO callsign (rade_text)
        ${RADE_DIR}/src/mpdecode_core.c
        ${RADE_DIR}/src/gp_interleaver.c
        ${RADE_DIR}/src/HRA_56_56.c
        ${RADE_DIR}/src/ldpc_codes.c
        ${RADE_DIR}/src/rade_text.c
    )
    if(MSVC)
        set(RADE_WARN_FLAG "/w")
    else()
        set(RADE_WARN_FLAG "-w")
    endif()
    set_source_files_properties(${RADE_SOURCES} PROPERTIES
        COMPILE_FLAGS "${RADE_WARN_FLAG} -DIS_BUILDING_RADE_API=1 -DRADE_PYTHON_FREE=1"
        INCLUDE_DIRECTORIES "${RADE_DIR}/src"
    )
    set(RADE_FOUND TRUE)
    message(STATUS "RADE enabled (bundled vendored Opus snapshot)")

    # Standalone offline diagnostic tool: demodulate a WAV file and decode the EOO callsign.
    # Guarded by source-file existence so builds against vendored RADE snapshots that
    # predate rade_demod_wav.c don't hard-fail. Independent of RADE_WAV_TAP.
    # Excluded on WIN32: rade_demod_wav.c uses POSIX getopt.h, unavailable in MSVC's SDK.
    if(EXISTS "${RADE_DIR}/src/rade_demod_wav.c" AND NOT WIN32)
        add_executable(rade_demod_wav
            ${RADE_SOURCES}
            ${RADE_DIR}/src/rade_demod_wav.c
        )
        set_source_files_properties(${RADE_DIR}/src/rade_demod_wav.c PROPERTIES
            COMPILE_FLAGS "${RADE_WARN_FLAG} -DIS_BUILDING_RADE_API=1 -DRADE_PYTHON_FREE=1"
            INCLUDE_DIRECTORIES "${RADE_DIR}/src"
        )
        target_include_directories(rade_demod_wav PRIVATE ${RADE_DIR}/src)
        target_link_libraries(rade_demod_wav PRIVATE opus
            $<$<NOT:$<PLATFORM_ID:Windows>>:m>)
        add_dependencies(rade_demod_wav build_opus)
    endif()

else()
    set(RADE_FOUND FALSE)
    set(RADE_SOURCES "")
    if(ENABLE_RADE)
        message(STATUS "RADE source not found at ${RADE_DIR}. Digital voice disabled.")
    else()
        message(STATUS "RADE disabled by ENABLE_RADE=OFF.")
    endif()
endif()

# Opus codec — required for SmartLink compressed audio, independent of RADE.
# When RADE is enabled, Opus comes from AetherSDR's vendored RADE snapshot
# (with FARGAN/OSCE).
# When RADE is disabled, find system libopus.
# Windows: run scripts/setup/setup-opus.ps1 first to download prebuilt DLLs
# Linux:   apt install libopus-dev
# macOS:   brew install opus
if(RADE_FOUND)
    set(OPUS_FOUND TRUE)
    message(STATUS "Opus: using RADE's bundled build")
elseif(WIN32)
    set(OPUS_ROOT "${CMAKE_SOURCE_DIR}/third_party/opus")
    if(EXISTS "${OPUS_ROOT}/include/opus/opus.h")
        set(OPUS_FOUND TRUE)
        set(OPUS_INCLUDE_DIRS "${OPUS_ROOT}/include/opus")
        set(OPUS_LIBRARIES "${OPUS_ROOT}/lib/opus.lib")
        message(STATUS "Opus: using prebuilt Windows library (static)")
    else()
        set(OPUS_FOUND FALSE)
        message(WARNING "Opus not found. Run scripts/setup/setup-opus.ps1 to download it. "
                        "SmartLink compressed audio will be unavailable.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(OPUS opus)
    endif()
    if(NOT OPUS_FOUND)
        find_library(OPUS_LIB opus)
        find_path(OPUS_INC opus/opus.h)
        if(OPUS_LIB AND OPUS_INC)
            set(OPUS_FOUND TRUE)
            set(OPUS_LIBRARIES ${OPUS_LIB})
            set(OPUS_INCLUDE_DIRS "${OPUS_INC}/opus")
        endif()
    endif()
    if(OPUS_FOUND)
        message(STATUS "Opus: using system libopus")
    else()
        message(WARNING "Opus not found — SmartLink compressed audio will be unavailable. "
                        "Install libopus-dev (Debian/Ubuntu), opus (Arch/vcpkg), or brew install opus (macOS).")
    endif()
endif()

# GPU-accelerated spectrum/waterfall rendering via QRhi (#391)
# Requires: Qt 6.7+ (QRhiWidget), Qt6::ShaderTools (build-time shader compilation)
# Defaults to ON but auto-disables on Qt < 6.7 (e.g. Ubuntu 24.04 ships Qt 6.4).
if(AETHER_GPU_SPECTRUM)
    if(Qt6_VERSION VERSION_LESS "6.7")
        set(AETHER_GPU_SPECTRUM OFF)
        message(STATUS "GPU spectrum rendering disabled (requires Qt 6.7+, found ${Qt6_VERSION} — pass -DCMAKE_PREFIX_PATH=/path/to/Qt/6.7.x/gcc_64 to use a newer Qt installation)")
    else()
        find_package(Qt6 REQUIRED COMPONENTS ShaderTools)
        # Qt6GuiPrivate provides QRhi headers needed for GPU rendering.
        find_package(Qt6GuiPrivate QUIET)

        # Some Qt distributions ship the private QtGui headers on disk but omit the
        # Qt6GuiPrivate CMake package (so no Qt6::GuiPrivate target): Debian multi-arch,
        # and Windows/macOS installs deployed via aqtinstall. Locate the private
        # include root directly and wire it up manually (see DEBIAN_GPU_FIX_REQUIRED).
        if(NOT Qt6GuiPrivate_FOUND)
            message(STATUS "Qt6GuiPrivate CMake package not found, searching for private headers directly...")
            set(DEB_HOST_MULTIARCH "")
            find_program(DPKG_ARCH dpkg-architecture)
            if(DPKG_ARCH)
                execute_process(COMMAND ${DPKG_ARCH} -qDEB_HOST_MULTIARCH
                    OUTPUT_VARIABLE DEB_HOST_MULTIARCH OUTPUT_STRIP_TRAILING_WHITESPACE
                    ERROR_QUIET)
            endif()
            # Derive the Qt include root from the Gui target — covers Windows/macOS SDK
            # layouts (<prefix>/include) where dpkg multi-arch paths don't apply.
            set(_qt_gui_inc_root "")
            if(TARGET Qt6::Gui)
                get_target_property(_qt_gui_incs Qt6::Gui INTERFACE_INCLUDE_DIRECTORIES)
                foreach(_inc IN LISTS _qt_gui_incs)
                    if(EXISTS "${_inc}/QtGui/${Qt6_VERSION}/QtGui/private/qhighdpiscaling_p.h")
                        set(_qt_gui_inc_root "${_inc}")
                        break()
                    endif()
                endforeach()
            endif()
            find_path(DEBIAN_PRIVATE_INC
                NAMES "QtGui/${Qt6_VERSION}/QtGui/private/qhighdpiscaling_p.h"
                PATHS
                    "${_qt_gui_inc_root}"
                    "/usr/include/${DEB_HOST_MULTIARCH}/qt6" "/usr/include/qt6"
                NO_DEFAULT_PATH
            )
            if(DEBIAN_PRIVATE_INC)
                set(Qt6GuiPrivate_FOUND TRUE)
                set(DEBIAN_GPU_FIX_REQUIRED TRUE) # Mark for Step 2 (manual include dirs)
            endif()
        endif()

        if(Qt6GuiPrivate_FOUND)
            message(STATUS "GPU spectrum rendering enabled (QRhi, Qt ${Qt6_VERSION})")
        else()
            set(AETHER_GPU_SPECTRUM OFF)
            message(STATUS "GPU spectrum rendering disabled — Qt6GuiPrivate not found "
                           "(install qt6-base-private-dev / qt6-qtbase-private-devel)")
        endif()
    endif()
else()
    message(STATUS "GPU spectrum rendering disabled (use -DAETHER_GPU_SPECTRUM=ON to enable)")
endif()

# libspecbleach NR4 — bundled spectral noise reduction (LGPL-2.1)
# MSVC: libspecbleach uses C99 VLAs and __attribute__ which MSVC doesn't support.
# When clang-cl is available, build as a separate static lib using it.
if(MSVC)
    find_program(CLANG_CL clang-cl HINTS "C:/Program Files/LLVM/bin")
    if(NOT CLANG_CL)
        set(ENABLE_SPECBLEACH OFF CACHE BOOL "Enable NR4 spectral bleach noise reduction" FORCE)
    endif()
endif()
if(ENABLE_SPECBLEACH)
    file(GLOB_RECURSE SPECBLEACH_SOURCES third_party/libspecbleach/src/*.c)
    if(MSVC AND CLANG_CL)
        # Pre-build specbleach.lib with clang-cl (supports VLAs and MSVC ABI)
        set(SPECBLEACH_BUILD_DIR "${CMAKE_BINARY_DIR}/specbleach")
        file(MAKE_DIRECTORY ${SPECBLEACH_BUILD_DIR})
        set(SPECBLEACH_STATIC_LIB "${SPECBLEACH_BUILD_DIR}/specbleach.lib")
        # When -T ClangCL is used CMAKE_C_COMPILER is the VS-bundled clang-cl which
        # auto-detects the Windows SDK and MSVC include paths. Prefer it over the
        # standalone LLVM found by find_program (which may not locate stdint.h in a
        # MSBuild custom-command environment).
        if(CMAKE_C_COMPILER_ID STREQUAL "Clang")
            set(_specbleach_cc "${CMAKE_C_COMPILER}")
        else()
            set(_specbleach_cc "${CLANG_CL}")
        endif()
        add_custom_command(
            OUTPUT ${SPECBLEACH_STATIC_LIB}
            COMMAND ${_specbleach_cc} -w --target=x86_64-pc-windows-msvc
                -DFFTW_DLL
                -I${CMAKE_SOURCE_DIR}/third_party/libspecbleach/include
                -I${CMAKE_SOURCE_DIR}/third_party/libspecbleach/src
                -I${CMAKE_SOURCE_DIR}/third_party/fftw3/include
                -c ${SPECBLEACH_SOURCES}
            COMMAND lib /nologo /out:specbleach.lib *.obj
            WORKING_DIRECTORY ${SPECBLEACH_BUILD_DIR}
            DEPENDS ${SPECBLEACH_SOURCES}
            COMMENT "Building libspecbleach with clang-cl"
        )
        add_custom_target(specbleach_build DEPENDS ${SPECBLEACH_STATIC_LIB})
        set(SPECBLEACH_SOURCES "")
        message(STATUS "NR4 (libspecbleach) enabled — building with clang-cl")
    else()
        message(STATUS "NR4 (libspecbleach) enabled — ${CMAKE_SOURCE_DIR}/third_party/libspecbleach")
    endif()
else()
    set(SPECBLEACH_SOURCES "")
    if(MSVC AND NOT CLANG_CL)
        message(STATUS "NR4 (libspecbleach) disabled — install LLVM for clang-cl VLA support")
    else()
        message(STATUS "NR4 (libspecbleach) disabled")
    endif()
endif()

# DeepFilterNet3 DFNR — bundled neural noise reduction (MIT/Apache-2.0)
# Pre-built libdf library from the Rust crate; model payload is handled below.
if(ENABLE_DFNR)
    set(DEEPFILTER_DIR ${CMAKE_SOURCE_DIR}/third_party/deepfilter)
    set(DFNR_MODEL "${DEEPFILTER_DIR}/models/DeepFilterNet3_onnx.tar.gz")
    set(DFNR_EMBEDDED_MODEL_NAME "DeepFilterNet3_onnx.dfmodel")
    if(WIN32)
        set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/windows-x86_64")
        if(MINGW)
            set(DFNR_LIB "${DFNR_LIB_DIR}/libdeepfilter.dll.a")
            set(DFNR_DLL "${DFNR_LIB_DIR}/deepfilter.dll")
        else()
            # MSVC: prefer .dll.lib import library, fall back to .lib
            if(EXISTS "${DFNR_LIB_DIR}/deepfilter.dll.lib")
                set(DFNR_LIB "${DFNR_LIB_DIR}/deepfilter.dll.lib")
            else()
                set(DFNR_LIB "${DFNR_LIB_DIR}/deepfilter.lib")
            endif()
            set(DFNR_DLL "${DFNR_LIB_DIR}/deepfilter.dll")
        endif()
    elseif(APPLE)
        if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64")
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/darwin-arm64")
        else()
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/darwin-x86_64")
        endif()
        set(DFNR_LIB "${DFNR_LIB_DIR}/libdeepfilter.a")
    else()
        if(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64")
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/linux-aarch64")
        else()
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/linux-x86_64")
        endif()
        set(DFNR_LIB "${DFNR_LIB_DIR}/libdeepfilter.a")
    endif()
    if(EXISTS ${DFNR_LIB})
        message(STATUS "DFNR (DeepFilterNet3) enabled — ${DFNR_LIB}")
    else()
        set(ENABLE_DFNR OFF)
        message(STATUS "DFNR (DeepFilterNet3) disabled — library not found at ${DFNR_LIB} "
                       "(run ./scripts/setup/setup-deepfilter.sh before cmake to enable)")
    endif()
endif()

# MQTT client support — bundled libmosquitto (#699)
if(ENABLE_MQTT)
    if (USE_SYSTEM_LIBMOSQUITTO)
        find_package(PkgConfig REQUIRED)
        if(PkgConfig_FOUND)
            pkg_check_modules(libmosquitto REQUIRED IMPORTED_TARGET libmosquitto)
        endif()
    else()
        if(MQTT_TLS)
            find_package(OpenSSL)
        endif()
        set(MOSQUITTO_DIR ${CMAKE_SOURCE_DIR}/third_party/mosquitto)
        file(GLOB MOSQUITTO_SOURCES ${MOSQUITTO_DIR}/src/*.c)
        # Remove broker-only and optional files we don't need
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "socks_mosq\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "http_client\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "picohttpparser\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "extended_auth\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "cjson_common\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "password_common\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "base64_common\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "json_help\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "srv_mosq\\.c$")
        if(OpenSSL_FOUND)
            message(STATUS "MQTT client support enabled (bundled libmosquitto, TLS via OpenSSL ${OPENSSL_VERSION})")
        else()
            # tls_mosq.c and file_common.c (cert file access) require OpenSSL — exclude when not available
            list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "tls_mosq\\.c$")
            list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "file_common\\.c$")
            message(STATUS "MQTT client support enabled (bundled libmosquitto, TLS disabled — OpenSSL not found)")
        endif()
    endif()
endif()

# Sources
set(CORE_SOURCES
    src/core/backends/flex/FlexBackend.cpp   # aetherd RFC step 2.2 (§5.5)
    src/core/AppSettings.cpp
    src/core/GpuSelector.cpp
    src/core/AgcTCalibrator.cpp
    src/core/AdaptiveFilterEngine.cpp
    src/core/OccupiedRegion.cpp
    src/core/SettingsHelpers.cpp
    src/core/ThemeManager.cpp
    src/core/BandStackSettings.cpp
    src/core/RadioDiscovery.cpp
    src/core/RadioConnection.cpp
    src/core/NetworkPathResolver.cpp
    src/core/TgxlConnection.cpp
    src/core/CommandParser.cpp
    src/core/AudioSummaryLogger.cpp
    src/core/AudioFormatNegotiator.cpp
    src/core/AudioDeviceNegotiator.cpp
    src/core/AudioOutputRouter.cpp
    src/core/AudioEngine.cpp
    src/core/TxMicChannelNormalizer.cpp
    src/core/ChannelStripPresets.cpp
    src/core/Biquad.cpp
    src/core/StereoBiquad.cpp
    src/core/ClientEq.cpp
    src/core/ClientComp.cpp
    src/core/ClientGate.cpp
    src/core/ClientDeEss.cpp
    src/core/ClientTube.cpp
    src/core/ClientPudu.cpp
    src/core/ClientPuduMonitor.cpp
    src/core/ClientReverb.cpp
    src/core/ClientPhaseRotator.cpp
    src/core/ClientFinalLimiter.cpp
    src/core/ClientTxTestTone.cpp
    src/core/ClientQuindarTone.cpp
    src/core/QuindarLocalSink.cpp
    src/core/CwSidetoneGenerator.cpp
    src/core/CwSidetoneQAudioSink.cpp
    src/core/CwxLocalKeyer.cpp
    src/core/IambicKeyer.cpp
    src/core/SpectralNR.cpp
    src/core/MonoDspStereoAdapter.cpp
    src/core/PanadapterStream.cpp
    src/core/PacketLossConcealment.cpp
    src/core/PerfTelemetry.cpp
    src/core/RigctlProtocol.cpp
    src/core/SmartCatProtocol.cpp
    src/core/SmartCatSession.cpp
    src/core/CatPort.cpp
    src/core/SmartLinkClient.cpp
    src/core/KiwiSdrProtocol.cpp
    src/core/KiwiSdrRedirectPolicy.cpp
    src/core/KiwiSdrCredentialStore.cpp
    src/core/KiwiSdrClient.cpp
    src/core/KiwiSdrManager.cpp
    src/core/ReceivePresentationSync.cpp
    src/core/KiwiPublicDirectory.cpp
    src/core/WanConnection.cpp
    src/core/DxClusterClient.cpp
    src/core/WsjtxClient.cpp
    src/core/SpotCollectorClient.cpp
    src/core/PotaClient.cpp
    src/core/PropForecastClient.cpp
    src/core/PskReporterClient.cpp
    src/core/MqttAntennaAlias.cpp
    src/core/MqttSettings.cpp
    src/core/AutomationBridgeSettings.cpp
    src/core/SpotCommandPolicy.cpp
    src/core/SpotModeResolver.cpp
    src/core/TciServer.cpp
    src/core/TciProtocol.cpp
    src/core/AutomationServer.cpp
    src/core/MqttClient.cpp
    src/core/PgxlConnection.cpp
    src/core/FirmwareUploader.cpp
    src/core/WaveformInstaller.cpp
    src/core/WaveformUploadState.cpp
    src/core/LegacyWaveformPackage.cpp
    src/core/DigitalVoiceModeRegistry.cpp
    src/core/DigitalVoiceWaveformTelemetry.cpp
    src/core/DigitalVoiceWaveformProcess.cpp
    src/core/DvkWavTransfer.cpp
    src/core/ZipArchive.cpp
    src/core/ProfileTransfer.cpp
    src/core/QsoRecorder.cpp
    src/core/FirmwareStager.cpp
    src/core/UpdateChecker.cpp
    src/core/OleCompoundFile.cpp
    src/core/CabExtractor.cpp
    src/core/RNNoiseFilter.cpp
    src/core/SpecbleachFilter.cpp
    src/core/CwDecoder.cpp
    src/core/CwCallsignSpotter.cpp
    src/core/CallsignInfo.cpp
    src/core/QrzClient.cpp
    src/core/CallsignLookupService.cpp
    src/core/RttyDecoder.cpp
    src/core/VoiceSignalDetector.cpp
    src/core/SpectrogramBuffer.cpp
    src/core/SignalClassifier.cpp
    src/core/Resampler.cpp
    src/core/NvidiaAfxFilter.cpp
    src/core/NvidiaAfxPack.cpp
    src/core/DeepFilterFilter.cpp
    src/core/RADEEngine.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/ShortcutManager.cpp
    src/core/SupportBundle.cpp
    src/core/DeviceDiagnostics.cpp
    src/core/SerialPortController.cpp
    src/core/FlexControlManager.cpp
    src/core/OpusCodec.cpp
    src/core/CtyDatParser.cpp
    src/core/AdifParser.cpp
    src/core/DxccWorkedStatus.cpp
    src/core/DxccColorProvider.cpp
    src/core/SleepInhibitor.cpp
    src/core/MemoryCsvCompat.cpp
    src/core/MemoryFieldValues.cpp
    src/core/MemoryRecallPolicy.cpp
    src/core/NetRecurrence.cpp
    src/core/NetSchedulePlanner.cpp
    src/core/NetScheduleStore.cpp
    src/core/NetScheduler.cpp
    src/core/WfmDemodulator.cpp
    src/core/WfmDsp.cpp
    src/core/WaveOutWriter.cpp
    src/core/tnc/AetherAx25LibmodemShim.cpp
    src/core/tnc/HdlcCodec.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
    src/core/tnc/Ax25.cpp
    src/core/tnc/Ax25Connection.cpp
    src/core/tnc/HeardList.cpp
    src/core/tnc/KissFraming.cpp
    src/core/tnc/KissTncServer.cpp
    src/core/tnc/TncTerminal.cpp
    src/core/pms/PmsMailbox.cpp
    src/core/aprs/AprsPacket.cpp
    src/core/aprs/AprsStationList.cpp
    src/core/aprs/AprsMessenger.cpp
    src/core/aprs/AprsBeacon.cpp
    src/core/aprs/AprsSettings.cpp
)

if(APPLE)
    list(APPEND CORE_SOURCES src/core/VirtualAudioBridge.cpp src/core/MacMicPermission.mm)
elseif(UNIX)
    # Linux DAX uses PulseAudio pipe modules via pactl (works with PipeWire too).
    # When libpipewire-0.3 dev headers are present we additionally compile a
    # native pw_stream-based RX source for sub-100ms DAX RX latency.
    list(APPEND CORE_SOURCES src/core/PipeWireAudioBridge.cpp)
    set(HAVE_PIPEWIRE TRUE)
    pkg_check_modules(PIPEWIRE_NATIVE libpipewire-0.3)
    if(PIPEWIRE_NATIVE_FOUND)
        list(APPEND CORE_SOURCES
            src/core/PipeWireNativeContext.cpp
            src/core/PipeWireNativeRxSource.cpp
        )
        set(HAVE_PIPEWIRE_NATIVE TRUE)
    endif()
endif()

set(MODEL_SOURCES
    src/models/RadioModel.cpp
    src/models/DeclaredBands.cpp
    src/models/RadioSession.cpp
    src/models/ModelCapabilities.cpp
    src/models/AntennaAliasStore.cpp
    src/models/SliceModel.cpp
    src/models/PanadapterModel.cpp
    src/models/MeterModel.cpp
    src/models/TunerModel.cpp
    src/models/AmpModel.cpp
    src/models/TransmitModel.cpp
    src/models/EqualizerModel.cpp
    src/models/TnfModel.cpp
    src/models/UsbCableModel.cpp
    src/models/DaxIqModel.cpp
    src/models/SpotModel.cpp
    src/models/CwxModel.cpp
    src/models/DvkModel.cpp
    src/models/NavtexModel.cpp
    src/models/FlexWaveformModel.cpp
    src/models/DStarModel.cpp
    src/models/DigitalVoiceWaveformHistory.cpp
    src/models/BandSettings.cpp
    src/models/BandPlanManager.cpp
    src/models/XvtrPolicy.cpp
    src/models/AntennaGeniusModel.cpp
)

# Vendored QGeoView (LGPL-3.0) — slippy-map widget for the common
# mapping engine (PSK Reporter map, future APRS map). Patched per
# third_party/QGeoView/AETHERSDR-PATCHES.md.
add_subdirectory(third_party/QGeoView/lib EXCLUDE_FROM_ALL)

set(GUI_SOURCES
    src/gui/MainWindow.cpp
    src/gui/MainWindowHelpers.cpp
    src/gui/MainWindow_Controllers.cpp
    src/gui/MainWindow_DspApplets.cpp
    src/gui/MainWindow_Menus.cpp
    src/gui/MainWindow_Session.cpp
    src/gui/MainWindow_Shortcuts.cpp
    src/gui/MainWindow_DigitalModes.cpp
    src/gui/MainWindow_Spots.cpp
    src/gui/MainWindow_SwrSweep.cpp
    src/gui/MainWindow_Wiring.cpp
    src/gui/MainWindow_Nets.cpp
    src/gui/MainWindow_Callsign.cpp
    src/gui/MainWindow_ReceiveSync.cpp
    src/gui/MainWindow_KiwiSdr.cpp
    src/gui/map/MapView.cpp
    src/gui/map/MapMarkerItem.cpp
    src/gui/map/MapPathItem.cpp
    src/gui/PskReporterMapDialog.cpp
    src/gui/AgcCalibrationDialog.cpp
    src/gui/AudioDeviceChangeDialog.cpp
    src/gui/WfmDeviceDialog.cpp
    src/gui/ConnectionPanel.cpp
    src/gui/ClientDisconnectDialog.cpp
    src/gui/ConnectedStationsDialog.cpp
    src/gui/PanadapterMessageOverlay.cpp
    src/gui/PanadapterMessageOverlay.h
    src/gui/PanadapterRenderScheduler.cpp
    src/gui/SpectrumWidget.cpp
    src/gui/SpectrumOverlayMenu.cpp
    src/gui/DssRenderer.cpp
    src/gui/FrequencyEntryParser.cpp
    src/gui/SliceColorManager.cpp
    src/gui/SliceLabel.cpp
    src/gui/VfoWidget.cpp
    src/gui/SmartMtrWidget.cpp
    src/gui/SmartMtrConfig.cpp
    src/gui/MeterViewController.cpp
    src/gui/RadioSetupDialog.cpp
    src/gui/NetworkDiagnosticsDialog.cpp
    src/gui/Ax25HfPacketDecodeDialog.cpp
    src/gui/DStarModemPage.cpp
    src/gui/AprsMessagesDialog.cpp
    src/gui/AprsSymbolIcons.cpp
    src/gui/FlexControlDialog.cpp
    src/gui/PropDashboardDialog.cpp
    src/gui/MemoryCommands.cpp
    src/gui/MemoryBrowsePanel.cpp
    src/gui/MemoryDialog.cpp
    src/gui/NetSchedulerDialog.cpp
    src/gui/NetReminderBanner.cpp
    src/gui/SpotSettingsDialog.cpp
    src/gui/AetherDspDialog.cpp
    src/gui/AetherDspWidget.cpp
    src/gui/WaveformsDialog.cpp
    src/gui/ClientRxDspApplet.cpp
    src/gui/DragValuePopup.cpp
    src/gui/DspParamPopup.cpp
    src/gui/DxClusterDialog.cpp
    src/gui/DxClusterStartupCommandsDialog.cpp
    src/gui/CallsignCard.cpp
    src/gui/CallsignLookupDialog.cpp
    src/gui/CwxPanel.cpp
    src/gui/BandStackPanel.cpp
    src/gui/FramelessWindowTitleBar.cpp
    src/gui/FramelessResizer.cpp
    src/gui/PanFloatingWindow.cpp
    src/gui/DvkPanel.cpp
    src/gui/AmpApplet.cpp
    src/gui/MeterApplet.cpp
    src/gui/ProfileSwitcherApplet.cpp
    src/gui/RadeApplet.cpp
    src/gui/HealthApplet.cpp
    src/gui/PersistentDialog.cpp
    src/gui/FramelessMessageBox.cpp
    src/gui/ProfileManagerDialog.cpp
    src/gui/ProfileImportExportDialog.cpp
    src/gui/TxBandDialog.cpp
    src/gui/PanadapterApplet.cpp
    src/gui/RangeSlider.cpp
    src/gui/PanadapterStack.cpp
    src/gui/PanLayoutDialog.cpp
    src/gui/AppletPanel.cpp
    src/gui/KiwiSdrApplet.cpp
    src/gui/KiwiPublicReceiverPicker.cpp
    src/gui/FavoritesPickerDialog.cpp
    src/gui/RxApplet.cpp
    src/gui/AdaptiveFilterControls.cpp
    src/gui/FilterPassbandWidget.cpp
    src/gui/SMeterWidget.cpp
    src/gui/TunerApplet.cpp
    src/gui/TxApplet.cpp
    src/gui/AtuPreTuneDialog.cpp
    src/gui/SwrSweepLicenseDialog.cpp
    src/gui/PhoneCwApplet.cpp
    src/gui/PhoneApplet.cpp
    src/gui/EqApplet.cpp
    src/gui/WaveApplet.cpp
    src/gui/WaveformScopeModel.cpp
    src/gui/WaveformWidget.cpp
    src/gui/ClientEqApplet.cpp
    src/gui/ClientEqCurveWidget.cpp
    src/gui/ClientEqEditor.cpp
    src/gui/ClientEqEditorCanvas.cpp
    src/gui/StripEqPanel.cpp
    src/gui/ClientEqFftAnalyzer.cpp
    src/gui/ClientEqIconRow.cpp
    src/gui/ClientEqOutputFader.cpp
    src/gui/ClientLevelMeter.cpp
    src/gui/ClientEqParamRow.cpp
    src/gui/ClientChainApplet.cpp
    src/gui/ClientChainWidget.cpp
    src/gui/StripChainWidget.cpp
    src/gui/StripRxChainWidget.cpp
    src/gui/StripRxOutputPanel.cpp
    src/gui/ClientRxChainWidget.cpp
    src/gui/EditorFramelessTitleBar.cpp
    src/gui/containers/ContainerManager.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/gui/ClientCompApplet.cpp
    src/gui/ClientCompCurveWidget.cpp
    src/gui/ClientCompKnob.cpp
    src/gui/ClientGateApplet.cpp
    src/gui/ClientGateCurveWidget.cpp
    src/gui/ClientGateEditor.cpp
    src/gui/StripGatePanel.cpp
    src/gui/ClientGateLevelView.cpp
    src/gui/ClientDeEssApplet.cpp
    src/gui/ClientDeEssCurveWidget.cpp
    src/gui/StripDeEssPanel.cpp
    src/gui/ClientTubeApplet.cpp
    src/gui/ClientTubeCurveWidget.cpp
    src/gui/ClientTubeEditor.cpp
    src/gui/StripTubePanel.cpp
    src/gui/ClientPuduApplet.cpp
    src/gui/ClientPuduEditor.cpp
    src/gui/StripPuduPanel.cpp
    src/gui/ClientReverbApplet.cpp
    src/gui/StripReverbPanel.cpp
    src/gui/StripWaveformPanel.cpp
    src/gui/StripFinalOutputPanel.cpp
    src/gui/AetherialAudioStrip.cpp
    src/gui/PooDooLogo.cpp
    src/gui/ClientCompLimiterButton.cpp
    src/gui/ClientCompMeter.cpp
    src/gui/ClientCompThresholdFader.cpp
    src/gui/ClientCompEditor.cpp
    src/gui/ClientCompEditorCanvas.cpp
    src/gui/StripCompPanel.cpp
    src/gui/CatControlApplet.cpp
    src/gui/DaxApplet.cpp
    src/gui/TciApplet.cpp
    src/gui/DaxIqApplet.cpp
    src/gui/MqttApplet.cpp
    src/gui/MqttSettingsDialog.cpp
    src/gui/MeterSlider.cpp
    src/gui/PhaseKnob.cpp
    src/gui/AntennaGeniusApplet.cpp
    src/gui/ShackSwitchApplet.cpp
    src/gui/TitleBar.cpp
    src/gui/SupportDialog.cpp
    src/gui/SliceTroubleshootingDialog.cpp
    src/gui/KeyboardMapWidget.cpp
    src/gui/ShortcutDialog.cpp
    src/gui/MultiFlexDialog.cpp
    src/gui/HelpDialog.cpp
    src/gui/WhatsNewDialog.cpp
    src/gui/ThemeEditorDialog.cpp
    src/gui/ThemeInspector.cpp
    src/gui/GradientEditorDialog.cpp
    src/gui/TokenEditorWidget.cpp
    src/gui/CompactColorPicker.cpp
)

# Bundled RNNoise (Mozilla/Xiph BSD-3) — client-side neural noise suppression
set(RNNOISE_DIR ${CMAKE_SOURCE_DIR}/third_party/rnnoise)
set(RNNOISE_SOURCES
    ${RNNOISE_DIR}/src/denoise.c
    ${RNNOISE_DIR}/src/celt_lpc.c
    ${RNNOISE_DIR}/src/kiss_fft.c
    ${RNNOISE_DIR}/src/pitch.c
    ${RNNOISE_DIR}/src/rnn.c
    ${RNNOISE_DIR}/src/nnet.c
    ${RNNOISE_DIR}/src/nnet_default.c
    ${RNNOISE_DIR}/src/rnnoise_data.c
    ${RNNOISE_DIR}/src/rnnoise_tables.c
    ${RNNOISE_DIR}/src/parse_lpcnet_weights.c
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|i[3-6]86")
    list(APPEND RNNOISE_SOURCES
        ${RNNOISE_DIR}/src/x86/x86cpu.c
        ${RNNOISE_DIR}/src/x86/x86_dnn_map.c
        ${RNNOISE_DIR}/src/x86/nnet_sse4_1.c
        ${RNNOISE_DIR}/src/x86/nnet_avx2.c
    )
endif()

# Suppress warnings and set include paths for bundled C code
set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
    INCLUDE_DIRECTORIES "${RNNOISE_DIR}/include;${RNNOISE_DIR}/src;${RNNOISE_DIR}/src/x86"
)
# x86 RTCD (runtime CPU detection) + per-file SIMD flags
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|i[3-6]86")
    if(MSVC)
        # MSVC: uses _MSC_VER path in x86cpu.c (intrin.h), no CPU_INFO_BY_C needed
        # MSVC doesn't need -msse4.1/-mavx flags — intrinsics are always available
        set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
            COMPILE_FLAGS "/w /DRNN_ENABLE_X86_RTCD=1"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_sse4_1.c PROPERTIES
            COMPILE_FLAGS "/w /DRNN_ENABLE_X86_RTCD=1 /D__SSE4_1__"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_avx2.c PROPERTIES
            COMPILE_FLAGS "/w /DRNN_ENABLE_X86_RTCD=1 /arch:AVX2 /D__AVX2__"
        )
    else()
        # GCC/Clang: use cpuid.h intrinsic and per-file ISA flags
        set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
            COMPILE_FLAGS "-w -DRNN_ENABLE_X86_RTCD=1 -DCPU_INFO_BY_C=1"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_sse4_1.c PROPERTIES
            COMPILE_FLAGS "-w -DRNN_ENABLE_X86_RTCD=1 -DCPU_INFO_BY_C=1 -msse4.1"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_avx2.c PROPERTIES
            COMPILE_FLAGS "-w -DRNN_ENABLE_X86_RTCD=1 -DCPU_INFO_BY_C=1 -mavx -mfma -mavx2"
        )
    endif()
else()
    set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
        COMPILE_FLAGS "-w"
    )
endif()

# Bundled ggmorse (MIT) — CW Morse code decoder
set(GGMORSE_DIR ${CMAKE_SOURCE_DIR}/third_party/ggmorse)
set(GGMORSE_SOURCES
    ${GGMORSE_DIR}/src/ggmorse.cpp
    ${GGMORSE_DIR}/src/resampler.cpp
)
set_source_files_properties(${GGMORSE_SOURCES} PROPERTIES
    INCLUDE_DIRECTORIES "${GGMORSE_DIR}/include;${GGMORSE_DIR}/src"
)
if(MSVC)
    set_source_files_properties(${GGMORSE_SOURCES} PROPERTIES COMPILE_FLAGS "/w")
else()
    set_source_files_properties(${GGMORSE_SOURCES} PROPERTIES COMPILE_FLAGS "-w")
endif()

add_library(aether_libmodem_core STATIC
    third_party/libmodem_core/bitstream.cpp
    third_party/libmodem_core/demodulator.cpp
)
target_include_directories(aether_libmodem_core PUBLIC
    ${CMAKE_SOURCE_DIR}/third_party/libmodem_core
)
target_compile_features(aether_libmodem_core PUBLIC cxx_std_20)
target_compile_definitions(aether_libmodem_core PUBLIC
    LIBMODEM_NAMESPACE=aether_libmodem_core
    "LIBMODEM_NAMESPACE_REFERENCE=aether_libmodem_core::"
)
if(MSVC)
    target_compile_options(aether_libmodem_core PRIVATE /w)
else()
    target_compile_options(aether_libmodem_core PRIVATE -w)
endif()

# AetherAFSKDemod — Direwolf profile-A AFSK demodulator.
# Vendored Dire Wolf source is GPL-2.0-or-later; the combined AetherSDR work is
# GPL-3.0-or-later (see THIRD_PARTY_LICENSES).
# Profile A: IQ-mix + RRC + AGC/multi-slicer (best for amplitude-imbalanced signals).
# VHF 1200 baud only; HF 300 baud stays on aether_libmodem_core.
# AX.25 framing (bitstream, HDLC, FCS) continues to use aether_libmodem_core.
add_library(aether_afskdemod STATIC
    third_party/direwolf_afsk/AetherAFSKDemod.cpp
)
target_include_directories(aether_afskdemod PUBLIC
    ${CMAKE_SOURCE_DIR}/third_party/direwolf_afsk
)
target_compile_features(aether_afskdemod PUBLIC cxx_std_20)
message(STATUS "AetherAFSKDemod: Direwolf profile-A (VHF 1200 baud), GPL-2.0-or-later")

# Bundled liquid-dsp (MIT) — comprehensive DSP toolkit covering modems
# (PSK/QAM/FSK/GMSK/OFDM), FEC (Hamming/Golay/RS/convolutional), adaptive
# filters (LMS/RLS), AGC, NCO, polyphase resampling, and equalizers.
# Vendored proactively under #3043 as foundation infrastructure for future
# digital-mode work (e.g. native FT8/FT4 — #85 Phase 4 — or PSK31 / RTTY).
#
# Linux + macOS only — liquid-dsp upstream targets GCC/Clang/MinGW; its
# include/liquid.h:473 uses C99 `float _Complex` typedefs which the MSVC C
# compiler doesn't accept (syntax error: identifier 'liquid_float_complex').
# Upstream has no first-class MSVC support and patching the header for
# `_Fcomplex`/`_Dcomplex` would be invasive against future upstream syncs.
# Tracked as a follow-up; today the Windows build skips this vendor and
# any future consumer that needs liquid-dsp on Windows needs to land MSVC
# support first.
#
# Built via the upstream CMakeLists.txt with examples / tests / benchmarks /
# sandbox / docs disabled to keep the build slim. FFTW lookup is disabled
# (FIND_FFTW=OFF) so liquid-dsp uses its own internal FFT — wiring it to our
# bundled third_party/fftw3 can be a follow-up if a future consumer wants
# FFTW-accelerated transforms.
#
# Disabled by default on every platform because no AetherSDR module currently
# consumes liquid-dsp symbols. Opt-in non-MSVC builds link liquid-static; the
# static linker then extracts only symbols used by a future consumer.
#
# USE_SYSTEM_LIQUID_DSP only takes effect once liquid-dsp itself is enabled, so
# warn if a packager set it without ENABLE_LIQUID_DSP rather than silently
# ignoring it (#4146 review).
if(USE_SYSTEM_LIQUID_DSP AND NOT ENABLE_LIQUID_DSP)
    message(WARNING
        "USE_SYSTEM_LIQUID_DSP=ON has no effect without ENABLE_LIQUID_DSP=ON — "
        "liquid-dsp is off by default. Pass -DENABLE_LIQUID_DSP=ON to build "
        "against it.")
endif()

if(ENABLE_LIQUID_DSP AND MSVC)
    message(FATAL_ERROR
        "ENABLE_LIQUID_DSP is not supported with MSVC because liquid-dsp's "
        "public headers use C99 _Complex types")
endif()

if(ENABLE_LIQUID_DSP AND NOT USE_SYSTEM_LIQUID_DSP)
    set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
    set(BUILD_AUTOTESTS OFF CACHE BOOL "" FORCE)
    set(BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE)
    set(BUILD_SANDBOX OFF CACHE BOOL "" FORCE)
    set(BUILD_DOC OFF CACHE BOOL "" FORCE)
    set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE)
    set(FIND_FFTW OFF CACHE BOOL "" FORCE)
    set(ENABLE_LOGGING OFF CACHE BOOL "" FORCE)
# liquid-dsp's cmake_minimum_required(VERSION 3.10) puts its option() calls
# under CMP0077 OLD behavior — option() with that policy does NOT honor a
# same-name *normal* variable. We must use CACHE BOOL FORCE so the override
# takes effect. Without this, BUILD_SHARED_LIBS stays at the option's ON
# default. Linux silently ships both libliquid.so and libliquid.a (different
# extensions); the OUTPUT_NAME `liquid` at upstream CMakeLists.txt:444
# renames the STATIC target's archive to drop the -static suffix.
#
# Cache pollution is undone with unset(CACHE) after add_subdirectory so
# this doesn't leak into the rest of the project's BUILD_SHARED_LIBS
# semantics.
    set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
    add_subdirectory(third_party/liquid-dsp EXCLUDE_FROM_ALL)
    unset(BUILD_SHARED_LIBS CACHE)
# Suppress warnings from vendored code (same pattern as other third_party
# integrations — we don't want -Werror surfaces on code we don't own).
    if(TARGET liquid-static)
        target_compile_options(liquid-static PRIVATE -w)
    endif()
endif()

if(ENABLE_LIQUID_DSP AND USE_SYSTEM_LIQUID_DSP)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(liquid-dsp REQUIRED IMPORTED_TARGET liquid-dsp)
    endif()
endif()

set(DFNR_RESOURCES "")
if(ENABLE_DFNR AND AETHER_EMBED_DFNR_MODEL)
    if(EXISTS "${DFNR_MODEL}")
        file(TO_CMAKE_PATH "${DFNR_MODEL}" DFNR_MODEL_QRC_PATH)
        set(DFNR_MODEL_QRC "${CMAKE_CURRENT_BINARY_DIR}/dfnr_model.qrc")
        file(WRITE "${DFNR_MODEL_QRC}"
"<RCC>
    <qresource prefix=\"/models\">
        <file alias=\"${DFNR_EMBEDDED_MODEL_NAME}\">${DFNR_MODEL_QRC_PATH}</file>
    </qresource>
</RCC>
")
        qt_add_resources(DFNR_RESOURCES "${DFNR_MODEL_QRC}")
        message(STATUS "DFNR model will be embedded in Qt resources")
    else()
        message(WARNING "AETHER_EMBED_DFNR_MODEL is ON, but DFNR model not found at ${DFNR_MODEL}")
    endif()
endif()

qt_add_resources(RESOURCES resources/resources.qrc)

# aetherd RFC step 1: engine static library — src/core + src/models
# plus the vendored C/C++ sources those engine files compile against
# (rnnoise, ggmorse, RADE, specbleach; mosquitto/rtmidi are appended below in
# their feature blocks). Qt resources (resources.qrc, DFNR model qrc) stay on
# the executable: rcc registration runs from the exe's static initializers and
# is process-global, so core code reading ":/..." keeps working.
add_library(aethercore STATIC
    ${CORE_SOURCES}
    ${MODEL_SOURCES}
    ${RNNOISE_SOURCES}
    ${GGMORSE_SOURCES}
    ${RADE_SOURCES}
    ${SPECBLEACH_SOURCES}
)

add_executable(AetherSDR
    src/main.cpp
    ${GUI_SOURCES}
    ${RESOURCES}
    ${DFNR_RESOURCES}
)
target_link_libraries(AetherSDR PRIVATE aethercore)

if (USE_SYSTEM_ZLIB)
    target_link_libraries(aethercore PRIVATE PkgConfig::zlib)      # core: ZipArchive
else()
    target_link_libraries(aethercore PRIVATE zlibstatic) # bundled third_party/zlib 1.3.1
endif()

if (USE_SYSTEM_MSPACK)
    target_link_libraries(aethercore PRIVATE PkgConfig::libmspack) # core: CabExtractor
else()
    target_link_libraries(aethercore PRIVATE mspack_static)
endif()

# Link the system libmosquitto target only when MQTT is enabled — the
# pkg_check_modules() call that defines PkgConfig::libmosquitto lives
# inside the ENABLE_MQTT block above, so without this guard the combo
# (ENABLE_MQTT=OFF + USE_SYSTEM_LIBMOSQUITTO=ON) would configure cleanly
# then fail at link time with a missing target.
if (ENABLE_MQTT AND USE_SYSTEM_LIBMOSQUITTO)
    target_link_libraries(aethercore PRIVATE PkgConfig::libmosquitto) # core: MqttClient
endif()

# Optionally change binary name to lower case on Linux
if (LINUX AND LOWER_CASE_BINARY_NAME)
    set_target_properties(AetherSDR PROPERTIES OUTPUT_NAME aethersdr)

    set(AETHERSDR_OUTPUT_BINARY_NAME aethersdr)
else()
    set(AETHERSDR_OUTPUT_BINARY_NAME AetherSDR)
endif()
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/AetherSDR.desktop.in
    ${CMAKE_CURRENT_BINARY_DIR}/packaging/linux/AetherSDR.desktop
    @ONLY
)

# Capture the git short SHA at configure time and surface it in the About
# dialog so dev/test builds are identifiable.  Falls back to "unknown" for
# source-tarball builds where .git is absent.
find_package(Git QUIET)
set(AETHER_GIT_SHA "unknown")
if(Git_FOUND)
    execute_process(
        COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        OUTPUT_VARIABLE _aether_git_sha_out
        OUTPUT_STRIP_TRAILING_WHITESPACE
        ERROR_QUIET
        RESULT_VARIABLE _aether_git_sha_rv
    )
    if(_aether_git_sha_rv EQUAL 0 AND NOT "${_aether_git_sha_out}" STREQUAL "")
        set(AETHER_GIT_SHA "${_aether_git_sha_out}")
    endif()
endif()
target_compile_definitions(AetherSDR PRIVATE AETHER_GIT_SHA="${AETHER_GIT_SHA}")
if(Git_FOUND AND _aether_git_sha_rv EQUAL 0 AND NOT "${_aether_git_sha_out}" STREQUAL "")
    message(STATUS "Build SHA: ${AETHER_GIT_SHA}")
else()
    message(STATUS "Build SHA: ${AETHER_GIT_SHA} (git unavailable or not a git checkout)")
endif()

# Reproducible-builds.org SOURCE_DATE_EPOCH support.  When a distro
# packager (Debian, Arch, NixOS, openSUSE, Fedora, etc.) sets
#   export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
# before invoking cmake, GCC 7.2+ and Clang automatically substitute
# __DATE__ and __TIME__ with that fixed timestamp.  Many archive tools
# (ar, tar, zip, objcopy) honour the same variable, so the built binary
# becomes byte-reproducible across hosts.
#
# No source-tree changes needed — this block just surfaces a configure-
# time status line so packagers can confirm at a glance that their
# env-var actually reached the build.  Originally proposed as
# https://github.com/aethersdr/AetherSDR/pull/3139 (closed in favour
# of this ecosystem-standard approach).
if(DEFINED ENV{SOURCE_DATE_EPOCH})
    message(STATUS "Reproducible build: honoring SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH}")
endif()

if((UNIX OR WIN32) AND ENABLE_DSTAR)
    target_compile_definitions(AetherSDR PRIVATE AETHER_ENABLE_DIGITAL_VOICE_HELPER)

    set(DIGITAL_VOICE_WAVEFORM_DIR "${CMAKE_SOURCE_DIR}/third_party/smartsdr-dsp")
    set(CRDV_DIR "${CMAKE_SOURCE_DIR}/third_party/crdv")
    add_library(aether_crdv STATIC
        ${CRDV_DIR}/src/air.c
        ${CRDV_DIR}/src/config.c
        ${CRDV_DIR}/src/control.c
        ${CRDV_DIR}/src/dv3000.c
        ${CRDV_DIR}/src/modem.c
        ${CRDV_DIR}/src/vita.c)
    add_library(crdv::crdv ALIAS aether_crdv)
    target_include_directories(aether_crdv PUBLIC ${CRDV_DIR}/include)
    target_compile_features(aether_crdv PUBLIC c_std_17)
    set_target_properties(aether_crdv PROPERTIES
        C_EXTENSIONS OFF
        AUTOMOC OFF
        AUTOUIC OFF
        AUTORCC OFF)
    if(MSVC)
        target_compile_options(aether_crdv PRIVATE /W4 /permissive-)
    else()
        target_compile_options(aether_crdv PRIVATE
            -Wall -Wextra -Wpedantic -Wconversion -Wshadow)
        target_link_libraries(aether_crdv PUBLIC m)
    endif()
    set(DIGITAL_VOICE_WAVEFORM_SOURCES
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_dstar_protocol.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/cmd_basics.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/cmd_engine.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_buffer_queue.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_ipv4_source_filter.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_smartsdr_command.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_tcp_frame_buffer.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_vita_packet_validator.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/discovery_client.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/digital_voice_mode_registry.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/digital_voice_slice_ownership.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/digital_voice_tx_gate.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_transmit_state.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_tx_output.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_tx_stream.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_waveform_metrics.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/hal_buffer.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/hal_listener.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/hal_vita.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/io_utils.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/sched_waveform.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/smartsdr_dsp_api.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/status_processor.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/traffic_cop.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/utils.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/vita_packet_sequence.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/vita_output.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/bit_pattern_matcher.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/gmsk_modem.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/thumbDV.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/aether_vocoder_backend.cpp
        ${DIGITAL_VOICE_WAVEFORM_DIR}/aether_sem_compat.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/aether_serial_compat.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/circular_buffer.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/main.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/resampler.c
    )

    add_executable(aether-dv-waveform ${DIGITAL_VOICE_WAVEFORM_SOURCES})
    set_target_properties(aether-dv-waveform PROPERTIES
        C_STANDARD 11
        C_STANDARD_REQUIRED ON
        CXX_STANDARD 20
        CXX_STANDARD_REQUIRED ON
    )
    target_compile_definitions(aether-dv-waveform PRIVATE _DEFAULT_SOURCE)
    if(WIN32)
        target_compile_definitions(aether-dv-waveform PRIVATE
            _CRT_SECURE_NO_WARNINGS
            _WINSOCK_DEPRECATED_NO_WARNINGS)
    endif()
    target_include_directories(aether-dv-waveform BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/compat
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV
        ${CMAKE_SOURCE_DIR}/third_party/crdv/include
    )
    find_package(Threads REQUIRED)
    target_link_libraries(aether-dv-waveform PRIVATE Threads::Threads crdv::crdv)
    if(NOT WIN32)
        target_link_libraries(aether-dv-waveform PRIVATE m)
    endif()
    if(WIN32)
        target_link_libraries(aether-dv-waveform PRIVATE ws2_32)
    endif()
    if(APPLE)
        target_link_libraries(aether-dv-waveform PRIVATE "-framework IOKit")
        set(DIGITAL_VOICE_WAVEFORM_RUNTIME_DIR "${CMAKE_BINARY_DIR}/AetherSDR.app/Contents/MacOS")
    else()
        set(DIGITAL_VOICE_WAVEFORM_RUNTIME_DIR "${CMAKE_BINARY_DIR}")
    endif()
    set_target_properties(aether-dv-waveform PROPERTIES
        RUNTIME_OUTPUT_DIRECTORY "${DIGITAL_VOICE_WAVEFORM_RUNTIME_DIR}"
    )
    if(NOT MSVC)
        target_compile_options(aether-dv-waveform PRIVATE -w)
    endif()
    add_custom_command(TARGET aether-dv-waveform POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${DIGITAL_VOICE_WAVEFORM_DIR}/config/AetherDV.cfg"
            "$<TARGET_FILE_DIR:aether-dv-waveform>/AetherDV.cfg"
        COMMAND ${CMAKE_COMMAND} -E rm -f
            "$<TARGET_FILE_DIR:aether-dv-waveform>/aether-dstar-waveform"
            "$<TARGET_FILE_DIR:aether-dv-waveform>/aether-dstar-waveform.exe"
            "$<TARGET_FILE_DIR:aether-dv-waveform>/ThumbDV.cfg"
        COMMENT "Copying AetherDV waveform manifest"
    )
    add_test(NAME aether_dv_waveform_no_args
        COMMAND ${CMAKE_COMMAND}
            -DHELPER=$<TARGET_FILE:aether-dv-waveform>
            -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/run_digital_voice_waveform_no_args.cmake
    )

    function(aether_enable_digital_voice_test_sanitizers target)
        if(NOT WIN32 AND CMAKE_C_COMPILER_ID MATCHES "Clang|GNU")
            target_compile_options(${target} PRIVATE
                -fsanitize=address,undefined
                -fno-omit-frame-pointer)
            target_link_options(${target} PRIVATE -fsanitize=address,undefined)
        endif()
    endfunction()

    add_executable(crdv_cleanroom_test
        third_party/crdv/tests/test_main.c)
    target_link_libraries(crdv_cleanroom_test PRIVATE crdv::crdv)
    aether_enable_digital_voice_test_sanitizers(crdv_cleanroom_test)
    add_test(NAME crdv_cleanroom_test COMMAND crdv_cleanroom_test)
    add_test(NAME crdv_manifest_test
        COMMAND ${CMAKE_COMMAND}
            -DCRDV_DIR=${CRDV_DIR}
            -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/verify_crdv_manifest.cmake)

    add_executable(crdv_quarantined_test
        third_party/crdv/tests/quarantined_acceptance.c)
    target_link_libraries(crdv_quarantined_test PRIVATE crdv::crdv)
    add_test(NAME crdv_quarantined_test COMMAND crdv_quarantined_test)
    set_tests_properties(crdv_quarantined_test PROPERTIES SKIP_RETURN_CODE 77)

    add_executable(digital_voice_protocol_test
        tests/digital_voice_protocol_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_ipv4_source_filter.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_smartsdr_command.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_tcp_frame_buffer.c)
    target_include_directories(digital_voice_protocol_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface)
    if(WIN32)
        target_link_libraries(digital_voice_protocol_test PRIVATE ws2_32)
    endif()
    aether_enable_digital_voice_test_sanitizers(digital_voice_protocol_test)
    add_test(NAME digital_voice_protocol_test COMMAND digital_voice_protocol_test)

    add_executable(digital_voice_buffer_queue_test
        tests/digital_voice_buffer_queue_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_buffer_queue.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/hal_buffer.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/utils.c)
    target_compile_definitions(digital_voice_buffer_queue_test PRIVATE _DEFAULT_SOURCE)
    target_include_directories(digital_voice_buffer_queue_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/compat
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface)
    target_link_libraries(digital_voice_buffer_queue_test PRIVATE Threads::Threads)
    if(NOT WIN32)
        target_link_libraries(digital_voice_buffer_queue_test PRIVATE m)
    endif()
    aether_enable_digital_voice_test_sanitizers(digital_voice_buffer_queue_test)
    add_test(NAME digital_voice_buffer_queue_test COMMAND digital_voice_buffer_queue_test)

    add_executable(digital_voice_slice_ownership_test
        tests/digital_voice_slice_ownership_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/digital_voice_slice_ownership.c)
    target_include_directories(digital_voice_slice_ownership_test PRIVATE
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface)
    aether_enable_digital_voice_test_sanitizers(digital_voice_slice_ownership_test)
    add_test(NAME digital_voice_slice_ownership_test
        COMMAND digital_voice_slice_ownership_test)

    add_executable(dstar_modem_test
        tests/dstar_modem_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_dstar_protocol.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_smartsdr_command.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/utils.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/bit_pattern_matcher.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/gmsk_modem.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/circular_buffer.c
    )
    target_compile_definitions(dstar_modem_test PRIVATE _DEFAULT_SOURCE)
    target_include_directories(dstar_modem_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/compat
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV
        ${CMAKE_SOURCE_DIR}/third_party/crdv/include)
    target_link_libraries(dstar_modem_test PRIVATE Threads::Threads crdv::crdv)
    if(NOT WIN32)
        target_link_libraries(dstar_modem_test PRIVATE m)
    endif()
    aether_enable_digital_voice_test_sanitizers(dstar_modem_test)
    add_test(NAME dstar_modem_test COMMAND dstar_modem_test)

    add_executable(dstar_transmit_state_test
        tests/dstar_transmit_state_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_transmit_state.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_tx_output.c)
    target_include_directories(dstar_transmit_state_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface)
    target_link_libraries(dstar_transmit_state_test PRIVATE Threads::Threads)
    if(NOT WIN32)
        target_link_libraries(dstar_transmit_state_test PRIVATE m)
    endif()
    aether_enable_digital_voice_test_sanitizers(dstar_transmit_state_test)
    add_test(NAME dstar_transmit_state_test COMMAND dstar_transmit_state_test)

    add_executable(digital_voice_tx_gate_test
        tests/digital_voice_tx_gate_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/digital_voice_tx_gate.c)
    target_include_directories(digital_voice_tx_gate_test PRIVATE
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface)
    aether_enable_digital_voice_test_sanitizers(digital_voice_tx_gate_test)
    add_test(NAME digital_voice_tx_gate_test COMMAND digital_voice_tx_gate_test)

    add_executable(vita_packet_admission_test
        tests/vita_packet_admission_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_vita_packet_validator.c)
    target_include_directories(vita_packet_admission_test PRIVATE
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface
        ${CMAKE_SOURCE_DIR}/third_party/crdv/include)
    target_link_libraries(vita_packet_admission_test PRIVATE crdv::crdv)
    aether_enable_digital_voice_test_sanitizers(vita_packet_admission_test)
    add_test(NAME vita_packet_admission_test COMMAND vita_packet_admission_test)

    add_executable(dstar_tx_path_test
        tests/dstar_tx_path_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_smartsdr_command.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_dstar_protocol.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/digital_voice_tx_gate.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_transmit_state.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_tx_stream.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/utils.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/vita_output.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/bit_pattern_matcher.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/gmsk_modem.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/circular_buffer.c)
    target_compile_definitions(dstar_tx_path_test PRIVATE _DEFAULT_SOURCE)
    target_include_directories(dstar_tx_path_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/compat
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV
        ${CMAKE_SOURCE_DIR}/third_party/crdv/include)
    target_link_libraries(dstar_tx_path_test PRIVATE Threads::Threads crdv::crdv)
    if(NOT WIN32)
        target_link_libraries(dstar_tx_path_test PRIVATE m)
    endif()
    if(WIN32)
        target_link_libraries(dstar_tx_path_test PRIVATE ws2_32)
    endif()
    aether_enable_digital_voice_test_sanitizers(dstar_tx_path_test)
    add_test(NAME dstar_tx_path_test COMMAND dstar_tx_path_test)

    add_executable(vita_packet_count_test
        tests/vita_packet_count_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/vita_packet_sequence.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/vita_output.c)
    target_compile_definitions(vita_packet_count_test PRIVATE _DEFAULT_SOURCE)
    target_include_directories(vita_packet_count_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/compat
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface)
    aether_enable_digital_voice_test_sanitizers(vita_packet_count_test)
    if(WIN32)
        target_link_libraries(vita_packet_count_test PRIVATE ws2_32)
    endif()
    add_test(NAME vita_packet_count_test COMMAND vita_packet_count_test)

    add_executable(dstar_waveform_metrics_test
        tests/dstar_waveform_metrics_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/dstar_waveform_metrics.c)
    target_include_directories(dstar_waveform_metrics_test PRIVATE
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface)
    if(NOT WIN32)
        target_link_libraries(dstar_waveform_metrics_test PRIVATE m)
    endif()
    aether_enable_digital_voice_test_sanitizers(dstar_waveform_metrics_test)
    add_test(NAME dstar_waveform_metrics_test COMMAND dstar_waveform_metrics_test)

    add_executable(digital_voice_mode_registry_test
        tests/digital_voice_mode_registry_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/aether_smartsdr_command.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/digital_voice_mode_registry.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/utils.c)
    target_compile_definitions(digital_voice_mode_registry_test PRIVATE _DEFAULT_SOURCE)
    target_include_directories(digital_voice_mode_registry_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/compat
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface
        ${CMAKE_SOURCE_DIR}/third_party/crdv/include)
    target_link_libraries(digital_voice_mode_registry_test PRIVATE
        Threads::Threads crdv::crdv)
    if(WIN32)
        target_link_libraries(digital_voice_mode_registry_test PRIVATE ws2_32)
    endif()
    aether_enable_digital_voice_test_sanitizers(digital_voice_mode_registry_test)
    add_test(NAME digital_voice_mode_registry_test COMMAND digital_voice_mode_registry_test)

    add_executable(thumbdv_queue_test
        tests/thumbdv_queue_test.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/hal_buffer.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface/utils.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV/thumbDV.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/aether_sem_compat.c
        ${DIGITAL_VOICE_WAVEFORM_DIR}/aether_serial_compat.c)
    target_compile_definitions(thumbdv_queue_test PRIVATE
        _DEFAULT_SOURCE
        AETHER_DSTAR_TESTING)
    target_include_directories(thumbdv_queue_test BEFORE PRIVATE
        $<$<BOOL:${WIN32}>:${DIGITAL_VOICE_WAVEFORM_DIR}/compat/windows>
        ${DIGITAL_VOICE_WAVEFORM_DIR}/compat
        ${DIGITAL_VOICE_WAVEFORM_DIR}/include
        ${DIGITAL_VOICE_WAVEFORM_DIR}
        ${DIGITAL_VOICE_WAVEFORM_DIR}/SmartSDR_Interface
        ${DIGITAL_VOICE_WAVEFORM_DIR}/ThumbDV)
    target_link_libraries(thumbdv_queue_test PRIVATE Threads::Threads)
    if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
        target_link_libraries(thumbdv_queue_test PRIVATE util)
    endif()
    if(APPLE)
        target_link_libraries(thumbdv_queue_test PRIVATE "-framework IOKit")
    endif()
    if(WIN32)
        target_link_libraries(thumbdv_queue_test PRIVATE ws2_32)
    endif()
    aether_enable_digital_voice_test_sanitizers(thumbdv_queue_test)
    add_test(NAME thumbdv_queue_test COMMAND thumbdv_queue_test)

    add_dependencies(AetherSDR aether-dv-waveform)
endif()

# Windows: GUI app (no console window) + icon resource
if(WIN32)
    set_target_properties(AetherSDR PROPERTIES WIN32_EXECUTABLE TRUE)
endif()

# macOS app bundle
if(APPLE)
    set_target_properties(AetherSDR PROPERTIES
        MACOSX_BUNDLE TRUE
        MACOSX_BUNDLE_GUI_IDENTIFIER "com.aethersdr.AetherSDR"
        MACOSX_BUNDLE_BUNDLE_NAME "AetherSDR"
        MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
        MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
        MACOSX_BUNDLE_ICON_FILE "AetherSDR.icns"
        MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/packaging/macos/Info.plist.in"
    )
    target_link_libraries(aethercore PRIVATE "-framework AVFoundation" "-framework Accelerate") # core: MacNRFilter/VirtualAudioBridge
    target_sources(aethercore PRIVATE src/core/MacNRFilter.cpp)

    # Generate AetherSDR.icns at build time from docs/assets/logo-circle.png so local
    # builds get an app icon without any extra setup (sips and iconutil are
    # part of macOS).
    set(ICON_SOURCE "${CMAKE_SOURCE_DIR}/docs/assets/logo-circle.png")
    set(ICNS_PATH "${CMAKE_BINARY_DIR}/AetherSDR.icns")
    set(ICONSET_PATH "${CMAKE_BINARY_DIR}/AetherSDR.iconset")
    add_custom_command(
        OUTPUT "${ICNS_PATH}"
        COMMAND ${CMAKE_COMMAND} -E make_directory "${ICONSET_PATH}"
        COMMAND sips -z 16  16  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_16x16.png"
        COMMAND sips -z 32  32  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_16x16@2x.png"
        COMMAND sips -z 32  32  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_32x32.png"
        COMMAND sips -z 64  64  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_32x32@2x.png"
        COMMAND sips -z 128 128 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_128x128.png"
        COMMAND sips -z 256 256 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_128x128@2x.png"
        COMMAND sips -z 256 256 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_256x256.png"
        COMMAND sips -z 512 512 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_256x256@2x.png"
        COMMAND sips -z 512 512 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_512x512.png"
        COMMAND sips -z 1024 1024 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_512x512@2x.png"
        COMMAND iconutil -c icns "${ICONSET_PATH}" -o "${ICNS_PATH}"
        DEPENDS "${ICON_SOURCE}"
        COMMENT "Generating AetherSDR.icns"
    )
    target_sources(AetherSDR PRIVATE "${ICNS_PATH}")
    set_source_files_properties("${ICNS_PATH}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
elseif(WIN32)
    # Windows application icon (taskbar, Start Menu, Alt-Tab)
    set(WIN_RC "${CMAKE_SOURCE_DIR}/packaging/windows/AetherSDR.rc")
    if(EXISTS "${WIN_RC}")
        target_sources(AetherSDR PRIVATE "${WIN_RC}")
    endif()
    if(MSVC)
        # Embed the application manifest the CMake-native way: list it as a
        # source so CMake hands it to its own vs_link_exe/mt.exe embed step.
        # Passing /MANIFEST:EMBED + /MANIFESTINPUT directly fights that
        # machinery — link.exe embeds inline and emits no side-car manifest,
        # so CMake's mt.exe step runs with an empty --manifests list and dies
        # with c10100a7 (cmake 4.x) / LNK1220 the other way (#3237).
        target_sources(AetherSDR PRIVATE
            "${CMAKE_SOURCE_DIR}/packaging/windows/AetherSDR.exe.manifest")
    endif()
endif()

# PUBLIC: src/ is the project include root for both layers ("core/..." and
# "gui/..." includes); the vendored include dirs leak into headers used by
# gui translation units today (e.g. hidapi via MainWindow_Controllers.cpp).
target_include_directories(aethercore PUBLIC
    src/
    ${RNNOISE_DIR}/include
    ${RNNOISE_DIR}/src
    ${GGMORSE_DIR}/include
    ${CMAKE_SOURCE_DIR}/third_party/r8brain
)

# Engine dependency surface. Qt6::Widgets is still linked into the engine
# because five core files use QtWidgets (TxKeyingMarker.h, ThemeManager.cpp,
# AutomationServer.cpp, ShortcutManager.cpp, SettingsHelpers.cpp). These are
# tracked as EB2 legacy in tools/check_engine_boundary.py (per-file baseline
# counts that may only shrink) and split out in step 3 — AutomationServer's
# widget-facing dumpTree/grab half is the bulk of it.
target_link_libraries(aethercore PUBLIC
    Qt6::Core
    Qt6::Concurrent
    Qt6::Gui
    Qt6::Widgets
    Qt6::Network
    Qt6::Multimedia
    aether_libmodem_core
    aether_afskdemod
    ${CMAKE_DL_LIBS}   # NvidiaAfxFilter dlopen
)

target_link_libraries(AetherSDR PRIVATE
    qgeoview           # gui/map only
    ${CMAKE_DL_LIBS}   # dlopen/dlsym for tolerant X11 error handler (#1839)
)

if(ENABLE_LIQUID_DSP)
    if(USE_SYSTEM_LIQUID_DSP)
        target_link_libraries(aethercore PRIVATE PkgConfig::liquid-dsp)
    else()
        target_link_libraries(aethercore PRIVATE liquid-static)
    endif()
endif()

if(Qt6SerialPort_FOUND)
    # PUBLIC: gui (MainWindow.cpp, RadioSetupDialog.cpp) uses QSerialPort and
    # #ifdef HAVE_SERIALPORT directly.
    target_compile_definitions(aethercore PUBLIC HAVE_SERIALPORT)
    target_link_libraries(aethercore PUBLIC Qt6::SerialPort)
endif()

if(Qt6WebSockets_FOUND)
    # PUBLIC: gui (MainWindow_Spots.cpp) uses QWebSocket directly.
    target_compile_definitions(aethercore PUBLIC HAVE_WEBSOCKETS)
    target_sources(aethercore PRIVATE
        src/core/FreeDvClient.cpp
    )
    target_sources(AetherSDR PRIVATE
        src/gui/FreeDvReporterDialog.cpp
        src/gui/FreeDvReporterModel.cpp
    )
    target_link_libraries(aethercore PUBLIC Qt6::WebSockets)
endif()

if(Qt6Keychain_FOUND)
    # PUBLIC (both): gui setup tabs test HAVE_KEYCHAIN *and* MqttApplet.cpp /
    # MqttSettingsDialog.cpp include <qt6keychain/keychain.h> directly, so the
    # exe's gui TUs need the imported target's INTERFACE_INCLUDE_DIRECTORIES.
    # A PRIVATE link on a static lib propagates only $<LINK_ONLY:...> — the
    # include dir would be lost and the gui TU fails to find the header on any
    # platform where qtkeychain is not on a default include path (e.g. the
    # bundled third_party/qtkeychain on Windows). Matches Qt6::SerialPort /
    # Qt6::WebSockets, which are PUBLIC for the same reason.
    target_compile_definitions(aethercore PUBLIC HAVE_KEYCHAIN)
    target_link_libraries(aethercore PUBLIC Qt6Keychain::Qt6Keychain)
    # Local Windows builds run straight from the build dir with no packaging
    # step — windeployqt, which stages qt6keychain.dll for the installer, isn't
    # involved — so the DLL needs to land next to the exe or the app fails to
    # start with "qt6keychain.dll was not found".
    if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/third_party/qtkeychain/bin/qt6keychain.dll")
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${CMAKE_SOURCE_DIR}/third_party/qtkeychain/bin/qt6keychain.dll"
                "$<TARGET_FILE_DIR:AetherSDR>"
        )
    endif()
    # Same problem on macOS: a staged third_party/qtkeychain dylib is found
    # via an absolute rpath into THIS checkout, so a bundle copied to another
    # Mac (SFTP test deploys) fails to load it.  Ship the dylib inside the
    # bundle and add the standard Frameworks rpath at link time (linker-added
    # rpath keeps the ad-hoc signature valid — no install_name_tool).
    # macdeployqt still handles release DMGs; copy_if_different is a no-op
    # for it.
    if(APPLE AND EXISTS "${CMAKE_SOURCE_DIR}/third_party/qtkeychain/lib/libqt6keychain.1.dylib")
        set_property(TARGET AetherSDR APPEND PROPERTY
            BUILD_RPATH "@executable_path/../Frameworks")
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E make_directory
                "$<TARGET_BUNDLE_CONTENT_DIR:AetherSDR>/Frameworks"
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${CMAKE_SOURCE_DIR}/third_party/qtkeychain/lib/libqt6keychain.1.dylib"
                "$<TARGET_BUNDLE_CONTENT_DIR:AetherSDR>/Frameworks"
        )
    endif()
endif()

if(Qt6DBus_FOUND)
    target_compile_definitions(aethercore PUBLIC HAVE_DBUS)
    target_link_libraries(aethercore PRIVATE Qt6::DBus)
endif()

if(AETHER_GPU_SPECTRUM)
    # PUBLIC on the engine: src/core/AutomationServer.cpp compiles QRhiWidget
    # grab paths under #ifdef AETHER_GPU_SPECTRUM (QRhiWidget is public
    # QtWidgets API on Qt 6.7+, so no GuiPrivate needed engine-side).
    target_compile_definitions(aethercore PUBLIC AETHER_GPU_SPECTRUM)

    # Apply the manual private-include paths now that AetherSDR exists. Covers
    # Debian multi-arch and Windows/macOS aqt installs that lack the Qt6::GuiPrivate
    # target (headers present on disk — located during detection above).
    if(DEBIAN_GPU_FIX_REQUIRED)
        message(STATUS "Applying private QtGui include paths to AetherSDR (${DEBIAN_PRIVATE_INC})")
        target_include_directories(AetherSDR PRIVATE
            "${DEBIAN_PRIVATE_INC}/QtGui/${Qt6_VERSION}"
            "${DEBIAN_PRIVATE_INC}/QtGui/${Qt6_VERSION}/QtGui"
        )
    endif()

    if(TARGET Qt6::GuiPrivate)
        target_link_libraries(AetherSDR PRIVATE Qt6::GuiPrivate)
    elseif(NOT DEBIAN_GPU_FIX_REQUIRED)
        # No GuiPrivate target and no manual path (e.g. some macOS layouts) —
        # locate the rhi/ headers within the Qt6::Gui include dirs.
        get_target_property(_qt_gui_inc Qt6::Gui INTERFACE_INCLUDE_DIRECTORIES)
        foreach(_dir IN LISTS _qt_gui_inc)
            if(EXISTS "${_dir}/rhi/qrhi.h")
                break() # already on the include path via Qt6::Gui
            endif()
            file(GLOB _priv_dirs "${_dir}/${Qt6_VERSION}/QtGui")
            foreach(_pd IN LISTS _priv_dirs)
                if(EXISTS "${_pd}/rhi/qrhi.h")
                    target_include_directories(AetherSDR PRIVATE "${_pd}")
                    message(STATUS "QRhi headers found at ${_pd}/rhi/")
                    break()
                endif()
            endforeach()
        endforeach()
    endif()

    qt_add_shaders(AetherSDR "aether_shaders"
        PREFIX "/shaders"
        FILES
            resources/shaders/texturedquad.vert
            resources/shaders/texturedquad.frag
            resources/shaders/overlay.vert
            resources/shaders/overlay.frag
            resources/shaders/spectrum.vert
            resources/shaders/spectrum.frag
            resources/shaders/panscope.frag
            resources/shaders/dss_mesh.vert
            resources/shaders/dss_mesh.frag
            resources/shaders/wavescope.frag
    )
endif()

# Bundled libmspack (LGPL-2.1) — CAB+LZX decompression for the v4.2+ MSI
# firmware-installer extraction path. See third_party/libmspack/README.md.
if (USE_SYSTEM_MSPACK)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(libmspack REQUIRED IMPORTED_TARGET libmspack)
    endif()
else()
    add_subdirectory(third_party/libmspack)
endif()

# Bundled RtMidi (MIT license) — MIDI controller support on all platforms
message(STATUS "MIDI controller support enabled (RtMidi)")
target_compile_definitions(aethercore PUBLIC HAVE_MIDI) # gui MidiMappingDialog + MainWindow check it too
target_sources(aethercore PRIVATE
    src/core/MidiControlManager.cpp
    src/core/MidiSettings.cpp)
target_sources(AetherSDR PRIVATE
    src/gui/MidiMappingDialog.cpp)
if (USE_SYSTEM_RTMIDI)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(rtmidi REQUIRED IMPORTED_TARGET rtmidi)
    endif()
    target_link_libraries(aethercore PRIVATE PkgConfig::rtmidi)
else()
    target_sources(aethercore PRIVATE third_party/rtmidi/RtMidi.cpp)
    # PUBLIC: MidiControlManager.h includes RtMidi.h
    target_include_directories(aethercore PUBLIC third_party/rtmidi)
endif()

if(APPLE)
    # RtMidi backend selectors — PUBLIC because MidiControlManager.h includes
    # RtMidi.h (its class layout is backend-conditional in places).
    target_compile_definitions(aethercore PUBLIC __MACOSX_CORE__)
    target_link_libraries(aethercore PRIVATE "-framework CoreMIDI" "-framework CoreAudio" "-framework CoreFoundation" "-framework IOKit")
elseif(WIN32)
    target_compile_definitions(aethercore PUBLIC __WINDOWS_MM__)
    target_link_libraries(aethercore PRIVATE winmm)
    target_link_libraries(aethercore PRIVATE dxgi)  # GpuSelector adapter enumeration
else()
    target_compile_definitions(aethercore PUBLIC __LINUX_ALSA__)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(alsa REQUIRED IMPORTED_TARGET alsa)
    endif()
    target_link_libraries(aethercore PRIVATE PkgConfig::alsa)
endif()

# hidapi — USB HID encoder support (Stream Deck, Icom RC-28, Griffin PowerMate, Contour Shuttle)
# Windows: run scripts/setup/setup-hidapi.ps1 first to download and build hidapi
# Linux:   apt install libhidapi-dev
# macOS:   brew install hidapi
if(WIN32)
    set(HIDAPI_ROOT "${CMAKE_SOURCE_DIR}/third_party/hidapi")
    if(EXISTS "${HIDAPI_ROOT}/include/hidapi/hidapi.h")
        set(HIDAPI_FOUND TRUE)
        set(HIDAPI_INCLUDE_DIRS "${HIDAPI_ROOT}/include")
        set(HIDAPI_LIBRARIES "${HIDAPI_ROOT}/lib/hidapi.lib")
        set(HIDAPI_DLL "${HIDAPI_ROOT}/bin/hidapi.dll")
    else()
        message(WARNING "hidapi not found. Run scripts/setup/setup-hidapi.ps1 to download it. "
                        "USB HID device support (Stream Deck, etc.) will be disabled.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(HIDAPI hidapi-hidraw)
        if(NOT HIDAPI_FOUND)
            pkg_check_modules(HIDAPI hidapi-libusb)
        endif()
        if(NOT HIDAPI_FOUND)
            pkg_check_modules(HIDAPI hidapi)
        endif()
    endif()
endif()
# Ulanzi Dial backend — one per platform.  All three implementations
# expose the same Qt signal contract (see UlanziDialBackend.h); the
# mapper dialog and MainWindow dispatcher are unchanged across
# platforms.  See #3232 for the design.
target_sources(aethercore PRIVATE
    src/core/UlanziDialBackend.h    # header-only Q_OBJECT — listed so AUTOMOC mocs it in the engine
    src/core/backends/IRadioBackend.h)  # aetherd RFC step 2.1: radio-facing seam (§5.5); header-only Q_OBJECT
target_sources(AetherSDR PRIVATE
    src/gui/UlanziDialMapperDialog.cpp
    src/gui/UlanziDialMapperDialog.h)
if(UNIX AND NOT APPLE)
    target_sources(aethercore PRIVATE
        src/core/EvdevEncoderManager.cpp
        src/core/EvdevEncoderManager.h)
endif()
if(WIN32)
    target_sources(aethercore PRIVATE
        src/core/UlanziDialWindowsManager.cpp
        src/core/UlanziDialWindowsManager.h)
endif()
if(APPLE)
    target_sources(aethercore PRIVATE
        src/core/UlanziDialMacOSManager.cpp
        src/core/UlanziDialMacOSManager.h)
    target_link_libraries(aethercore PRIVATE
        "-framework IOKit"
        "-framework CoreFoundation")
endif()

if(HIDAPI_FOUND)
    message(STATUS "hidapi found — USB HID encoder support enabled")
    # PUBLIC: gui (MainWindow_Controllers.cpp) includes hidapi.h and checks
    # HAVE_HIDAPI directly.
    target_compile_definitions(aethercore PUBLIC HAVE_HIDAPI)
    target_sources(aethercore PRIVATE
        src/core/HidEncoderManager.cpp
        src/core/HidEncoderManager.h
        src/core/HidDeviceParser.cpp
        src/core/HidDeviceParser.h)
    target_sources(AetherSDR PRIVATE
        src/gui/RC28MappingDialog.cpp
        src/gui/RC28MappingDialog.h)
    set(HIDAPI_NORMALIZED_INCLUDE_DIRS ${HIDAPI_INCLUDE_DIRS})
    foreach(hidapi_dir IN LISTS HIDAPI_INCLUDE_DIRS)
        if(EXISTS "${hidapi_dir}/hidapi.h")
            get_filename_component(hidapi_leaf "${hidapi_dir}" NAME)
            if(hidapi_leaf STREQUAL "hidapi")
                get_filename_component(hidapi_parent "${hidapi_dir}" DIRECTORY)
                list(APPEND HIDAPI_NORMALIZED_INCLUDE_DIRS "${hidapi_parent}")
            endif()
        endif()
    endforeach()
    list(REMOVE_DUPLICATES HIDAPI_NORMALIZED_INCLUDE_DIRS)
    target_include_directories(aethercore PUBLIC ${HIDAPI_NORMALIZED_INCLUDE_DIRS})
    target_link_libraries(aethercore PRIVATE ${HIDAPI_LIBRARIES})
    # Copy DLL to build dir on Windows
    if(WIN32 AND HIDAPI_DLL)
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${HIDAPI_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
            COMMENT "Copying hidapi.dll to build directory"
        )
    endif()
else()
    message(STATUS "hidapi not found — USB HID encoder support disabled")
endif()

# ONNX Runtime — optional CNN signal classifier for S-History v2
# Windows: run scripts/setup/setup-onnxruntime.ps1 first to download prebuilt DLLs
# Linux:   apt install libonnxruntime-dev  (or build from source)
# macOS:   brew install onnxruntime
if(WIN32)
    set(ORT_ROOT "${CMAKE_SOURCE_DIR}/third_party/onnxruntime")
    if(EXISTS "${ORT_ROOT}/include/onnxruntime_cxx_api.h")
        set(ORT_FOUND TRUE)
        set(ORT_INCLUDE_DIRS "${ORT_ROOT}/include")
        set(ORT_LIBRARIES "${ORT_ROOT}/lib/onnxruntime.lib")
        file(GLOB ORT_DLLS "${ORT_ROOT}/bin/*.dll")
    else()
        message(STATUS "ONNX Runtime not found — CNN signal classifier disabled. "
                       "Run scripts/setup/setup-onnxruntime.ps1 to enable.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(ORT libonnxruntime)
    endif()
    if(NOT ORT_FOUND)
        find_library(ORT_LIB onnxruntime)
        find_path(ORT_INC onnxruntime_cxx_api.h)
        if(ORT_LIB AND ORT_INC)
            set(ORT_FOUND TRUE)
            set(ORT_LIBRARIES ${ORT_LIB})
            set(ORT_INCLUDE_DIRS ${ORT_INC})
        endif()
    endif()
endif()
if(ORT_FOUND)
    message(STATUS "ONNX Runtime found — CNN signal classifier enabled")
else()
    message(STATUS "ONNX Runtime not found — CNN signal classifier disabled")
endif()

if(PORTAUDIO_FOUND)
    target_sources(aethercore PRIVATE src/core/CwSidetonePortAudioSink.cpp)
    target_compile_definitions(aethercore PUBLIC HAVE_PORTAUDIO)
    target_include_directories(aethercore PRIVATE ${PORTAUDIO_INCLUDE_DIRS})
    target_link_directories(aethercore PRIVATE ${PORTAUDIO_LIBRARY_DIRS})
    target_link_libraries(aethercore PRIVATE ${PORTAUDIO_LIBRARIES})
endif()

if(FFTW3_FOUND)
    target_compile_definitions(aethercore PUBLIC HAVE_FFTW3
        # On Windows lld-link (used by ClangCL) requires explicit dllimport
        # declarations; define FFTW_DLL so fftw3.h emits __declspec(dllimport)
        # and the linker can resolve __imp_fftwf_* symbols from the import lib.
        $<$<BOOL:${WIN32}>:FFTW_DLL>)
    # PUBLIC: core/SpectralNR.h includes <fftw3.h> under HAVE_FFTW3 (also
    # PUBLIC), and SpectralNR.h is pulled in by AudioEngine.h — a header many
    # gui TUs include. A PRIVATE include dir is lost to the exe, so those gui
    # TUs fail to find fftw3.h wherever it is off the default path (Windows
    # bundled third_party/fftw3). The library link stays PRIVATE (symbols reach
    # the exe transitively through the static lib).
    target_include_directories(aethercore PUBLIC ${FFTW3_INCLUDE_DIRS})
    target_link_directories(aethercore PRIVATE ${FFTW3_LIBRARY_DIRS})
    target_link_libraries(aethercore PRIVATE ${FFTW3_LIBRARIES})
    # Copy DLL to build dir on Windows
    if(WIN32 AND FFTW3_DLL)
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${FFTW3_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
            COMMENT "Copying libfftw3-3.dll to build directory"
        )
    endif()
endif()

if(ORT_FOUND)
    target_compile_definitions(aethercore PUBLIC HAVE_ONNX)
    # PUBLIC include dir: core/SignalClassifier.h includes onnxruntime under
    # HAVE_ONNX (PUBLIC) and is pulled in by gui/MainWindow.h. Same transitive
    # leak as FFTW3 above. Library link stays PRIVATE (transitive via the lib).
    target_include_directories(aethercore PUBLIC ${ORT_INCLUDE_DIRS})
    target_link_libraries(aethercore PRIVATE ${ORT_LIBRARIES})
    if(WIN32 AND ORT_DLLS)
        foreach(_ort_dll IN LISTS ORT_DLLS)
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${_ort_dll}" "$<TARGET_FILE_DIR:AetherSDR>"
                COMMENT "Copying ONNX Runtime DLL to build directory")
        endforeach()
    endif()
endif()

if(RADE_FOUND)
    # PUBLIC defines: gui (RadeApplet etc.) checks HAVE_RADE/HAVE_OPUS.
    target_compile_definitions(aethercore PUBLIC HAVE_RADE HAVE_OPUS IS_BUILDING_RADE_API=1)
    if(RADE_WAV_TAP)
        set(RADE_TAP_DIR "${CMAKE_BINARY_DIR}/rade_taps"
            CACHE PATH "Output directory for RADE WAV tap files")
        # PUBLIC: RADEEngine.h guards two QByteArray members with
        # #ifdef RADE_WAV_TAP mid-class, so the flag changes sizeof(RADEEngine)
        # and every later member offset. A gui TU allocates it
        # (MainWindow_DigitalModes.cpp `new RADEEngine`); a PRIVATE define would
        # give the exe a smaller layout than the constructor compiled here →
        # ODR violation / heap corruption under -DRADE_WAV_TAP=ON. Matches the
        # sibling HAVE_RADE/HAVE_OPUS defines.
        target_compile_definitions(aethercore PUBLIC RADE_WAV_TAP
            RADE_TAP_DIR="${RADE_TAP_DIR}")
    endif()
    target_include_directories(aethercore PRIVATE ${RADE_DIR}/src)
    if(WIN32)
        target_link_libraries(aethercore PRIVATE opus)
    else()
        target_link_libraries(aethercore PRIVATE opus m)
    endif()
elseif(OPUS_FOUND)
    target_compile_definitions(aethercore PUBLIC HAVE_OPUS)
    target_include_directories(aethercore PRIVATE ${OPUS_INCLUDE_DIRS})
    target_link_libraries(aethercore PRIVATE ${OPUS_LIBRARIES})
endif()

if(ENABLE_SPECBLEACH)
    target_compile_definitions(aethercore PUBLIC HAVE_SPECBLEACH)
    target_include_directories(aethercore PRIVATE
        ${CMAKE_SOURCE_DIR}/third_party/libspecbleach/include
        ${CMAKE_SOURCE_DIR}/third_party/libspecbleach/src)
    if(MSVC AND SPECBLEACH_STATIC_LIB)
        # Link pre-built clang-cl static lib
        add_dependencies(aethercore specbleach_build)
        target_link_libraries(aethercore PRIVATE ${SPECBLEACH_STATIC_LIB})
        # fftw3f (float precision) for Windows
        set(FFTW3F_LIB "${CMAKE_SOURCE_DIR}/third_party/fftw3/lib/fftw3f.lib")
        if(EXISTS ${FFTW3F_LIB})
            target_link_libraries(aethercore PRIVATE ${FFTW3F_LIB})
            set(FFTW3F_DLL "${CMAKE_SOURCE_DIR}/third_party/fftw3/bin/libfftw3f-3.dll")
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${FFTW3F_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
                COMMENT "Copying libfftw3f-3.dll to build directory")
        else()
            message(WARNING "fftw3f.lib not found — run scripts/setup/setup-fftw.ps1 and gen-fftw3f-lib.bat")
        endif()
    else()
        # libspecbleach C sources include <fftw3.h> directly — ensure it's
        # findable. PUBLIC for the same transitive reason as the main FFTW3
        # block above (AudioEngine.h -> SpectralNR.h -> <fftw3.h> reaches gui).
        if(FFTW3_INCLUDE_DIRS)
            target_include_directories(aethercore PUBLIC ${FFTW3_INCLUDE_DIRS})
        else()
            find_path(FFTW3_H_DIR fftw3.h HINTS ${CMAKE_SOURCE_DIR}/third_party/fftw3/include)
            if(FFTW3_H_DIR)
                target_include_directories(aethercore PUBLIC ${FFTW3_H_DIR})
            endif()
        endif()
        # libspecbleach uses fftwf (float precision FFTW3)
        find_library(FFTW3F_LIB fftw3f HINTS /opt/homebrew/lib /usr/local/lib ${CMAKE_SOURCE_DIR}/third_party/fftw3/lib)
        if(FFTW3F_LIB)
            target_link_libraries(aethercore PRIVATE ${FFTW3F_LIB})
        else()
            message(WARNING "fftw3f not found — NR4 will fail to link")
        endif()
        # Suppress warnings from third-party C code
        set_source_files_properties(${SPECBLEACH_SOURCES} PROPERTIES COMPILE_FLAGS "-w")
    endif()
endif()

if(ENABLE_DFNR)
    target_compile_definitions(aethercore PUBLIC HAVE_DFNR)
    target_include_directories(aethercore PRIVATE ${DEEPFILTER_DIR}/include)
    target_link_libraries(aethercore PRIVATE ${DFNR_LIB})
    if(WIN32)
        target_link_libraries(aethercore PRIVATE ws2_32 bcrypt userenv ntdll)
    elseif(APPLE)
        # Rust runtime deps on macOS
        target_link_libraries(aethercore PRIVATE "-framework Security" "-framework CoreFoundation")
    else()
        # Rust runtime deps on Linux
        target_link_libraries(aethercore PRIVATE pthread dl m)
    endif()
    # Copy DLL and model to build directory
    if(DFNR_DLL AND EXISTS ${DFNR_DLL})
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${DFNR_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
            COMMENT "Copying deepfilter.dll to build directory")
    endif()
    if(EXISTS "${DFNR_MODEL}" AND NOT AETHER_EMBED_DFNR_MODEL)
        if(APPLE)
            # macOS: place in Resources/ so notarization doesn't reject a
            # non-Mach-O file inside Contents/MacOS/
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E make_directory
                    "$<TARGET_BUNDLE_DIR:AetherSDR>/Contents/Resources"
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${DFNR_MODEL}" "$<TARGET_BUNDLE_DIR:AetherSDR>/Contents/Resources/"
                COMMENT "Copying DeepFilterNet3 model to app bundle Resources")
        else()
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${DFNR_MODEL}" "$<TARGET_FILE_DIR:AetherSDR>"
                COMMENT "Copying DeepFilterNet3 model to build directory")
        endif()
    endif()
    # Install the model alongside the binary for cmake --install
    if(NOT APPLE AND NOT AETHER_EMBED_DFNR_MODEL)
        install(FILES "${DFNR_MODEL}"
            DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/AetherSDR"
            OPTIONAL)
    endif()
endif()

# Optional NVIDIA Maxine AFX GPU denoiser. The AFX runtime (libnv_audiofx +
# the ~2 GB CUDA/TensorRT libs) is NOT linked here — it is dlopen'd at runtime
# from a downloaded/cached pack, so the shipped binary carries none of it.
# We only compile the wrapper (it uses its own minimal NvAFX_* declarations,
# so no NVIDIA headers are vendored) and link the dynamic loader.
if(ENABLE_NVIDIA_AFX)
    # x86_64 ONLY: NVIDIA Maxine AFX ships no ARM runtime, so aarch64 (Raspberry
    # Pi etc. — which also have no NVIDIA GPU) and macOS get the DFNR fallback,
    # not BNR. The arch guard means BNR is simply absent there (no dead button).
    if(((UNIX AND NOT APPLE) OR WIN32) AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
        # PUBLIC: gui surfaces the BNR button under #ifdef HAVE_NVIDIA_AFX.
        target_compile_definitions(aethercore PUBLIC HAVE_NVIDIA_AFX)
        # Linux loads the AFX runtime via libdl; Windows uses the Win32
        # LoadLibrary family from kernel32 (linked by default).
        if(UNIX)
            target_link_libraries(aethercore PRIVATE ${CMAKE_DL_LIBS})
        endif()
        message(STATUS "NVIDIA AFX GPU denoiser enabled (runtime-loaded)")
    else()
        message(STATUS "NVIDIA AFX GPU denoiser not built (x86_64 Linux/Windows only; macOS/aarch64 use DFNR)")
    endif()
endif()

if(ENABLE_MQTT)
    # HAVE_MQTT gates the project's MQTT client code (MqttApplet,
    # RadioModel plumbing) — must be set regardless of whether
    # libmosquitto is the bundled or the system copy.
    target_compile_definitions(aethercore PUBLIC HAVE_MQTT) # gui MqttApplet checks it
    if (USE_SYSTEM_LIBMOSQUITTO)
        # System libmosquitto is conventionally built with TLS support on
        # distro packages — assume HAVE_MQTT_TLS unless the packager
        # explicitly opted out via -DMQTT_TLS=OFF.
        if(MQTT_TLS)
            target_compile_definitions(aethercore PUBLIC HAVE_MQTT_TLS)
        endif()
    else()
        target_include_directories(aethercore PRIVATE
            ${MOSQUITTO_DIR}/include
            ${MOSQUITTO_DIR}/src)
        target_sources(aethercore PRIVATE ${MOSQUITTO_SOURCES})
        if(OpenSSL_FOUND)
            set(MQTT_TLS_FLAG " -DWITH_TLS")
            target_compile_definitions(aethercore PUBLIC HAVE_MQTT_TLS)
            target_link_libraries(aethercore PRIVATE OpenSSL::SSL OpenSSL::Crypto)
        else()
            set(MQTT_TLS_FLAG "")
        endif()
        if(WIN32)
            set_source_files_properties(${MOSQUITTO_SOURCES} PROPERTIES
                COMPILE_FLAGS "-w -DLIBMOSQUITTO_STATIC -DLIBMOSQCOMMON_STATIC${MQTT_TLS_FLAG}")
            target_compile_definitions(aethercore PRIVATE LIBMOSQUITTO_STATIC LIBMOSQCOMMON_STATIC)
        else()
            set_source_files_properties(${MOSQUITTO_SOURCES} PROPERTIES
                COMPILE_FLAGS "-w -DWITH_THREADING${MQTT_TLS_FLAG}")
        endif()
    endif()
    if(NOT WIN32)
        target_link_libraries(aethercore PRIVATE pthread)
    endif()
endif()

if(HAVE_PIPEWIRE)
    target_compile_definitions(aethercore PUBLIC HAVE_PIPEWIRE) # gui checks HAVE_PIPEWIRE too
    message(STATUS "Linux DAX bridge enabled (PulseAudio pipe modules)")
endif()

if(HAVE_PIPEWIRE_NATIVE)
    target_compile_definitions(aethercore PUBLIC HAVE_PIPEWIRE_NATIVE)
    target_include_directories(aethercore PRIVATE ${PIPEWIRE_NATIVE_INCLUDE_DIRS})
    target_link_libraries(aethercore PRIVATE ${PIPEWIRE_NATIVE_LIBRARIES})
    target_compile_options(aethercore PRIVATE ${PIPEWIRE_NATIVE_CFLAGS_OTHER})
    message(STATUS "Linux DAX RX uses native pw_stream (libpipewire-0.3 ${PIPEWIRE_NATIVE_VERSION})")
endif()

# Pass project version to code — PUBLIC so main.cpp and gui (About dialog,
# Ax25HfPacketDecodeDialog) see it as before.
target_compile_definitions(aethercore PUBLIC
    AETHERSDR_VERSION="${PROJECT_VERSION}"
)

# Compiler warnings — same flags for both first-party targets. Vendored
# sources compiled into aethercore keep their per-source -w COMPILE_FLAGS,
# which land after these target options and win (unchanged from monolith).
foreach(_aether_tgt aethercore AetherSDR)
    if(MSVC)
        target_compile_options(${_aether_tgt} PRIVATE /W3 /Zc:__cplusplus /permissive- /utf-8 /bigobj)
    else()
        target_compile_options(${_aether_tgt} PRIVATE
            -Wall -Wextra -Wpedantic
            $<$<CONFIG:Debug>:-g3 -fsanitize=address>
            $<$<CONFIG:Debug>:-fno-omit-frame-pointer>
        )
        target_link_options(${_aether_tgt} PRIVATE
            $<$<CONFIG:Debug>:-fsanitize=address>
        )
    endif()
endforeach()


# ── Unit test harnesses ──────────────────────────────────────────────────────
# Standalone DSP smoke tests. Built alongside the main target so they share
# the same toolchain and warning flags. Run manually with ./build/<target>.

add_executable(client_eq_test
    tests/client_eq_test.cpp
    src/core/ClientEq.cpp
)
target_include_directories(client_eq_test PRIVATE src)

# Fractional-octave smoothing — exercises the static helper on
# ClientEqCurveWidget with no live widget required.
add_executable(client_eq_smoothing_test
    tests/client_eq_smoothing_test.cpp
    src/gui/ClientEqCurveWidget.cpp
    src/core/ClientEq.cpp
)
target_include_directories(client_eq_smoothing_test PRIVATE src)
target_link_libraries(client_eq_smoothing_test PRIVATE Qt6::Widgets)
set_target_properties(client_eq_smoothing_test PROPERTIES AUTOMOC ON)

add_executable(client_comp_test
    tests/client_comp_test.cpp
    src/core/ClientComp.cpp
    src/core/ClientPhaseRotator.cpp
)
target_include_directories(client_comp_test PRIVATE src)

add_executable(slice_label_test
    tests/slice_label_test.cpp
    src/gui/SliceLabel.cpp
    src/core/AppSettings.cpp
)
target_include_directories(slice_label_test PRIVATE src)
target_link_libraries(slice_label_test PRIVATE Qt6::Gui)
add_test(NAME slice_label_test COMMAND slice_label_test)

add_executable(vfo_flag_placement_test
    tests/vfo_flag_placement_test.cpp
)
target_include_directories(vfo_flag_placement_test PRIVATE src)
target_link_libraries(vfo_flag_placement_test PRIVATE Qt6::Widgets)
add_test(NAME vfo_flag_placement_test COMMAND vfo_flag_placement_test)

add_executable(slice_model_letter_test
    tests/slice_model_letter_test.cpp
    src/models/SliceModel.cpp
    src/core/DigitalVoiceModeRegistry.cpp
)
target_include_directories(slice_model_letter_test PRIVATE src)
target_link_libraries(slice_model_letter_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME slice_model_letter_test COMMAND slice_model_letter_test)

# ThemeManager — RFC #3076 Phase 1.  Verifies the built-in default-dark
# theme loads from Qt resources, scalar tokens resolve, missing tokens
# don't crash, and the stylesheet template resolver substitutes correctly.
qt_add_resources(THEME_TEST_RESOURCES resources/resources.qrc)
add_executable(theme_manager_test
    tests/theme_manager_test.cpp
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    ${THEME_TEST_RESOURCES}
)
target_include_directories(theme_manager_test PRIVATE src)
target_link_libraries(theme_manager_test PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Test)
add_test(NAME theme_manager_test COMMAND theme_manager_test)

add_executable(panadapter_model_rx_antenna_test
    tests/panadapter_model_rx_antenna_test.cpp
    src/models/PanadapterModel.cpp
    src/core/PerfTelemetry.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(panadapter_model_rx_antenna_test PRIVATE src)
target_link_libraries(panadapter_model_rx_antenna_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME panadapter_model_rx_antenna_test COMMAND panadapter_model_rx_antenna_test)

add_executable(packet_loss_concealment_test
    tests/packet_loss_concealment_test.cpp
    src/core/PacketLossConcealment.cpp
)
target_include_directories(packet_loss_concealment_test PRIVATE src)
target_link_libraries(packet_loss_concealment_test PRIVATE Qt6::Core)
add_test(NAME packet_loss_concealment_test COMMAND packet_loss_concealment_test)

add_executable(model_capabilities_test
    tests/model_capabilities_test.cpp
    src/models/ModelCapabilities.cpp
)
target_include_directories(model_capabilities_test PRIVATE src)
target_link_libraries(model_capabilities_test PRIVATE Qt6::Core)
add_test(NAME model_capabilities_test COMMAND model_capabilities_test)

add_executable(declared_bands_test
    tests/declared_bands_test.cpp
    src/models/DeclaredBands.cpp
)
target_include_directories(declared_bands_test PRIVATE src)
target_link_libraries(declared_bands_test PRIVATE Qt6::Core)
add_test(NAME declared_bands_test COMMAND declared_bands_test)

add_executable(radio_discovery_test
    tests/radio_discovery_test.cpp
    src/core/RadioDiscovery.cpp
)
target_include_directories(radio_discovery_test PRIVATE src)
target_compile_definitions(radio_discovery_test PRIVATE AETHERSDR_TESTING)
target_link_libraries(radio_discovery_test PRIVATE Qt6::Core Qt6::Network)
add_test(NAME radio_discovery_test COMMAND radio_discovery_test)

add_executable(client_quindar_test
    tests/client_quindar_test.cpp
    src/core/ClientQuindarTone.cpp
)
target_include_directories(client_quindar_test PRIVATE src)
target_link_libraries(client_quindar_test PRIVATE Qt6::Core)

add_executable(tx_mic_channel_normalizer_test
    tests/tx_mic_channel_normalizer_test.cpp
    src/core/TxMicChannelNormalizer.cpp
    src/core/Resampler.cpp
)
target_include_directories(tx_mic_channel_normalizer_test PRIVATE
    src
    ${CMAKE_SOURCE_DIR}/third_party/r8brain
)
target_link_libraries(tx_mic_channel_normalizer_test PRIVATE Qt6::Core)
add_test(NAME tx_mic_channel_normalizer_test COMMAND tx_mic_channel_normalizer_test)

# Pins the SkyRoof-parity WFM DSP chain: NCO offset correction removes the
# discriminator DC term (fixed pan + Doppler-stepped slice), twin linear-phase
# resamplers deliver exactly 48 kHz from any native DAX IQ rate, and streaming
# state is continuous across block boundaries.
add_executable(wfm_dsp_test
    tests/wfm_dsp_test.cpp
    src/core/WfmDsp.cpp
    src/core/Resampler.cpp
)
target_include_directories(wfm_dsp_test PRIVATE
    src
    ${CMAKE_SOURCE_DIR}/third_party/r8brain
)
target_link_libraries(wfm_dsp_test PRIVATE Qt6::Core)
add_test(NAME wfm_dsp_test COMMAND wfm_dsp_test)

# Hardware-gated functional test for the optional NVIDIA AFX GPU denoiser.
# Built only when the feature is enabled; SKIPs at runtime without a pack/GPU.
if(ENABLE_NVIDIA_AFX AND ((UNIX AND NOT APPLE) OR WIN32) AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
    add_executable(nvidia_afx_filter_test
        tests/nvidia_afx_filter_test.cpp
        src/core/NvidiaAfxFilter.cpp
        src/core/MonoDspStereoAdapter.cpp
        src/core/Resampler.cpp
    )
    target_compile_definitions(nvidia_afx_filter_test PRIVATE HAVE_NVIDIA_AFX)
    target_include_directories(nvidia_afx_filter_test PRIVATE
        src
        ${CMAKE_SOURCE_DIR}/third_party/r8brain
    )
    target_link_libraries(nvidia_afx_filter_test PRIVATE Qt6::Core)
    if(UNIX)
        target_link_libraries(nvidia_afx_filter_test PRIVATE ${CMAKE_DL_LIBS})
    endif()
    add_test(NAME nvidia_afx_filter_test COMMAND nvidia_afx_filter_test)
endif()

# Pure, headless, hardware-free golden matrix for the consolidated audio
# format/rate negotiation policy (#3306). TargetOs is data, so this one binary
# exercises the Windows/macOS/Linux ladders regardless of the CI host.
add_executable(audio_format_negotiation_test
    tests/audio_format_negotiation_test.cpp
    src/core/AudioFormatNegotiator.cpp
)
target_include_directories(audio_format_negotiation_test PRIVATE src)
target_link_libraries(audio_format_negotiation_test PRIVATE Qt6::Core)
add_test(NAME audio_format_negotiation_test COMMAND audio_format_negotiation_test)

# Smoke test for the live Qt-Multimedia wrapper (AudioDeviceNegotiator): probes
# the real default devices and round-trips to an openable QAudioFormat. Tolerant
# of headless runners with no audio hardware.
add_executable(audio_device_negotiator_test
    tests/audio_device_negotiator_test.cpp
    src/core/AudioDeviceNegotiator.cpp
    src/core/AudioFormatNegotiator.cpp
)
target_include_directories(audio_device_negotiator_test PRIVATE src)
target_link_libraries(audio_device_negotiator_test PRIVATE Qt6::Core Qt6::Multimedia)
add_test(NAME audio_device_negotiator_test COMMAND audio_device_negotiator_test)

# Unit test for the AudioOutputRouter sink registry (#3306) — seeding, fan-out,
# and the QPointer guard. Hardware-independent. AudioOutputRouter is a QObject,
# so this relies on the project-global AUTOMOC.
add_executable(audio_output_router_test
    tests/audio_output_router_test.cpp
    src/core/AudioOutputRouter.cpp
)
target_include_directories(audio_output_router_test PRIVATE src)
target_link_libraries(audio_output_router_test PRIVATE Qt6::Core Qt6::Multimedia)
add_test(NAME audio_output_router_test COMMAND audio_output_router_test)

# Regression test for #4003 — QsoRecorder must not dereference a SliceModel that
# was freed (reconnect prune) before recording starts. QPointer auto-nulls the
# reference; the test deletes the slice and asserts the metadata is cleared.
add_executable(qso_recorder_slice_lifetime_test
    tests/qso_recorder_slice_lifetime_test.cpp
    src/core/QsoRecorder.cpp
    src/core/AppSettings.cpp
    src/core/AudioDeviceNegotiator.cpp
    src/core/AudioFormatNegotiator.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/Resampler.cpp
    src/models/SliceModel.cpp
    src/core/DigitalVoiceModeRegistry.cpp
)
target_include_directories(qso_recorder_slice_lifetime_test PRIVATE
    src
    ${CMAKE_SOURCE_DIR}/third_party/r8brain
)
target_link_libraries(qso_recorder_slice_lifetime_test PRIVATE Qt6::Core Qt6::Multimedia)
add_test(NAME qso_recorder_slice_lifetime_test COMMAND qso_recorder_slice_lifetime_test)

add_executable(profile_transfer_test
    tests/profile_transfer_test.cpp
)
target_include_directories(profile_transfer_test PRIVATE src)
target_link_libraries(profile_transfer_test PRIVATE Qt6::Core)
add_test(NAME profile_transfer_test COMMAND profile_transfer_test)

add_executable(waveform_upload_state_test
    tests/waveform_upload_state_test.cpp
    src/core/WaveformUploadState.cpp
)
target_include_directories(waveform_upload_state_test PRIVATE src)
target_link_libraries(waveform_upload_state_test PRIVATE Qt6::Core)
add_test(NAME waveform_upload_state_test COMMAND waveform_upload_state_test)

add_executable(zip_archive_test
    tests/zip_archive_test.cpp
    src/core/ZipArchive.cpp
)
target_include_directories(zip_archive_test PRIVATE src)
target_link_libraries(zip_archive_test PRIVATE Qt6::Core)
if (USE_SYSTEM_ZLIB)
    target_link_libraries(zip_archive_test PRIVATE PkgConfig::zlib)
else()
    target_link_libraries(zip_archive_test PRIVATE zlibstatic)
endif()
add_test(NAME zip_archive_test COMMAND zip_archive_test)

add_executable(legacy_waveform_package_test
    tests/legacy_waveform_package_test.cpp
    src/core/LegacyWaveformPackage.cpp
    src/core/ZipArchive.cpp
)
target_include_directories(legacy_waveform_package_test PRIVATE src)
target_link_libraries(legacy_waveform_package_test PRIVATE Qt6::Core)
if (USE_SYSTEM_ZLIB)
    target_link_libraries(legacy_waveform_package_test PRIVATE PkgConfig::zlib)
else()
    target_link_libraries(legacy_waveform_package_test PRIVATE zlibstatic)
endif()
add_test(NAME legacy_waveform_package_test COMMAND legacy_waveform_package_test)

# Pins the filter-before-merge invariant on the license-class-aware overload
# of BandPlanManager::contiguousRegionsForBand (PR #3050, closing #2649). (#3060)
add_executable(band_plan_license_filter_test
    tests/band_plan_license_filter_test.cpp
    src/models/BandPlanManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(band_plan_license_filter_test PRIVATE src)
target_link_libraries(band_plan_license_filter_test PRIVATE Qt6::Core Qt6::Gui)
add_test(NAME band_plan_license_filter_test COMMAND band_plan_license_filter_test)

add_executable(biquad_test
    tests/biquad_test.cpp
    src/core/Biquad.cpp
    src/core/StereoBiquad.cpp
)
target_include_directories(biquad_test PRIVATE src)
add_test(NAME biquad_test COMMAND biquad_test)

add_executable(spectral_nr_test
    tests/spectral_nr_test.cpp
    src/core/SpectralNR.cpp
)
target_include_directories(spectral_nr_test PRIVATE src)
target_link_libraries(spectral_nr_test PRIVATE Qt6::Core)
add_test(NAME spectral_nr_test COMMAND spectral_nr_test)

add_executable(mono_dsp_stereo_adapter_test
    tests/mono_dsp_stereo_adapter_test.cpp
    src/core/MonoDspStereoAdapter.cpp
)
target_include_directories(mono_dsp_stereo_adapter_test PRIVATE src)
target_link_libraries(mono_dsp_stereo_adapter_test PRIVATE Qt6::Core)
add_test(NAME mono_dsp_stereo_adapter_test COMMAND mono_dsp_stereo_adapter_test)

add_executable(rnnoise_filter_test
    tests/rnnoise_filter_test.cpp
)
target_link_libraries(rnnoise_filter_test PRIVATE aethercore Qt6::Core)
add_test(NAME rnnoise_filter_test COMMAND rnnoise_filter_test)

add_executable(adaptive_filter_test
    tests/adaptive_filter_test.cpp
    src/core/OccupiedRegion.cpp
)
target_include_directories(adaptive_filter_test PRIVATE src)
target_link_libraries(adaptive_filter_test PRIVATE Qt6::Core)
add_test(NAME adaptive_filter_test COMMAND adaptive_filter_test)

add_executable(waveform_scope_model_test
    tests/waveform_scope_model_test.cpp
    src/gui/WaveformScopeModel.cpp
)
target_include_directories(waveform_scope_model_test PRIVATE src)
target_link_libraries(waveform_scope_model_test PRIVATE Qt6::Core)
add_test(NAME waveform_scope_model_test COMMAND waveform_scope_model_test)

# Engine-level test: drives AdaptiveFilterEngine::processFrame through a real
# SliceModel (signals only, no radio) with monotonic timestamps — covers the
# wall-clock pacing / send-throttle / QSO-handoff logic the measurement test
# can't reach. Both AdaptiveFilterEngine and SliceModel are Q_OBJECT (AUTOMOC).
add_executable(adaptive_engine_test
    tests/adaptive_engine_test.cpp
    src/core/AdaptiveFilterEngine.cpp
    src/core/OccupiedRegion.cpp
    src/models/SliceModel.cpp
    src/core/DigitalVoiceModeRegistry.cpp
    src/core/KiwiSdrProtocol.cpp
)
target_include_directories(adaptive_engine_test PRIVATE src)
target_link_libraries(adaptive_engine_test PRIVATE Qt6::Core)
set_target_properties(adaptive_engine_test PROPERTIES AUTOMOC ON)
add_test(NAME adaptive_engine_test COMMAND adaptive_engine_test)

add_executable(kiwi_sdr_protocol_test
    tests/kiwi_sdr_protocol_test.cpp
    src/core/KiwiSdrProtocol.cpp
)
target_include_directories(kiwi_sdr_protocol_test PRIVATE src)
target_link_libraries(kiwi_sdr_protocol_test PRIVATE Qt6::Core)
add_test(NAME kiwi_sdr_protocol_test COMMAND kiwi_sdr_protocol_test)

add_executable(kiwi_sdr_manager_password_test
    tests/kiwi_sdr_manager_password_test.cpp
)
target_include_directories(kiwi_sdr_manager_password_test PRIVATE src)
target_link_libraries(kiwi_sdr_manager_password_test PRIVATE
    aethercore Qt6::Core Qt6::Test)
set_target_properties(kiwi_sdr_manager_password_test PROPERTIES AUTOMOC ON)
add_test(NAME kiwi_sdr_manager_password_test
         COMMAND kiwi_sdr_manager_password_test)

add_executable(kiwi_sdr_trace_math_test
    tests/kiwi_sdr_trace_math_test.cpp
)
target_include_directories(kiwi_sdr_trace_math_test PRIVATE src)
target_link_libraries(kiwi_sdr_trace_math_test PRIVATE Qt6::Core)
add_test(NAME kiwi_sdr_trace_math_test COMMAND kiwi_sdr_trace_math_test)

add_executable(dss_renderer_test
    tests/dss_renderer_test.cpp
    src/gui/DssRenderer.cpp
)
target_include_directories(dss_renderer_test PRIVATE src)
target_link_libraries(dss_renderer_test PRIVATE Qt6::Core Qt6::Gui)
add_test(NAME dss_renderer_test COMMAND dss_renderer_test)

add_executable(kiwi_sdr_redirect_policy_test
    tests/kiwi_sdr_redirect_policy_test.cpp
    src/core/KiwiSdrRedirectPolicy.cpp
)
target_include_directories(kiwi_sdr_redirect_policy_test PRIVATE src)
target_link_libraries(kiwi_sdr_redirect_policy_test PRIVATE Qt6::Core)
add_test(NAME kiwi_sdr_redirect_policy_test COMMAND kiwi_sdr_redirect_policy_test)

add_executable(receive_presentation_sync_test
    tests/receive_presentation_sync_test.cpp
    src/core/ReceivePresentationSync.cpp
)
target_include_directories(receive_presentation_sync_test PRIVATE src)
target_link_libraries(receive_presentation_sync_test PRIVATE Qt6::Core)
add_test(NAME receive_presentation_sync_test COMMAND receive_presentation_sync_test)

# Public-directory parser + external-API (ext_api) policy honoring.
add_executable(kiwi_public_directory_test
    tests/kiwi_public_directory_test.cpp
    src/core/KiwiPublicDirectory.cpp
)
target_include_directories(kiwi_public_directory_test PRIVATE src)
target_link_libraries(kiwi_public_directory_test PRIVATE Qt6::Core Qt6::Network)
target_compile_definitions(kiwi_public_directory_test PRIVATE AETHERSDR_VERSION="${PROJECT_VERSION}")
set_target_properties(kiwi_public_directory_test PROPERTIES AUTOMOC ON)
add_test(NAME kiwi_public_directory_test COMMAND kiwi_public_directory_test)

# Demonstration tool: honest, API-policy-aware read of kiwisdr.com/public
# (proof-of-concept shown to operators — see docs/kiwisdr-public-directory.md).
add_executable(kiwi_directory_poc
    tools/kiwi_directory_poc.cpp
    src/core/KiwiPublicDirectory.cpp
)
target_include_directories(kiwi_directory_poc PRIVATE src)
target_link_libraries(kiwi_directory_poc PRIVATE Qt6::Core Qt6::Network)
target_compile_definitions(kiwi_directory_poc PRIVATE AETHERSDR_VERSION="${PROJECT_VERSION}")
set_target_properties(kiwi_directory_poc PROPERTIES AUTOMOC ON)

add_executable(client_gate_test
    tests/client_gate_test.cpp
    src/core/ClientGate.cpp
)
target_include_directories(client_gate_test PRIVATE src)

add_executable(client_deess_test
    tests/client_deess_test.cpp
    src/core/ClientDeEss.cpp
)
target_include_directories(client_deess_test PRIVATE src)

add_executable(client_tube_test
    tests/client_tube_test.cpp
    src/core/ClientTube.cpp
)
target_include_directories(client_tube_test PRIVATE src)

add_executable(client_pudu_test
    tests/client_pudu_test.cpp
    src/core/ClientPudu.cpp
)
target_include_directories(client_pudu_test PRIVATE src)

add_executable(client_reverb_test
    tests/client_reverb_test.cpp
    src/core/ClientReverb.cpp
)
target_include_directories(client_reverb_test PRIVATE src)

add_executable(iambic_keyer_test
    tests/iambic_keyer_test.cpp
    src/core/IambicKeyer.cpp
)
target_include_directories(iambic_keyer_test PRIVATE src)
if(UNIX)
    target_link_libraries(iambic_keyer_test PRIVATE pthread)
endif()

add_executable(passive_spots_policy_test
    tests/passive_spots_policy_test.cpp
    src/core/AppSettings.cpp
    src/core/SpotCommandPolicy.cpp
)
target_include_directories(passive_spots_policy_test PRIVATE src)
target_link_libraries(passive_spots_policy_test PRIVATE Qt6::Core)

add_executable(spot_mode_resolver_test
    tests/spot_mode_resolver_test.cpp
    src/core/SpotModeResolver.cpp
)
target_include_directories(spot_mode_resolver_test PRIVATE src)
target_link_libraries(spot_mode_resolver_test PRIVATE Qt6::Core)
add_test(NAME spot_mode_resolver_test COMMAND spot_mode_resolver_test)

add_executable(navtex_model_test
    tests/navtex_model_test.cpp
    src/models/NavtexModel.cpp
)
target_include_directories(navtex_model_test PRIVATE src)
target_link_libraries(navtex_model_test PRIVATE Qt6::Core Qt6::Test)

add_executable(flex_waveform_model_test
    tests/flex_waveform_model_test.cpp
    src/models/FlexWaveformModel.cpp
)
target_include_directories(flex_waveform_model_test PRIVATE src)
target_link_libraries(flex_waveform_model_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME flex_waveform_model_test COMMAND flex_waveform_model_test)

# Docker-waveform install gate policy (#4210) — header-only, pure logic.
add_executable(waveform_install_gate_test
    tests/waveform_install_gate_test.cpp
)
target_include_directories(waveform_install_gate_test PRIVATE src)
target_link_libraries(waveform_install_gate_test PRIVATE Qt6::Core)
add_test(NAME waveform_install_gate_test COMMAND waveform_install_gate_test)

add_executable(digital_voice_waveform_process_test
    tests/digital_voice_waveform_process_test.cpp
    src/core/DigitalVoiceWaveformTelemetry.cpp
    src/core/DigitalVoiceWaveformProcess.cpp
    src/core/DigitalVoiceModeRegistry.cpp
    src/models/DigitalVoiceWaveformHistory.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(digital_voice_waveform_process_test PRIVATE src)
target_link_libraries(digital_voice_waveform_process_test PRIVATE Qt6::Core Qt6::Network)
add_test(NAME digital_voice_waveform_process_test COMMAND digital_voice_waveform_process_test)

add_executable(digital_voice_slice_lifecycle_test
    tests/digital_voice_slice_lifecycle_test.cpp
    src/models/SliceModel.cpp
    src/core/DigitalVoiceModeRegistry.cpp
)
target_include_directories(digital_voice_slice_lifecycle_test PRIVATE src)
target_link_libraries(digital_voice_slice_lifecycle_test PRIVATE Qt6::Core)
add_test(NAME digital_voice_slice_lifecycle_test COMMAND digital_voice_slice_lifecycle_test)

add_executable(dstar_model_test
    tests/dstar_model_test.cpp
    src/models/DStarModel.cpp
    src/core/DigitalVoiceWaveformTelemetry.cpp
    src/core/DigitalVoiceWaveformProcess.cpp
    src/core/DigitalVoiceModeRegistry.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(dstar_model_test PRIVATE src)
target_link_libraries(dstar_model_test PRIVATE Qt6::Core Qt6::Network)
if(Qt6SerialPort_FOUND)
    target_compile_definitions(dstar_model_test PRIVATE HAVE_SERIALPORT)
    target_link_libraries(dstar_model_test PRIVATE Qt6::SerialPort)
endif()
add_test(NAME dstar_model_test COMMAND dstar_model_test)

add_executable(ole_compound_file_test
    tests/ole_compound_file_test.cpp
    src/core/OleCompoundFile.cpp
    src/core/CabExtractor.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(ole_compound_file_test PRIVATE src)
if (USE_SYSTEM_MSPACK)
    target_link_libraries(ole_compound_file_test PRIVATE Qt6::Core PkgConfig::libmspack)
else()
    target_link_libraries(ole_compound_file_test PRIVATE Qt6::Core mspack_static)
endif()
if(UNIX)
    target_link_libraries(ole_compound_file_test PRIVATE pthread)
endif()

add_executable(xvtr_policy_test
    tests/xvtr_policy_test.cpp
    src/models/XvtrPolicy.cpp
)
target_include_directories(xvtr_policy_test PRIVATE src)
target_link_libraries(xvtr_policy_test PRIVATE Qt6::Core)

# #3449 — VITA-49 waterfall tile frequency decode (no 1 GHz ceiling)
add_executable(vita_tile_frequency_test
    tests/vita_tile_frequency_test.cpp
)
target_include_directories(vita_tile_frequency_test PRIVATE src)
target_link_libraries(vita_tile_frequency_test PRIVATE Qt6::Core)
add_test(NAME vita_tile_frequency_test COMMAND vita_tile_frequency_test)

add_executable(frequency_entry_parser_test
    tests/frequency_entry_parser_test.cpp
    src/gui/FrequencyEntryParser.cpp
)
target_include_directories(frequency_entry_parser_test PRIVATE src)
target_link_libraries(frequency_entry_parser_test PRIVATE Qt6::Core)
add_test(NAME frequency_entry_parser_test COMMAND frequency_entry_parser_test)

add_executable(radio_status_ownership_test
    tests/radio_status_ownership_test.cpp
    src/core/CommandParser.cpp
)
target_include_directories(radio_status_ownership_test PRIVATE src)
target_link_libraries(radio_status_ownership_test PRIVATE Qt6::Core)
enable_testing()
add_test(NAME radio_status_ownership_test COMMAND radio_status_ownership_test)

# MCP server field-mapping / protocol regression test (#4177). Pure Python,
# no app or Qt needed — guards the schema ↔ bridge verb field mapping.
find_program(PYTHON3_EXECUTABLE NAMES python3 python)
if(PYTHON3_EXECUTABLE)
    add_test(NAME aether_mcp_field_mapping
             COMMAND ${PYTHON3_EXECUTABLE}
                     ${CMAKE_CURRENT_SOURCE_DIR}/tools/test_aether_mcp.py)
    # Bridge docs must stay in sync with the verb registry (#4174 Phase 3):
    # fail CI if the generated verb table drifts or a detail heading is dup'd.
    add_test(NAME bridge_docs_check
             COMMAND ${PYTHON3_EXECUTABLE}
                     ${CMAKE_CURRENT_SOURCE_DIR}/tools/gen_bridge_docs.py --check)
endif()

add_executable(gui_client_identity_policy_test
    tests/gui_client_identity_policy_test.cpp
)
target_include_directories(gui_client_identity_policy_test PRIVATE src)
target_link_libraries(gui_client_identity_policy_test PRIVATE Qt6::Core)
add_test(NAME gui_client_identity_policy_test COMMAND gui_client_identity_policy_test)

add_executable(profile_load_command_test
    tests/profile_load_command_test.cpp
)
target_include_directories(profile_load_command_test PRIVATE src)
target_link_libraries(profile_load_command_test PRIVATE Qt6::Core)
add_test(NAME profile_load_command_test COMMAND profile_load_command_test)

# #3212 — slice recreate policy (reuse restored pan vs create new; slice freq)
add_executable(slice_recreate_policy_test
    tests/slice_recreate_policy_test.cpp
)
target_include_directories(slice_recreate_policy_test PRIVATE src)
target_link_libraries(slice_recreate_policy_test PRIVATE Qt6::Core)
add_test(NAME slice_recreate_policy_test COMMAND slice_recreate_policy_test)

# #3856 — radio-side display inventory policy (Layer B leak classification)
add_executable(display_inventory_policy_test
    tests/display_inventory_policy_test.cpp
)
target_include_directories(display_inventory_policy_test PRIVATE src)
target_link_libraries(display_inventory_policy_test PRIVATE Qt6::Core)
add_test(NAME display_inventory_policy_test COMMAND display_inventory_policy_test)

# QRZ callsign lookup — spotter stream detection, callsign regex,
# CallsignInfo JSON round-trip, 7-day cache TTL rule, and the QRZ XML
# response parser (QrzClient.cpp needs Qt6::Network for QNetworkReply).
add_executable(qrz_callsign_test
    tests/qrz_callsign_test.cpp
    src/core/CwCallsignSpotter.cpp
    src/core/CallsignInfo.cpp
    src/core/CtyDatParser.cpp
    src/core/QrzClient.cpp
    # LogManager provides lcQrz (spotter's qCDebug category); it drags
    # AsyncLogWriter + AppSettings for its writer/ctor chain at link time.
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(qrz_callsign_test PRIVATE src)
target_link_libraries(qrz_callsign_test PRIVATE Qt6::Core Qt6::Network Qt6::Test)
add_test(NAME qrz_callsign_test COMMAND qrz_callsign_test)

add_executable(shortcut_manager_test
    tests/shortcut_manager_test.cpp
    src/core/ShortcutManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(shortcut_manager_test PRIVATE src)
target_link_libraries(shortcut_manager_test PRIVATE Qt6::Core Qt6::Widgets)
add_test(NAME shortcut_manager_test COMMAND shortcut_manager_test)

add_executable(antenna_alias_test
    tests/antenna_alias_test.cpp
    src/models/AntennaAliasStore.cpp
    src/models/SliceModel.cpp
    src/core/DigitalVoiceModeRegistry.cpp
    src/core/AppSettings.cpp
)
target_include_directories(antenna_alias_test PRIVATE src)
target_link_libraries(antenna_alias_test PRIVATE Qt6::Core)
set_target_properties(antenna_alias_test PROPERTIES AUTOMOC ON)
add_test(NAME antenna_alias_test COMMAND antenna_alias_test)

add_executable(mqtt_antenna_alias_test
    tests/mqtt_antenna_alias_test.cpp
    src/core/MqttAntennaAlias.cpp
)
target_include_directories(mqtt_antenna_alias_test PRIVATE src)
target_link_libraries(mqtt_antenna_alias_test PRIVATE Qt6::Core)
add_test(NAME mqtt_antenna_alias_test COMMAND mqtt_antenna_alias_test)

add_executable(mqtt_settings_test
    tests/mqtt_settings_test.cpp
    src/core/MqttSettings.cpp
    src/core/MqttAntennaAlias.cpp
    src/core/AppSettings.cpp
)
target_include_directories(mqtt_settings_test PRIVATE src)
target_link_libraries(mqtt_settings_test PRIVATE Qt6::Core)
add_test(NAME mqtt_settings_test COMMAND mqtt_settings_test)

# Pure-math test for the ATU pre-tune center-frequency calculator.
# Locks in the IARU R1 reference table from issue #2624 so future edits
# to computeCenters() can't silently regress the per-band point counts.
add_executable(atu_pretune_centers_test
    tests/atu_pretune_centers_test.cpp
)
target_include_directories(atu_pretune_centers_test PRIVATE src)
target_link_libraries(atu_pretune_centers_test PRIVATE Qt6::Core)
add_test(NAME atu_pretune_centers_test COMMAND atu_pretune_centers_test)

add_executable(cw_sidetone_test
    tests/cw_sidetone_test.cpp
    src/core/CwSidetoneGenerator.cpp
)
target_include_directories(cw_sidetone_test PRIVATE src)
target_link_libraries(cw_sidetone_test PRIVATE Qt6::Core)

add_executable(cwx_local_keyer_drift_test
    tests/cwx_local_keyer_drift_test.cpp
    src/core/CwxLocalKeyer.cpp
    src/core/CwxLocalKeyer.h
)
target_include_directories(cwx_local_keyer_drift_test PRIVATE src)
target_link_libraries(cwx_local_keyer_drift_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(cwx_local_keyer_drift_test PRIVATE pthread)
endif()
add_test(NAME cwx_local_keyer_drift_test COMMAND cwx_local_keyer_drift_test)

add_executable(ax25_frame_formatter_test
    tests/ax25_frame_formatter_test.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
)
target_include_directories(ax25_frame_formatter_test PRIVATE src)
target_link_libraries(ax25_frame_formatter_test PRIVATE Qt6::Core)
add_test(NAME ax25_frame_formatter_test COMMAND ax25_frame_formatter_test)

add_executable(ax25_libmodem_shim_test
    tests/ax25_libmodem_shim_test.cpp
    src/core/tnc/AetherAx25LibmodemShim.cpp
    src/core/tnc/HdlcCodec.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
    src/core/tnc/Ax25.cpp
    src/core/tnc/KissFraming.cpp
    # LogManager.cpp provides lcAx25 (the shim's qCDebug category, #2763);
    # LogManager.cpp depends on AsyncLogWriter via the m_writer member, so
    # AppSettings + AsyncLogWriter come along to satisfy the constructor /
    # destructor chain at link time.
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(ax25_libmodem_shim_test PRIVATE src)
target_link_libraries(ax25_libmodem_shim_test PRIVATE Qt6::Core aether_libmodem_core
    aether_afskdemod)
add_test(NAME ax25_libmodem_shim_test COMMAND ax25_libmodem_shim_test)

add_executable(hdlc_codec_test
    tests/hdlc_codec_test.cpp
    src/core/tnc/HdlcCodec.cpp
)
target_include_directories(hdlc_codec_test PRIVATE src)
target_link_libraries(hdlc_codec_test PRIVATE aether_libmodem_core)
add_test(NAME hdlc_codec_test COMMAND hdlc_codec_test)

# Offline AX.25 decode diagnostic: replays a captured WAV through the decoder.
# Not a ctest (needs an input file); built on demand for troubleshooting.
add_executable(ax25_replay EXCLUDE_FROM_ALL
    tools/ax25_replay.cpp
    src/core/tnc/AetherAx25LibmodemShim.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
    src/core/tnc/KissFraming.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(ax25_replay PRIVATE src)
target_link_libraries(ax25_replay PRIVATE Qt6::Core aether_libmodem_core aether_afskdemod)

add_executable(ax25_session_analyze EXCLUDE_FROM_ALL
    tools/ax25_session_analyze.cpp
    src/core/tnc/AetherAx25LibmodemShim.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
    src/core/tnc/Ax25.cpp
    src/core/tnc/Ax25Connection.cpp
    src/core/tnc/KissFraming.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(ax25_session_analyze PRIVATE src)
target_link_libraries(ax25_session_analyze PRIVATE Qt6::Core aether_libmodem_core aether_afskdemod)

add_executable(pms_mailbox_test
    tests/pms_mailbox_test.cpp
    src/core/tnc/Ax25.cpp
    src/core/tnc/Ax25Connection.cpp
    src/core/pms/PmsMailbox.cpp
    src/core/AppSettings.cpp
)
target_include_directories(pms_mailbox_test PRIVATE src)
target_link_libraries(pms_mailbox_test PRIVATE Qt6::Core)
add_test(NAME pms_mailbox_test COMMAND pms_mailbox_test)

# APRS info-field codec (pure parsing/encoding, no DSP).
add_executable(aprs_packet_test
    tests/aprs_packet_test.cpp
    src/core/aprs/AprsPacket.cpp
    src/core/tnc/Ax25.cpp
)
target_include_directories(aprs_packet_test PRIVATE src)
target_link_libraries(aprs_packet_test PRIVATE Qt6::Core)
add_test(NAME aprs_packet_test COMMAND aprs_packet_test)

# APRS messaging engine + station roster. LogManager.cpp provides lcAx25
# (the qCWarning category used by the persistence paths); it drags in
# AsyncLogWriter + AppSettings, same as ax25_libmodem_shim_test.
add_executable(aprs_messenger_test
    tests/aprs_messenger_test.cpp
    src/core/aprs/AprsPacket.cpp
    src/core/aprs/AprsMessenger.cpp
    src/core/aprs/AprsStationList.cpp
    src/core/tnc/Ax25.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(aprs_messenger_test PRIVATE src)
target_link_libraries(aprs_messenger_test PRIVATE Qt6::Core)
add_test(NAME aprs_messenger_test COMMAND aprs_messenger_test)

add_executable(tnc_terminal_test
    tests/tnc_terminal_test.cpp
    src/core/tnc/Ax25.cpp
    src/core/tnc/Ax25Connection.cpp
    src/core/tnc/HeardList.cpp
    src/core/tnc/TncTerminal.cpp
    # HeardList now emits qCWarning(lcAx25) on persistence failure; pull in
    # LogManager + its deps so the category symbol resolves.
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(tnc_terminal_test PRIVATE src)
target_link_libraries(tnc_terminal_test PRIVATE Qt6::Core)
add_test(NAME tnc_terminal_test COMMAND tnc_terminal_test)

add_executable(cwx_speed_modifier_test
    tests/cwx_speed_modifier_test.cpp
    src/models/CwxModel.cpp
    src/models/CwxModel.h
    # CwxModel now logs via lcCw (qCWarning) — pull in the logging category.
    # LogManager depends on AsyncLogWriter (member) + AppSettings (retention
    # config), so both come along to satisfy the link. (#3949)
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(cwx_speed_modifier_test PRIVATE src)
target_link_libraries(cwx_speed_modifier_test PRIVATE Qt6::Core)
add_test(NAME cwx_speed_modifier_test COMMAND cwx_speed_modifier_test)

# Queue-drain watch state machine: epoch guard, live-char arming, and the
# CwxModel-side invariant the RadioModel flicker-immune release latch relies on. (#3949)
add_executable(cwx_drain_watch_test
    tests/cwx_drain_watch_test.cpp
    src/models/CwxModel.cpp
    src/models/CwxModel.h
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(cwx_drain_watch_test PRIVATE src)
target_link_libraries(cwx_drain_watch_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME cwx_drain_watch_test COMMAND cwx_drain_watch_test)

add_executable(cwx_panel_test
    tests/cwx_panel_test.cpp
    src/gui/CwxPanel.cpp
    src/gui/CwxPanel.h
    src/models/CwxModel.cpp
    src/models/CwxModel.h
    # CwxPanel.cpp calls ThemeManager::resolve() post-Phase-2 migration;
    # pull in the manager + its logging deps so the test links.
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(cwx_panel_test PRIVATE src)
target_link_libraries(cwx_panel_test PRIVATE
    Qt6::Core Qt6::Widgets
)

add_executable(meter_model_test
    tests/meter_model_test.cpp
    src/models/MeterModel.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(meter_model_test PRIVATE src)
target_link_libraries(meter_model_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(meter_model_test PRIVATE pthread)
endif()
set_target_properties(meter_model_test PROPERTIES AUTOMOC ON)

# aetherd RFC step 2.2b regression guard: FlexBackend ctor/dtor thread ownership
# + #502 teardown ordering. FlexBackend pulls RadioConnection/PanadapterStream
# and their deep deps, so link the engine library rather than list sources.
add_executable(flex_backend_lifecycle_test tests/flex_backend_lifecycle_test.cpp)
target_include_directories(flex_backend_lifecycle_test PRIVATE src)
target_link_libraries(flex_backend_lifecycle_test PRIVATE aethercore Qt6::Core)
set_target_properties(flex_backend_lifecycle_test PROPERTIES AUTOMOC ON)
add_test(NAME flex_backend_lifecycle_test COMMAND flex_backend_lifecycle_test)

# aetherd RFC 2.3 template — pan decode/normalize chain (incl. wnb_level guard).
add_executable(aetherd_pan_decode_test tests/aetherd_pan_decode_test.cpp)
target_include_directories(aetherd_pan_decode_test PRIVATE src)
target_link_libraries(aetherd_pan_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_pan_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_pan_decode_test COMMAND aetherd_pan_decode_test)

if(Qt6WebSockets_FOUND)
    add_executable(tci_protocol_test tests/tci_protocol_test.cpp)
    target_include_directories(tci_protocol_test PRIVATE src)
    target_link_libraries(tci_protocol_test PRIVATE aethercore Qt6::Core)
    add_test(NAME tci_protocol_test COMMAND tci_protocol_test)
endif()

# aetherd RFC 2.3 — MeterModel touchpoint: meter-status wire decode.
add_executable(aetherd_meter_decode_test tests/aetherd_meter_decode_test.cpp)
target_include_directories(aetherd_meter_decode_test PRIVATE src)
target_link_libraries(aetherd_meter_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_meter_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_meter_decode_test COMMAND aetherd_meter_decode_test)

# aetherd RFC 2.3 — SliceModel touchpoint: slice-status wire → canonical decode.
add_executable(aetherd_slice_decode_test tests/aetherd_slice_decode_test.cpp)
target_include_directories(aetherd_slice_decode_test PRIVATE src)
target_link_libraries(aetherd_slice_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_slice_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_slice_decode_test COMMAND aetherd_slice_decode_test)

# aetherd RFC 2.3 — TransmitModel touchpoint: transmit-family wire → typed delta.
add_executable(aetherd_transmit_decode_test tests/aetherd_transmit_decode_test.cpp)
target_include_directories(aetherd_transmit_decode_test PRIVATE src)
target_link_libraries(aetherd_transmit_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_transmit_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_transmit_decode_test COMMAND aetherd_transmit_decode_test)

# aetherd RFC 2.3 (RadioModel residual): radio-global wire → typed delta.
add_executable(aetherd_radio_decode_test tests/aetherd_radio_decode_test.cpp)
target_include_directories(aetherd_radio_decode_test PRIVATE src)
target_link_libraries(aetherd_radio_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_radio_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_radio_decode_test COMMAND aetherd_radio_decode_test)

add_executable(aetherd_residual_decode_test tests/aetherd_residual_decode_test.cpp)
target_include_directories(aetherd_residual_decode_test PRIVATE src)
target_link_libraries(aetherd_residual_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_residual_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_residual_decode_test COMMAND aetherd_residual_decode_test)

add_executable(amp_model_test tests/amp_model_test.cpp)
target_include_directories(amp_model_test PRIVATE src)
target_link_libraries(amp_model_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(amp_model_test PROPERTIES AUTOMOC ON)
add_test(NAME amp_model_test COMMAND amp_model_test)

add_executable(aetherd_amp_decode_test tests/aetherd_amp_decode_test.cpp)
target_include_directories(aetherd_amp_decode_test PRIVATE src)
target_link_libraries(aetherd_amp_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_amp_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_amp_decode_test COMMAND aetherd_amp_decode_test)

add_executable(tuner_model_test tests/tuner_model_test.cpp)
target_include_directories(tuner_model_test PRIVATE src)
target_link_libraries(tuner_model_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(tuner_model_test PROPERTIES AUTOMOC ON)
add_test(NAME tuner_model_test COMMAND tuner_model_test)

add_executable(aetherd_tuner_decode_test tests/aetherd_tuner_decode_test.cpp)
target_include_directories(aetherd_tuner_decode_test PRIVATE src)
target_link_libraries(aetherd_tuner_decode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_tuner_decode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_tuner_decode_test COMMAND aetherd_tuner_decode_test)

add_executable(aetherd_amp_tuner_encode_test tests/aetherd_amp_tuner_encode_test.cpp)
target_include_directories(aetherd_amp_tuner_encode_test PRIVATE src)
target_link_libraries(aetherd_amp_tuner_encode_test PRIVATE aethercore Qt6::Core Qt6::Test)
set_target_properties(aetherd_amp_tuner_encode_test PROPERTIES AUTOMOC ON)
add_test(NAME aetherd_amp_tuner_encode_test COMMAND aetherd_amp_tuner_encode_test)

add_executable(usb_cable_model_test
    tests/usb_cable_model_test.cpp
    src/models/UsbCableModel.cpp
)
target_include_directories(usb_cable_model_test PRIVATE src)
target_link_libraries(usb_cable_model_test PRIVATE Qt6::Core Qt6::Test)
set_target_properties(usb_cable_model_test PROPERTIES AUTOMOC ON)
add_test(NAME usb_cable_model_test COMMAND usb_cable_model_test)

add_executable(async_log_writer_test
    tests/async_log_writer_test.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(async_log_writer_test PRIVATE src)
target_link_libraries(async_log_writer_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(async_log_writer_test PRIVATE pthread)
endif()
set_target_properties(async_log_writer_test PROPERTIES AUTOMOC ON)

add_executable(perf_telemetry_test
    tests/perf_telemetry_test.cpp
    src/core/PerfTelemetry.cpp
    # LogManager.cpp owns the lcPerf Q_LOGGING_CATEGORY definition (per
    # the Q_LOGGING_CATEGORY consolidation in #2770); LogManager has AsyncLogWriter
    # as a member which transitively needs AppSettings, so all three .cpp
    # files come along to satisfy the ctor/dtor chain at link time.
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(perf_telemetry_test PRIVATE src)
target_link_libraries(perf_telemetry_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(perf_telemetry_test PRIVATE pthread)
endif()
set_target_properties(perf_telemetry_test PROPERTIES AUTOMOC ON)
add_test(NAME perf_telemetry_test COMMAND perf_telemetry_test)

add_executable(memory_recall_policy_test
    tests/memory_recall_policy_test.cpp
    src/core/MemoryRecallPolicy.cpp
)
target_include_directories(memory_recall_policy_test PRIVATE src)
target_link_libraries(memory_recall_policy_test PRIVATE Qt6::Core)
add_test(NAME memory_recall_policy_test COMMAND memory_recall_policy_test)

add_executable(net_recurrence_test
    tests/net_recurrence_test.cpp
    src/core/NetRecurrence.cpp
)
target_include_directories(net_recurrence_test PRIVATE src)
target_link_libraries(net_recurrence_test PRIVATE Qt6::Core)
add_test(NAME net_recurrence_test COMMAND net_recurrence_test)

add_executable(net_schedule_store_test
    tests/net_schedule_store_test.cpp
    src/core/NetScheduleStore.cpp
)
target_include_directories(net_schedule_store_test PRIVATE src)
target_link_libraries(net_schedule_store_test PRIVATE Qt6::Core)
add_test(NAME net_schedule_store_test COMMAND net_schedule_store_test)

add_executable(net_schedule_planner_test
    tests/net_schedule_planner_test.cpp
    src/core/NetSchedulePlanner.cpp
    src/core/NetRecurrence.cpp
)
target_include_directories(net_schedule_planner_test PRIVATE src)
target_link_libraries(net_schedule_planner_test PRIVATE Qt6::Core)
add_test(NAME net_schedule_planner_test COMMAND net_schedule_planner_test)

add_executable(memory_field_values_test
    tests/memory_field_values_test.cpp
    src/core/MemoryFieldValues.cpp
)
target_include_directories(memory_field_values_test PRIVATE src)
target_link_libraries(memory_field_values_test PRIVATE Qt6::Core)
add_test(NAME memory_field_values_test COMMAND memory_field_values_test)

add_executable(memory_csv_compat_test
    tests/memory_csv_compat_test.cpp
    src/core/MemoryCsvCompat.cpp
    src/core/MemoryFieldValues.cpp
)
target_include_directories(memory_csv_compat_test PRIVATE src)
target_compile_definitions(memory_csv_compat_test PRIVATE
    CHIRP_SAMPLE_CSV="${CMAKE_CURRENT_SOURCE_DIR}/docs/automation/sample-chirp-memories.csv")
target_link_libraries(memory_csv_compat_test PRIVATE Qt6::Core)
add_test(NAME memory_csv_compat_test COMMAND memory_csv_compat_test)

add_executable(transmit_model_apd_test
    tests/transmit_model_apd_test.cpp
    src/models/TransmitModel.cpp
    src/core/ClientQuindarTone.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(transmit_model_apd_test PRIVATE src)
target_link_libraries(transmit_model_apd_test PRIVATE Qt6::Core Qt6::Test)
if(UNIX)
    target_link_libraries(transmit_model_apd_test PRIVATE pthread)
endif()
set_target_properties(transmit_model_apd_test PROPERTIES AUTOMOC ON)
add_test(NAME transmit_model_apd_test COMMAND transmit_model_apd_test)

# Help guide search tests - needs QApplication + Widgets.
add_executable(help_dialog_test
    tests/help_dialog_test.cpp
    src/gui/HelpDialog.cpp
    src/gui/PersistentDialog.cpp
    src/gui/FramelessResizer.cpp
    src/gui/FramelessWindowTitleBar.cpp
    # HelpDialog.cpp calls ThemeManager::resolve() post-Phase-2 migration;
    # pull in the manager + its logging deps so the test links.
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(help_dialog_test PRIVATE src)
target_link_libraries(help_dialog_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(help_dialog_test PROPERTIES AUTOMOC ON)

# Regression guard for #3662: the AetherControl window must never demand a
# minimum height taller than the screen (auto-engages compact when it would).
add_executable(flex_control_dialog_size_test
    tests/flex_control_dialog_size_test.cpp
    src/gui/FlexControlDialog.cpp
    src/gui/PersistentDialog.cpp
    src/gui/FramelessResizer.cpp
    src/gui/FramelessWindowTitleBar.cpp
    src/gui/SliceLabel.cpp
    src/gui/DragValuePopup.cpp
    src/models/SliceModel.cpp
    src/core/DigitalVoiceModeRegistry.cpp
    src/core/KiwiSdrProtocol.cpp
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(flex_control_dialog_size_test PRIVATE src)
target_link_libraries(flex_control_dialog_size_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(flex_control_dialog_size_test PROPERTIES AUTOMOC ON)
add_test(NAME flex_control_dialog_size_test COMMAND flex_control_dialog_size_test)

add_executable(pan_layout_dialog_size_test
    tests/pan_layout_dialog_size_test.cpp
    src/gui/PanLayoutDialog.cpp
    src/gui/PersistentDialog.cpp
    src/gui/FramelessResizer.cpp
    src/gui/FramelessWindowTitleBar.cpp
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(pan_layout_dialog_size_test PRIVATE src)
target_link_libraries(pan_layout_dialog_size_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(pan_layout_dialog_size_test PROPERTIES AUTOMOC ON)
add_test(NAME pan_layout_dialog_size_test COMMAND pan_layout_dialog_size_test)

add_executable(device_diagnostics_test
    tests/device_diagnostics_test.cpp
)
target_include_directories(device_diagnostics_test PRIVATE src)
target_link_libraries(device_diagnostics_test PRIVATE Qt6::Core)

add_executable(midi_settings_test
    tests/midi_settings_test.cpp
    src/core/MidiSettings.cpp
)
target_compile_definitions(midi_settings_test PRIVATE HAVE_MIDI)

if (USE_SYSTEM_RTMIDI)
    target_link_libraries(midi_settings_test PRIVATE PkgConfig::rtmidi)
else()
    target_include_directories(midi_settings_test PRIVATE third_party/rtmidi)
endif()
target_include_directories(midi_settings_test PRIVATE src)
target_link_libraries(midi_settings_test PRIVATE Qt6::Core)
add_test(NAME midi_settings_test COMMAND midi_settings_test)

add_executable(transmit_model_test
    tests/transmit_model_test.cpp
    src/models/TransmitModel.cpp
    src/core/ClientQuindarTone.cpp
    src/core/AppSettings.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
)
target_include_directories(transmit_model_test PRIVATE src)
target_link_libraries(transmit_model_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(transmit_model_test PRIVATE pthread)
endif()
add_test(NAME transmit_model_test COMMAND transmit_model_test)

add_executable(transmit_inhibit_policy_test
    tests/transmit_inhibit_policy_test.cpp
    src/core/CommandParser.cpp
)
target_include_directories(transmit_inhibit_policy_test PRIVATE src)
target_link_libraries(transmit_inhibit_policy_test PRIVATE Qt6::Core)
add_test(NAME transmit_inhibit_policy_test COMMAND transmit_inhibit_policy_test)

# Container system Phase 1 tests — needs QApplication + Widgets.
add_executable(container_widget_test
    tests/container_widget_test.cpp
    src/gui/FramelessResizer.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/core/AppSettings.cpp
    # FloatingContainerWindow now calls applyAppTheme() which routes
    # through ThemeManager::applyStyleSheet — pull in the manager + its
    # logging deps so the test links.  ThemeManager's compiled-in
    # seedBuiltinDefaults() means we don't need the theme resource here.
    src/core/ThemeManager.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(container_widget_test PRIVATE src)
target_link_libraries(container_widget_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(container_widget_test PROPERTIES AUTOMOC ON)

add_executable(amp_applet_test
    tests/amp_applet_test.cpp
    src/gui/AmpApplet.cpp
    src/gui/DragValuePopup.cpp
    src/core/AppSettings.cpp
    src/core/ThemeManager.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(amp_applet_test PRIVATE src)
target_link_libraries(amp_applet_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(amp_applet_test PROPERTIES AUTOMOC ON)
add_test(NAME amp_applet_test COMMAND amp_applet_test)

add_executable(container_manager_test
    tests/container_manager_test.cpp
    src/gui/FramelessResizer.cpp
    src/gui/containers/ContainerManager.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/core/AppSettings.cpp
    src/core/ThemeManager.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(container_manager_test PRIVATE src)
target_link_libraries(container_manager_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(container_manager_test PROPERTIES AUTOMOC ON)

add_executable(container_nesting_test
    tests/container_nesting_test.cpp
    src/gui/FramelessResizer.cpp
    src/gui/containers/ContainerManager.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/core/AppSettings.cpp
    src/core/ThemeManager.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(container_nesting_test PRIVATE src)
target_link_libraries(container_nesting_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(container_nesting_test PROPERTIES AUTOMOC ON)

# Integration test — requires a running AetherSDR instance.
# Not added to ctest; run manually:
#   ./build/rigctld_test [--host HOST] [--port PORT] [--ptt] [--cw]
add_executable(rigctld_test
    tests/rigctld_test.cpp
)
target_include_directories(rigctld_test PRIVATE src)
target_link_libraries(rigctld_test PRIVATE Qt6::Core Qt6::Network)

# Integration tests — require a running AetherSDR instance with CAT ports enabled.
# Not added to ctest; run manually:
#   ./build/CAT_TS-2000_test  [--host HOST] [--port PORT] [--ptt] [--cw] [--pty PATH]
#   ./build/CAT_Flex_test     [--host HOST] [--port PORT] [--ptt] [--cw] [--pty PATH]
add_executable(CAT_TS-2000_test
    tests/CAT_TS-2000_test.cpp
)
target_include_directories(CAT_TS-2000_test PRIVATE src)
target_link_libraries(CAT_TS-2000_test PRIVATE Qt6::Core Qt6::Network)

add_executable(CAT_Flex_test
    tests/CAT_Flex_test.cpp
)
target_include_directories(CAT_Flex_test PRIVATE src)
target_link_libraries(CAT_Flex_test PRIVATE Qt6::Core Qt6::Network)


# ── Install rules ────────────────────────────────────────────────────────────
include(GNUInstallDirs)

if(APPLE)
    install(TARGETS AetherSDR
        BUNDLE DESTINATION .
    )
    # BUNDLE installation copies the existing app directory. Remove the
    # superseded helper if an incremental build tree still contains it.
    install(CODE [[
        file(REMOVE
            "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/AetherSDR.app/Contents/MacOS/aether-dstar-waveform"
            "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/AetherSDR.app/Contents/MacOS/aether-dstar-waveform.exe"
            "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/AetherSDR.app/Contents/MacOS/ThumbDV.cfg")
    ]])
    if(TARGET aether-dv-waveform)
        install(PROGRAMS "$<TARGET_FILE:aether-dv-waveform>"
            DESTINATION AetherSDR.app/Contents/MacOS
        )
        install(FILES third_party/smartsdr-dsp/config/AetherDV.cfg
            DESTINATION AetherSDR.app/Contents/MacOS
        )
    endif()
else()
    install(TARGETS AetherSDR
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    )
    if(TARGET aether-dv-waveform)
        install(TARGETS aether-dv-waveform
            RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
        )
        install(FILES third_party/smartsdr-dsp/config/AetherDV.cfg
            DESTINATION ${CMAKE_INSTALL_BINDIR}
        )
    endif()
    install(FILES ${CMAKE_CURRENT_BINARY_DIR}/packaging/linux/AetherSDR.desktop
        DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
    )
    install(FILES docs/assets/logo-circle-256.png
        DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps
        RENAME aethersdr.png
    )
    install(FILES packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml
        DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo
    )
    # Install the man page uncompressed; distro packaging compresses man pages
    # per its own policy (Debian/Arch re-gzip, so shipping a .gz would clash).
    install(FILES packaging/linux/aethersdr.1
        DESTINATION ${CMAKE_INSTALL_MANDIR}/man1
    )
    if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
        install(FILES packaging/linux/70-aethersdr-thumbdv.rules
            DESTINATION lib/udev/rules.d
        )
    endif()
endif()
