// Standalone tests for reasampler::instrument::note::musical_division — no VST3, no REAPER,
// no framework. Same fast assert loop as the sibling pure tests.
//
// Covers: the beat length of all 39 divisions against a literal rung table (NOT the module's
// own exponent formula); the 1/64 and 64/1 extremes; the four named example divisions; the
// label notation; picker order and index round-trip; and off-ladder clamping of BOTH persisted
// fields, measured through the readers rather than by comparing two clamped values.

#include "../src/core/instrument/note/musical_division.h"

#include <cmath>
#include <cstdio>

using namespace reasampler::instrument::note;

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

static bool almostEqual(double a, double b) {
    const double d = a - b;
    return (d < 0 ? -d : d) < 1e-12;
}

// Length in beats (quarter notes) of each straight rung, written out rather than computed,
// so a broken exponent formula cannot agree with its own mistake.
static const double kStraightBeats[kRungCount] = {
    0.0625,  // 1/64
    0.125,   // 1/32
    0.25,    // 1/16
    0.5,     // 1/8
    1.0,     // 1/4
    2.0,     // 1/2
    4.0,     // 1/1
    8.0,     // 2/1
    16.0,    // 4/1
    32.0,    // 8/1
    64.0,    // 16/1
    128.0,   // 32/1
    256.0,   // 64/1
};

static const char* const kStraightLabels[kRungCount] = {
    "1/64", "1/32", "1/16", "1/8", "1/4", "1/2", "1/1",
    "2/1", "4/1", "8/1", "16/1", "32/1", "64/1",
};

// --- The ladder ---------------------------------------------------------------

static void testLadderSpansSixtyfourthToSixtyFourWhole() {
    CHECK(kRungCount == 13);
    CHECK(kDivisionCount == 39);
    CHECK(divisionLabel(divisionAt(0)) == "1/64");
    CHECK(divisionLabel(divisionAt(kDivisionCount - 1)) == "64/1t");
}

static void testEveryStraightRungHasItsWrittenBeatLength() {
    for (int rung = 0; rung < kRungCount; ++rung) {
        const Division d = makeDivision(kMinQuarterExponent + rung, DivisionModifier::Straight);
        CHECK(almostEqual(divisionBeats(d), kStraightBeats[rung]));
        CHECK(divisionLabel(d) == kStraightLabels[rung]);
    }
}

static void testDottedIsHalfAgainAndTripletIsTwoThirds() {
    for (int rung = 0; rung < kRungCount; ++rung) {
        const int e = kMinQuarterExponent + rung;
        CHECK(almostEqual(divisionBeats(makeDivision(e, DivisionModifier::Dotted)),
                   kStraightBeats[rung] * 1.5));
        CHECK(almostEqual(divisionBeats(makeDivision(e, DivisionModifier::Triplet)),
                   kStraightBeats[rung] * 2.0 / 3.0));
    }
}

static void testExtremes() {
    // 1/64 straight is the shortest rung; 64/1 straight is the longest.
    CHECK(almostEqual(divisionBeats(makeDivision(kMinQuarterExponent, DivisionModifier::Straight)),
               0.0625));
    CHECK(almostEqual(divisionBeats(makeDivision(kMaxQuarterExponent, DivisionModifier::Straight)),
               256.0));
    // The dotted 64/1 is the single longest programmable note, and kMaxDivisionBeats — which
    // note_program checks the tempo conversions' domain against — must name exactly it.
    CHECK(almostEqual(divisionBeats(makeDivision(kMaxQuarterExponent, DivisionModifier::Dotted)),
               384.0));
    CHECK(almostEqual(kMaxDivisionBeats, 384.0));
    for (int i = 0; i < kDivisionCount; ++i) {
        CHECK(divisionBeats(divisionAt(i)) <= kMaxDivisionBeats);
    }
    // The 1/64 triplet is the shortest.
    CHECK(almostEqual(divisionBeats(makeDivision(kMinQuarterExponent, DivisionModifier::Triplet)),
               0.0625 * 2.0 / 3.0));
}

// --- The four named examples --------------------------------------------------

static void testNamedExamples() {
    // 1/8. — an eighth is half a beat, dotted is three quarters of one.
    const Division dottedEighth = makeDivision(-1, DivisionModifier::Dotted);
    CHECK(almostEqual(divisionBeats(dottedEighth), 0.75));
    CHECK(divisionLabel(dottedEighth) == "1/8.");

    // 1/4t — a quarter is one beat, the triplet is two thirds of one.
    const Division quarterTriplet = makeDivision(0, DivisionModifier::Triplet);
    CHECK(almostEqual(divisionBeats(quarterTriplet), 2.0 / 3.0));
    CHECK(divisionLabel(quarterTriplet) == "1/4t");

    // 1/16 — a quarter of a beat.
    const Division sixteenth = makeDivision(-2, DivisionModifier::Straight);
    CHECK(almostEqual(divisionBeats(sixteenth), 0.25));
    CHECK(divisionLabel(sixteenth) == "1/16");

    // 4/1 — four whole notes, sixteen beats.
    const Division fourWhole = makeDivision(4, DivisionModifier::Straight);
    CHECK(almostEqual(divisionBeats(fourWhole), 16.0));
    CHECK(divisionLabel(fourWhole) == "4/1");
}

// --- Picker order -------------------------------------------------------------

static void testPickerOrderIsShortestFirst() {
    // Straight lengths ascend across rungs; within EVERY rung (not just rung 0) the order is
    // straight, dotted, triplet — so the index is not itself sorted by duration.
    for (int rung = 1; rung < kRungCount; ++rung) {
        const double prev = divisionBeats(divisionAt((rung - 1) * kModifierCount));
        const double here = divisionBeats(divisionAt(rung * kModifierCount));
        CHECK(here > prev);
    }
    for (int rung = 0; rung < kRungCount; ++rung) {
        const int e = kMinQuarterExponent + rung;
        CHECK(divisionAt(rung * kModifierCount + 0) == makeDivision(e, DivisionModifier::Straight));
        CHECK(divisionAt(rung * kModifierCount + 1) == makeDivision(e, DivisionModifier::Dotted));
        CHECK(divisionAt(rung * kModifierCount + 2) == makeDivision(e, DivisionModifier::Triplet));
    }
}

static void testIndexRoundTripsOverTheWholeSet() {
    for (int i = 0; i < kDivisionCount; ++i) {
        CHECK(divisionIndex(divisionAt(i)) == i);
    }
}

static void testEverySetMemberIsDistinct() {
    // No two indices name the same division, so the picker offers 39 real choices.
    for (int i = 0; i < kDivisionCount; ++i) {
        for (int j = i + 1; j < kDivisionCount; ++j) {
            CHECK(divisionAt(i) != divisionAt(j));
        }
    }
}

// --- Clamping -----------------------------------------------------------------

static void testOffLadderExponentClampsToTheNearestRung() {
    // Asserted through the readers, never by comparing two clamped Divisions: a clamp that
    // collapsed every exponent to one rung would make Division-to-Division comparisons agree
    // with their own mistake.
    CHECK(almostEqual(divisionBeats(makeDivision(-99, DivisionModifier::Straight)), 0.0625));
    CHECK(divisionLabel(makeDivision(-99, DivisionModifier::Straight)) == "1/64");
    CHECK(divisionIndex(makeDivision(-99, DivisionModifier::Straight)) == 0);

    CHECK(almostEqual(divisionBeats(makeDivision(99, DivisionModifier::Triplet)),
                      256.0 * 2.0 / 3.0));
    CHECK(divisionLabel(makeDivision(99, DivisionModifier::Triplet)) == "64/1t");
    CHECK(divisionIndex(makeDivision(99, DivisionModifier::Triplet)) == kDivisionCount - 1);

    // The exponent that only a corrupt persisted record could carry still names a real rung.
    CHECK(almostEqual(divisionBeats(makeDivision(120, DivisionModifier::Straight)), 256.0));
}

static void testUnnamedModifierClampsToStraight() {
    // The other half of the persisted pair. Neither divisionBeats nor divisionLabel can see
    // an unnamed modifier — both already fall through to the straight case — so the clamp is
    // measured where it does show: the picker index and equality.
    const DivisionModifier junk = static_cast<DivisionModifier>(7);
    CHECK(divisionIndex(makeDivision(0, junk))
          == divisionIndex(makeDivision(0, DivisionModifier::Straight)));
    CHECK(divisionLabel(makeDivision(0, junk)) == "1/4");  // and no junk reaches the readout
    CHECK(makeDivision(0, junk) == makeDivision(0, DivisionModifier::Straight));
    // Measured (clamp removed from clampModifier): still passes. junk(7) != Dotted's stored
    // modifier either way, clamped or raw — this discriminates a degenerate operator== that
    // ignores the modifier field, not the clamp itself.
    CHECK(makeDivision(0, junk) != makeDivision(0, DivisionModifier::Dotted));
}

static void testEveryConstructibleDivisionIndexesIntoThePickerSet() {
    // divisionIndex is what a picker array is subscripted with, so an out-of-set index is an
    // overrun in the caller. Both corrupt fields at once is the worst case: 12*3+7 without a
    // modifier clamp.
    const int exponents[] = {-9000, -99, kMinQuarterExponent, 0, kMaxQuarterExponent, 120, 9000};
    for (int e : exponents) {
        // 260, not 256: m=256..259 wrap modulo uint8_t back to 0..3, re-covering the four
        // lowest bytes rather than reaching any byte 256 alone couldn't already reach.
        for (int m = 0; m < 260; ++m) {
            const Division d = makeDivision(e, static_cast<DivisionModifier>(m));
            const int index = divisionIndex(d);
            CHECK(index >= 0 && index < kDivisionCount);
            CHECK(divisionAt(index) == d);  // and the picker round-trips it back
        }
    }
}

static void testOutOfRangeIndexClampsIntoTheSet() {
    CHECK(divisionAt(-1) == divisionAt(0));
    CHECK(divisionAt(kDivisionCount) == divisionAt(kDivisionCount - 1));
}

int main() {
    testLadderSpansSixtyfourthToSixtyFourWhole();
    testEveryStraightRungHasItsWrittenBeatLength();
    testDottedIsHalfAgainAndTripletIsTwoThirds();
    testExtremes();

    testNamedExamples();

    testPickerOrderIsShortestFirst();
    testIndexRoundTripsOverTheWholeSet();
    testEverySetMemberIsDistinct();

    testOffLadderExponentClampsToTheNearestRung();
    testUnnamedModifierClampsToStraight();
    testEveryConstructibleDivisionIndexesIntoThePickerSet();
    testOutOfRangeIndexClampsIntoTheSet();

    if (g_fail == 0) std::printf("musical_division: all tests passed\n");
    else std::printf("musical_division: %d FAILED\n", g_fail);
    return g_fail == 0 ? 0 : 1;
}
