// Standalone tests for the live-parameter block itself — no VST3, no REAPER, no framework:
// the single fold from the parameter set, the seqlock's coherence under a concurrent writer,
// and the ramp's exact termination. The block's effect on a sounding voice is
// live_delivery_tests.

#include "../src/core/instrument/engine/live_params.h"

#include <atomic>
#include <cstdio>
#include <thread>
#include <type_traits>

using namespace reasampler;
using instrument::engine::LiveParams;
using instrument::engine::LiveValues;
using instrument::engine::ValueRamp;
using instrument::engine::foldLive;
using instrument::engine::liveRampStep;

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

// The block is copied wholesale under the seqlock, so anything that owns memory here would be
// a use-after-free waiting for a racing publish.
static_assert(std::is_trivially_copyable_v<LiveValues>, "the live block must stay plain data");

static void testFoldCarriesEveryContinuousControl() {
    PlayParams p;
    p.adsr = AdsrParams{11, 22, 33, 0.44, 55, 2.0, 0.5, 3.0};
    p.trigAhd = AhdParams{61, 62, 0.63, 4.0, 0.25};
    p.filter.trigEnv = AhdParams{71, 72, 0.73, 5.0, 0.2};
    p.filter.enabled = true;
    p.filter.settings.cutoffNorm = 0.25f;
    p.filter.settings.resonanceNorm = 0.5f;
    p.filter.settings.morphNorm = 0.75f;
    p.filter.settings.driveNorm = 0.125f;
    p.filter.modAmount = -0.6;
    p.filter.keyTrack = 1.5;
    p.filter.env = AdsrParams{1, 2, 3, 0.4, 5};
    p.pitchEnv.shape = AhdParams{7, 9, 0.4, 1.5, 0.75};
    p.pitchEnv.peakSemitones = -3.5;

    // Distinct from filter.keyTrack below on purpose: the two are different controls and a fold
    // that crossed them would pass under a shared value.
    const LiveValues v = foldLive(p, /*keyTrack=*/0.8);
    CHECK(v.adsr.attackFrames == 11);
    CHECK(v.adsr.holdFrames == 22);
    CHECK(v.adsr.decayFrames == 33);
    CHECK(v.adsr.sustainLevel == 0.44);
    CHECK(v.adsr.releaseFrames == 55);
    CHECK(v.adsr.attackCurve == 2.0);
    CHECK(v.adsr.decayCurve == 0.5);
    CHECK(v.adsr.releaseCurve == 3.0);
    CHECK(v.ampAhd.attackFrames == 61);
    CHECK(v.ampAhd.decayFrames == 62);
    CHECK(v.ampAhd.holdFraction == 0.63);
    CHECK(v.ampAhd.attackCurve == 4.0);
    CHECK(v.ampAhd.decayCurve == 0.25);
    CHECK(v.filterAhd.attackFrames == 71);
    CHECK(v.filterAhd.holdFraction == 0.73);
    CHECK(v.filterAhd.decayCurve == 0.2);
    CHECK(v.filterSettings.cutoffNorm == 0.25f);
    CHECK(v.filterSettings.resonanceNorm == 0.5f);
    CHECK(v.filterSettings.morphNorm == 0.75f);
    CHECK(v.filterSettings.driveNorm == 0.125f);
    CHECK(v.filterModAmount == -0.6);
    CHECK(v.filterKeyTrack == 1.5);
    CHECK(v.filterEnv.decayFrames == 3);
    CHECK(v.filterEnv.sustainLevel == 0.4);
    CHECK(v.pitchEnv.shape.attackFrames == 7);
    CHECK(v.pitchEnv.shape.decayFrames == 9);
    CHECK(v.pitchEnv.shape.holdFraction == 0.4);
    CHECK(v.pitchEnv.shape.attackCurve == 1.5);
    CHECK(v.pitchEnv.shape.decayCurve == 0.75);
    CHECK(v.pitchEnv.peakSemitones == -3.5);
    CHECK(v.keyTrack == 0.8);
    // Spline-folded on the way in, so the block carries what the voice will actually play.
    CHECK(!v.splineActive);
    CHECK(v.lengthFraction == p.trigger.lengthFraction);
}

// The fold, not the voice, is where a drawn contour pins the Trigger span — so the block a
// note-on latches already carries the folded value.
static void testADrawnEnvelopePinsTheFoldedTriggerLength() {
    PlayParams p;
    p.trigger.lengthFraction = 0.25;
    p.ampSpline.mode = EnvMode::Spline;
    const LiveValues v = foldLive(p, kKeyTrackDefault);
    CHECK(v.splineActive);
    CHECK(v.lengthFraction == 1.0);
}

// Poisons the block ONE LEAF FIELD AT A TIME and checks operator== catches every one — the half
// that actually catches a forgotten field, since the static_assert above only fires when a
// member changes sizeof(LiveValues), which padding can absorb. Covers every leaf of every
// nested struct, not just the 14 top-level members, so a member dropped from sameAdsr/sameAhd/
// sameFilterSettings is caught here too, not just a member dropped from operator== itself.
static void testEveryFieldOfLiveValuesIsCompared() {
    using instrument::engine::filter::MorphLaw;
    const LiveValues base{};
    auto poisoned = [&](auto mutate) {
        LiveValues v = base;
        mutate(v);
        return v;
    };
    CHECK(base == base);
    CHECK(poisoned([](LiveValues& v) { v.filterSettings.cutoffNorm += 0.1f; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterSettings.resonanceNorm += 0.1f; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterSettings.morphNorm += 0.1f; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterSettings.driveNorm += 0.1f; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterSettings.morphLaw = MorphLaw::HighNotchLow; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterModAmount += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterVelAmount += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterKeyTrack += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.attackFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.holdFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.decayFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.sustainLevel += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.releaseFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.attackCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.decayCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterEnv.releaseCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterAhd.attackFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterAhd.decayFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterAhd.holdFraction += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterAhd.attackCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.filterAhd.decayCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.attackFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.holdFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.decayFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.sustainLevel += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.releaseFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.attackCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.decayCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.adsr.releaseCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.ampAhd.attackFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.ampAhd.decayFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.ampAhd.holdFraction += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.ampAhd.attackCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.ampAhd.decayCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchEnv.enabled = !v.pitchEnv.enabled; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchEnv.peakSemitones += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.attackFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.decayFrames += 1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.holdFraction += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.attackCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchEnv.shape.decayCurve += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.playRate += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.pitchOffsetSemitones += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.keyTrack += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.lengthFraction += 0.1; }) != base);
    CHECK(poisoned([](LiveValues& v) { v.splineActive = !v.splineActive; }) != base);
}

static void testUnpublishedBlockReadsAsNothing() {
    LiveParams block;
    LiveValues out;
    CHECK(block.read(out) == 0);  // never published: the caller must keep its own defaults

    LiveValues v;
    v.filterModAmount = 0.5;
    block.publish(v);
    const std::uint32_t first = block.read(out);
    CHECK(first != 0);
    CHECK(out.filterModAmount == 0.5);
    // An unchanged block reports the SAME generation, which is how the reader knows there is
    // nothing to push into the voices.
    CHECK(block.read(out) == first);
    block.publish(v);
    CHECK(block.read(out) != first);
}

// The torn-read hazard, exercised rather than argued: a writer publishes multi-field edits as
// fast as it can while a reader copies the block; every observed block must be one the writer
// actually published, never half of one and half of another.
static void testConcurrentReaderNeverSeesAHalfAppliedEdit() {
    LiveParams block;
    std::atomic<bool> stop{false};
    std::atomic<int> observed{0};
    std::atomic<int> torn{0};

    LiveValues seed;
    seed.adsr = AdsrParams{0, 0, 0, 0.0, 0};
    block.publish(seed);

    std::thread writer([&] {
        for (std::int64_t i = 1; !stop.load(std::memory_order_relaxed); ++i) {
            LiveValues v;
            // One coherent edit: every AHDSR field derives from the same i, so any mixture of
            // two edits is detectable from the values alone.
            v.adsr = AdsrParams{i, 2 * i, 3 * i, static_cast<double>(i), 5 * i};
            v.filterEnv = AdsrParams{4 * i, 5 * i, 6 * i, static_cast<double>(i), 7 * i};
            v.filterModAmount = static_cast<double>(i);
            block.publish(v);
        }
    });

    for (int n = 0; n < 200000; ++n) {
        LiveValues out;
        if (block.read(out) == 0) continue;  // abandoned read: the caller discards it
        observed.fetch_add(1, std::memory_order_relaxed);
        const std::int64_t i = out.adsr.attackFrames;
        const bool coherent =
            out.adsr.holdFrames == 2 * i && out.adsr.decayFrames == 3 * i &&
            out.adsr.releaseFrames == 5 * i && out.adsr.sustainLevel == static_cast<double>(i) &&
            out.filterEnv.attackFrames == 4 * i && out.filterEnv.holdFrames == 5 * i &&
            out.filterEnv.decayFrames == 6 * i && out.filterEnv.releaseFrames == 7 * i &&
            out.filterModAmount == static_cast<double>(i);
        if (!coherent) torn.fetch_add(1, std::memory_order_relaxed);
    }
    stop.store(true, std::memory_order_relaxed);
    writer.join();

    CHECK(torn.load() == 0);
    CHECK(observed.load() > 0);  // the run proved something only if reads actually landed
}

static void testRampTerminatesExactlyOnTheTarget() {
    ValueRamp r;
    r.set(0.0);
    r.step = 1.0 / 960.0;  // the 20 ms step at 48 kHz
    r.aim(0.4);
    int frames = 0;
    while (r.moving() && frames < 100000) { r.tick(); ++frames; }
    // Exact equality, not a tolerance: the filter's cutoff-skip fast path compares the value
    // itself, so an asymptotic smoother would pin it on the always-re-solve path forever.
    CHECK(r.value == 0.4);
    CHECK(!r.moving());
    CHECK(!r.tick());  // parked: no further movement reported
    CHECK(frames > 1);  // it glided rather than jumping
}

static void testRampWithNoRateSnaps() {
    ValueRamp r;
    r.set(0.2);
    r.step = liveRampStep(0.0);  // rate unknown: never invent one
    CHECK(r.step == 0.0);
    r.aim(0.9);
    CHECK(r.tick());
    CHECK(r.value == 0.9);
}

static void testRampStepIsRateDerived() {
    CHECK(liveRampStep(48000.0) == 1.0 / (0.020 * 48000.0));
    CHECK(liveRampStep(96000.0) == 1.0 / (0.020 * 96000.0));
    CHECK(liveRampStep(-1.0) == 0.0);
}

int main() {
    testFoldCarriesEveryContinuousControl();
    testADrawnEnvelopePinsTheFoldedTriggerLength();
    testEveryFieldOfLiveValuesIsCompared();
    testUnpublishedBlockReadsAsNothing();
    testConcurrentReaderNeverSeesAHalfAppliedEdit();
    testRampTerminatesExactlyOnTheTarget();
    testRampWithNoRateSnaps();
    testRampStepIsRateDerived();
    if (g_fail == 0) std::printf("live_params tests passed\n");
    return g_fail == 0 ? 0 : 1;
}
