// Standalone tests for reasampler::instrument_drop — no REAPER, no VST3 SDK, no framework.
// The S17 drop-and-load blob-construction contract: the extension builds a vst_chunk blob
// whose bytes are EXACTLY what ReaSampler 9000's own setState (deserializeComponentState)
// accepts, with the dragged capture pre-selected. The round-trip proof (build -> base64
// decode -> the instrument's OWN reader -> assert the capture selected) IS the cross-artifact
// contract guard — the same pattern assignment_request_tests uses for its wire format.

#include "../src/instrument_drop.h"
#include "../src/vst/sample_map.h"  // deserializeComponentState — the instrument's OWN reader

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

using namespace reasampler;

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 v5
// blob never consumes it, but the reader signature requires a positive rate).
static constexpr double kRate = 48000.0;

// THE contract test: a blob built for a capture id decodes — through the instrument's OWN
// reader — to a ComponentState with THAT id selected, no zones, default mono. If this fails,
// the extension would inject bytes the instrument's setState rejects and the drop would load
// a silent/wrong instance.
static void testBlobRoundTripsThroughInstrumentReader() {
    const std::string id = "cap-7f3a-guid";
    const std::string b64 = buildInstrumentDropChunk(id);
    CHECK(!b64.empty());

    const std::vector<std::uint8_t> bytes = decodeBase64(b64);
    CHECK(!bytes.empty());
    // The base64 must decode to EXACTLY the pre-encode state bytes (no corruption).
    CHECK(bytes == instrumentDropStateBytes(id));

    const ComponentState cs = deserializeComponentState(bytes, kRate);
    CHECK(cs.selectionId == id);          // the capture IS selected — the whole point
    CHECK(cs.map.zones.empty());          // a drop selects one capture, authors no zones
    CHECK(cs.channelMode == ChannelMode::Mono);           // fresh-instance default
    CHECK(cs.lastConsumedAssignGeneration == 0);          // fresh instance, no consumed assign
}

// A GUID-shaped id with bytes that would trip a naive delimiter-based encoder round-trips
// whole (the length-prefixed component-state framing + base64 carry arbitrary bytes).
static void testGuidLikeIdRoundTrips() {
    const std::string id = "{9A2F0C11-4B6E-4D01-8F3A-0011223344FF}";
    const std::vector<std::uint8_t> bytes = decodeBase64(buildInstrumentDropChunk(id));
    const ComponentState cs = deserializeComponentState(bytes, kRate);
    CHECK(cs.selectionId == id);
}

// An empty id yields the empty-state blob: it still decodes cleanly to {"", no zones} — the
// S10 silent empty state. (The shell guards against dropping nothing; the pure contract holds.)
static void testEmptyIdYieldsEmptyState() {
    const std::vector<std::uint8_t> bytes = decodeBase64(buildInstrumentDropChunk(""));
    CHECK(!bytes.empty());  // still a versioned envelope, just an empty selection
    const ComponentState cs = deserializeComponentState(bytes, kRate);
    CHECK(cs.selectionId.empty());
    CHECK(cs.map.zones.empty());
}

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

// --- base64 codec unit coverage (the encode side the shell actually ships) -----

static std::vector<std::uint8_t> b(std::initializer_list<int> v) {
    std::vector<std::uint8_t> out;
    for (int x : v) out.push_back(static_cast<std::uint8_t>(x));
    return out;
}

// Known RFC-4648 vectors, incl. every padding case (0/1/2 trailing bytes).
static void testBase64KnownVectors() {
    CHECK(encodeBase64(b({})) == "");
    CHECK(encodeBase64(b({'f'})) == "Zg==");
    CHECK(encodeBase64(b({'f', 'o'})) == "Zm8=");
    CHECK(encodeBase64(b({'f', 'o', 'o'})) == "Zm9v");
    CHECK(encodeBase64(b({'f', 'o', 'o', 'b'})) == "Zm9vYg==");
    CHECK(encodeBase64(b({'f', 'o', 'o', 'b', 'a'})) == "Zm9vYmE=");
    CHECK(encodeBase64(b({'f', 'o', 'o', 'b', 'a', 'r'})) == "Zm9vYmFy");
}

// encode -> decode is identity across every residue class + all-byte values.
static void testBase64RoundTripAllBytes() {
    for (int len = 0; len <= 300; ++len) {
        std::vector<std::uint8_t> in;
        for (int i = 0; i < len; ++i) in.push_back(static_cast<std::uint8_t>((i * 37 + 11) & 0xFF));
        CHECK(decodeBase64(encodeBase64(in)) == in);
    }
}

// Malformed decode inputs return empty (never throw / never UB): bad length, illegal char,
// misplaced padding.
static void testBase64DecodeRejectsMalformed() {
    CHECK(decodeBase64("Zg=").empty());     // length not a multiple of 4
    CHECK(decodeBase64("Zm9v!ba=").empty()); // illegal char '!'
    CHECK(decodeBase64("Z===").empty());     // illegal char in v1 position
    CHECK(decodeBase64("Zg==Zg==").empty()); // interior padding (pad before the final quad)
}

// --- FX-hotspot classification (S-VIEW-BUG-1) ---------------------------------
//
// THE BUG: dropping a capture onto a track's FX button (in the TCP) never armed the
// instrument drop, because the old predicate matched only "fx_" — which the SDK reserves for
// the FX CHAIN / FLOATING-FX windows. A track-panel FX-button hit reports "tcp.fx"/"mcp.fx"
// (SDK §GetThingFromPoint: "string will begin with 'tcp' or 'mcp' or 'tcp.mute' etc").
//
// THE FIX: narrow to two documented FX-bearing surfaces:
//   * "tcp.fx" / "mcp.fx"  — the TCP/MCP FX button (exact token, NOT bare tcp/mcp)
//   * "fx_*"               — the FX-chain / floating-FX windows (prefix, as before)
// Bare "tcp"/"mcp" and any other "tcp.*"/"mcp.*" sub-element (e.g. "tcp.mute", "tcp.vol")
// are non-FX track-panel regions — an instrument drop must NOT fire there.

// The TCP/MCP FX button specifically — the surface that arms an instrument drop (S-VIEW-BUG-1
// fix). Under the old "fx_"-only predicate these returned false — the exact miss that produced
// the "drops as audio to arrange" symptom on the FX button.
static void testTcpMcpFxButtonIsHotspot() {
    CHECK(infoNamesFxHotspot("tcp.fx"));    // TCP FX button (SDK token)
    CHECK(infoNamesFxHotspot("mcp.fx"));    // MCP FX area (SDK token)
}

// The FX chain / floating-FX windows (the surfaces the ORIGINAL predicate matched) still
// classify as hotspots — the fix does not regress the "fx_" surface.
static void testFxWindowStillHotspot() {
    CHECK(infoNamesFxHotspot("fx_chain"));  // FX chain window
    CHECK(infoNamesFxHotspot("fx_0"));      // first FX, floating
    CHECK(infoNamesFxHotspot("fx_12"));     // arbitrary floating-FX index
}

// Non-FX surfaces are NOT hotspots — a drop here is not an instrument drop (it would fall
// through to the OS drag / no-op). This includes the bare TCP/MCP tokens and all non-FX
// "tcp.*"/"mcp.*" sub-elements (e.g. mute button, volume fader, track name, meter).
static void testNonFxSurfacesAreNotHotspot() {
    CHECK(!infoNamesFxHotspot("tcp"));       // bare track control panel — NOT an FX hotspot
    CHECK(!infoNamesFxHotspot("mcp"));       // bare mixer control panel — NOT an FX hotspot
    CHECK(!infoNamesFxHotspot("tcp.mute"));  // mute button — track panel, not FX
    CHECK(!infoNamesFxHotspot("tcp.vol"));   // volume fader — track panel, not FX
    CHECK(!infoNamesFxHotspot("arrange"));
    CHECK(!infoNamesFxHotspot("spacer_0"));
    CHECK(!infoNamesFxHotspot(""));          // pointer over nothing REAPER classifies
    CHECK(!infoNamesFxHotspot("trans"));     // transport
    CHECK(!infoNamesFxHotspot("envcp"));     // envelope control panel — a track thing, not FX
}

int main() {
    testBlobRoundTripsThroughInstrumentReader();
    testGuidLikeIdRoundTrips();
    testEmptyIdYieldsEmptyState();
    testDeterministic();
    testBase64KnownVectors();
    testBase64RoundTripAllBytes();
    testBase64DecodeRejectsMalformed();

    testTcpMcpFxButtonIsHotspot();
    testFxWindowStillHotspot();
    testNonFxSurfacesAreNotHotspot();

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