#include "message_bus/pipeline/stages/1_framers/ModbusRtuFramer.h"
#include "plugins/protocols/modbus_rtu/framer/ModbusRtuFraming.h"

#include <QtTest/QtTest>

#include <cstring>
#include <vector>

using softbus::core::memory::MemoryPool;
using softbus::message_bus::pipeline::FrameKind;
using softbus::message_bus::pipeline::PipelineContext;
using softbus::message_bus::pipeline::stages::framers::ModbusRtuFramer;
using softbus::message_bus::pipeline::stages::framers::ModbusRtuFramerConfig;
using softbus::message_bus::pipeline::protocol::modbusrtu::crc16;

namespace
{

void appendCrcLe(std::vector<std::uint8_t>& v)
{
    const std::uint16_t c = crc16(v.data(), v.size());
    v.push_back(static_cast<std::uint8_t>(c & 0xFF));
    v.push_back(static_cast<std::uint8_t>(c >> 8));
}

MemoryPool::BlockPtr makeBlock(MemoryPool& pool, const std::vector<std::uint8_t>& bytes)
{
    auto blk = pool.allocate(bytes.size());
    if (!blk || !blk->data) {
        QTest::qFail("pool.allocate failed", __FILE__, __LINE__);
        return {};
    }
    std::memcpy(blk->data, bytes.data(), bytes.size());
    blk->size = bytes.size();
    return blk;
}

} // namespace

class ModbusRtuFramerTest : public QObject
{
    Q_OBJECT

private slots:
    void half_then_rest_emits_one_frame();
    void sticky_two_frames_in_one_chunk();
    void crc_error_triggers_resync_and_recovery();
};

void ModbusRtuFramerTest::half_then_rest_emits_one_frame()
{
    auto                    pool = std::make_shared<MemoryPool>(512, 64);
    ModbusRtuFramer         framer(pool, ModbusRtuFramerConfig{}, QStringLiteral("/ttyUSB0"));
    std::vector<std::uint8_t> a{0x01, 0x03, 0x04, 0x12, 0x34, 0x56, 0x78};
    appendCrcLe(a);

    std::vector<std::uint8_t> part1(a.begin(), a.begin() + 4);
    std::vector<std::uint8_t> part2(a.begin() + 4, a.end());

    PipelineContext c1;
    c1.endpoint     = QStringLiteral("/ttyUSB0");
    c1.protocolHint = QStringLiteral("modbus_rtu");
    c1.frameKind    = FrameKind::StreamChunk;
    c1.payload.block = makeBlock(*pool, part1);
    c1.payload.offset = 0;
    c1.payload.length = part1.size();
    c1.payloadSize      = part1.size();

    std::vector<PipelineContext> out;
    framer.feed(c1, out);
    QCOMPARE(int(out.size()), 0);

    PipelineContext c2;
    c2.endpoint      = QStringLiteral("/ttyUSB0");
    c2.protocolHint  = QStringLiteral("modbus_rtu");
    c2.frameKind     = FrameKind::StreamChunk;
    c2.payload.block = makeBlock(*pool, part2);
    c2.payload.offset  = 0;
    c2.payload.length  = part2.size();
    c2.payloadSize       = part2.size();
    framer.feed(c2, out);
    QCOMPARE(int(out.size()), 1);
    QCOMPARE(out[0].frameKind, FrameKind::CompleteFrame);
    QCOMPARE(out[0].payloadSize, a.size());
}

void ModbusRtuFramerTest::sticky_two_frames_in_one_chunk()
{
    auto              pool = std::make_shared<MemoryPool>(512, 64);
    ModbusRtuFramer   framer(pool, ModbusRtuFramerConfig{}, QStringLiteral("/ttyUSB0"));
    std::vector<std::uint8_t> f;
    f.insert(f.end(), {0x01, 0x03, 0x02, 0x00, 0x01});
    appendCrcLe(f);
    std::vector<std::uint8_t> g;
    g.insert(g.end(), {0x02, 0x03, 0x04, 0xAA, 0xBB, 0xCC, 0xDD});
    appendCrcLe(g);
    std::vector<std::uint8_t> both;
    both.insert(both.end(), f.begin(), f.end());
    both.insert(both.end(), g.begin(), g.end());

    PipelineContext c;
    c.endpoint      = QStringLiteral("/ttyUSB0");
    c.protocolHint  = QStringLiteral("modbus_rtu");
    c.frameKind     = FrameKind::StreamChunk;
    c.payload.block = makeBlock(*pool, both);
    c.payload.offset  = 0;
    c.payload.length  = both.size();
    c.payloadSize       = both.size();

    std::vector<PipelineContext> out;
    framer.feed(c, out);
    QCOMPARE(int(out.size()), 2);
}

void ModbusRtuFramerTest::crc_error_triggers_resync_and_recovery()
{
    auto              pool = std::make_shared<MemoryPool>(512, 64);
    ModbusRtuFramer   framer(pool, ModbusRtuFramerConfig{}, QStringLiteral("/ttyUSB0"));
    std::vector<std::uint8_t> bad{0x01, 0x03, 0x02, 0x00, 0x00, 0xDE, 0xAD};
    std::vector<std::uint8_t> good;
    good.insert(good.end(), {0x01, 0x03, 0x02, 0x12, 0x34});
    appendCrcLe(good);

    std::vector<std::uint8_t> stream;
    stream.insert(stream.end(), bad.begin(), bad.end());
    stream.insert(stream.end(), good.begin(), good.end());

    PipelineContext c;
    c.endpoint      = QStringLiteral("/ttyUSB0");
    c.protocolHint  = QStringLiteral("modbus_rtu");
    c.frameKind     = FrameKind::StreamChunk;
    c.payload.block = makeBlock(*pool, stream);
    c.payload.offset  = 0;
    c.payload.length  = stream.size();
    c.payloadSize       = stream.size();

    std::vector<PipelineContext> out;
    framer.feed(c, out);
    QVERIFY(!out.empty());
    QCOMPARE(out[0].payloadSize, good.size());
}

QTEST_MAIN(ModbusRtuFramerTest)
#include "modbus_rtu_framer_test.moc"
