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

#include "../rfnoc_graph_mock_nodes.hpp"
#include <uhd/convert.hpp>
#include <uhd/rfnoc/actions.hpp>
#include <uhd/rfnoc/ddc_block_control.hpp>
#include <uhd/rfnoc/defaults.hpp>
#include <uhd/rfnoc/mock_block.hpp>
#include <uhd/rfnoc/replay_block_control.hpp>
#include <uhdlib/rfnoc/graph.hpp>
#include <uhdlib/rfnoc/node_accessor.hpp>
#include <uhdlib/utils/narrow.hpp>
#include <boost/test/unit_test.hpp>
#include <iostream>

using namespace uhd::rfnoc;

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

/*
 * 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 replay block. This is important,
 * because upon construction in software, the Replay Block will read from the
 * REG_MEM_SIZE_ADDR to determine the word size and memory address width. These constant
 * read-only values are crucial for the initialization of the other properties.
 * Additionally, the record fullness is initialized here. This read-only value changes
 * during recording.
 */
class replay_mock_reg_iface_t : public mock_reg_iface_t
{
public:
    replay_mock_reg_iface_t(size_t mem_addr_size, size_t word_size, size_t num_channels)
    {
        for (size_t chan = 0; chan < num_channels; chan++) {
            const uint32_t reg_compat =
                replay_block_control::REG_COMPAT_ADDR
                + chan * replay_block_control::REPLAY_BLOCK_OFFSET;
            read_memory[reg_compat] = (replay_block_control::MINOR_COMPAT
                                       | (replay_block_control::MAJOR_COMPAT << 16));
        }
        for (size_t chan = 0; chan < num_channels; chan++) {
            const uint32_t reg_mem_size =
                replay_block_control::REG_MEM_SIZE_ADDR
                + chan * replay_block_control::REPLAY_BLOCK_OFFSET;
            read_memory[reg_mem_size] = (mem_addr_size | (word_size << 16));
        }
        for (size_t chan = 0; chan < num_channels; chan++) {
            const uint32_t reg_rec_fullness =
                replay_block_control::REG_REC_FULLNESS_LO_ADDR
                + chan * replay_block_control::REPLAY_BLOCK_OFFSET;
            read_memory[reg_rec_fullness]     = 0x0010;
            read_memory[reg_rec_fullness + 4] = 0x0000;
        }
    }
};

/*
 * replay_block_fixture is a class which is instantiated before each test
 * case is run. It sets up the block container, mock register interface,
 * and replay_block_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;

struct replay_block_fixture
{
    replay_block_fixture()
        : num_channels(4)
        , num_input_ports(num_channels)
        , num_output_ports(num_channels)
        , mem_addr_size(32)
        , max_buffer_size(1ULL << mem_addr_size)
        , default_item_size(4)
        , word_size(8)
        , reg_iface(std::make_shared<replay_mock_reg_iface_t>(
              mem_addr_size, (word_size * 8), num_channels))
        , block_container(get_mock_block(REPLAY_BLOCK,
              num_channels,
              num_channels,
              uhd::device_addr_t("foo=bar"),
              DEFAULT_MTU,
              ANY_DEVICE,
              reg_iface))
        , test_replay(block_container.get_block<replay_block_control>())
    {
        node_accessor.init_props(test_replay.get());
    }

    size_t num_channels;
    size_t num_input_ports;
    size_t num_output_ports;
    size_t mem_addr_size;
    uint64_t max_buffer_size; // in bytes
    size_t default_item_size; // in bytes
    size_t word_size; // in bytes
    std::shared_ptr<replay_mock_reg_iface_t> reg_iface;
    mock_block_container block_container;
    std::shared_ptr<replay_block_control> test_replay;
    node_accessor_t node_accessor{};
};

inline uint32_t get_addr(uint32_t offset, size_t chan)
{
    return offset + chan * replay_block_control::REPLAY_BLOCK_OFFSET;
}

/*
 * This test case ensures that the hardware is programmed correctly with
 * defaults when the replay block is constructed.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_construction, replay_block_fixture)
{
    BOOST_REQUIRE(test_replay);
    BOOST_CHECK_EQUAL(test_replay->get_block_args().get("foo"), "bar");

    BOOST_CHECK_EQUAL(test_replay->get_mem_size(), max_buffer_size);
    BOOST_CHECK_EQUAL(test_replay->get_word_size(), word_size);

    for (size_t chan = 0; chan < num_channels; chan++) {
        const uint32_t reg_compat = get_addr(replay_block_control::REG_COMPAT_ADDR, chan);
        const uint32_t reg_mem_size =
            get_addr(replay_block_control::REG_MEM_SIZE_ADDR, chan);
        const uint32_t reg_rec_buff_size =
            get_addr(replay_block_control::REG_REC_BUFFER_SIZE_LO_ADDR, chan);
        const uint32_t reg_rec_base_addr =
            get_addr(replay_block_control::REG_REC_BASE_ADDR_LO_ADDR, chan);
        const uint32_t reg_play_buff_size =
            get_addr(replay_block_control::REG_PLAY_BUFFER_SIZE_LO_ADDR, chan);
        const uint32_t reg_play_base_addr =
            get_addr(replay_block_control::REG_PLAY_BASE_ADDR_LO_ADDR, chan);
        const uint32_t reg_words_per_pkt =
            get_addr(replay_block_control::REG_PLAY_WORDS_PER_PKT_ADDR, chan);
        const uint32_t reg_play_item_size =
            get_addr(replay_block_control::REG_PLAY_ITEM_SIZE_ADDR, chan);
        BOOST_CHECK_EQUAL(reg_iface->read_memory[reg_compat] & 0xFFFF,
            replay_block_control::MINOR_COMPAT);
        BOOST_CHECK_EQUAL((reg_iface->read_memory[reg_compat] >> 16) & 0xFFFF,
            replay_block_control::MAJOR_COMPAT);
        BOOST_CHECK_EQUAL(reg_iface->read_memory[reg_mem_size] & 0xFFFF, mem_addr_size);
        BOOST_CHECK_EQUAL(
            (reg_iface->read_memory[reg_mem_size] >> 16) & 0xFFFF, word_size * 8);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_rec_buff_size], max_buffer_size & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_rec_buff_size + 4],
            (max_buffer_size >> 32) & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_rec_base_addr], 0);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_rec_base_addr + 4], 0);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_play_buff_size], max_buffer_size & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_buff_size + 4],
            (max_buffer_size >> 32) & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_base_addr], 0);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_base_addr + 4], 0);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_words_per_pkt],
            (DEFAULT_MTU - test_replay->get_chdr_hdr_len()) / word_size);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_item_size], default_item_size);
    }
}


/**************************************************************************
 * Record Buffer tests
 *************************************************************************/

/*
 * This test case ensures that the hardware is programmed correctly when a record restart
 * occurs. Any value written to REG_REC_RESTART_ADDR triggers a record restart.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_record_restart, replay_block_fixture)
{
    for (size_t port = 0; port < num_input_ports; port++) {
        const uint32_t reg_rec_restart =
            get_addr(replay_block_control::REG_REC_RESTART_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory.count(reg_rec_restart), 0);
        test_replay->record_restart(port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory.count(reg_rec_restart), 1);
    }
}

/*
 * This test case ensures that the get_record_fullness() API call reads correctly from
 * hardware.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_record_fullness, replay_block_fixture)
{
    for (size_t port = 0; port < num_input_ports; port++) {
        const uint64_t fullness = 0x123456789ABCDEF0 | port;
        const uint32_t reg_rec_fullness =
            get_addr(replay_block_control::REG_REC_FULLNESS_LO_ADDR, port);
        reg_iface->read_memory[reg_rec_fullness]     = fullness & 0xFFFFFFFF;
        reg_iface->read_memory[reg_rec_fullness + 4] = (fullness >> 32) & 0xFFFFFFFF;

        BOOST_CHECK_EQUAL(test_replay->get_record_fullness(port), fullness);
    }
}

/*
 * This test case ensures that the record type API calls interact correctly.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_record_type, replay_block_fixture)
{
    for (size_t port = 0; port < num_input_ports; port++) {
        // Test the defaults
        const io_type_t default_type = IO_TYPE_SC16;
        BOOST_CHECK_EQUAL(test_replay->get_record_type(port), default_type);
        BOOST_CHECK_EQUAL(test_replay->get_record_item_size(port),
            uhd::convert::get_bytes_per_item(default_type));
    }

    for (size_t port = 0; port < num_input_ports; port++) {
        const io_type_t type = IO_TYPE_U8;
        test_replay->set_record_type(type, port);
        BOOST_CHECK_EQUAL(test_replay->get_record_type(port), type);
        BOOST_CHECK_EQUAL(test_replay->get_record_item_size(port),
            uhd::convert::get_bytes_per_item(type));
    }
}

/*
 * This test case ensures that the hardware is programmed correctly when the record buffer
 * is configured. This includes testing that a record restart takes place. The test
 * also ensures that any configuration of the base address and buffer size are word
 * addressable. Additionally, it exercises the get_record_size() and
 * get_record_offset() API calls.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_record, replay_block_fixture)
{
    for (size_t port = 0; port < num_input_ports; port++) {
        // Test the defaults
        BOOST_CHECK_EQUAL(test_replay->get_record_size(port), max_buffer_size);
        BOOST_CHECK_EQUAL(test_replay->get_record_offset(port), 0);
    }

    for (size_t port = 0; port < num_input_ports; port++) {
        const uint32_t reg_rec_restart =
            get_addr(replay_block_control::REG_REC_RESTART_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory.count(reg_rec_restart), 0);

        const uint64_t base_addr   = 16 + max_buffer_size / num_input_ports * port;
        const uint64_t buffer_size = max_buffer_size / num_input_ports / 2;
        test_replay->record(base_addr, buffer_size, port);

        const uint32_t reg_rec_buff_size =
            get_addr(replay_block_control::REG_REC_BUFFER_SIZE_LO_ADDR, port);
        const uint32_t reg_rec_base_addr =
            get_addr(replay_block_control::REG_REC_BASE_ADDR_LO_ADDR, port);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_rec_buff_size], buffer_size & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_rec_buff_size + 4],
            (buffer_size >> 32) & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_rec_base_addr], base_addr & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_rec_base_addr + 4],
            (base_addr >> 32) & 0xFFFFFFFF);

        BOOST_CHECK_EQUAL(test_replay->get_record_size(port), buffer_size);
        BOOST_CHECK_EQUAL(test_replay->get_record_offset(port), base_addr);
        // There should be a record restart on config
        // (with any value written to the register)
        BOOST_CHECK_EQUAL(reg_iface->write_memory.count(reg_rec_restart), 1);

        // Valid base address and buffer size values are multiples of the word size
        for (uint64_t offset = 1; offset <= word_size - 1; offset++) {
            BOOST_CHECK_THROW(test_replay->record(base_addr + offset, buffer_size, port),
                uhd::value_error);
        }
        for (uint64_t offset = 1; offset <= word_size - 1; offset++) {
            BOOST_CHECK_THROW(test_replay->record(base_addr, buffer_size + offset, port),
                uhd::value_error);
        }
        // The play buffer must be within the bounds of the Replay memory
        BOOST_CHECK_THROW(
            test_replay->record(max_buffer_size, buffer_size, port), uhd::value_error);
        BOOST_CHECK_THROW(
            test_replay->record(base_addr, max_buffer_size, port), uhd::value_error);
    }
}

/**************************************************************************
 * Playback tests
 *************************************************************************/

/*
 * This test case ensures that the hardware is programmed correctly when the play buffer
 * is configured. The test also ensures that any configuration of the base address and
 * buffer size are word addressable. Additionally, it exercises the get_play_size() and
 * get_play_offset() API calls.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_configure_play, replay_block_fixture)
{
    for (size_t port = 0; port < num_output_ports; port++) {
        // Test the defaults
        BOOST_CHECK_EQUAL(test_replay->get_play_size(port), max_buffer_size);
        BOOST_CHECK_EQUAL(test_replay->get_play_offset(port), 0);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        const uint64_t base_addr = word_size + max_buffer_size / num_output_ports * port;
        const uint64_t buffer_size = max_buffer_size / num_output_ports / 2;
        test_replay->config_play(base_addr, buffer_size, port);

        const uint32_t reg_play_buff_size =
            get_addr(replay_block_control::REG_PLAY_BUFFER_SIZE_LO_ADDR, port);
        const uint32_t reg_play_base_addr =
            get_addr(replay_block_control::REG_PLAY_BASE_ADDR_LO_ADDR, port);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_play_buff_size], buffer_size & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_buff_size + 4],
            (buffer_size >> 32) & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_play_base_addr], base_addr & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_base_addr + 4],
            (base_addr >> 32) & 0xFFFFFFFF);

        BOOST_CHECK_EQUAL(test_replay->get_play_size(port), buffer_size);
        BOOST_CHECK_EQUAL(test_replay->get_play_offset(port), base_addr);

        // Valid base address and buffer size values are multiples of the word size
        for (uint64_t offset = 1; offset <= word_size - 1; offset++) {
            BOOST_CHECK_THROW(
                test_replay->config_play(base_addr + offset, buffer_size, port),
                uhd::value_error);
        }
        for (uint64_t offset = 1; offset <= word_size - 1; offset++) {
            BOOST_CHECK_THROW(
                test_replay->config_play(base_addr, buffer_size + offset, port),
                uhd::value_error);
        }
        // Valid base address and buffer size values are multiples of the item size for
        // playback
        size_t item_size = test_replay->get_play_item_size(port);
        for (uint64_t offset = 1; offset <= item_size - 1; offset++) {
            BOOST_CHECK_THROW(
                test_replay->config_play(base_addr + offset, buffer_size, port),
                uhd::value_error);
        }
        for (uint64_t offset = 1; offset <= item_size - 1; offset++) {
            BOOST_CHECK_THROW(
                test_replay->config_play(base_addr, buffer_size + offset, port),
                uhd::value_error);
        }
        // The play buffer must be within the bounds of the Replay memory
        BOOST_CHECK_THROW(test_replay->config_play(max_buffer_size, buffer_size, port),
            uhd::value_error);
        BOOST_CHECK_THROW(
            test_replay->config_play(base_addr, max_buffer_size, port), uhd::value_error);
    }
}

/*
 * This test case ensures that the hardware is programmed correctly through the playback
 * packet API calls.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_packet_size, replay_block_fixture)
{
    for (size_t port = 0; port < num_output_ports; port++) {
        // Test the defaults
        const uint32_t item_size = test_replay->get_play_item_size(port);
        const uint32_t expected_ipp =
            test_replay->get_max_payload_size({res_source_info::OUTPUT_EDGE, port})
            / item_size;
        BOOST_CHECK_EQUAL(test_replay->get_max_items_per_packet(port), expected_ipp);
        const uint32_t default_packet_size =
            expected_ipp * item_size + test_replay->get_chdr_hdr_len();
        BOOST_CHECK_EQUAL(test_replay->get_max_packet_size(port), default_packet_size);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        const uint32_t ipp = 1024;
        test_replay->set_max_items_per_packet(ipp, port);
        BOOST_CHECK_EQUAL(test_replay->get_max_items_per_packet(port), ipp);

        const uint32_t item_size   = test_replay->get_play_item_size(port);
        const uint32_t packet_size = ipp * item_size + test_replay->get_chdr_hdr_len();
        BOOST_CHECK_EQUAL(test_replay->get_max_packet_size(port), packet_size);

        const uint32_t wpp = ipp * item_size / word_size;
        const uint32_t reg_words_per_pkt =
            get_addr(replay_block_control::REG_PLAY_WORDS_PER_PKT_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_words_per_pkt], wpp);
    }
}

/*
 * This test case ensures that the play type and item size API calls interact correctly
 * and program the hardware.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_play_type, replay_block_fixture)
{
    for (size_t port = 0; port < num_output_ports; port++) {
        // Test the defaults
        const io_type_t default_type = IO_TYPE_SC16;
        BOOST_CHECK_EQUAL(test_replay->get_play_type(port), default_type);
        BOOST_CHECK_EQUAL(test_replay->get_play_item_size(port),
            uhd::convert::get_bytes_per_item(default_type));
        const uint32_t reg_play_item_size =
            get_addr(replay_block_control::REG_PLAY_ITEM_SIZE_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_item_size],
            uhd::convert::get_bytes_per_item(default_type));
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        const io_type_t type = IO_TYPE_U8;
        test_replay->set_play_type(type, port);
        BOOST_CHECK_EQUAL(test_replay->get_play_type(port), type);
        BOOST_CHECK_EQUAL(test_replay->get_play_item_size(port),
            uhd::convert::get_bytes_per_item(type));
        const uint32_t reg_play_item_size =
            get_addr(replay_block_control::REG_PLAY_ITEM_SIZE_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_item_size],
            uhd::convert::get_bytes_per_item(type));
    }
}

/*
 * This test case ensures that the hardware is programmed correctly when a stream command
 * is issued. Note that there is not a distinction between STREAM_MODE_NUM_SAMPS_AND_DONE
 * and STREAM_MODE_NUM_SAMPS_AND_MORE in USRP3 devices and newer.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_issue_stream_cmd, replay_block_fixture)
{
    for (size_t port = 0; port < num_output_ports; port++) {
        uhd::stream_cmd_t cmd_stop(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_STOP_CONTINUOUS);
        test_replay->issue_stream_cmd(cmd_stop, port);

        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_stream_cmd], replay_block_control::PLAY_CMD_STOP);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        uhd::stream_cmd_t cmd_cont(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_START_CONTINUOUS);
        test_replay->issue_stream_cmd(cmd_cont, port);

        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_CONTINUOUS);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        uhd::stream_cmd_t cmd_finite(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_NUM_SAMPS_AND_DONE);
        const uint64_t num_words = 0x00123ABC;
        cmd_finite.num_samps     = num_words * word_size / 4;
        test_replay->issue_stream_cmd(cmd_finite, port);

        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_FINITE);
        // PLAY_CMD_FINITE writes the number of words to hardware
        const uint32_t reg_num_words =
            get_addr(replay_block_control::REG_PLAY_CMD_NUM_WORDS_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_num_words], num_words & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_num_words + 4], (num_words >> 32) & 0xFFFFFFFF);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        uhd::stream_cmd_t cmd_finite(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_NUM_SAMPS_AND_MORE);
        const uint64_t num_words = 0x00DEF456;
        cmd_finite.num_samps     = num_words * word_size / 4;
        test_replay->issue_stream_cmd(cmd_finite, port);

        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_FINITE);
        // PLAY_CMD_FINITE writes the number of words to hardware
        const uint32_t reg_num_words =
            get_addr(replay_block_control::REG_PLAY_CMD_NUM_WORDS_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_num_words], num_words & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_num_words + 4], (num_words >> 32) & 0xFFFFFFFF);
    }
}

/*
 * This test case ensures that the hardware is programmed correctly when a stream command
 * is issued with delay. Note that there is not a distinction between
 * STREAM_MODE_NUM_SAMPS_AND_DONE and STREAM_MODE_NUM_SAMPS_AND_MORE in USRP3 devices and
 * newer.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_issue_stream_cmd_timed, replay_block_fixture)
{
    for (size_t port = 0; port < num_output_ports; port++) {
        uhd::stream_cmd_t cmd_cont(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_START_CONTINUOUS);
        cmd_cont.stream_now = false;
        test_replay->issue_stream_cmd(cmd_cont, port);

        const uint32_t cmd_time_mask = 1 << 31;
        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_CONTINUOUS | cmd_time_mask);

        const uint64_t num_ticks = 0;
        const uint32_t reg_cmd_time =
            get_addr(replay_block_control::REG_PLAY_CMD_TIME_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_cmd_time], num_ticks & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_cmd_time + 4], (num_ticks >> 32) & 0xFFFFFFFF);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        uhd::stream_cmd_t cmd_finite(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_NUM_SAMPS_AND_DONE);
        const uint64_t num_words = 0x00123ABC;
        cmd_finite.num_samps     = num_words * word_size / 4;
        cmd_finite.stream_now    = false;
        test_replay->issue_stream_cmd(cmd_finite, port);

        const uint32_t cmd_time_mask = 1 << 31;
        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_FINITE | cmd_time_mask);
        // PLAY_CMD_FINITE writes the number of words to hardware
        const uint32_t reg_num_words =
            get_addr(replay_block_control::REG_PLAY_CMD_NUM_WORDS_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_num_words], num_words & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_num_words + 4], (num_words >> 32) & 0xFFFFFFFF);

        const uint64_t num_ticks = 0;
        const uint32_t reg_cmd_time =
            get_addr(replay_block_control::REG_PLAY_CMD_TIME_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_cmd_time], num_ticks & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_cmd_time + 4], (num_ticks >> 32) & 0xFFFFFFFF);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        uhd::stream_cmd_t cmd_finite(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_NUM_SAMPS_AND_MORE);
        const uint64_t num_words = 0x00DEF456;
        cmd_finite.num_samps     = num_words * word_size / 4;
        cmd_finite.stream_now    = false;
        test_replay->issue_stream_cmd(cmd_finite, port);

        const uint32_t cmd_time_mask = 1 << 31;
        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_FINITE | cmd_time_mask);
        // PLAY_CMD_FINITE writes the number of words to hardware
        const uint32_t reg_num_words =
            get_addr(replay_block_control::REG_PLAY_CMD_NUM_WORDS_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_num_words], num_words & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_num_words + 4], (num_words >> 32) & 0xFFFFFFFF);

        const uint64_t num_ticks = 0;
        const uint32_t reg_cmd_time =
            get_addr(replay_block_control::REG_PLAY_CMD_TIME_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_cmd_time], num_ticks & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_cmd_time + 4], (num_ticks >> 32) & 0xFFFFFFFF);
    }
}

/*
 * This test case ensures that the hardware is programmed correctly when a stop command is
 * issued to the replay block via an API call.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_stop, replay_block_fixture)
{
    for (size_t port = 0; port < num_output_ports; port++) {
        test_replay->stop(port);

        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_stream_cmd], replay_block_control::PLAY_CMD_STOP);
    }
}

/*
 * This test case ensures that the hardware is programmed correctly when the record buffer
 * is configured. This includes testing that a record restart takes place. The test
 * also ensures that any configuration of the base address and buffer size are word
 * addressable. Additionally, it exercises the get_play_size() and
 * get_play_offset() API calls.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_play, replay_block_fixture)
{
    // Configure play buffer
    for (size_t port = 0; port < num_output_ports; port++) {
        const uint64_t base_addr = word_size + max_buffer_size / num_output_ports * port;
        const uint64_t buffer_size = max_buffer_size / num_output_ports / 2;
        test_replay->play(base_addr, buffer_size, port);

        const uint32_t reg_play_buff_size =
            get_addr(replay_block_control::REG_PLAY_BUFFER_SIZE_LO_ADDR, port);
        const uint32_t reg_play_base_addr =
            get_addr(replay_block_control::REG_PLAY_BASE_ADDR_LO_ADDR, port);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_play_buff_size], buffer_size & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_buff_size + 4],
            (buffer_size >> 32) & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_play_base_addr], base_addr & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_play_base_addr + 4],
            (base_addr >> 32) & 0xFFFFFFFF);

        BOOST_CHECK_EQUAL(test_replay->get_play_size(port), buffer_size);
        BOOST_CHECK_EQUAL(test_replay->get_play_offset(port), base_addr);

        // Valid base address and buffer size values are multiples of the word size
        for (uint64_t offset = 1; offset <= word_size - 1; offset++) {
            BOOST_CHECK_THROW(test_replay->play(base_addr + offset, buffer_size, port),
                uhd::value_error);
        }
        for (uint64_t offset = 1; offset <= word_size - 1; offset++) {
            BOOST_CHECK_THROW(test_replay->play(base_addr, buffer_size + offset, port),
                uhd::value_error);
        }
        // Valid base address and buffer size values are multiples of the item size for
        // playback
        size_t item_size = test_replay->get_play_item_size(port);
        for (uint64_t offset = 1; offset <= item_size - 1; offset++) {
            BOOST_CHECK_THROW(test_replay->play(base_addr + offset, buffer_size, port),
                uhd::value_error);
        }
        for (uint64_t offset = 1; offset <= item_size - 1; offset++) {
            BOOST_CHECK_THROW(test_replay->play(base_addr, buffer_size + offset, port),
                uhd::value_error);
        }
        // The play buffer must be within the bounds of the Replay memory
        BOOST_CHECK_THROW(
            test_replay->play(max_buffer_size, buffer_size, port), uhd::value_error);
        BOOST_CHECK_THROW(
            test_replay->play(base_addr, max_buffer_size, port), uhd::value_error);
    }

    // Non-timed commands
    for (size_t port = 0; port < num_output_ports; port++) {
        const uint64_t base_addr =
            2 * word_size + max_buffer_size / num_output_ports * port;
        const uint64_t buffer_size = max_buffer_size / num_output_ports / 4;
        uhd::stream_cmd_t cmd_cont(
            uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_START_CONTINUOUS);
        test_replay->play(base_addr, buffer_size, port, 0.0, true);

        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_CONTINUOUS);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        const uint64_t base_addr =
            3 * word_size + max_buffer_size / num_output_ports * port;
        const uint64_t buffer_size = 0x1230;
        test_replay->play(base_addr, buffer_size, port);

        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_FINITE);
        // PLAY_CMD_FINITE writes the number of words to hardware
        const uint64_t num_words = buffer_size / word_size;
        const uint32_t reg_num_words =
            get_addr(replay_block_control::REG_PLAY_CMD_NUM_WORDS_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_num_words], num_words & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_num_words + 4], (num_words >> 32) & 0xFFFFFFFF);
    }

    // Timed Commands
    for (size_t port = 0; port < num_output_ports; port++) {
        const uint64_t base_addr =
            4 * word_size + max_buffer_size / num_output_ports * port;
        const uint64_t buffer_size = max_buffer_size / num_output_ports / 4;
        test_replay->play(base_addr, buffer_size, port, 1.23, true);

        const uint32_t cmd_time_mask = 1 << 31;
        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_CONTINUOUS | cmd_time_mask);

        const uint64_t num_ticks = 0;
        const uint32_t reg_cmd_time =
            get_addr(replay_block_control::REG_PLAY_CMD_TIME_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_cmd_time], num_ticks & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_cmd_time + 4], (num_ticks >> 32) & 0xFFFFFFFF);
    }

    for (size_t port = 0; port < num_output_ports; port++) {
        const uint64_t base_addr =
            5 * word_size + max_buffer_size / num_output_ports * port;
        const uint64_t buffer_size = 0xABC0;
        test_replay->play(base_addr, buffer_size, port, 4.56, false);

        const uint32_t cmd_time_mask = 1 << 31;
        const uint32_t reg_stream_cmd =
            get_addr(replay_block_control::REG_PLAY_CMD_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_stream_cmd],
            replay_block_control::PLAY_CMD_FINITE | cmd_time_mask);
        // PLAY_CMD_FINITE writes the number of words to hardware
        const uint64_t num_words = buffer_size / word_size;
        const uint32_t reg_num_words =
            get_addr(replay_block_control::REG_PLAY_CMD_NUM_WORDS_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_num_words], num_words & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_num_words + 4], (num_words >> 32) & 0xFFFFFFFF);

        const uint64_t num_ticks = 0;
        const uint32_t reg_cmd_time =
            get_addr(replay_block_control::REG_PLAY_CMD_TIME_LO_ADDR, port);
        BOOST_CHECK_EQUAL(reg_iface->write_memory[reg_cmd_time], num_ticks & 0xFFFFFFFF);
        BOOST_CHECK_EQUAL(
            reg_iface->write_memory[reg_cmd_time + 4], (num_ticks >> 32) & 0xFFFFFFFF);
    }
}

/*
 * This test case ensures that the Replay Block can be added to an RFNoC graph.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_graph, replay_block_fixture)
{
    detail::graph_t graph{};
    detail::graph_t::graph_edge_t edge_port_info;
    edge_port_info.src_port                    = 0;
    edge_port_info.dst_port                    = 0;
    edge_port_info.property_propagation_active = true;
    edge_port_info.edge                        = detail::graph_t::graph_edge_t::DYNAMIC;

    mock_radio_node_t mock_radio_block{0};
    mock_ddc_node_t mock_ddc_block{};
    mock_terminator_t mock_sink_term(1, {}, "MOCK_SINK");

    UHD_LOG_INFO("TEST", "Priming mock block properties");
    node_accessor.init_props(&mock_radio_block);
    node_accessor.init_props(&mock_ddc_block);
    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_radio_block, &mock_ddc_block, edge_port_info);
    graph.connect(&mock_ddc_block, test_replay.get(), edge_port_info);
    graph.connect(test_replay.get(), &mock_sink_term, edge_port_info);
    UHD_LOG_INFO("TEST", "Committing graph...");
    graph.commit();
    UHD_LOG_INFO("TEST", "Commit complete.");
}

/*
 * This test case ensures that the Replay Block can be added to an RFNoC graph
 * in a loop.
 */
BOOST_FIXTURE_TEST_CASE(replay_test_graph_loop, replay_block_fixture)
{
    detail::graph_t graph{};
    detail::graph_t::graph_edge_t edge_port_info;
    edge_port_info.src_port                    = 0;
    edge_port_info.dst_port                    = 0;
    edge_port_info.property_propagation_active = false;
    edge_port_info.edge                        = detail::graph_t::graph_edge_t::DYNAMIC;

    // Now create a DDC block
    UHD_LOG_DEBUG("TEST", "Making DDC block control....");
    node_accessor_t node_accessor{};
    constexpr uint32_t num_hb  = 2;
    constexpr uint32_t max_cic = 128;
    constexpr size_t num_chans = 1;
    constexpr noc_id_t noc_id  = DDC_BLOCK;
    auto block_container =
        get_mock_block(noc_id, num_chans, num_chans, uhd::device_addr_t(""));
    auto& ddc_reg_iface = block_container.reg_iface;
    ddc_reg_iface->read_memory[ddc_block_control::RB_COMPAT_NUM] =
        (ddc_block_control::MAJOR_COMPAT << 16) | ddc_block_control::MINOR_COMPAT;
    ddc_reg_iface->read_memory[ddc_block_control::RB_NUM_HB]        = num_hb;
    ddc_reg_iface->read_memory[ddc_block_control::RB_CIC_MAX_DECIM] = max_cic;
    auto test_ddc = block_container.get_block<ddc_block_control>();

    node_accessor.init_props(test_ddc.get());
    UHD_LOG_DEBUG("TEST", "DDC done.");

    UHD_LOG_INFO("TEST", "Creating graph...");
    graph.connect(test_ddc.get(), test_replay.get(), edge_port_info);
    // Graph must be DAG, disable prop prop on back-edge (normally,
    // rfnoc_graph::connect() would do this for us if we declare a back-edge
    edge_port_info.property_propagation_active = true;
    graph.connect(test_replay.get(), test_ddc.get(), edge_port_info);
    UHD_LOG_INFO("TEST", "Committing graph...");
    graph.commit();
    UHD_LOG_INFO("TEST", "Commit complete.");
}