// Standalone tests for reasampler::wav_trim — no REAPER, no test framework.
// Builds synthetic 32-bit-float WAV byte buffers, asserts the parse geometry, the
// float extraction, and the truncate-plan arithmetic (the header size-field patch).
//
// Covers: canonical stereo/mono 32-bit-float parse; a leading unknown chunk skipped;
// format rejection (16-bit PCM, non-WAV, data-before-fmt, truncated data); frame
// extraction (whole / tail window / clamp / out-of-range); truncate plan (kept<all,
// no-op keep-all, kept==0, grow rejected) with exact size-field values.

#include "../src/wav_trim.h"

#include <cstdint>
#include <cstdio>
#include <cstring>
#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)

// --- Synthetic WAV builder ---------------------------------------------------

static void putU16(std::vector<std::uint8_t>& b, std::uint16_t v) {
    b.push_back(static_cast<std::uint8_t>(v & 0xFF));
    b.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
}
static void putU32(std::vector<std::uint8_t>& b, std::uint32_t v) {
    b.push_back(static_cast<std::uint8_t>(v & 0xFF));
    b.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
    b.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFF));
    b.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFF));
}
static void putTag(std::vector<std::uint8_t>& b, const char* t) {
    for (int i = 0; i < 4; ++i) b.push_back(static_cast<std::uint8_t>(t[i]));
}
static void putFloat(std::vector<std::uint8_t>& b, float f) {
    std::uint8_t tmp[4];
    std::memcpy(tmp, &f, 4);
    for (int i = 0; i < 4; ++i) b.push_back(tmp[i]);
}

// A canonical 32-bit-float WAV: RIFF/WAVE, fmt (tag 3, 16-byte body), data holding
// `frames` interleaved frames of `channels`. `leadingJunk` optionally inserts an
// unknown chunk before fmt to exercise the chunk walk. Samples: frame f, channel c
// = value(f,c).
template <typename Fn>
static std::vector<std::uint8_t> buildFloatWav(std::uint16_t channels,
                                               std::uint32_t sampleRate,
                                               std::size_t frames,
                                               Fn value,
                                               bool leadingJunk = false,
                                               std::uint16_t fmtTag = 3,
                                               std::uint16_t bits = 32) {
    const std::uint32_t dataBytes =
        static_cast<std::uint32_t>(frames * channels * (bits / 8));

    std::vector<std::uint8_t> chunks;  // everything after "WAVE"
    if (leadingJunk) {
        putTag(chunks, "LIST");
        putU32(chunks, 4);
        putTag(chunks, "INFO");  // 4-byte body, even -> no pad
    }
    // fmt chunk (16-byte body).
    putTag(chunks, "fmt ");
    putU32(chunks, 16);
    putU16(chunks, fmtTag);            // format tag
    putU16(chunks, channels);
    putU32(chunks, sampleRate);
    const std::uint32_t byteRate = sampleRate * channels * (bits / 8);
    putU32(chunks, byteRate);
    putU16(chunks, static_cast<std::uint16_t>(channels * (bits / 8)));  // block align
    putU16(chunks, bits);
    // data chunk.
    putTag(chunks, "data");
    putU32(chunks, dataBytes);
    for (std::size_t f = 0; f < frames; ++f)
        for (std::uint16_t c = 0; c < channels; ++c)
            putFloat(chunks, value(f, c));

    std::vector<std::uint8_t> wav;
    putTag(wav, "RIFF");
    putU32(wav, static_cast<std::uint32_t>(4 + chunks.size()));  // "WAVE" + chunks
    putTag(wav, "WAVE");
    wav.insert(wav.end(), chunks.begin(), chunks.end());
    return wav;
}

// Builds a WAVE_FORMAT_EXTENSIBLE (0xFFFE) WAV with a 40-byte fmt body.
// `subFormatTag` is the 2-byte leading tag embedded in the SubFormat GUID:
//   0x0003 = IEEE float, 0x0001 = PCM integer (and any other value to exercise rejection).
// bitsPerSample and the PCM data are always 32-bit float bytes regardless of subFormatTag
// (we're testing that the parser correctly rejects/accepts based on the GUID, not the data).
template <typename Fn>
static std::vector<std::uint8_t> buildExtensibleWav(std::uint16_t channels,
                                                     std::uint32_t sampleRate,
                                                     std::size_t frames,
                                                     Fn value,
                                                     std::uint16_t subFormatTag) {
    const std::uint32_t dataBytes =
        static_cast<std::uint32_t>(frames * channels * 4u);

    // WAVEFORMATEXTENSIBLE fmt body (40 bytes):
    //   [0..1]   wFormatTag      = 0xFFFE
    //   [2..3]   nChannels
    //   [4..7]   nSamplesPerSec
    //   [8..11]  nAvgBytesPerSec
    //   [12..13] nBlockAlign
    //   [14..15] wBitsPerSample  = 32
    //   [16..17] cbSize          = 22 (extension size beyond the 18-byte WAVEFORMATEX)
    //   [18..19] wValidBitsPerSample = 32
    //   [20..23] dwChannelMask   = 0
    //   [24..39] SubFormat GUID: first 2 bytes = subFormatTag (LE), rest = standard
    //            KSDATAFORMAT_SUBTYPE base GUID {00000000-0000-0010-8000-00aa00389b71}
    std::vector<std::uint8_t> fmt;
    putU16(fmt, 0xFFFE);                                    // wFormatTag
    putU16(fmt, channels);                                  // nChannels
    putU32(fmt, sampleRate);                                // nSamplesPerSec
    putU32(fmt, sampleRate * channels * 4u);                // nAvgBytesPerSec
    putU16(fmt, static_cast<std::uint16_t>(channels * 4)); // nBlockAlign
    putU16(fmt, 32);                                        // wBitsPerSample
    putU16(fmt, 22);                                        // cbSize
    putU16(fmt, 32);                                        // wValidBitsPerSample
    putU32(fmt, 0);                                         // dwChannelMask
    // SubFormat GUID (16 bytes): [subFormatTag, 0x0000, 0x00, 0x00, 0x10, 0x00,
    //                             0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]
    putU16(fmt, subFormatTag);   // bytes [24..25]: the effective format tag
    putU16(fmt, 0x0000);         // bytes [26..27]
    fmt.push_back(0x00); fmt.push_back(0x00); // bytes [28..29]
    fmt.push_back(0x10); fmt.push_back(0x00); // bytes [30..31]
    fmt.push_back(0x80); fmt.push_back(0x00); // bytes [32..33]
    fmt.push_back(0x00); fmt.push_back(0xaa); // bytes [34..35]
    fmt.push_back(0x00); fmt.push_back(0x38); // bytes [36..37]
    fmt.push_back(0x9b); fmt.push_back(0x71); // bytes [38..39]

    std::vector<std::uint8_t> chunks;
    putTag(chunks, "fmt ");
    putU32(chunks, static_cast<std::uint32_t>(fmt.size()));  // 40
    chunks.insert(chunks.end(), fmt.begin(), fmt.end());
    putTag(chunks, "data");
    putU32(chunks, dataBytes);
    for (std::size_t f = 0; f < frames; ++f)
        for (std::uint16_t c = 0; c < channels; ++c)
            putFloat(chunks, value(f, c));

    std::vector<std::uint8_t> wav;
    putTag(wav, "RIFF");
    putU32(wav, static_cast<std::uint32_t>(4 + chunks.size()));
    putTag(wav, "WAVE");
    wav.insert(wav.end(), chunks.begin(), chunks.end());
    return wav;
}

// --- Parse tests -------------------------------------------------------------

static void testParseCanonicalStereo() {
    auto wav = buildFloatWav(2, 48000, 5,
                             [](std::size_t f, std::uint16_t c) {
                                 return static_cast<float>(f) + 0.1f * c;
                             });
    WavLayout L = parseWavLayout(wav);
    CHECK(L.valid);
    CHECK(L.channelCount == 2);
    CHECK(L.sampleRate == 48000);
    CHECK(L.dataByteLength == 5 * 2 * 4);
    CHECK(L.frameCount() == 5);
    // data body sits after RIFF(12) + fmt(8 header + 16 body) + data(8 header) = 44.
    CHECK(L.dataByteOffset == 44);
    CHECK(L.dataSizeFieldOffset == 40);  // the 4 bytes before dataByteOffset
    CHECK(L.riffSizeFieldOffset == 4);
}

static void testParseMonoAndLeadingChunk() {
    // A leading LIST/INFO chunk before fmt must be skipped by the walk.
    auto wav = buildFloatWav(1, 44100, 3,
                             [](std::size_t f, std::uint16_t) {
                                 return static_cast<float>(f);
                             },
                             /*leadingJunk=*/true);
    WavLayout L = parseWavLayout(wav);
    CHECK(L.valid);
    CHECK(L.channelCount == 1);
    CHECK(L.frameCount() == 3);
    // Data still parses correctly despite the leading chunk shifting its offset.
    auto pcm = extractFloatFrames(wav, L, 0, 3);
    CHECK(pcm.size() == 3);
    CHECK(pcm[0] == 0.0f && pcm[1] == 1.0f && pcm[2] == 2.0f);
}

static void testParseRejectsNon32BitAndNonWav() {
    // 16-bit PCM (tag 1, bits 16) -> rejected.
    auto pcm16 = buildFloatWav(2, 48000, 4,
                               [](std::size_t, std::uint16_t) { return 0.0f; },
                               false, /*fmtTag=*/1, /*bits=*/16);
    CHECK(!parseWavLayout(pcm16).valid);

    // Not a RIFF file.
    std::vector<std::uint8_t> junk = {'N','O','P','E', 0,0,0,0, 'W','A','V','E'};
    CHECK(!parseWavLayout(junk).valid);

    // Too short to hold even the RIFF header.
    std::vector<std::uint8_t> tiny = {'R','I','F','F'};
    CHECK(!parseWavLayout(tiny).valid);
}

static void testParseRejectsLyingDataLength() {
    // Build a valid WAV, then inflate the `data` size field so it claims more bytes
    // than the buffer holds -> must be rejected (no OOB trust).
    auto wav = buildFloatWav(2, 48000, 4,
                             [](std::size_t, std::uint16_t) { return 1.0f; });
    WavLayout good = parseWavLayout(wav);
    CHECK(good.valid);
    // Overwrite the data size field with a huge value.
    wav[good.dataSizeFieldOffset + 0] = 0xFF;
    wav[good.dataSizeFieldOffset + 1] = 0xFF;
    wav[good.dataSizeFieldOffset + 2] = 0xFF;
    wav[good.dataSizeFieldOffset + 3] = 0x7F;
    CHECK(!parseWavLayout(wav).valid);
}

// --- Extraction tests --------------------------------------------------------

static void testExtractTailWindow() {
    // Stereo, 10 frames. Sample value encodes frame+channel so a mis-index is caught.
    auto wav = buildFloatWav(2, 48000, 10,
                             [](std::size_t f, std::uint16_t c) {
                                 return static_cast<float>(f) * 10.0f + c;
                             });
    WavLayout L = parseWavLayout(wav);
    CHECK(L.valid);

    // The "tail region" the realtime trim scans: frames 6..9 (start at frame 6).
    auto tail = extractFloatFrames(wav, L, 6, 100 /*clamps*/);
    CHECK(tail.size() == 4 * 2);           // frames 6,7,8,9, 2 channels each
    CHECK(tail[0] == 60.0f && tail[1] == 61.0f);  // frame 6: L=60,R=61
    CHECK(tail[6] == 90.0f && tail[7] == 91.0f);  // frame 9: L=90,R=91

    // Out-of-range start -> empty.
    CHECK(extractFloatFrames(wav, L, 10, 4).empty());
    CHECK(extractFloatFrames(wav, L, 99, 4).empty());
}

// --- Truncate-plan tests -----------------------------------------------------

static void testTruncatePlanKeepFewer() {
    auto wav = buildFloatWav(2, 48000, 10,
                             [](std::size_t, std::uint16_t) { return 0.0f; });
    WavLayout L = parseWavLayout(wav);
    CHECK(L.valid);

    // Keep 4 of 10 frames.
    WavTruncatePlan p = planWavTruncate(L, 4);
    CHECK(p.valid);
    const std::size_t bpf = 2 * 4;                 // channels * 4 bytes
    CHECK(p.newDataSize == 4 * bpf);               // 32 bytes of PCM kept
    CHECK(p.newFileByteLength == L.dataByteOffset + 4 * bpf);  // 44 + 32 = 76
    CHECK(p.newRiffSize == p.newFileByteLength - 8);
    CHECK(p.dataSizeFieldOffset == L.dataSizeFieldOffset);
    CHECK(p.riffSizeFieldOffset == 4);

    // Applying the plan yields a buffer that re-parses to exactly 4 frames.
    std::vector<std::uint8_t> trimmed(wav.begin(),
                                      wav.begin() + p.newFileByteLength);
    // Patch the two size fields (what the shell does before truncating on disk).
    auto writeU32 = [](std::vector<std::uint8_t>& b, std::size_t off, std::uint32_t v) {
        b[off + 0] = static_cast<std::uint8_t>(v & 0xFF);
        b[off + 1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
        b[off + 2] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
        b[off + 3] = static_cast<std::uint8_t>((v >> 24) & 0xFF);
    };
    writeU32(trimmed, p.dataSizeFieldOffset, p.newDataSize);
    writeU32(trimmed, p.riffSizeFieldOffset, p.newRiffSize);

    WavLayout L2 = parseWavLayout(trimmed);
    CHECK(L2.valid);
    CHECK(L2.frameCount() == 4);
    CHECK(L2.dataByteLength == 4 * bpf);
}

static void testTruncatePlanKeepAllIsNoOp() {
    auto wav = buildFloatWav(1, 48000, 6,
                             [](std::size_t, std::uint16_t) { return 0.0f; });
    WavLayout L = parseWavLayout(wav);
    WavTruncatePlan p = planWavTruncate(L, 6);  // keep all
    CHECK(p.valid);
    CHECK(p.newFileByteLength == wav.size());   // unchanged
    CHECK(p.newDataSize == L.dataByteLength);
}

// --- Extensible format tests -------------------------------------------------

// A WAVE_FORMAT_EXTENSIBLE fmt with SubFormat tag 0x0001 (PCM integer) and
// bitsPerSample==32 must be REJECTED — it is 32-bit integer, not 32-bit float.
static void testExtensiblePcmIntegerRejected() {
    auto wav = buildExtensibleWav(2, 48000, 4,
                                  [](std::size_t, std::uint16_t) { return 0.0f; },
                                  /*subFormatTag=*/0x0001);  // PCM integer
    CHECK(!parseWavLayout(wav).valid);
}

// A WAVE_FORMAT_EXTENSIBLE fmt with SubFormat tag 0x0003 (IEEE float) and
// bitsPerSample==32 must be ACCEPTED and parse + trim correctly.
static void testExtensibleFloatAccepted() {
    auto wav = buildExtensibleWav(2, 48000, 5,
                                  [](std::size_t f, std::uint16_t c) {
                                      return static_cast<float>(f) + 0.1f * c;
                                  },
                                  /*subFormatTag=*/0x0003);  // IEEE float
    WavLayout L = parseWavLayout(wav);
    CHECK(L.valid);
    CHECK(L.channelCount == 2);
    CHECK(L.sampleRate == 48000);
    CHECK(L.frameCount() == 5);

    // Frame extraction works correctly.
    auto pcm = extractFloatFrames(wav, L, 0, 2);
    CHECK(pcm.size() == 4);
    CHECK(pcm[0] == 0.0f);   // frame 0, channel 0
    CHECK(pcm[1] == 0.1f);   // frame 0, channel 1

    // Truncate plan is valid and re-parses cleanly.
    WavTruncatePlan p = planWavTruncate(L, 3);
    CHECK(p.valid);
    CHECK(p.newDataSize == 3 * 2 * 4u);
    std::vector<std::uint8_t> trimmed(wav.begin(), wav.begin() + p.newFileByteLength);
    auto writeU32 = [](std::vector<std::uint8_t>& b, std::size_t off, std::uint32_t v) {
        b[off + 0] = static_cast<std::uint8_t>(v & 0xFF);
        b[off + 1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
        b[off + 2] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
        b[off + 3] = static_cast<std::uint8_t>((v >> 24) & 0xFF);
    };
    writeU32(trimmed, p.dataSizeFieldOffset, p.newDataSize);
    writeU32(trimmed, p.riffSizeFieldOffset, p.newRiffSize);
    WavLayout L2 = parseWavLayout(trimmed);
    CHECK(L2.valid);
    CHECK(L2.frameCount() == 3);
}

static void testTruncatePlanKeepZeroAndGrowRejected() {
    auto wav = buildFloatWav(2, 48000, 5,
                             [](std::size_t, std::uint16_t) { return 0.0f; });
    WavLayout L = parseWavLayout(wav);

    WavTruncatePlan zero = planWavTruncate(L, 0);
    CHECK(zero.valid);
    CHECK(zero.newDataSize == 0);
    CHECK(zero.newFileByteLength == L.dataByteOffset);  // header only

    // keptFrames > total -> refused (never grow a file).
    CHECK(!planWavTruncate(L, 6).valid);

    // Invalid layout -> invalid plan.
    WavLayout bad;
    CHECK(!planWavTruncate(bad, 0).valid);
}

int main() {
    testParseCanonicalStereo();
    testParseMonoAndLeadingChunk();
    testParseRejectsNon32BitAndNonWav();
    testParseRejectsLyingDataLength();
    testExtractTailWindow();
    testTruncatePlanKeepFewer();
    testTruncatePlanKeepAllIsNoOp();
    testTruncatePlanKeepZeroAndGrowRejected();
    testExtensiblePcmIntegerRejected();
    testExtensibleFloatAccepted();

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