// Standalone tests for reasampler::instrument::map::trigger_seam — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests.
//
// Covers triggerPlayLength: zero play length, startFrame set, startFrame past frameCount,
// rounding, the out-of-domain clamps that keep it identical to Voice::start's inline copy of
// the same formula, and the Finding 1 regression (start-point set — the case that was broken
// before this module existed). The spline fold that used to live here is now beside its
// siblings in play_params.h, and pinned with them in test_spline_egs.

#include "../src/core/instrument/map/trigger_seam.h"

#include <cmath>
#include <cstdio>
#include <limits>

using namespace reasampler;
using namespace reasampler::instrument::map;

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

// --- triggerPlayLength --------------------------------------------------------

static void testPlayLengthNoStartPoint() {
    // startFrame == 0 (no start marker): postStart = frameCount.
    // 1000 frames, lengthFraction 1.0 -> 1000.
    CHECK(triggerPlayLength(1.0, 1000, 0) == 1000);
    // 1000 frames, lengthFraction 0.5 -> round(500.0) = 500.
    CHECK(triggerPlayLength(0.5, 1000, 0) == 500);
    // 1000 frames, lengthFraction 0.333 -> round(333.0) = 333.
    CHECK(triggerPlayLength(0.333, 1000, 0) == 333);
}

static void testPlayLengthWithStartPoint() {
    // The Finding 1 regression: startFrame set, play length must be shorter.
    // frameCount=1000, startFrame=200 -> postStart=800.
    // lengthFraction 1.0 -> 800 (NOT 1000 as the pre-fix code produced).
    CHECK(triggerPlayLength(1.0, 1000, 200) == 800);
    // lengthFraction 0.5 -> round(400.0) = 400.
    CHECK(triggerPlayLength(0.5, 1000, 200) == 400);
}

static void testPlayLengthZeroFrameCount() {
    // No decoded audio -> 0.
    CHECK(triggerPlayLength(1.0, 0, 0) == 0);
}

static void testPlayLengthStartFramePastEnd() {
    // startFrame >= frameCount -> postStart clamped to 0 -> play length 0.
    CHECK(triggerPlayLength(1.0, 500, 500) == 0);
    CHECK(triggerPlayLength(1.0, 500, 600) == 0);
}

static void testPlayLengthRounding() {
    // round(0.5) = 1 (round half up via +0.5 truncation: 0.5 + 0.5 = 1.0 -> 1).
    CHECK(triggerPlayLength(0.5, 1, 0) == 1);
    // round(lengthFraction * 3): 0.4 * 3 = 1.2 -> 1.
    CHECK(triggerPlayLength(0.4, 3, 0) == 1);
    // 0.6 * 3 = 1.8 -> 2.
    CHECK(triggerPlayLength(0.6, 3, 0) == 2);
}

// The stored fraction is a wire double with no codec-side range check beyond finiteness, and
// Voice::start clamps every one of these the same way. A span that ran past the source would
// read off the end of the PCM; one derived from NaN would reach an undefined narrowing.
static void testOutOfDomainFractionsClampToTheSpan() {
    CHECK(triggerPlayLength(1.5, 1000, 0) == 1000);    // never past the post-start span
    CHECK(triggerPlayLength(1.5, 1000, 200) == 800);
    CHECK(triggerPlayLength(-0.5, 1000, 0) == 0);
    CHECK(triggerPlayLength(std::nan(""), 1000, 0) == 0);
    CHECK(triggerPlayLength(std::numeric_limits<double>::infinity(), 1000, 0) == 1000);
}

int main() {
    testOutOfDomainFractionsClampToTheSpan();
    testPlayLengthNoStartPoint();
    testPlayLengthWithStartPoint();
    testPlayLengthZeroFrameCount();
    testPlayLengthStartFramePastEnd();
    testPlayLengthRounding();

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