#pragma once

#include "rm_control/interfaces/tool_modbus_bus.h"

#include <cstddef>
#include <cstdint>
#include <map>
#include <string>
#include <utility>
#include <vector>

namespace rm_control::tests {

struct InputRegisterReadCall {
    std::uint8_t device_id = 0U;
    std::uint16_t start_address = 0U;
    std::size_t register_count = 0U;
};

struct HoldingRegisterReadCall {
    std::uint8_t device_id = 0U;
    std::uint16_t start_address = 0U;
    std::size_t register_count = 0U;
};

struct HoldingRegisterWriteCall {
    std::uint8_t device_id = 0U;
    std::uint16_t start_address = 0U;
    std::vector<std::uint16_t> values;
};

class MockToolModbusBus final : public interfaces::IToolModbusBus {
public:
    common::Result<interfaces::ToolModbusBusStatus> status_result =
        common::Result<interfaces::ToolModbusBusStatus>::success(
            interfaces::ToolModbusBusStatus{true, 1, 1, 115200, 3, 10});
    std::map<std::uint16_t, common::Result<std::vector<std::uint16_t>>>
        input_register_results;
    std::map<std::uint16_t, common::Result<std::uint16_t>>
        single_input_register_results;
    std::map<std::uint16_t, common::Result<std::vector<std::uint16_t>>>
        holding_register_results;
    std::map<std::uint16_t, common::Result<void>> write_results_by_start_address;
    common::Result<void> write_result = common::Result<void>::success();
    int status_calls = 0;
    int configure_calls = 0;
    interfaces::ToolModbusBusConfiguration last_configuration;
    std::vector<InputRegisterReadCall> read_calls;
    std::vector<InputRegisterReadCall> single_read_calls;
    std::vector<HoldingRegisterReadCall> holding_read_calls;
    std::vector<HoldingRegisterWriteCall> write_calls;
    std::vector<std::string> operation_calls;

    /** @copydoc interfaces::IToolModbusBus::Configure */
    common::Result<interfaces::ToolModbusBusStatus> Configure(
        const interfaces::ToolModbusBusConfiguration &configuration) override {
        ++configure_calls;
        last_configuration = configuration;
        operation_calls.push_back("configure");
        return status_result;
    }

    /** @copydoc interfaces::IToolModbusBus::GetStatus */
    common::Result<interfaces::ToolModbusBusStatus> GetStatus() override {
        ++status_calls;
        operation_calls.push_back("get_status");
        return status_result;
    }

    /** @copydoc interfaces::IToolModbusBus::ReadInputRegister */
    common::Result<std::uint16_t> ReadInputRegister(
        std::uint8_t device_id,
        std::uint16_t address) override {
        single_read_calls.push_back(InputRegisterReadCall{device_id, address, 1U});
        operation_calls.push_back("read_single_input:" + std::to_string(address));
        const auto configured = single_input_register_results.find(address);
        if (configured != single_input_register_results.end()) {
            return configured->second;
        }
        for (const auto &group : input_register_results) {
            if (!group.second.ok && address == group.first) {
                return common::Result<std::uint16_t>::failure(
                    group.second.error_message);
            }
            if (!group.second.ok || address < group.first) {
                continue;
            }
            const std::size_t offset = address - group.first;
            if (offset < group.second.value.size()) {
                return common::Result<std::uint16_t>::success(
                    group.second.value[offset]);
            }
        }
        return common::Result<std::uint16_t>::failure(
            "mock single input register was not configured");
    }

    /** @copydoc interfaces::IToolModbusBus::ReadInputRegisters */
    common::Result<std::vector<std::uint16_t>> ReadInputRegisters(
        std::uint8_t device_id,
        std::uint16_t start_address,
        std::size_t register_count) override {
        read_calls.push_back(
            InputRegisterReadCall{device_id, start_address, register_count});
        operation_calls.push_back(
            "read_input:" + std::to_string(start_address));
        const auto configured = input_register_results.find(start_address);
        if (configured == input_register_results.end()) {
            return common::Result<std::vector<std::uint16_t>>::failure(
                "mock input register group was not configured");
        }
        return configured->second;
    }

    /** @copydoc interfaces::IToolModbusBus::ReadHoldingRegisters */
    common::Result<std::vector<std::uint16_t>> ReadHoldingRegisters(
        std::uint8_t device_id,
        std::uint16_t start_address,
        std::size_t register_count) override {
        holding_read_calls.push_back(
            HoldingRegisterReadCall{device_id, start_address, register_count});
        operation_calls.push_back(
            "read_holding:" + std::to_string(start_address));
        const auto configured = holding_register_results.find(start_address);
        if (configured == holding_register_results.end()) {
            return common::Result<std::vector<std::uint16_t>>::failure(
                "mock holding register group was not configured");
        }
        return configured->second;
    }

    /** @copydoc interfaces::IToolModbusBus::WriteHoldingRegisters */
    common::Result<void> WriteHoldingRegisters(
        std::uint8_t device_id,
        std::uint16_t start_address,
        const std::vector<std::uint16_t> &values) override {
        write_calls.push_back(
            HoldingRegisterWriteCall{device_id, start_address, values});
        operation_calls.push_back(
            "write_holding:" + std::to_string(start_address));
        const auto configured = write_results_by_start_address.find(start_address);
        if (configured != write_results_by_start_address.end()) {
            return configured->second;
        }
        return write_result;
    }
};

} // namespace rm_control::tests
