// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
// SPDX-License-Identifier: GPL-3.0-or-later

/** \file
 * \brief libFuzzer harness for the .panda deserializer.
 *
 * Drives the same code path as File > Open: header read, embedded-IC blob
 * registry parsing, the element/connection stream loop in
 * Serialization::deserialize(), and the Scene::addItem() side effects that
 * follow.  Run under ASan + UBSan to catch the H2/HC family of bugs on inputs
 * no human would hand-write.  See Tests/Fuzz/README.md for build and run
 * instructions.
 *
 * Custom mutator (LLVMFuzzerCustomMutator):
 *   libFuzzer's default byte-level mutations are too coarse for the .panda
 *   binary format.  Random bit-flips almost always corrupt the WPCF magic or
 *   a length field, causing an early throw before reaching interesting code.
 *   The custom mutator applies one structure-aware operation per call so that
 *   the corpus stays parseable while still reaching deep code paths:
 *
 *     - patch_version:    replace the 8-byte version field with a known-valid one
 *     - patch_map_key:    splice a known map key (UTF-16-BE with length prefix)
 *                         at a random position inside the payload
 *     - patch_type_id:    splice a known QMetaType wire typeId at a random position
 *     - patch_element_type: replace one element-type string inside the payload
 *     - fallback:         call LLVMFuzzerMutate (libFuzzer's own mutator)
 *
 *   The mutator operates on raw bytes so it is safe even when the input does
 *   not parse — it simply writes into whatever slice it picked.
 */

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <random>

#include <QApplication>
#include <QByteArray>
#include <QCoreApplication>
#include <QDataStream>
#include <QEvent>
#include <QScopeGuard>
#include <QString>
#include <QVersionNumber>

#include "App/Core/Application.h"
#include "App/Core/Common.h"
#include "App/IO/Serialization.h"
#include "App/Scene/Scene.h"
#include "App/Scene/Workspace.h"
#include "App/Simulation/Simulation.h"

namespace {

QApplication *g_app = nullptr;
int  g_fuzzer_argc = 0;
char **g_fuzzer_argv = nullptr;

// ---- Known byte sequences for structure-aware mutation ----

// Version field: QList<int>{major,minor} = quint32(2) + quint32(maj) + quint32(min)
// (the version count is always 2 for wiRedPanda files)
struct VersionPatch { uint8_t bytes[12]; };
constexpr VersionPatch kVersions[] = {
    {{0,0,0,2, 0,0,0,1, 0,0,0,1}},  // 1.1  (old format: hasClock)
    {{0,0,0,2, 0,0,0,1, 0,0,0,6}},  // 1.6  (old format: hasDisplay7Color)
    {{0,0,0,2, 0,0,0,2, 0,0,0,4}},  // 2.4  (old format: hasAudio)
    {{0,0,0,2, 0,0,0,3, 0,0,0,0}},  // 3.0  (old format: hasDolphinFilename)
    {{0,0,0,2, 0,0,0,3, 0,0,0,3}},  // 3.3  (old format: hasDolphinSentinelFix)
    {{0,0,0,2, 0,0,0,4, 0,0,0,0}},  // 4.0  (old format: just before QMap)
    {{0,0,0,2, 0,0,0,4, 0,0,0,1}},  // 4.1  (new format: first QMap version)
    {{0,0,0,2, 0,0,0,4, 0,0,0,4}},  // 4.4  (hasWirelessMode)
    {{0,0,0,2, 0,0,0,4, 0,0,0,7}},  // 4.7  (hasConnectionQMap)
    {{0,0,0,2, 0,0,0,5, 0,0,0,0}},  // 5.0
    {{0,0,0,2, 0,0,0,5, 0,0,0,1}},  // 5.1  (hasVersionedBlobRegistry)
};

// Item type discriminators (quint32 big-endian) for Serialization::deserialize() switch
// GraphicElement::Type = QGraphicsItem::UserType+3 = 65539 = 0x00010003
// QNEConnection::Type  = QGraphicsItem::UserType+2 = 65538 = 0x00010002
constexpr uint8_t kItemTypes[][4] = {
    {0,1,0,3},  // GraphicElement::Type
    {0,1,0,2},  // QNEConnection::Type
};

// Known QMetaType wire typeIds (4 bytes, big-endian) used in property maps
constexpr uint8_t kTypeIds[][4] = {
    {0,0,0, 1},   // Bool
    {0,0,0, 2},   // Int
    {0,0,0, 5},   // ULongLong
    {0,0,0, 6},   // Double
    {0,0,0,10},   // QString
    {0,0,0,11},   // QStringList
    {0,0,0,12},   // QByteArray
    {0,0,0,13},   // QBitArray
    {0,0,0,26},   // QPointF
    {0,0,0,75},   // QKeySequence (Qt_5_12 wire)
    {0,0,16,11},  // QKeySequence (Qt 6 native = 4107)
};

// Map keys as UTF-16-BE with quint32 byte-length prefix: {4-byte len}{utf16 bytes}
// Generated by: python3 -c "
//   keys = ['pos','rotation','label','trigger','isOn','locked',
//           'color','frequency','delay','name','serialId','skinName']
//   for k in keys:
//       b = k.encode('utf-16-be')
//       print(f'  // \"{k}\"')
//       vals = ','.join(f'0x{x:02x}' for x in [0,0,0,len(b)]+list(b))
//       print(f'  {{{vals}}},')"
constexpr uint8_t kMapKeys[][32] = {
    // "pos"  (6 bytes of UTF-16)
    {0,0,0,6, 0,'p',0,'o',0,'s'},
    // "rotation"  (16 bytes)
    {0,0,0,16, 0,'r',0,'o',0,'t',0,'a',0,'t',0,'i',0,'o',0,'n'},
    // "label"  (10 bytes)
    {0,0,0,10, 0,'l',0,'a',0,'b',0,'e',0,'l'},
    // "trigger"  (14 bytes)
    {0,0,0,14, 0,'t',0,'r',0,'i',0,'g',0,'g',0,'e',0,'r'},
    // "isOn"  (8 bytes)
    {0,0,0,8, 0,'i',0,'s',0,'O',0,'n'},
    // "locked"  (12 bytes)
    {0,0,0,12, 0,'l',0,'o',0,'c',0,'k',0,'e',0,'d'},
    // "color"  (10 bytes)
    {0,0,0,10, 0,'c',0,'o',0,'l',0,'o',0,'r'},
    // "frequency"  (18 bytes)
    {0,0,0,18, 0,'f',0,'r',0,'e',0,'q',0,'u',0,'e',0,'n',0,'c',0,'y'},
    // "delay"  (10 bytes)
    {0,0,0,10, 0,'d',0,'e',0,'l',0,'a',0,'y'},
    // "name"  (8 bytes)
    {0,0,0,8, 0,'n',0,'a',0,'m',0,'e'},
    // "serialId"  (16 bytes)
    {0,0,0,16, 0,'s',0,'e',0,'r',0,'i',0,'a',0,'l',0,'I',0,'d'},
    // "skinName"  (16 bytes)
    {0,0,0,16, 0,'s',0,'k',0,'i',0,'n',0,'N',0,'a',0,'m',0,'e'},
    // "ptr"  (6 bytes)
    {0,0,0,6, 0,'p',0,'t',0,'r'},
    // "blobName"  (16 bytes)
    {0,0,0,16, 0,'b',0,'l',0,'o',0,'b',0,'N',0,'a',0,'m',0,'e'},
    // "startPortId"  (22 bytes)
    {0,0,0,22, 0,'s',0,'t',0,'a',0,'r',0,'t',0,'P',0,'o',0,'r',0,'t',0,'I',0,'d'},
    // "endPortId"  (18 bytes)
    {0,0,0,18, 0,'e',0,'n',0,'d',0,'P',0,'o',0,'r',0,'t',0,'I',0,'d'},
    // "flippedX"  (16 bytes)
    {0,0,0,16, 0,'f',0,'l',0,'i',0,'p',0,'p',0,'e',0,'d',0,'X'},
    // "flippedY"  (16 bytes)
    {0,0,0,16, 0,'f',0,'l',0,'i',0,'p',0,'p',0,'e',0,'d',0,'Y'},
    // "wirelessMode"  (24 bytes)
    {0,0,0,24, 0,'w',0,'i',0,'r',0,'e',0,'l',0,'e',0,'s',0,'s',0,'M',0,'o',0,'d',0,'e'},
    // "currentPort"  (22 bytes)
    {0,0,0,22, 0,'c',0,'u',0,'r',0,'r',0,'e',0,'n',0,'t',0,'P',0,'o',0,'r',0,'t'},
    // "audiobox"  (16 bytes)
    {0,0,0,16, 0,'a',0,'u',0,'d',0,'i',0,'o',0,'b',0,'o',0,'x'},
    // "volume"  (12 bytes)
    {0,0,0,12, 0,'v',0,'o',0,'l',0,'u',0,'m',0,'e'},
    // "key"  (6 bytes) — TruthTable output bit-array
    {0,0,0,6, 0,'k',0,'e',0,'y'},
};

// Element type patches as quint64 big-endian (8 bytes each).
std::vector<std::vector<uint8_t>> g_elemTypePatches;

// Element sequence patches: 4-byte GraphicElement::Type + 8-byte ElementType.
// Splicing a 12-byte sequence at once creates a valid element header that
// guides the fuzzer to the element-loading code path in one mutation step.
std::vector<std::vector<uint8_t>> g_elemSeqPatches;

// Appearances list entry: quint32(count=1) + QMap<"skinName": "test"> in Qt_5_12 wire format.
//
// Wire layout (45 bytes):
//   [0..3]   quint32(1)      — appearances count = 1
//   [4..7]   quint32(1)      — QMap size = 1 entry
//   [8..11]  quint32(16)     — "skinName" UTF-16 byte-length
//   [12..27] UTF-16-BE       — "skinName"
//   [28..31] quint32(10)     — QVariant typeId = QString
//   [32]     0x00            — isNull = false
//   [33..36] quint32(8)      — "test" UTF-16 byte-length
//   [37..44] UTF-16-BE       — "test"
//
// Splicing this sequence at the right position (after the output port list) exercises
// the skinName reading path in GraphicElement::loadNewFormat (lines 338-342 of
// GraphicElementSerializer.cpp) which was dark in all raw-mutation corpus entries.
constexpr uint8_t kAppearanceEntry[] = {
    0,0,0,1,                                          // appearances count = 1
    0,0,0,1,                                          // QMap size = 1 entry
    0,0,0,16,                                         // "skinName" UTF-16 byte-length
    0,'s',0,'k',0,'i',0,'n',0,'N',0,'a',0,'m',0,'e', // "skinName" as UTF-16-BE
    0,0,0,10,                                         // QVariant typeId = QString (10)
    0,                                                // isNull = false
    0,0,0,8,                                          // "test" UTF-16 byte-length
    0,'t',0,'e',0,'s',0,'t',                          // "test" as UTF-16-BE
};

void buildElemTypePatches()
{
    // All valid ElementType enum values
    static const quint64 etypes[] = {
        1,  // InputButton
        2,  // InputSwitch
        3,  // Led
        4,  // Not
        5,  // And
        6,  // Or
        7,  // Nand
        8,  // Nor
        9,  // Clock
        10, // Xor
        11, // Xnor
        12, // InputVcc
        13, // InputGnd
        14, // Display7
        15, // DLatch
        16, // JKLatch
        17, // DFlipFlop
        18, // JKFlipFlop
        19, // SRFlipFlop
        20, // TFlipFlop
        21, // InputRotary
        22, // IC
        23, // Node
        24, // Mux
        25, // Demux
        26, // Buzzer
        27, // Display14
        28, // Text
        29, // Line
        30, // TruthTable
        31, // AudioBox
        32, // SRLatch
        33, // Display16
    };
    for (quint64 v : etypes) {
        // 8-byte quint64 element-type patch
        std::vector<uint8_t> patch(8);
        patch[0] = uint8_t(v >> 56); patch[1] = uint8_t(v >> 48);
        patch[2] = uint8_t(v >> 40); patch[3] = uint8_t(v >> 32);
        patch[4] = uint8_t(v >> 24); patch[5] = uint8_t(v >> 16);
        patch[6] = uint8_t(v >>  8); patch[7] = uint8_t(v);
        g_elemTypePatches.push_back(patch);

        // 12-byte sequence: GraphicElement::Type (0x00010003) + ElementType quint64
        // Splicing this creates a complete element header in one mutation step.
        std::vector<uint8_t> seq(12);
        seq[0] = 0; seq[1] = 1; seq[2] = 0; seq[3] = 3; // GraphicElement::Type = 65539
        seq[4]  = patch[0]; seq[5]  = patch[1]; seq[6]  = patch[2]; seq[7]  = patch[3];
        seq[8]  = patch[4]; seq[9]  = patch[5]; seq[10] = patch[6]; seq[11] = patch[7];
        g_elemSeqPatches.push_back(std::move(seq));
    }
}

// Splice `patch` (of `patchLen` bytes) at position `pos` inside [Data, Data+Size],
// without exceeding MaxSize.  Returns new size.
size_t splice(uint8_t *Data, size_t Size, size_t MaxSize,
              size_t pos, const uint8_t *patch, size_t patchLen)
{
    if (pos > Size || patchLen == 0) return Size;
    const size_t tail   = Size - pos;
    const size_t newEnd = std::min(pos + patchLen + tail, MaxSize);
    // Shift tail right to make room (may truncate if MaxSize is tight)
    const size_t keep = std::min(tail, newEnd - std::min(pos + patchLen, newEnd));
    if (pos + patchLen < newEnd)
        std::memmove(Data + pos + patchLen, Data + pos, keep);
    const size_t copy = std::min(patchLen, newEnd - pos);
    std::memcpy(Data + pos, patch, copy);
    return newEnd;
}

} // namespace

// libFuzzer calls this to mutate one corpus entry.
extern "C" size_t LLVMFuzzerCustomMutator(
    uint8_t *Data, size_t Size, size_t MaxSize, unsigned int Seed)
{
    // libFuzzer is declared (but not defined) here so we can call it.
    extern size_t LLVMFuzzerMutate(uint8_t *, size_t, size_t);

    if (Size == 0 || MaxSize == 0)
        return LLVMFuzzerMutate(Data, Size, MaxSize);

    std::mt19937 rng(Seed);
    const int op = std::uniform_int_distribution<int>(0, 8)(rng);

    switch (op) {
    case 0: { // patch_version: write a known version field at byte 4
        const auto &vp = kVersions[rng() % std::size(kVersions)];
        const size_t patchOff = 4; // right after "WPCF"
        if (Size >= patchOff + sizeof(vp.bytes) && MaxSize >= patchOff + sizeof(vp.bytes)) {
            std::memcpy(Data + patchOff, vp.bytes, sizeof(vp.bytes));
            return Size;
        }
        break;
    }
    case 1: { // patch_map_key: splice a known key at a random byte offset
        const auto &key = kMapKeys[rng() % std::size(kMapKeys)];
        // Find how long this key is (4 byte-len header + key bytes)
        const uint32_t keyDataLen = (uint32_t(key[0])<<24)|(uint32_t(key[1])<<16)
                                   |(uint32_t(key[2])<<8)|key[3];
        const size_t totalLen = 4 + keyDataLen;
        const size_t pos = Size > 1 ? std::uniform_int_distribution<size_t>(0, Size-1)(rng) : 0;
        return splice(Data, Size, MaxSize, pos, key, totalLen);
    }
    case 2: { // patch_type_id: splice a known QMetaType typeId at a random position
        const auto *tid = kTypeIds[rng() % std::size(kTypeIds)];
        const size_t pos = Size > 1 ? std::uniform_int_distribution<size_t>(0, Size-1)(rng) : 0;
        return splice(Data, Size, MaxSize, pos, tid, 4);
    }
    case 3: { // patch_element_type: splice a valid ElementType as quint64 big-endian
        if (!g_elemTypePatches.empty()) {
            const auto &ep = g_elemTypePatches[rng() % g_elemTypePatches.size()];
            const size_t pos = Size > 1 ? std::uniform_int_distribution<size_t>(0, Size-1)(rng) : 0;
            return splice(Data, Size, MaxSize, pos, ep.data(), ep.size());
        }
        break;
    }
    case 4: { // patch_header: write WPCF magic + version at byte 0 (whole preamble)
        const auto &vp = kVersions[rng() % std::size(kVersions)];
        // Full preamble: 4-byte magic + 12-byte version = 16 bytes
        constexpr uint8_t magic[4] = {0x57, 0x50, 0x43, 0x46}; // "WPCF"
        if (MaxSize >= 16) {
            const size_t newSize = std::max(Size, size_t(16));
            if (newSize > Size && newSize <= MaxSize) {
                std::memset(Data + Size, 0, newSize - Size);
            }
            std::memcpy(Data, magic, 4);
            std::memcpy(Data + 4, vp.bytes, 12);
            return std::max(newSize, Size);
        }
        break;
    }
    case 5: { // patch_item_type: splice a GraphicElement or QNEConnection type discriminator
        // These 4-byte big-endian values are the switch keys in Serialization::deserialize().
        // Splicing them into the payload helps the fuzzer discover the element/connection
        // loading code paths from raw-byte mutations.
        const auto *it = kItemTypes[rng() % std::size(kItemTypes)];
        const size_t pos = Size > 1 ? std::uniform_int_distribution<size_t>(0, Size-1)(rng) : 0;
        return splice(Data, Size, MaxSize, pos, it, 4);
    }
    case 6: { // patch_element_seq: splice 12-byte GraphicElement::Type + ElementType quint64
        // A single mutation that creates a complete element header (type discriminator
        // + element type) so the fuzzer reaches element-loading code in one step.
        if (!g_elemSeqPatches.empty()) {
            const auto &seq = g_elemSeqPatches[rng() % g_elemSeqPatches.size()];
            const size_t pos = Size > 1 ? std::uniform_int_distribution<size_t>(0, Size-1)(rng) : 0;
            return splice(Data, Size, MaxSize, pos, seq.data(), seq.size());
        }
        break;
    }
    case 7: { // patch_appearance_entry: splice a valid appearances list entry with a skinName key
        // Splicing this 45-byte sequence at the position right after the output port list
        // exercises the skinName reading path in GraphicElement::loadNewFormat.
        const size_t pos = Size > 1 ? std::uniform_int_distribution<size_t>(0, Size-1)(rng) : 0;
        return splice(Data, Size, MaxSize, pos, kAppearanceEntry, sizeof(kAppearanceEntry));
    }
    default:
        break;
    }

    // fallback: let libFuzzer do its own mutation
    return LLVMFuzzerMutate(Data, Size, MaxSize);
}

extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv)
{
    qputenv("QT_QPA_PLATFORM", "offscreen");
    qputenv("QT_IM_MODULES", "none");

    Comment::setVerbosity(-1);
    Application::interactiveMode = false;
    Application::migrationEnabled = false;

    g_fuzzer_argc = *argc;
    g_fuzzer_argv = *argv;
    g_app = new QApplication(g_fuzzer_argc, g_fuzzer_argv);

    buildElemTypePatches();

    return 0;
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    // Drain Qt's deferred-delete queue on every exit path.  Audio elements
    // (Buzzer, AudioBox) queue DeferredDelete events for the audio backend's
    // internal QObject helpers when destroyed; without spinning the event
    // loop, those helpers stay alive and hold PipeWire stream FDs open
    // across iterations, exhausting the audio subsystem after ~1000 inputs.
    auto drainQtEvents = qScopeGuard([] {
        QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
        QCoreApplication::processEvents();
    });

    const QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<const char *>(data),
                                                      static_cast<int>(size));
    QDataStream stream(buffer);

    QVersionNumber version;
    try {
        version = Serialization::readPandaHeader(stream);
    } catch (...) {
        return 0;
    }

    WorkSpace workspace;
    try {
        workspace.load(stream, version, /*contextDir=*/QString());
    } catch (...) {}

    workspace.scene()->simulation()->update();

    return 0;
}
