// Standalone tests for reasampler::package's format contract — no REAPER, no
// test framework. Pins the version-ladder classification (both integers, every
// branch) and the three naming rules: the entry-name rule, the ASCII-folding
// name equivalence, and the nested-path traversal guard.

#include "../src/core/package/package_format.h"

#include <cstdio>
#include <string>

using namespace reasampler::package;

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

// --- classifyPackageVersion --------------------------------------------------

static void testClassifyReadable() {
    CHECK(classifyPackageVersion(kPackageFormatVersion, kPackageMinReaderVersion) ==
          PackageReadability::Readable);
    // The additive-forward-compat direction: a newer writer whose minReader
    // still reaches back to this build reads fine.
    CHECK(classifyPackageVersion(kPackageFormatVersion + 5, kPackageMinReaderVersion) ==
          PackageReadability::Readable);
    // Boundary: minReader exactly this build's format version.
    CHECK(classifyPackageVersion(kPackageFormatVersion + 1, kPackageFormatVersion) ==
          PackageReadability::Readable);
}

static void testClassifyTooNew() {
    // Boundary: one past this build's format version refuses.
    CHECK(classifyPackageVersion(kPackageFormatVersion + 1, kPackageFormatVersion + 1) ==
          PackageReadability::TooNew);
    CHECK(classifyPackageVersion(99, 42) == PackageReadability::TooNew);
}

static void testClassifyMalformed() {
    // Zero versions: no honest writer emits them (the ladder starts at 1).
    CHECK(classifyPackageVersion(0, 0) == PackageReadability::Malformed);
    CHECK(classifyPackageVersion(1, 0) == PackageReadability::Malformed);
    CHECK(classifyPackageVersion(0, 1) == PackageReadability::Malformed);
    // A writer cannot require a reader newer than what it wrote.
    CHECK(classifyPackageVersion(1, 2) == PackageReadability::Malformed);
    // Incoherence outranks TooNew: even with both above this build, minReader >
    // formatVersion is Malformed, not a refusal message.
    CHECK(classifyPackageVersion(5, 9) == PackageReadability::Malformed);
}

// --- isValidEntryName --------------------------------------------------------

static void testEntryNameAccepts() {
    CHECK(isValidEntryName("kick.wav"));
    CHECK(isValidEntryName("Snare 03 (wet).wav"));
    CHECK(isValidEntryName("no-extension"));
    CHECK(isValidEntryName(".hidden"));          // a leading dot is a bare name
    CHECK(isValidEntryName("a.b.c.wav"));        // single dots are fine
    CHECK(isValidEntryName(std::string(kMaxEntryNameBytes, 'x')));  // at the cap
    // Legal names containing a ".." substring that is not the whole name: a
    // name can only ever be one path component (separators are banned), so
    // ".." as a component is the only expressible traversal.
    CHECK(isValidEntryName("take..final.wav"));
    CHECK(isValidEntryName("loop...wav"));
    CHECK(isValidEntryName("a..b"));
}

static void testEntryNameRejectsSeparatorsAndDots() {
    CHECK(!isValidEntryName(""));
    CHECK(!isValidEntryName("."));
    CHECK(!isValidEntryName(".."));
    CHECK(!isValidEntryName("..\\evil.wav"));
    CHECK(!isValidEntryName("../evil.wav"));
    CHECK(!isValidEntryName("dir/inner.wav"));
    CHECK(!isValidEntryName("dir\\inner.wav"));
    CHECK(!isValidEntryName("/rooted.wav"));
    CHECK(!isValidEntryName("\\rooted.wav"));
    // Embedded NUL: every plausible filesystem call (ofstream, fopen,
    // CreateFileW off .c_str()) truncates at it, so two names differing only
    // after the NUL would collide on one file.
    CHECK(!isValidEntryName(std::string("a\0b.wav", 7)));
    // Other control bytes (newline here) are equally hostile to logs/UI.
    CHECK(!isValidEntryName("a\nb.wav"));
}

static void testEntryNameRejectsAbsolutePrefixes() {
    CHECK(!isValidEntryName("C:\\abs.wav"));
    CHECK(!isValidEntryName("C:/abs.wav"));
    CHECK(!isValidEntryName("c:relative-to-drive.wav"));  // ':' bans drive forms
    CHECK(!isValidEntryName("\\\\server\\share.wav"));    // UNC
    CHECK(!isValidEntryName(std::string(kMaxEntryNameBytes + 1, 'x')));  // over cap
}

static void testEntryNameRejectsWindowsHostileNames() {
    // Reserved characters.
    CHECK(!isValidEntryName("a*b.wav"));
    CHECK(!isValidEntryName("a?b.wav"));
    CHECK(!isValidEntryName("a|b.wav"));
    CHECK(!isValidEntryName("a<b>.wav"));
    CHECK(!isValidEntryName("\"q\".wav"));
    // Trailing dot or space (silently stripped at creation on Windows).
    CHECK(!isValidEntryName("trailing "));
    CHECK(!isValidEntryName("trailing."));
    CHECK(!isValidEntryName(" "));
    CHECK(!isValidEntryName("  "));
    // DOS device names, case-insensitive, with and without an extension.
    CHECK(!isValidEntryName("NUL"));
    CHECK(!isValidEntryName("CON"));
    CHECK(!isValidEntryName("con.wav"));
    CHECK(!isValidEntryName("PRN"));
    CHECK(!isValidEntryName("AUX"));
    CHECK(!isValidEntryName("COM1"));
    CHECK(!isValidEntryName("com1.txt"));
    CHECK(!isValidEntryName("LPT1"));
    // The superscript device forms (COM¹ COM² COM³ LPT¹ LPT² LPT³ in UTF-8):
    // Windows reads those as digits in a device name, so "COM².wav" is COM2.
    CHECK(!isValidEntryName("COM\xC2\xB9.wav"));
    CHECK(!isValidEntryName("com\xC2\xB2"));
    CHECK(!isValidEntryName("COM\xC2\xB3.wav"));
    CHECK(!isValidEntryName("LPT\xC2\xB9"));
    CHECK(!isValidEntryName("lpt\xC2\xB2.txt"));
    CHECK(!isValidEntryName("LPT\xC2\xB3.wav"));
    // Not a device name: a real filename that merely starts with one, and the
    // zero forms, which Windows does not reserve.
    CHECK(isValidEntryName("console.wav"));
    CHECK(isValidEntryName("COM0.wav"));
    CHECK(isValidEntryName("LPT0"));
    // Nor does a superscript past 3 name a device.
    CHECK(isValidEntryName("COM\xE2\x81\xB4.wav"));  // U+2074 SUPERSCRIPT FOUR
}

// --- isValidEntryName: UTF-8 well-formedness ---------------------------------

static void testEntryNameAcceptsWellFormedUtf8() {
    CHECK(isValidEntryName("caf\xC3\xA9.wav"));              // 2-byte: é
    CHECK(isValidEntryName("\xE2\x99\xAA.wav"));             // 3-byte: ♪
    CHECK(isValidEntryName("\xF0\x9F\x8E\xB5.wav"));         // 4-byte: 🎵
    CHECK(isValidEntryName("\xEF\xBB\xBF.wav"));             // U+FEFF, ugly but well-formed
    CHECK(isValidEntryName("\xF4\x8F\xBF\xBF.wav"));         // U+10FFFF, the last code point
}

static void testEntryNameRejectsIllFormedUtf8() {
    // Two names differing ONLY in their invalid bytes: a host converting to
    // UTF-16 substitutes U+FFFD for both by default, collapsing them onto one
    // file — the duplicate-name collision the manifest cannot otherwise see.
    CHECK(!isValidEntryName("a\x80.wav"));       // stray continuation byte
    CHECK(!isValidEntryName("a\x81.wav"));
    // Structural: truncated sequences (a lead byte the name ends inside).
    CHECK(!isValidEntryName("a\xC3"));
    CHECK(!isValidEntryName("a\xE2\x99"));
    CHECK(!isValidEntryName("a\xF0\x9F\x8E"));
    // A lead byte followed by a non-continuation.
    CHECK(!isValidEntryName("a\xC3\x41.wav"));
    // Overlong encodings: an alternate spelling of an ASCII byte we ban.
    CHECK(!isValidEntryName("a\xC0\xAF.wav"));           // overlong '/'
    CHECK(!isValidEntryName("a\xC0\x80.wav"));           // overlong NUL
    CHECK(!isValidEntryName("a\xE0\x80\xAF.wav"));       // overlong '/', 3-byte
    CHECK(!isValidEntryName("a\xF0\x80\x80\xAF.wav"));   // overlong '/', 4-byte
    // Surrogate halves: no code point, and unrepresentable in UTF-16.
    CHECK(!isValidEntryName("a\xED\xA0\x80.wav"));       // U+D800
    CHECK(!isValidEntryName("a\xED\xBF\xBF.wav"));       // U+DFFF
    // Past U+10FFFF, and the 5/6-byte leads that never encode anything.
    CHECK(!isValidEntryName("a\xF4\x90\x80\x80.wav"));   // U+110000
    CHECK(!isValidEntryName("a\xF5\x80\x80\x80.wav"));
    CHECK(!isValidEntryName("a\xFC\x80\x80\x80\x80\x80.wav"));
    CHECK(!isValidEntryName("a\xFF.wav"));
}

// --- sameEntryName -----------------------------------------------------------

static void testSameEntryNameFoldsAsciiCase() {
    // A bank authored on a case-sensitive filesystem produces this pair
    // honestly; Windows and default APFS would extract both onto one file.
    CHECK(sameEntryName("Kick.wav", "kick.wav"));
    CHECK(sameEntryName("KICK.WAV", "kick.wav"));
    CHECK(sameEntryName("kick.wav", "kick.wav"));
    CHECK(!sameEntryName("kick.wav", "snare.wav"));
    CHECK(!sameEntryName("kick.wav", "kick.wave"));  // length alone decides
    CHECK(!sameEntryName("", "a"));
    CHECK(sameEntryName("", ""));
    // ASCII only: "é" vs "É" are two names here (the NFC/NFD limitation this
    // shares — see this directory's CLAUDE.md).
    CHECK(!sameEntryName("caf\xC3\xA9.wav", "caf\xC3\x89.wav"));
    // Only the letters fold — the bytes flanking the ASCII range must not.
    CHECK(!sameEntryName("a[b", "a{b"));  // 0x5B vs 0x7B, 'Z'+1 and 'z'+1
    CHECK(!sameEntryName("a@b", "a`b"));  // 0x40 vs 0x60, 'A'-1 and 'a'-1
}

// --- isValidNestedSamplePath -------------------------------------------------

static void testNestedSamplePathAcceptsRelative() {
    CHECK(isValidNestedSamplePath("a.wav"));
    CHECK(isValidNestedSamplePath("reasampler_bank/kick.wav"));
    CHECK(isValidNestedSamplePath("reasampler_bank\\kick.wav"));
    CHECK(isValidNestedSamplePath("deep/dir/tree/a.wav"));
    // A ".." that is not a whole component is an ordinary name.
    CHECK(isValidNestedSamplePath("take..final/a.wav"));
    CHECK(isValidNestedSamplePath("bank/..hidden"));
    CHECK(isValidNestedSamplePath("a..b"));
}

static void testNestedSamplePathRejectsTraversalAndAbsolute() {
    // BankModel::add catches only the absolute forms, so traversal reaches the
    // format unless this rule stops it.
    CHECK(!isValidNestedSamplePath(".."));
    CHECK(!isValidNestedSamplePath("../evil.wav"));
    CHECK(!isValidNestedSamplePath("..\\evil.wav"));
    CHECK(!isValidNestedSamplePath("bank/../../evil.wav"));
    CHECK(!isValidNestedSamplePath("bank\\..\\evil.wav"));
    CHECK(!isValidNestedSamplePath("bank/.."));
    CHECK(!isValidNestedSamplePath("bank/../"));
    // Everything util::isAbsolutePath already catches.
    CHECK(!isValidNestedSamplePath("/rooted.wav"));
    CHECK(!isValidNestedSamplePath("\\rooted.wav"));
    CHECK(!isValidNestedSamplePath("C:/abs.wav"));
    CHECK(!isValidNestedSamplePath("C:\\abs.wav"));
    CHECK(!isValidNestedSamplePath("c:relative-to-drive.wav"));
    CHECK(!isValidNestedSamplePath("\\\\server\\share.wav"));
}

int main() {
    testClassifyReadable();
    testClassifyTooNew();
    testClassifyMalformed();
    testEntryNameAccepts();
    testEntryNameRejectsSeparatorsAndDots();
    testEntryNameRejectsAbsolutePrefixes();
    testEntryNameRejectsWindowsHostileNames();
    testEntryNameAcceptsWellFormedUtf8();
    testEntryNameRejectsIllFormedUtf8();
    testSameEntryNameFoldsAsciiCase();
    testNestedSamplePathAcceptsRelative();
    testNestedSamplePathRejectsTraversalAndAbsolute();

    if (g_fail == 0) {
        std::printf("package_format_tests: all passed\n");
        return 0;
    }
    std::printf("package_format_tests: %d failure(s)\n", g_fail);
    return 1;
}
