// Standalone tests for reasampler::instrument::ui::bake_hold — the Hold knob's map onto the
// note-length ladder. No VST3, no REAPER, no framework.
//
// Covers: both ends of the knob, the round trip from every rung, out-of-range and non-finite
// input, that every rung is reachable (no rung is skipped by the rounding), that the travel is
// monotone in DURATION, and that each rung owns an equal slice of it.

#include "../src/core/instrument/ui/bake_hold.h"

#include <cmath>
#include <cstdio>
#include <vector>

using namespace reasampler::instrument::ui;
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)

namespace {

// The shortest and longest lengths ON THE LADDER, found by scanning rather than by indexing an
// end of the picker order — which is exactly the assumption under test.
Division shortestRung() {
    Division best = divisionAt(0);
    for (int i = 1; i < kDivisionCount; ++i)
        if (divisionBeats(divisionAt(i)) < divisionBeats(best)) best = divisionAt(i);
    return best;
}

Division longestRung() {
    Division best = divisionAt(0);
    for (int i = 1; i < kDivisionCount; ++i)
        if (divisionBeats(divisionAt(i)) > divisionBeats(best)) best = divisionAt(i);
    return best;
}

}  // namespace

int main() {
    // --- The ends of the travel are the SHORTEST and LONGEST notes -------------------
    // Not divisionAt(0) / divisionAt(kDivisionCount - 1): those are the ends of the picker's
    // presentation order, which is not length order.
    CHECK(bakeHoldFromNorm(0.0) == shortestRung());
    CHECK(bakeHoldFromNorm(1.0) == longestRung());

    // --- Out of range clamps rather than wrapping ------------------------------------
    CHECK(bakeHoldFromNorm(-3.0) == shortestRung());
    CHECK(bakeHoldFromNorm(9.5) == longestRung());
    CHECK(bakeHoldFromNorm(std::nan("")) == shortestRung());

    // --- Round trip: a knob painted from a stored rung and released reproduces it -----
    for (int i = 0; i < kDivisionCount; ++i) {
        const Division d = divisionAt(i);
        CHECK(bakeHoldFromNorm(bakeHoldNorm(d)) == d);
    }

    // --- Every rung is reachable from the knob, and each owns a contiguous slice ------
    // Swept finely enough to catch a rounding that skipped one: 39 rungs over [0,1].
    {
        std::vector<bool> seen(static_cast<std::size_t>(kDivisionCount), false);
        for (int step = 0; step <= 4000; ++step) {
            const Division d = bakeHoldFromNorm(static_cast<double>(step) / 4000.0);
            seen[static_cast<std::size_t>(divisionIndex(d))] = true;
        }
        for (int i = 0; i < kDivisionCount; ++i) CHECK(seen[static_cast<std::size_t>(i)]);
    }

    // --- The map is monotone IN DURATION: turning the knob up never shortens the note --
    // Asserted over divisionBeats, not divisionIndex: the index is presentation order, in which
    // a rung's triplet sits after (and is shorter than) the previous rung's dotted — so an
    // index sweep can be non-decreasing while the note it selects gets shorter.
    {
        double previous = 0.0;
        for (int step = 0; step <= 4000; ++step) {
            const double beats =
                divisionBeats(bakeHoldFromNorm(static_cast<double>(step) / 4000.0));
            CHECK(beats >= previous);
            previous = beats;
        }
    }

    // --- Each rung owns an EQUAL slice, centred on its own position -------------------
    // A floor-based map passes every property above while giving each rung the slice ABOVE its
    // position; only the boundaries tell the two apart. Slice k spans [(k-0.5)/L, (k+0.5)/L).
    {
        constexpr int last = kDivisionCount - 1;
        const double slice = 1.0 / static_cast<double>(last);
        const double eps = slice / 1000.0;
        for (int k = 1; k < last; ++k) {
            const double centre = static_cast<double>(k) * slice;
            CHECK(bakeHoldFromNorm(centre) == bakeHoldFromNorm(centre - slice * 0.5 + eps));
            CHECK(bakeHoldFromNorm(centre) == bakeHoldFromNorm(centre + slice * 0.5 - eps));
            // …and one step past the upper boundary is already the NEXT rung.
            CHECK(divisionBeats(bakeHoldFromNorm(centre + slice * 0.5 + eps)) >
                  divisionBeats(bakeHoldFromNorm(centre)));
        }
    }

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