// Standalone tests for reasampler::instrument_drop — no REAPER, no VST3 SDK, no framework.
// The S17 drop-and-load payload contract (S-GA-DropFX revision): the extension builds a
// Steinberg-format .vstpreset image whose 'Comp' chunk is EXACTLY what ReaSampler 9000's own
// setState (deserializeComponentState) accepts, with the dragged capture pre-selected, and
// whose header carries the channel-active class ID. The round-trip proof (build -> parse the
// container -> the instrument's OWN reader -> assert the capture selected) IS the
// cross-artifact contract guard — the same pattern assignment_request_tests uses.

#include "../src/core/wire/instrument_drop.h"
#include "../src/core/instrument/map/component_state_io.h"  // deserializeComponentState — the instrument's OWN reader

#include "version_generated.h"  // REASAMPLER_CHANNEL_IS_BETA — pins the per-channel class ID

#include <cstdio>
#include <cstdint>
#include <string>
#include <vector>

using namespace reasampler;
using namespace reasampler::wire;
using namespace reasampler::instrument::map;  // ComponentState + the codec (Q-W2v re-namespace)

static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
    std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)

// A representative project rate for the reader (the legacy-v3 conversion parameter; our
// current-version blob never consumes it, but the reader signature requires a positive rate).
static constexpr double kRate = 48000.0;

// --- A tiny independent .vstpreset reader (test-local, little-endian) ----------
// Mirrors public.sdk/source/vst/vstpresetfile.cpp's READ side so the builder is proven
// against an independent decode, not against itself.

static std::uint32_t readU32LE(const std::vector<std::uint8_t>& b, std::size_t at) {
    return static_cast<std::uint32_t>(b[at]) | (static_cast<std::uint32_t>(b[at + 1]) << 8) |
           (static_cast<std::uint32_t>(b[at + 2]) << 16) |
           (static_cast<std::uint32_t>(b[at + 3]) << 24);
}
static std::uint64_t readU64LE(const std::vector<std::uint8_t>& b, std::size_t at) {
    std::uint64_t v = 0;
    for (int i = 7; i >= 0; --i) v = (v << 8) | b[at + static_cast<std::size_t>(i)];
    return v;
}
static bool fourCCAt(const std::vector<std::uint8_t>& b, std::size_t at, const char* id) {
    return b.size() >= at + 4 && b[at] == static_cast<std::uint8_t>(id[0]) &&
           b[at + 1] == static_cast<std::uint8_t>(id[1]) &&
           b[at + 2] == static_cast<std::uint8_t>(id[2]) &&
           b[at + 3] == static_cast<std::uint8_t>(id[3]);
}

struct ParsedPreset {
    bool ok = false;
    std::string classId;
    std::vector<std::uint8_t> compChunk;
};

static ParsedPreset parsePreset(const std::vector<std::uint8_t>& b) {
    ParsedPreset p;
    if (b.size() < 48) return p;
    if (!fourCCAt(b, 0, "VST3")) return p;                       // header magic
    if (readU32LE(b, 4) != 1) return p;                          // kFormatVersion
    p.classId.assign(b.begin() + 8, b.begin() + 40);             // 32-char ASCII class ID
    const std::uint64_t listOffset = readU64LE(b, 40);
    if (listOffset + 8 > b.size()) return p;
    if (!fourCCAt(b, static_cast<std::size_t>(listOffset), "List")) return p;
    const std::uint32_t count = readU32LE(b, static_cast<std::size_t>(listOffset) + 4);
    std::size_t at = static_cast<std::size_t>(listOffset) + 8;
    for (std::uint32_t i = 0; i < count; ++i, at += 20) {
        if (at + 20 > b.size()) return p;
        const std::uint64_t off = readU64LE(b, at + 4);
        const std::uint64_t size = readU64LE(b, at + 12);
        if (off + size > b.size()) return p;
        if (fourCCAt(b, at, "Comp")) {
            p.compChunk.assign(b.begin() + static_cast<std::ptrdiff_t>(off),
                               b.begin() + static_cast<std::ptrdiff_t>(off + size));
            p.ok = true;
        }
    }
    return p;
}

// --- class-ID derivation ------------------------------------------------------

// The class-ID string is pinned PER CHANNEL to the FROZEN UID rendered as FUID::toString
// would render it (the four INLINE_UID words as %08X in order — platform-stable, see
// instrument_drop.h). These literals are derived independently from the frozen constants in
// reasampler_uid.h; if this fails, the .vstpreset would address a class the loaded VST does
// not present and TrackFX_SetPreset would refuse the drop.
static void testClassIdHexPinnedPerChannel() {
#if REASAMPLER_CHANNEL_IS_BETA
    CHECK(vstClassIdHex() == "CCFFEB3A4FF532A69E1817984256955F");
#else
    CHECK(vstClassIdHex() == "5E45A11E9C7B4D6AB1E3F2084A6C1D9F");
#endif
    CHECK(vstClassIdHex().size() == 32);
}

// --- the drop payload contract ------------------------------------------------

// THE contract test: a preset built for a capture id parses as a well-formed VST3 preset
// container addressed to the channel-active class, whose Comp chunk decodes — through the
// instrument's OWN reader — to a ComponentState with THAT id selected, no zones, default
// mono. If this fails, the drop would instantiate a blank/wrong instance.
static void testPresetRoundTripsThroughInstrumentReader() {
    const std::string id = "cap-7f3a-guid";
    const std::vector<std::uint8_t> preset = buildInstrumentDropPreset(id);
    CHECK(!preset.empty());

    const ParsedPreset p = parsePreset(preset);
    CHECK(p.ok);
    CHECK(p.classId == vstClassIdHex());
    // The Comp chunk must be EXACTLY the pre-wrap state bytes (no corruption, no framing
    // bleed into the component stream).
    CHECK(p.compChunk == instrumentDropStateBytes(id));

    const ComponentState cs = deserializeComponentState(p.compChunk, kRate);
    CHECK(cs.selectionId == id);          // the capture IS selected — the whole point
    // A drop selects one capture and leaves the parameter set at its defaults.
    CHECK(!cs.params.rootOverride && !cs.params.loopOverride && !cs.params.startPoint);
    CHECK(cs.channelMode == ChannelMode::Mono);           // fresh-instance default
    CHECK(cs.lastConsumedAssignGeneration == 0);          // fresh instance, no consumed assign
}

// Container layout invariants pinned against the reference writer's constants
// (public.sdk vstpresetfile.cpp: kHeaderSize 48, data area first, list last).
static void testPresetLayoutInvariants() {
    const std::string id = "abc";
    const std::vector<std::uint8_t> state = instrumentDropStateBytes(id);
    const std::vector<std::uint8_t> preset = buildInstrumentDropPreset(id);

    CHECK(fourCCAt(preset, 0, "VST3"));
    CHECK(readU32LE(preset, 4) == 1);
    const std::uint64_t listOffset = readU64LE(preset, 40);
    CHECK(listOffset == 48 + state.size());               // Comp data sits at offset 48
    CHECK(fourCCAt(preset, static_cast<std::size_t>(listOffset), "List"));
    CHECK(readU32LE(preset, static_cast<std::size_t>(listOffset) + 4) == 1);  // one entry
    CHECK(fourCCAt(preset, static_cast<std::size_t>(listOffset) + 8, "Comp"));
    CHECK(readU64LE(preset, static_cast<std::size_t>(listOffset) + 12) == 48);
    CHECK(readU64LE(preset, static_cast<std::size_t>(listOffset) + 20) == state.size());
    CHECK(preset.size() == static_cast<std::size_t>(listOffset) + 8 + 20);  // nothing trails
}

// A GUID-shaped id with bytes that would trip a naive delimiter-based encoder round-trips
// whole (the length-prefixed component-state framing carries arbitrary bytes).
static void testGuidLikeIdRoundTrips() {
    const std::string id = "{9A2F0C11-4B6E-4D01-8F3A-0011223344FF}";
    const ParsedPreset p = parsePreset(buildInstrumentDropPreset(id));
    CHECK(p.ok);
    const ComponentState cs = deserializeComponentState(p.compChunk, kRate);
    CHECK(cs.selectionId == id);
}

// An empty id yields the empty-state preset: it still parses cleanly to {"", no zones} — the
// S10 silent empty state. (The shell guards against dropping nothing; the pure contract holds.)
static void testEmptyIdYieldsEmptyState() {
    const ParsedPreset p = parsePreset(buildInstrumentDropPreset(""));
    CHECK(p.ok);
    CHECK(!p.compChunk.empty());  // still a versioned envelope, just an empty selection
    const ComponentState cs = deserializeComponentState(p.compChunk, kRate);
    CHECK(cs.selectionId.empty());
    CHECK(!cs.params.rootOverride && !cs.params.loopOverride && !cs.params.startPoint);
}

// Deterministic: the same id always produces the same bytes (no time/random in the path).
static void testDeterministic() {
    CHECK(buildInstrumentDropPreset("abc") == buildInstrumentDropPreset("abc"));
    CHECK(buildInstrumentDropPreset("abc") != buildInstrumentDropPreset("abd"));
}

// A malformed class ID (not exactly 32 chars) yields an empty image — contract violation,
// never a truncated/garbage preset handed to REAPER.
static void testBadClassIdRejected() {
    const std::vector<std::uint8_t> state = instrumentDropStateBytes("x");
    CHECK(buildVstPresetBytes("TOO-SHORT", state).empty());
    CHECK(buildVstPresetBytes(std::string(33, 'A'), state).empty());
    CHECK(!buildVstPresetBytes(std::string(32, 'A'), state).empty());
}

// --- Surface classification ---------------------------------------------------
//
// THE RULE (prefix-based — see instrument_drop.h): the SDK warns GetThingFromPoint "may append
// additional information", so exact-token matching (a previous, DAW-falsified predicate) is
// wrong. Ordering is load-bearing: the embed strip is matched BEFORE the track panel, and the
// track panel now claims the WHOLE "tcp*"/"mcp*" family rather than just its FX sub-elements.

using ui::ReaperSurface;

static ReaperSurface onTrack(const std::string& info) {
    return classifyReaperSurface(info, /*haveTrack=*/true);
}

// The FX chain / floating-FX windows.
static void testFxWindowIsFxSurface() {
    CHECK(onTrack("fx_chain") == ReaperSurface::FxSurface);
    CHECK(onTrack("fx_0") == ReaperSurface::FxSurface);
    CHECK(onTrack("fx_12") == ReaperSurface::FxSurface);
}

// The FX-button family within the TCP/MCP — the surface the glyph-only rule used to be limited
// to, still an instrument surface (as TrackPanel, which resolves identically).
static void testTcpMcpFxFamilyIsTrackPanel() {
    CHECK(onTrack("tcp.fx") == ReaperSurface::TrackPanel);
    CHECK(onTrack("mcp.fx") == ReaperSurface::TrackPanel);
    CHECK(onTrack("tcp.fxbyp") == ReaperSurface::TrackPanel);
    CHECK(onTrack("tcp.fxparm") == ReaperSurface::TrackPanel);
    CHECK(onTrack("mcp.fxlist") == ReaperSurface::TrackPanel);
    CHECK(onTrack("tcp.fx.1") == ReaperSurface::TrackPanel);      // appended info
    CHECK(onTrack("tcp.fx extra") == ReaperSurface::TrackPanel);  // appended info, arbitrary form
}

// THE ROOT-CAUSE FIX: the bare panel token and every non-FX sub-element are now the instrument
// hotspot too. A TCP too narrow to draw the FX button reports "tcp", which under the old
// glyph-only rule produced a cue-less no-op.
static void testWholeTrackPanelIsTheHotspot() {
    CHECK(onTrack("tcp") == ReaperSurface::TrackPanel);       // bare track control panel
    CHECK(onTrack("mcp") == ReaperSurface::TrackPanel);       // bare mixer control panel
    CHECK(onTrack("tcp.mute") == ReaperSurface::TrackPanel);  // mute button
    CHECK(onTrack("tcp.vol") == ReaperSurface::TrackPanel);   // volume fader
    CHECK(onTrack("tcp.meter") == ReaperSurface::TrackPanel); // meter
    CHECK(onTrack("tcp.f") == ReaperSurface::TrackPanel);     // truncated token — still the panel
}

// The embed strip is where a ReaSampler 9000 instance already draws inline via
// IReaperUIEmbedInterface. It must NOT resolve to a hotspot — a drop there would stack a second
// instance on the first. Matched before the "tcp"/"mcp" rule, so widening the panel hotspot
// cannot swallow it; do not reorder these two checks in the classifier.
static void testEmbedStripIsItsOwnSurface() {
    CHECK(onTrack("tcp.fxembed") == ReaperSurface::FxEmbed);
    CHECK(onTrack("mcp.fxembed") == ReaperSurface::FxEmbed);
    CHECK(onTrack("tcp.fxembed.1") == ReaperSurface::FxEmbed);
    CHECK(onTrack("mcp.fxembed extra") == ReaperSurface::FxEmbed);
}

// The arrange, including a token with appended information.
static void testArrangeIsArrange() {
    CHECK(onTrack("arrange") == ReaperSurface::Arrange);
    CHECK(onTrack("arrange extra") == ReaperSurface::Arrange);
}

// Anything else REAPER names is Other — a defined refusal, never a guessed outcome. Includes
// tokens REAPER may add in future versions.
static void testUnnamedReaperSurfacesAreOther() {
    CHECK(onTrack("spacer_0") == ReaperSurface::Other);
    CHECK(onTrack("trans") == ReaperSurface::Other);
    CHECK(onTrack("envcp") == ReaperSurface::Other);
    CHECK(onTrack("ruler") == ReaperSurface::Other);
    CHECK(onTrack("something_reaper_adds_in_2030") == ReaperSurface::Other);
}

// The empty info string splits on whether a track came back with it. No track means the pointer
// has left REAPER (the OS hand-off's trigger); a track with no info means we are over REAPER on
// a surface we cannot name, which must refuse rather than be treated as off-REAPER.
static void testEmptyInfoSplitsOnTrackPresence() {
    CHECK(classifyReaperSurface("", /*haveTrack=*/false) == ReaperSurface::OffReaper);
    CHECK(classifyReaperSurface("", /*haveTrack=*/true) == ReaperSurface::Other);
}

// The SDK's documented null-track-with-valid-info case: the surface is read from the string
// alone, so the classifier reports it faithfully and the gesture law decides what a missing
// track means for that surface.
static void testNullTrackStillClassifiesTheSurface() {
    CHECK(classifyReaperSurface("arrange", false) == ReaperSurface::Arrange);
    CHECK(classifyReaperSurface("tcp", false) == ReaperSurface::TrackPanel);
    CHECK(classifyReaperSurface("fx_chain", false) == ReaperSurface::FxSurface);
}

// Do not reintroduce a per-surface "capture carries" loop test: buildInstrumentDropPreset takes
// only sampleId (proven by testPresetRoundTripsThroughInstrumentReader), and per-surface
// coverage already exists above — a loop with an identical body per surface string can't
// distinguish them.

// --- All-or-nothing rollback --------------------------------------------------

// The add failed: nothing was created, so there is nothing to delete and nothing loaded.
static void testAddFailureLeavesNothingToRollBack() {
    const DropOutcome out = decideDropOutcome(DropAttempt{-1, false});
    CHECK(!out.loaded);
    CHECK(out.rollbackFxIndex < 0);
}

// The instance was created but the preset did not apply: the caller MUST delete that exact
// index — an instance without its capture is the orphan the contract forbids.
static void testPresetFailureRollsBackTheCreatedIndex() {
    const DropOutcome zero = decideDropOutcome(DropAttempt{0, false});
    CHECK(!zero.loaded);
    CHECK(zero.rollbackFxIndex == 0);
    const DropOutcome later = decideDropOutcome(DropAttempt{4, false});
    CHECK(!later.loaded);
    CHECK(later.rollbackFxIndex == 4);
    // Container-addressed indices (0x2000000-flagged) roll back by the same rule — the
    // obligation follows the index REAPER handed back, whatever space it names.
    const DropOutcome inContainer = decideDropOutcome(DropAttempt{0x2000000 + 5, false});
    CHECK(!inContainer.loaded);
    CHECK(inContainer.rollbackFxIndex == 0x2000000 + 5);
}

// Both halves succeeded: loaded, and nothing to undo.
static void testSuccessKeepsTheInstance() {
    const DropOutcome out = decideDropOutcome(DropAttempt{2, true});
    CHECK(out.loaded);
    CHECK(out.rollbackFxIndex < 0);
}

// A "preset applied" report with no instance behind it can never read as loaded.
static void testNoInstanceIsNeverLoaded() {
    const DropOutcome out = decideDropOutcome(DropAttempt{-1, true});
    CHECK(!out.loaded);
    CHECK(out.rollbackFxIndex < 0);
}

int main() {
    testClassIdHexPinnedPerChannel();
    testPresetRoundTripsThroughInstrumentReader();
    testPresetLayoutInvariants();
    testGuidLikeIdRoundTrips();
    testEmptyIdYieldsEmptyState();
    testDeterministic();
    testBadClassIdRejected();

    testFxWindowIsFxSurface();
    testTcpMcpFxFamilyIsTrackPanel();
    testWholeTrackPanelIsTheHotspot();
    testEmbedStripIsItsOwnSurface();
    testArrangeIsArrange();
    testUnnamedReaperSurfacesAreOther();
    testEmptyInfoSplitsOnTrackPresence();
    testNullTrackStillClassifiesTheSurface();

    testAddFailureLeavesNothingToRollBack();
    testPresetFailureRollsBackTheCreatedIndex();
    testSuccessKeepsTheInstance();
    testNoInstanceIsNeverLoaded();

    if (g_fail == 0) std::printf("All tests passed.\n");
    return g_fail ? 1 : 0;
}
