//
// Copyright 2020 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: GPL-3.0-or-later
//

#include "../../lib/usrp/x400/x400_radio_control.hpp"
#include "../rfnoc_graph_mock_nodes.hpp"
#include "x4xx_zbx_mpm_mock.hpp"
#include <uhd/rfnoc/actions.hpp>
#include <uhd/rfnoc/defaults.hpp>
#include <uhd/rfnoc/mock_block.hpp>
#include <uhd/utils/log.hpp>
#include <uhd/utils/math.hpp>
#include <uhdlib/rfnoc/graph.hpp>
#include <uhdlib/rfnoc/node_accessor.hpp>
#include <uhdlib/usrp/dboard/zbx/zbx_constants.hpp>
#include <uhdlib/usrp/dboard/zbx/zbx_dboard.hpp>
#include <uhdlib/utils/narrow.hpp>
#include <math.h>
#include <boost/test/unit_test.hpp>
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <thread>

using namespace uhd;
using namespace uhd::rfnoc;
using namespace std::chrono_literals;
using namespace uhd::usrp::zbx;
using namespace uhd::experts;

// Redeclare this here, since it's only defined outside of UHD_API
noc_block_base::make_args_t::~make_args_t() = default;

namespace {

/* This class extends mock_reg_iface_t by adding a constructor that initializes
 * some of the read memory to contain the memory size for the radio block.
 */
class x4xx_radio_mock_reg_iface_t : public mock_reg_iface_t
{
    // Start address of CPLD register space
    static constexpr uint32_t cpld_offset = radio_control_impl::regmap::PERIPH_BASE;
    // Start address of RFDC control register space
    static constexpr uint32_t rfdc_offset =
        radio_control_impl::regmap::PERIPH_BASE + 0x8000;

public:
    x4xx_radio_mock_reg_iface_t(size_t num_channels)
    {
        for (size_t chan = 0; chan < num_channels; chan++) {
            const uint32_t reg_compat =
                radio_control_impl::regmap::REG_COMPAT_NUM
                + chan * radio_control_impl::regmap::REG_CHAN_OFFSET;
            read_memory[reg_compat] = (radio_control_impl::MINOR_COMPAT
                                       | (radio_control_impl::MAJOR_COMPAT << 16));
        }
        read_memory[radio_control_impl::regmap::REG_RADIO_WIDTH] =
            (32 /* bits per sample */ << 16) | 1 /* sample per clock */;
    }

    void _poke_cb(uint32_t addr, uint32_t data, uhd::time_spec_t, bool) override
    {
        // Are we on the peripheral?
        if (addr >= radio_control_impl::regmap::PERIPH_BASE) {
            // handle all the periphs stuff that is not CPLD here
        } else {
            return;
        }

        // Are we on the CPLD?
        if (addr >= cpld_offset && addr < rfdc_offset) {
            _poke_cpld_cb(addr, data);
            return;
        }

        // Are we poking the RFDC controls?
        if (addr >= rfdc_offset) {
            _poke_rfdc_cb(addr, data);
            return;
        }
    }

    void _poke_cpld_cb(const uint32_t addr, const uint32_t data)
    {
        switch (addr - cpld_offset) {
            /// CURRENT_CONFIG_REG
            case 0x1000:
                // FIXME: We write to all regs during init
                // BOOST_REQUIRE(false); // Not a write-register
                break;
            /// SW_CONFIG
            case 0x1008: {
                // This register is RW so update read_memory
                read_memory[addr] = data;
                // If we're in SW-defined mode, also update CURRENT_CONFIG_REG
                uint32_t& rf_opt = read_memory[cpld_offset + 0x1004];
                uint32_t& ccr    = read_memory[cpld_offset + 0x1000];
                // Check if RF0_OPTION is SW_DEFINED
                if ((rf_opt & 0x00FF) == 0) {
                    ccr = (ccr & 0xFF00) | (data & 0x00FF);
                }
                // Check if RF1_OPTION is SW_DEFINED
                if ((rf_opt & 0xFF00) == 0) {
                    ccr = (ccr & 0x00FF) | (data & 0xFF00);
                }
            } break;
            /// LO SPI transactions
            case 0x1020:
                _poke_lo_spi(addr, data);
                return;
            /// LO SYNC
            case 0x1024:
                // We make these bits sticky, because they might get strobed in
                // multiple calls. In order to see what was strobed within an
                // API call, we keep bits as they are.
                read_memory[addr] |= data;
                return;
            // TX0 Table Select
            case 0x4000:
            case 0x4004:
            case 0x4008:
            case 0x400C:
            case 0x4010:
            case 0x4014: {
                read_memory[addr]               = data;
                const uint32_t src_table_offset = data * 4;
                const uint32_t dst_table_offset = (addr - cpld_offset) - 0x4000;
                // Now we fake the transaction that copies ?X?_TABLE_* to
                // ?X?_DSA*
                read_memory[cpld_offset + 0x3000 + dst_table_offset] =
                    read_memory[cpld_offset + 0x5000 + src_table_offset];
            }
                return;
            // RX0 Table Select
            case 0x4800:
            case 0x4804:
            case 0x4808:
            case 0x480C:
            case 0x4810:
            case 0x4814: {
                read_memory[addr]               = data;
                const uint32_t src_table_offset = data * 4;
                const uint32_t dst_table_offset = (addr - cpld_offset) - 0x4800;
                // Now we fake the transaction that copies ?X?_TABLE_* to
                // ?X?_DSA*
                read_memory[cpld_offset + 0x3800 + dst_table_offset] =
                    read_memory[cpld_offset + 0x5800 + src_table_offset];
            }
                return;
            default: // All other CPLD registers are read-write
                read_memory[addr] = data;
                return;
        }
    }

    void _poke_rfdc_cb(const uint32_t addr, const uint32_t data)
    {
        read_memory[addr] |= data;
    }

    void _poke_lo_spi(const uint32_t addr, const uint32_t data)
    {
        // UHD_LOG_INFO("TEST", "Detected LO SPI transaction!");
        const uint16_t spi_data = data & 0xFFFF;
        const uint8_t spi_addr  = (data >> 16) & 0x7F;
        const bool read         = bool(data & (1 << 23));
        const uint8_t lo_sel    = (data >> 24) & 0x7;
        const bool start_xact   = bool(data & (1 << 28));
        // UHD_LOG_INFO("TEST",
        //     "Transaction record: Read: "
        //         << (read ? "yes" : "no") << " Address: " << int(spi_addr) << std::hex
        //         << " Data: 0x" << spi_data << " LO sel: " << int(lo_sel) << std::dec
        //         << " Start Transaction: " << start_xact);
        if (!start_xact) {
            // UHD_LOG_INFO("TEST", "Register probably just initialized. Ignoring.");
            return;
        }
        switch (spi_addr) {
            case 0:
                _muxout_to_lock = spi_data & (1 << 2);
                break;
            case 125:
                BOOST_REQUIRE(read);
                read_memory[addr] = 0x2288;
                break;
            default:
                break;
        }
        if (read) {
            read_memory[addr] = (read_memory[addr] & 0xFFFF) | (spi_addr << 16)
                                | (lo_sel << 24) | (1 << 31);
        }
        if (_muxout_to_lock) {
            // UHD_LOG_INFO("TEST", "Muxout set to lock. Returning all ones.");
            read_memory[addr] = 0xFFFF;
            return;
        }
        return;
    }

    bool _muxout_to_lock = false;
}; // class x4xx_radio_mock_reg_iface_t

/*
 * x400_radio_fixture is a class which is instantiated before each test
 * case is run. It sets up the block container, mock register interface,
 * and x400_radio_control object, all of which are accessible to the test
 * case. The instance of the object is destroyed at the end of each test
 * case.
 */
constexpr size_t DEFAULT_MTU = 8000;

//! Helper class to make sure we get the most logging regardless of environment
// settings
struct uhd_log_enabler
{
    uhd_log_enabler(uhd::log::severity_level level)
    {
        std::cout << "Setting log level to " << level << "..." << std::endl;
        uhd::log::set_log_level(level);
        uhd::log::set_console_level(level);
        std::this_thread::sleep_for(10ms);
    }
};

struct x400_radio_fixture
{
    x400_radio_fixture()
        : ule(uhd::log::warning) // Note: When debugging this test, either set
                                 // this to a lower level, or create a
                                 // uhd_log_enabler in the test-under-test
        , num_channels(uhd::usrp::zbx::ZBX_NUM_CHANS)
        , num_input_ports(num_channels)
        , num_output_ports(num_channels)
        , reg_iface(std::make_shared<x4xx_radio_mock_reg_iface_t>(num_channels))
        , rpcs(std::make_shared<uhd::test::x4xx_mock_rpc_server>(device_info))
        , mbc(std::make_shared<mpmd_mb_controller>(rpcs, device_info))
        , block_container(get_mock_block(RADIO_BLOCK,
              num_channels,
              num_channels,
              device_info,
              DEFAULT_MTU,
              X400,
              reg_iface,
              mbc))
        , test_radio(block_container.get_block<x400_radio_control_impl>())
    {
        node_accessor.init_props(test_radio.get());
    }

    ~x400_radio_fixture() {}


    // Must remain the first member so we make sure the log level is high
    uhd_log_enabler ule;
    const size_t num_channels;
    const size_t num_input_ports;
    const size_t num_output_ports;
    uhd::device_addr_t device_info = uhd::device_addr_t("master_clock_rate=122.88e6");
    std::shared_ptr<x4xx_radio_mock_reg_iface_t> reg_iface;
    std::shared_ptr<uhd::test::x4xx_mock_rpc_server> rpcs;
    mpmd_mb_controller::sptr mbc;

    mock_block_container block_container;
    std::shared_ptr<x400_radio_control_impl> test_radio;
    node_accessor_t node_accessor{};
};

} // namespace


/******************************************************************************
 * RFNoC Graph Test
 *
 * This test case ensures that the Radio Block can be added to an RFNoC graph.
 *****************************************************************************/
BOOST_FIXTURE_TEST_CASE(x400_radio_test_graph, x400_radio_fixture)
{
    detail::graph_t graph{};
    detail::graph_t::graph_edge_t edge_port_info0;
    edge_port_info0.src_port                    = 0;
    edge_port_info0.dst_port                    = 0;
    edge_port_info0.property_propagation_active = true;
    edge_port_info0.edge                        = detail::graph_t::graph_edge_t::DYNAMIC;
    detail::graph_t::graph_edge_t edge_port_info1;
    edge_port_info1.src_port                    = 1;
    edge_port_info1.dst_port                    = 1;
    edge_port_info1.property_propagation_active = true;
    edge_port_info1.edge                        = detail::graph_t::graph_edge_t::DYNAMIC;

    mock_radio_node_t mock_radio_block{0};
    mock_terminator_t mock_sink_term(2, {}, "MOCK_SINK");
    mock_terminator_t mock_source_term(2, {}, "MOCK_SOURCE");

    UHD_LOG_INFO("TEST", "Priming mock block properties");
    node_accessor.init_props(&mock_radio_block);
    mock_source_term.set_edge_property<std::string>(
        "type", "sc16", {res_source_info::OUTPUT_EDGE, 0});
    mock_source_term.set_edge_property<std::string>(
        "type", "sc16", {res_source_info::OUTPUT_EDGE, 1});
    mock_sink_term.set_edge_property<std::string>(
        "type", "sc16", {res_source_info::INPUT_EDGE, 0});
    mock_sink_term.set_edge_property<std::string>(
        "type", "sc16", {res_source_info::INPUT_EDGE, 1});

    UHD_LOG_INFO("TEST", "Creating graph...");
    graph.connect(&mock_source_term, test_radio.get(), edge_port_info0);
    graph.connect(&mock_source_term, test_radio.get(), edge_port_info1);
    graph.connect(test_radio.get(), &mock_sink_term, edge_port_info0);
    graph.connect(test_radio.get(), &mock_sink_term, edge_port_info1);
    UHD_LOG_INFO("TEST", "Committing graph...");
    graph.commit();
    UHD_LOG_INFO("TEST", "Commit complete.");
}

BOOST_FIXTURE_TEST_CASE(zbx_api_freq_tx_test, x400_radio_fixture)
{
    const std::string log = "ZBX_API_TX_FREQUENCY_TEST";
    const double ep       = 10;
    // TODO: consult step size
    uhd::freq_range_t zbx_freq(ZBX_MIN_FREQ, ZBX_MAX_FREQ, 100e6);
    for (size_t chan : {0, 1}) {
        UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " FREQ CHANGE (SET->RETURN)\n");
        for (double iter = zbx_freq.start(); iter <= zbx_freq.stop();
             iter += zbx_freq.step()) {
            UHD_LOG_INFO(log, "Testing freq: " << iter);

            const double freq = test_radio->set_tx_frequency(iter, chan);
            BOOST_REQUIRE(abs(iter - freq) < ep);
        }

        UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " FREQ CHANGE (SET->GET)\n");
        for (double iter = zbx_freq.start(); iter <= zbx_freq.stop();
             iter += zbx_freq.step()) {
            UHD_LOG_INFO(log, "Testing freq: " << iter);

            test_radio->set_tx_frequency(iter, chan);
            const double freq = test_radio->get_tx_frequency(chan);
            BOOST_REQUIRE(abs(iter - freq) < ep);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_api_freq_rx_test, x400_radio_fixture)
{
    const std::string log = "ZBX_API_RX_FREQUENCY_TEST";
    const double ep       = 10;
    // TODO: consult step size
    uhd::freq_range_t zbx_freq(ZBX_MIN_FREQ, ZBX_MAX_FREQ, 100e6);

    for (size_t chan : {0, 1}) {
        UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " FREQ CHANGE (SET->RETURN)\n");
        for (double iter = zbx_freq.start(); iter <= zbx_freq.stop();
             iter += zbx_freq.step()) {
            UHD_LOG_INFO(log, "Testing freq: " << iter);

            const double freq = test_radio->set_rx_frequency(iter, chan);
            BOOST_REQUIRE(abs(iter - freq) < ep);
        }
        UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " FREQ CHANGE (SET->GET\n");
        for (double iter = zbx_freq.start(); iter <= zbx_freq.stop();
             iter += zbx_freq.step()) {
            UHD_LOG_INFO(log, "Testing freq: " << iter);

            test_radio->set_rx_frequency(iter, chan);
            const double freq = test_radio->get_rx_frequency(chan);
            BOOST_REQUIRE(abs(iter - freq) < ep);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_frequency_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX_FREQUENCY_TEST";
    const double ep       = 10;
    // TODO: consult step size
    uhd::freq_range_t zbx_freq(ZBX_MIN_FREQ, ZBX_MAX_FREQ, 100e6);

    for (auto fe_path : {
             fs_path("dboard/tx_frontends/0"),
             fs_path("dboard/tx_frontends/1"),
             fs_path("dboard/rx_frontends/0"),
             fs_path("dboard/rx_frontends/1"),
         }) {
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " FREQ CHANGE\n");
        for (double iter = zbx_freq.start(); iter <= zbx_freq.stop();
             iter += zbx_freq.step()) {
            UHD_LOG_INFO(log, "Testing freq: " << iter);

            tree->access<double>(fe_path / "freq").set(iter);

            const double ret_value = tree->access<double>(fe_path / "freq").get();

            BOOST_REQUIRE(abs(iter - ret_value) < ep);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_api_tx_gain_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX TX GAIN TEST";
    uhd::freq_range_t zbx_gain(TX_MIN_GAIN, TX_MAX_GAIN, 1);

    for (size_t chan : {0, 1}) {
        UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " GAIN CHANGE (SET->RETURN)\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            UHD_LOG_INFO(log, "Testing gain: " << iter);

            const double ret_gain = test_radio->set_tx_gain(iter, chan);

            BOOST_CHECK_EQUAL(iter, ret_gain);
        }
        UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " GAIN CHANGE (SET->GET)\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            UHD_LOG_INFO(log, "Testing gain: " << iter);

            test_radio->set_tx_gain(iter, chan);
            const double ret_gain = test_radio->get_tx_gain(chan);

            BOOST_CHECK_EQUAL(iter, ret_gain);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_api_tx_gain_stage_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX API TX GAIN STAGE TEST";

    for (size_t chan : {0, 1}) {
        test_radio->set_tx_gain_profile(ZBX_GAIN_PROFILE_MANUAL, chan);

        UHD_LOG_INFO(
            log, "BEGIN TEST: tx" << chan << " GAIN STAGE CHANGE (SET->RETURN)\n");
        for (auto gain_stage : ZBX_TX_GAIN_STAGES) {
            if (gain_stage == ZBX_GAIN_STAGE_AMP) {
                for (double amp : {ZBX_TX_LOWBAND_GAIN, ZBX_TX_HIGHBAND_GAIN}) {
                    UHD_LOG_INFO(log, "Testing dsa: " << amp);
                    const double ret_gain =
                        test_radio->set_tx_gain(amp, gain_stage, chan);
                    UHD_LOG_INFO(log, "return: " << ret_gain);
                    BOOST_CHECK_EQUAL(amp, ret_gain);
                }
            } else {
                for (unsigned int iter = 0; iter <= ZBX_TX_DSA_MAX_ATT; iter++) {
                    UHD_LOG_INFO(log, "Testing dsa: " << iter);
                    const double ret_gain =
                        test_radio->set_tx_gain(iter, gain_stage, chan);
                    BOOST_CHECK_EQUAL(iter, ret_gain);
                }
            }
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_api_tx_gain_stage_test_set_get, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX API TX GAIN STAGE TEST";

    for (size_t chan : {0, 1}) {
        test_radio->set_tx_gain_profile(ZBX_GAIN_PROFILE_MANUAL, chan);
        UHD_LOG_INFO(log, "BEGIN TEST: tx" << chan << " GAIN STAGE CHANGE (SET->GET)\n");
        for (auto gain_stage : ZBX_TX_GAIN_STAGES) {
            if (gain_stage == ZBX_GAIN_STAGE_AMP) {
                for (double amp :
                    {/*ZBX_TX_BYPASS_GAIN, currently disabled*/ ZBX_TX_LOWBAND_GAIN,
                        ZBX_TX_HIGHBAND_GAIN}) {
                    UHD_LOG_INFO(log, "Testing amp: " << amp);
                    test_radio->set_tx_gain(amp, gain_stage, chan);
                    const double ret_gain = test_radio->get_tx_gain(gain_stage, chan);
                    BOOST_CHECK_EQUAL(amp, ret_gain);
                }
            } else {
                for (unsigned int iter = 0; iter <= ZBX_TX_DSA_MAX_ATT; iter++) {
                    UHD_LOG_INFO(log, "Testing dsa: " << iter);
                    test_radio->set_tx_gain(iter, gain_stage, chan);
                    const double ret_gain = test_radio->get_tx_gain(gain_stage, chan);
                    BOOST_CHECK_EQUAL(iter, ret_gain);
                }
            }
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_api_rx_gain_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX RX API GAIN TEST";
    uhd::freq_range_t zbx_gain(TX_MIN_GAIN, TX_MAX_GAIN, 1);

    for (size_t chan : {0, 1}) {
        UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " GAIN CHANGE (SET->RETURN)\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            UHD_LOG_INFO(log, "Testing gain: " << iter);

            const double ret_gain = test_radio->set_rx_gain(iter, chan);

            BOOST_CHECK_EQUAL(iter, ret_gain);
        }
        UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " GAIN CHANGE (SET->GET)\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            UHD_LOG_INFO(log, "Testing gain: " << iter);

            test_radio->set_rx_gain(iter, chan);
            const double ret_gain = test_radio->get_rx_gain(chan);

            BOOST_CHECK_EQUAL(iter, ret_gain);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_api_rx_gain_stage_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX API RX GAIN STAGE TEST";

    for (size_t chan : {0, 1}) {
        test_radio->set_rx_gain_profile(ZBX_GAIN_PROFILE_MANUAL, chan);

        UHD_LOG_INFO(
            log, "BEGIN TEST: rx" << chan << " GAIN STAGE CHANGE (SET->RETURN)\n");
        for (auto gain_stage : ZBX_RX_GAIN_STAGES) {
            for (unsigned int iter = 0; iter <= ZBX_RX_DSA_MAX_ATT; iter++) {
                UHD_LOG_INFO(log, "Testing dsa: " << gain_stage << " " << iter);
                const double ret_gain = test_radio->set_rx_gain(iter, gain_stage, chan);

                BOOST_CHECK_EQUAL(iter, ret_gain);
            }
        }

        UHD_LOG_INFO(log, "BEGIN TEST: rx" << chan << " GAIN STAGE CHANGE (SET->GET)\n");
        for (auto gain_stage : ZBX_RX_GAIN_STAGES) {
            for (unsigned int iter = 0; iter <= ZBX_RX_DSA_MAX_ATT; iter++) {
                UHD_LOG_INFO(log, "Testing " << gain_stage << " " << iter);

                test_radio->set_rx_gain(iter, gain_stage, chan);
                const double ret_gain = test_radio->get_rx_gain(gain_stage, chan);

                BOOST_CHECK_EQUAL(iter, ret_gain);
            }
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_tx_gain_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX GAIN TEST";
    uhd::freq_range_t zbx_gain(TX_MIN_GAIN, TX_MAX_GAIN, 1);

    for (auto fe_path :
        {fs_path("dboard/tx_frontends/0"), fs_path("dboard/tx_frontends/1")}) {
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " GAIN CHANGE\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            UHD_LOG_INFO(log, "Testing gain: " << iter);
            const auto gain_path = fe_path / "gains" / ZBX_GAIN_STAGE_ALL / "value";
            tree->access<double>(gain_path).set(iter);
            const double ret_gain = tree->access<double>(gain_path).get();
            BOOST_CHECK_EQUAL(iter, ret_gain);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_rx_gain_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX GAIN TEST";
    uhd::freq_range_t zbx_gain(RX_MIN_GAIN, RX_MAX_GAIN, 1);

    for (auto fe_path :
        {fs_path("dboard/rx_frontends/0"), fs_path("dboard/rx_frontends/1")}) {
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " GAIN CHANGE\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            UHD_LOG_INFO(log, "Testing gain: " << iter);
            const auto gain_path = fe_path / "gains" / ZBX_GAIN_STAGE_ALL / "value";
            tree->access<double>(gain_path).set(iter);
            const double ret_gain = tree->access<double>(gain_path).get();
            BOOST_CHECK_EQUAL(iter, ret_gain);
        }
    }
}

// Have to be careful about LO testing; it'll throw off the coerced frequency a bunch,
// possibly to illegal values like negative frequencies, and could make the gain API
// freak out. We use the center frequency to set initial mixer values, then try to test
// all LO's in the valid zbx range.
// TODO: expand this
const std::map<double, std::vector<std::array<double, 2>>> valid_lo_freq_map = {
    {1e9, {{4.5e9, 4.5e9}, {5e9, 5e9}, {5.5e9, 5.5e9}, {6e9, 6e9}}},
    {2e9, {{4.5e9, 4.5e9}, {5e9, 5e9}, {5.5e9, 5.5e9}, {6e9, 6e9}}}};

// TODO: More frequencies_are_equal issues, too much variance
BOOST_FIXTURE_TEST_CASE(zbx_api_tx_lo_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX TX TEST";
    const double ep       = 10;

    for (size_t chan : {0, 1}) {
        UHD_LOG_INFO(log, "BEGIN TEST: TX" << chan << " FREQ CHANGE (SET->RETURN)\n");
        for (auto iter = valid_lo_freq_map.begin(); iter != valid_lo_freq_map.end();
             iter++) {
            for (auto iter_lo = iter->second.begin(); iter_lo != iter->second.end();
                 iter_lo++) {
                // Just so we're clear about our value mapping
                const double req_freq = iter->first;
                const double req_lo1  = iter_lo->at(0);
                const double req_lo2  = iter_lo->at(1);

                UHD_LOG_INFO(log,
                    "Testing center freq " << req_freq / 1e6 << "MHz, lo1 freq "
                                           << req_lo1 / 1e6 << "MHz, lo2 freq "
                                           << req_lo2 / 1e6 << "MHz");
                // Need to set center frequency first, it'll set all the mixer values
                test_radio->set_tx_frequency(iter->first, chan);
                const double lo1_ret =
                    test_radio->set_tx_lo_freq(iter_lo->at(0), ZBX_LO1, chan);
                const double lo2_ret =
                    test_radio->set_tx_lo_freq(iter_lo->at(1), ZBX_LO2, chan);
                // No use comparing set_tx_freq, we've already ran that test and
                // get_tx_frequency would return who knows what at this point
                BOOST_REQUIRE(abs(iter_lo->at(0) - lo1_ret) < ep);
                BOOST_REQUIRE(abs(iter_lo->at(1) - lo2_ret) < ep);
            }
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_api_rx_lo_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX RX LO TEST";
    const double ep       = 10;

    for (size_t chan : {0, 1}) {
        UHD_LOG_INFO(log, "BEGIN TEST: RX" << chan << " FREQ CHANGE (SET->RETURN)\n");
        for (auto iter = valid_lo_freq_map.begin(); iter != valid_lo_freq_map.end();
             iter++) {
            for (auto iter_lo = iter->second.begin(); iter_lo != iter->second.end();
                 iter_lo++) {
                // Just so we're clear about our value mapping
                const double req_freq = iter->first;
                const double req_lo1  = iter_lo->at(0);
                const double req_lo2  = iter_lo->at(1);

                UHD_LOG_INFO(log,
                    "Testing center freq " << req_freq / 1e6 << "MHz, lo1 freq "
                                           << req_lo1 / 1e6 << "MHz, lo2 freq "
                                           << req_lo2 / 1e6 << "MHz");
                // Need to set center frequency first, it'll set all the mixer values
                test_radio->set_rx_frequency(iter->first, chan);
                const double lo1_ret =
                    test_radio->set_rx_lo_freq(iter_lo->at(0), ZBX_LO1, chan);
                const double lo2_ret =
                    test_radio->set_rx_lo_freq(iter_lo->at(1), ZBX_LO2, chan);
                // No use comparing set_tx_freq, we've already ran that test and
                // get_tx_frequency would return who knows what at this point
                BOOST_REQUIRE(abs(iter_lo->at(0) - lo1_ret) < ep);
                BOOST_REQUIRE(abs(iter_lo->at(1) - lo2_ret) < ep);
            }
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_lo_tree_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX LO1 TEST";
    const double ep       = 10;

    for (auto fe_path : {
             fs_path("dboard/tx_frontends/0"),
             fs_path("dboard/tx_frontends/1"),
             fs_path("dboard/rx_frontends/0"),
             fs_path("dboard/rx_frontends/1"),
         }) {
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " LO FREQ CHANGE (SET->RETURN)\n");
        for (auto iter = valid_lo_freq_map.begin(); iter != valid_lo_freq_map.end();
             iter++) {
            for (auto iter_lo = iter->second.begin(); iter_lo != iter->second.end();
                 iter_lo++) {
                // Just so we're clear about our value mapping
                const double req_freq = iter->first;
                const double req_lo1  = iter_lo->at(0);
                const double req_lo2  = iter_lo->at(1);
                UHD_LOG_INFO(log,
                    "Testing lo1 freq " << req_lo1 / 1e6 << "MHz, lo2 freq "
                                         << req_lo2 / 1e6 << "MHz at center frequency "
                                         << req_freq / 1e6 << "MHz");
                tree->access<double>(fe_path / "freq").set(req_freq);
                const double ret_lo1 =
                    tree->access<double>(fe_path / "los" / ZBX_LO1 / "freq" / "value")
                        .set(req_lo1)
                        .get();
                const double ret_lo2 =
                    tree->access<double>(fe_path / "los" / ZBX_LO2 / "freq" / "value")
                        .set(req_lo2)
                        .get();
                BOOST_REQUIRE(abs(req_lo1 - ret_lo1) < ep);
                BOOST_REQUIRE(abs(req_lo2 - ret_lo2) < ep);
            }
        }
    }
}


BOOST_FIXTURE_TEST_CASE(zbx_ant_test, x400_radio_fixture)
{
    auto tree       = test_radio->get_tree();
    std::string log = "ZBX RX ANTENNA TEST";

    for (auto fe_path :
        {fs_path("dboard/rx_frontends/0"), fs_path("dboard/rx_frontends/1")}) {
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " ANTENNA CHANGE\n");
        for (auto iter : RX_ANTENNAS) {
            UHD_LOG_INFO(log, "Testing Antenna: " << iter);

            tree->access<std::string>(fe_path / "antenna/value").set(iter);

            std::string ret_ant =
                tree->access<std::string>(fe_path / "antenna/value").get();
            BOOST_CHECK_EQUAL(iter, ret_ant);
        }
    }
    log = "ZBX TX ANTENNA TEST";
    for (auto fe_path :
        {fs_path("dboard/tx_frontends/0"), fs_path("dboard/tx_frontends/1")}) {
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " ANTENNA CHANGE\n");
        for (auto iter : TX_ANTENNAS) {
            UHD_LOG_INFO(log, "Testing Antenna: " << iter);

            tree->access<std::string>(fe_path / "antenna/value").set(iter);

            std::string ret_ant =
                tree->access<std::string>(fe_path / "antenna/value").get();
            BOOST_CHECK_EQUAL(iter, ret_ant);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_freq_coercion_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX_FREQUENCY_COERCION_TEST";
    const double ep       = 10;

    for (auto fe_path : {
             fs_path("dboard/tx_frontends/0"),
             fs_path("dboard/tx_frontends/1"),
             fs_path("dboard/rx_frontends/0"),
             fs_path("dboard/rx_frontends/1"),
         }) {
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " FREQUENCY COERCION\n");
        double ret_value =
            tree->access<double>(fe_path / "freq").set(ZBX_MIN_FREQ - 1e6).get();

        BOOST_REQUIRE(abs(ZBX_MIN_FREQ - ret_value) < ep);

        ret_value = tree->access<double>(fe_path / "freq").set(ZBX_MAX_FREQ + 1e6).get();

        BOOST_REQUIRE(abs(ZBX_MAX_FREQ - ret_value) < ep);
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_tx_gain_coercion_test, x400_radio_fixture)
{
    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX_GAIN_COERCION_TEST";

    for (auto fe_path :
        {fs_path("dboard/tx_frontends/0"), fs_path("dboard/tx_frontends/1")}) {
        uhd::gain_range_t zbx_gain(TX_MIN_GAIN, TX_MAX_GAIN, 0.1);
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " TX GAIN COERCION\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            const auto gain_path = fe_path / "gains" / ZBX_GAIN_STAGE_ALL / "value";
            const double ret_val = tree->access<double>(gain_path).set(iter).get();
            BOOST_CHECK_EQUAL(ret_val, std::round(iter));
        }
    }
    for (auto fe_path :
        {fs_path("dboard/rx_frontends/0"), fs_path("dboard/rx_frontends/1")}) {
        uhd::gain_range_t zbx_gain(RX_MIN_GAIN, RX_MAX_GAIN, 0.1);
        UHD_LOG_INFO(log, "BEGIN TEST: " << fe_path << " RX GAIN COERCION\n");
        for (double iter = zbx_gain.start(); iter <= zbx_gain.stop();
             iter += zbx_gain.step()) {
            const auto gain_path = fe_path / "gains" / ZBX_GAIN_STAGE_ALL / "value";
            const double ret_val = tree->access<double>(gain_path).set(iter).get();
            BOOST_CHECK_EQUAL(ret_val, std::round(iter));
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_phase_sync_test, x400_radio_fixture)
{
    auto tree                        = test_radio->get_tree();
    const std::string log            = "ZBX_PHASE_SYNC_TEST";
    constexpr uint32_t lo_sync_addr  = 0x1024 + 0x80000;
    constexpr uint32_t nco_sync_addr = 0x88000;
    constexpr uint32_t gearbox_addr  = 0x88004;
    auto& regs                       = reg_iface->read_memory;
    UHD_LOG_INFO("TEST", "Setting 1 GHz defaults...");
    // Confirm default
    test_radio->set_rx_frequency(1e9, 0);
    test_radio->set_rx_frequency(1e9, 1);
    test_radio->set_tx_frequency(1e9, 0);
    test_radio->set_tx_frequency(1e9, 1);
    // Enable time stamp
    UHD_LOG_INFO("TEST", "Enabling time stamp chan 0...");
    test_radio->set_command_time(uhd::time_spec_t(2.0), 0);
    // Don't pick the ZBX default frequency here
    UHD_LOG_INFO("TEST", "Setting RX chan 0 to 2.3 GHz...");
    test_radio->set_rx_frequency(2.3e9, 0);
    // Check we synced RX LOs chan 0 and RX NCO chan 0, and ADC gearboxes
    BOOST_CHECK_EQUAL(regs[lo_sync_addr], 0b11 << 4);
    BOOST_CHECK_EQUAL(regs[nco_sync_addr], 1);
    BOOST_CHECK_EQUAL(regs[gearbox_addr], 1);
    // Reset strobes
    regs[lo_sync_addr]  = 0;
    regs[nco_sync_addr] = 0;
    regs[gearbox_addr]  = 0;
    UHD_LOG_INFO("TEST", "Enabling time stamp chan 1...");
    test_radio->set_command_time(uhd::time_spec_t(2.0), 1);
    UHD_LOG_INFO("TEST", "Setting RX chan 1 to 2.3 GHz...");
    test_radio->set_rx_frequency(2.3e9, 1);
    // Check we synced RX LOs chan 1 and RX NCO chan 1. ADC gearbox only gets
    // reset once, and should be left untouched.
    BOOST_CHECK_EQUAL(regs[lo_sync_addr], 0b11 << 6);
    BOOST_CHECK_EQUAL(regs[nco_sync_addr], 1);
    BOOST_CHECK_EQUAL(regs[gearbox_addr], 0);
    // Reset strobes
    regs[lo_sync_addr]  = 0;
    regs[nco_sync_addr] = 0;
    regs[gearbox_addr]  = 0;
    UHD_LOG_INFO("TEST", "Setting TX chan 0 to 2.3 GHz...");
    test_radio->set_tx_frequency(2.3e9, 0);
    // Check we synced TX LOs chan 0 and TX NCO chan 0, and DAC gearboxes
    BOOST_CHECK_EQUAL(regs[lo_sync_addr], 0x3 << 0);
    BOOST_CHECK_EQUAL(regs[nco_sync_addr], 1);
    BOOST_CHECK_EQUAL(regs[gearbox_addr], 1 << 1);
    // Reset strobe
    regs[lo_sync_addr]  = 0;
    regs[nco_sync_addr] = 0;
    regs[gearbox_addr]  = 0;
    UHD_LOG_INFO("TEST", "Setting TX chan 1 to 2.3 GHz...");
    test_radio->set_tx_frequency(2.3e9, 1);
    // Check we synced TX LOs chan 1 and TX NCO chan 1. DAC gearbox only gets
    // reset once, and should be left untouched.
    BOOST_CHECK_EQUAL(regs[lo_sync_addr], 0xC << 0);
    BOOST_CHECK_EQUAL(regs[nco_sync_addr], 1);
    BOOST_CHECK_EQUAL(regs[gearbox_addr], 0);
    // Reset strobe
    regs[lo_sync_addr]  = 0;
    regs[nco_sync_addr] = 0;
    regs[gearbox_addr]  = 0;
}

BOOST_FIXTURE_TEST_CASE(can_set_rfdc_test, x400_radio_fixture)
{
    test_radio->set_tx_lo_freq(3.141e9, "rfdc", 1);
    test_radio->get_tx_lo_freq("rfdc", 1);

    test_radio->set_rx_lo_freq(2.141e9, "rfdc", 0);
    test_radio->get_rx_lo_freq("rfdc", 0);
}

BOOST_FIXTURE_TEST_CASE(zbx_tx_power_api, x400_radio_fixture)
{
    constexpr double tx_given_gain  = 30;
    constexpr double tx_given_power = -30;

    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX_TX_POWER_TRACKING_TEST";
    auto tx_pwr_mgr       = test_radio->get_pwr_mgr(TX_DIRECTION);

    for (size_t chan = 0; chan < ZBX_NUM_CHANS; chan++) {
        // Start in gain tracking mode
        double gain_coerced = test_radio->set_tx_gain(tx_given_gain, chan);
        BOOST_CHECK_EQUAL(gain_coerced, tx_given_gain);
        for (const double freq : {6e+08, 1e+09, 2e+09, 3e+09, 4e+09, 5e+09, 6e+09}) {
            // Setting a power reference should kick us into power tracking mode
            test_radio->set_tx_power_reference(tx_given_power, chan);

            test_radio->set_tx_frequency(freq, chan);
            // If the tracking mode is properly set, we should not deviate much
            // regarding power
            const double pow_diff = std::abs<double>(
                tx_given_power - test_radio->get_tx_power_reference(chan));
            BOOST_CHECK_MESSAGE(pow_diff < 3.0, "power differential is too large: " << pow_diff);

            // Back to gain mode
            gain_coerced = test_radio->set_tx_gain(tx_given_gain, chan);
            BOOST_CHECK_EQUAL(gain_coerced, tx_given_gain);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_rx_power_api, x400_radio_fixture)
{
    constexpr double rx_given_gain  = 30;
    constexpr double rx_given_power = -30;

    auto tree             = test_radio->get_tree();
    const std::string log = "ZBX_RX_POWER_TRACKING_TEST";
    auto rx_pwr_mgr       = test_radio->get_pwr_mgr(RX_DIRECTION);

    for (size_t chan = 0; chan < ZBX_NUM_CHANS; chan++) {
        // Start in gain tracking mode
        double gain_coerced = test_radio->set_rx_gain(rx_given_gain, chan);
        BOOST_REQUIRE_EQUAL(gain_coerced, rx_given_gain);
        for (const double freq : {1e+09, 2e+09, 3e+09, 4e+09, 5e+09, 6e+09}) {
            // Setting a power reference should kick us into power tracking mode
            test_radio->set_rx_power_reference(rx_given_power, chan);
            // Now go tune
            test_radio->set_rx_frequency(freq, chan);
            // If the tracking mode is properly set, we should match our expected criteria
            // for power reference levels
            const double actual_power = test_radio->get_rx_power_reference(chan);
            const double pow_diff     = std::abs<double>(rx_given_power - actual_power);
            BOOST_CHECK_MESSAGE(pow_diff < 3.0,
                "power differential is too large ("
                    << pow_diff << "): Expected close to: " << rx_given_power
                    << " Actual: " << actual_power << " Frequency: " << (freq/1e6));

            gain_coerced = test_radio->set_rx_gain(rx_given_gain, chan);
            BOOST_REQUIRE_EQUAL(gain_coerced, rx_given_gain);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_tx_lo_injection_locking, x400_radio_fixture)
{
    auto tree = test_radio->get_tree();

    // As of right now, we don't have a way to directly get the DB prc rate, this is the
    // value of the prc map per DEFAULT_MCR, in the mock RPC server:db_0_get_db_prc_rate()
    constexpr double db_prc_rate  = 61.44e6;
    constexpr double lo_step_size = db_prc_rate / ZBX_RELATIVE_LO_STEP_SIZE;

    uhd::freq_range_t zbx_freq(ZBX_MIN_FREQ, ZBX_MAX_FREQ, 100e6);

    for (double iter = zbx_freq.start(); iter <= zbx_freq.stop();
         iter += zbx_freq.step()) {
        for (const size_t chan : {0, 1}) {
            test_radio->set_tx_frequency(iter, chan);

            // The step alignment only applies to the desired LO frequency, the actual
            // returned frequency may vary slightly
            const double lo1_freq = std::round(test_radio->get_tx_lo_freq(ZBX_LO1, chan));
            const double lo2_freq = std::round(test_radio->get_tx_lo_freq(ZBX_LO2, chan));

            const double lo1_div = lo1_freq / lo_step_size;
            const double lo2_div = lo2_freq / lo_step_size;

            // Test whether our tuned frequencies align with the lo step size
            BOOST_CHECK_EQUAL(std::floor(lo1_div), lo1_div);
            BOOST_CHECK_EQUAL(std::floor(lo2_div), lo2_div);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_rx_lo_injection_locking, x400_radio_fixture)
{
    auto tree = test_radio->get_tree();

    // As of right now, we don't have a way to directly get the DB prc rate, this is the
    // value of the prc map per DEFAULT_MCR, in the mock RPC server:db_0_get_db_prc_rate()
    constexpr double db_prc_rate  = 61.44e6;
    constexpr double lo_step_size = db_prc_rate / ZBX_RELATIVE_LO_STEP_SIZE;

    uhd::freq_range_t zbx_freq(ZBX_MIN_FREQ, ZBX_MAX_FREQ, 100e6);

    for (double iter = zbx_freq.start(); iter <= zbx_freq.stop();
         iter += zbx_freq.step()) {
        for (const size_t chan : {0, 1}) {
            test_radio->set_rx_frequency(iter, chan);

            // The step alignment only applies to the desired LO frequency, the actual
            // returned frequency may vary slightly
            const double lo1_freq = std::round(test_radio->get_rx_lo_freq(ZBX_LO1, chan));
            const double lo2_freq = std::round(test_radio->get_rx_lo_freq(ZBX_LO2, chan));

            const double lo1_div = lo1_freq / lo_step_size;
            const double lo2_div = lo2_freq / lo_step_size;

            // Test whether our tuned frequencies align with the lo step size
            BOOST_CHECK_EQUAL(std::floor(lo1_div), lo1_div);
            BOOST_CHECK_EQUAL(std::floor(lo2_div), lo2_div);
        }
    }
}

BOOST_FIXTURE_TEST_CASE(zbx_rx_gain_profile_test, x400_radio_fixture)
{
    auto tree                         = test_radio->get_tree();
    const std::string log             = "ZBX_GAIN_PROFILE_TEST";
    auto& regs                        = reg_iface->read_memory;
    constexpr uint32_t current_config = radio_control_impl::regmap::PERIPH_BASE + 0x1000;
    constexpr uint32_t rf_option      = radio_control_impl::regmap::PERIPH_BASE + 0x1004;
    constexpr uint32_t sw_config      = radio_control_impl::regmap::PERIPH_BASE + 0x1008;
    constexpr uint32_t rx0_dsa        = radio_control_impl::regmap::PERIPH_BASE + 0x3800;
    constexpr uint32_t rx0_table      = radio_control_impl::regmap::PERIPH_BASE + 0x5800;
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(0), "default");
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(0), "default");
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(1), "default");
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(1), "default");
    // Everything should be classic_atr
    BOOST_CHECK_EQUAL(regs[0x81004], 0x01010101);
    // Can't set gain stages in this profile
    BOOST_REQUIRE_THROW(test_radio->set_rx_gain(10, "DSA1", 0), uhd::key_error);
    BOOST_REQUIRE_THROW(test_radio->set_tx_gain(10, "DSA1", 0), uhd::key_error);

    //** manual gain profile **
    test_radio->set_rx_gain_profile("manual", 0);
    // Must provide valid gain name in this profile
    BOOST_REQUIRE_THROW(test_radio->set_rx_gain(23, 0), uhd::runtime_error);
    BOOST_REQUIRE_THROW(test_radio->set_rx_gain(10, "banana", 0), uhd::key_error);
    // Now manually set the DSAs
    BOOST_CHECK_EQUAL(5, test_radio->set_rx_gain(5, "DSA1", 0));
    BOOST_CHECK_EQUAL(5, test_radio->set_rx_gain(5, "DSA2", 0));
    BOOST_CHECK_EQUAL(5, test_radio->set_rx_gain(5, "DSA3A", 0));
    BOOST_CHECK_EQUAL(5, test_radio->set_rx_gain(5, "DSA3B", 0));
    // Check the registers were written to correctly (gain 5 == att 10)
    BOOST_CHECK_EQUAL(regs[rx0_dsa + 1 * 4], 0xAAAA);
    BOOST_CHECK_EQUAL(regs[rx0_dsa + 3 * 4], 0xAAAA);
    // Check the getters:
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain("DSA1", 0), 5);
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain("DSA2", 0), 5);
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain("DSA3A", 0), 5);
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain("DSA3B", 0), 5);
    // Even in 'manual', we can load from the table. Let's create a table entry:
    regs[rx0_table + 5 * 4] = 0x7777;
    // Now, let it be loaded into RX and XX:
    BOOST_CHECK_EQUAL(5, test_radio->set_rx_gain(5, "TABLE", 0));
    BOOST_CHECK_EQUAL(regs[rx0_dsa + 1 * 4], 0x7777);
    BOOST_CHECK_EQUAL(regs[rx0_dsa + 3 * 4], 0x7777);
    // Note: If we read back the DSAs via get_rx_gain() now, they will still say
    // 5. We might want to change that, but it will require extra peeks. The
    // only good way to do that is to amend set_?x_gain() to do that peek when
    // updating gains via table.
    // Test DSA coercion
    BOOST_CHECK_EQUAL(15, test_radio->set_rx_gain(39, "DSA1", 0));
    BOOST_CHECK_EQUAL(0, test_radio->set_rx_gain(-17, "DSA1", 0));

    // If we go back to 'default', we also reset the DSAs. That's because the
    // desired, previously loaded default value will trigger the previous DSA
    // values again.
    UHD_LOG_INFO(log, "resetting to default");
    test_radio->set_rx_gain_profile("default", 0);
    BOOST_CHECK_EQUAL(0, test_radio->get_rx_gain("DSA1", 0));

    //** table_noatr profile : **
    UHD_LOG_INFO(log, "setting to table_noatr");
    test_radio->set_rx_gain_profile("table_noatr", 0);
    // This will set DSA config for chan 0 to 0 == SW_DEFINED
    BOOST_CHECK_EQUAL(regs[rf_option], 0x01000101);
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(0), "table_noatr");
    // Yup, this will also change TX gain profile; they're coupled.
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(0), "table_noatr");
    BOOST_REQUIRE_THROW(test_radio->set_rx_gain(10, "all", 0), uhd::key_error);
    BOOST_CHECK_EQUAL(8.0, test_radio->set_rx_gain(8, "TABLE", 0));
    BOOST_CHECK_EQUAL(regs[sw_config], 0x80000);
    // Returns the current config. Note the asymmetry to the previous API call.
    // We can't, however, know which entry from the TABLE we used, so we just
    // return the current config (which is the entry from the DSA table, not the
    // TABLE it writes to).
    BOOST_CHECK_EQUAL(0, test_radio->get_rx_gain("TABLE", 0));
    // Let's pretend we're using config 7
    regs[current_config] = 0x70000;
    BOOST_CHECK_EQUAL(7, test_radio->get_rx_gain("TABLE", 0));
    // And back
    regs[current_config] = 0x00000;
    // Now we fake an FPGA-gain-change transaction that UHD is unaware of. We
    // keep the current config of 0, and update RX0_DSA*[0].
    regs[rx0_dsa + 0 * 4] = 0x4444; // Turn it up to attenuation 4 == gain 11
    BOOST_CHECK_EQUAL(11.0, test_radio->get_rx_gain("DSA1", 0));

    //** table profile **
    test_radio->set_rx_gain_profile("table", 0);
    BOOST_CHECK_EQUAL(regs[rf_option], 0x01010101);
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(0), "table");
    // Yup, this will also change TX gain profile; they're coupled.
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(0), "table");
    // Create another table entry
    regs[rx0_table + 23 * 4] = 0xBBBB;
    BOOST_CHECK_EQUAL(23.0, test_radio->set_rx_gain(23, "TABLE", 0));
    // get_rx_gain() for "TABLE" returns the current DSA table index, not actual gain
    BOOST_CHECK_EQUAL(0.0, test_radio->get_rx_gain("TABLE", 0));
    // This will update RX and XX registers (that's the difference to table_noatr)
    BOOST_CHECK_EQUAL(regs[rx0_dsa + 1 * 4], 0xBBBB); // att 0xB == gain 4.0
    BOOST_CHECK_EQUAL(regs[rx0_dsa + 3 * 4], 0xBBBB);
    BOOST_CHECK_EQUAL(4.0, test_radio->get_rx_gain("DSA1", 0));
    BOOST_CHECK_EQUAL(4.0, test_radio->get_rx_gain("DSA2", 0));
    BOOST_CHECK_EQUAL(4.0, test_radio->get_rx_gain("DSA3A", 0));
    BOOST_CHECK_EQUAL(4.0, test_radio->get_rx_gain("DSA3B", 0));
    // Test table coercion
    UHD_LOG_INFO(log, "Testing TABLE coercion");
    BOOST_CHECK_EQUAL(0.0, test_radio->set_rx_gain(-17, "TABLE", 0));
    BOOST_CHECK_EQUAL(255.0, test_radio->set_rx_gain(1e9, "TABLE", 0));
}

BOOST_FIXTURE_TEST_CASE(zbx_tx_gain_profile_test, x400_radio_fixture)
{
    auto tree                         = test_radio->get_tree();
    const std::string log             = "ZBX_GAIN_PROFILE_TEST";
    auto& regs                        = reg_iface->read_memory;
    constexpr uint32_t current_config = radio_control_impl::regmap::PERIPH_BASE + 0x1000;
    constexpr uint32_t rf_option      = radio_control_impl::regmap::PERIPH_BASE + 0x1004;
    constexpr uint32_t sw_config      = radio_control_impl::regmap::PERIPH_BASE + 0x1008;
    constexpr uint32_t tx0_dsa        = radio_control_impl::regmap::PERIPH_BASE + 0x3000;
    constexpr uint32_t tx0_table      = radio_control_impl::regmap::PERIPH_BASE + 0x5000;
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(0), "default");
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(0), "default");
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(1), "default");
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(1), "default");
    const double default_dsa1 = test_radio->get_tx_gain("DSA1", 0);
    // Everything should be classic_atr
    BOOST_CHECK_EQUAL(regs[0x81004], 0x01010101);
    // Can't set gain stages in this profile
    BOOST_REQUIRE_THROW(test_radio->set_rx_gain(10, "DSA1", 0), uhd::key_error);
    BOOST_REQUIRE_THROW(test_radio->set_tx_gain(10, "DSA1", 0), uhd::key_error);

    //** manual gain profile **
    test_radio->set_tx_gain_profile("manual", 0);
    // Must provide valid gain name in this profile
    BOOST_REQUIRE_THROW(test_radio->set_tx_gain(23, 0), uhd::runtime_error);
    BOOST_REQUIRE_THROW(test_radio->set_tx_gain(23, "all", 0), uhd::key_error);
    BOOST_REQUIRE_THROW(test_radio->set_tx_gain(10, "banana", 0), uhd::key_error);
    // Now manually set the DSAs
    BOOST_CHECK_EQUAL(21, test_radio->set_tx_gain(21, "DSA1", 0));
    BOOST_CHECK_EQUAL(21, test_radio->set_tx_gain(21, "DSA2", 0));
    // Check the registers were written to correctly (gain 5 == att 10)
    BOOST_CHECK_EQUAL(regs[tx0_dsa + 2 * 4], 0x0A0A);
    BOOST_CHECK_EQUAL(regs[tx0_dsa + 3 * 4], 0x0A0A);
    // Check the getters:
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain("DSA1", 0), 21);
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain("DSA2", 0), 21);
    // Even in 'manual', we can load from the table. Let's create a table entry:
    regs[tx0_table + 5 * 4] = 0x0707;
    // Now, let it be loaded into RX and XX:
    BOOST_CHECK_EQUAL(5, test_radio->set_tx_gain(5, "TABLE", 0));
    BOOST_CHECK_EQUAL(regs[tx0_dsa + 2 * 4], 0x0707);
    BOOST_CHECK_EQUAL(regs[tx0_dsa + 3 * 4], 0x0707);
    // Note: If we read back the DSAs via get_tx_gain() now, they will still say
    // 5. We might want to change that, but it will require extra peeks. The
    // only good way to do that is to amend set_?x_gain() to do that peek when
    // updating gains via table.
    // Test DSA coercion
    BOOST_CHECK_EQUAL(31, test_radio->set_tx_gain(39, "DSA1", 0));
    BOOST_CHECK_EQUAL(0, test_radio->set_tx_gain(-17, "DSA1", 0));

    // If we go back to 'default', we also reset the DSAs. That's because the
    // desired, previously loaded default value will trigger the previous DSA
    // values again.
    UHD_LOG_INFO(log, "resetting to default");
    test_radio->set_tx_gain_profile("default", 0);
    BOOST_CHECK_EQUAL(default_dsa1, test_radio->get_tx_gain("DSA1", 0));

    //** table_noatr profile : **
    UHD_LOG_INFO(log, "setting to table_noatr");
    test_radio->set_tx_gain_profile("table_noatr", 0);
    // This will set DSA config for chan 0 to 0 == SW_DEFINED
    BOOST_CHECK_EQUAL(regs[rf_option], 0x01000101);
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(0), "table_noatr");
    // Yup, this will also change RX gain profile; they're coupled.
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(0), "table_noatr");
    BOOST_REQUIRE_THROW(test_radio->set_tx_gain(10, "all", 0), uhd::key_error);
    BOOST_CHECK_EQUAL(8.0, test_radio->set_tx_gain(8, "TABLE", 0));
    BOOST_CHECK_EQUAL(regs[sw_config], 0x80000);
    // Returns the current config. Note the asymmetry to the previous API call.
    // We can't, however, know which entry from the TABLE we used, so we just
    // return the current config (which is the entry from the DSA table, not the
    // TABLE it writes to).
    BOOST_CHECK_EQUAL(0, test_radio->get_tx_gain("TABLE", 0));
    // Let's pretend we're using config 7
    regs[current_config] = 0x70000;
    BOOST_CHECK_EQUAL(7, test_radio->get_tx_gain("TABLE", 0));
    // And back
    regs[current_config] = 0x00000;
    // Now we fake an FPGA-gain-change transaction that UHD is unaware of. We
    // keep the current config of 0, and update TX0_DSA*[0].
    regs[tx0_dsa + 0 * 4] = 0x0404; // Turn it up to attenuation 4 == gain 27
    BOOST_CHECK_EQUAL(27.0, test_radio->get_tx_gain("DSA1", 0));

    //** table profile **
    test_radio->set_tx_gain_profile("table", 0);
    BOOST_CHECK_EQUAL(regs[rf_option], 0x01010101);
    BOOST_CHECK_EQUAL(test_radio->get_tx_gain_profile(0), "table");
    // Yup, this will also change RX gain profile; they're coupled.
    BOOST_CHECK_EQUAL(test_radio->get_rx_gain_profile(0), "table");
    // Create another table entry
    regs[tx0_table + 23 * 4] = 0x0B0B;
    BOOST_CHECK_EQUAL(23.0, test_radio->set_tx_gain(23, "TABLE", 0));
    // get_tx_gain() for "TABLE" returns the current DSA table index, not actual gain
    BOOST_CHECK_EQUAL(0.0, test_radio->get_tx_gain("TABLE", 0));
    // This will update RX and XX registers (that's the difference to table_noatr)
    BOOST_CHECK_EQUAL(regs[tx0_dsa + 2 * 4], 0x0B0B); // att 0xB == gain 20.0
    BOOST_CHECK_EQUAL(regs[tx0_dsa + 3 * 4], 0x0B0B);
    BOOST_CHECK_EQUAL(20.0, test_radio->get_tx_gain("DSA1", 0));
    BOOST_CHECK_EQUAL(20.0, test_radio->get_tx_gain("DSA2", 0));
    // Test table coercion
    UHD_LOG_INFO(log, "Testing TABLE coercion");
    BOOST_CHECK_EQUAL(0.0, test_radio->set_tx_gain(-17, "TABLE", 0));
    BOOST_CHECK_EQUAL(255.0, test_radio->set_tx_gain(1e9, "TABLE", 0));
}

// TODO:
// - concurrent/consecutive configuration
// - Threading tests
// - Error cases