// Standalone tests for reasampler::instrument::ui::deck_values — no VST3, no REAPER, no
// framework. Covers the deck's parameter-set binding: the norm <-> stored-value round trip on a
// representative control of each domain, the DOUBLE-CLICK RESET (each ring of a dual-ring knob
// resetting only its own field), and the ms time-constant formatter across its whole range.

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

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

#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>

using namespace reasampler;
using namespace reasampler::instrument::ui;
namespace engine = reasampler::instrument::engine;  // the stretcher's own rate bounds + clamp

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 stage-time ceiling has TWO names — the overlay's schematic domain and the knob's — and they
// must be the same number or a maxed knob stops landing on the canvas edge. Asserted, not assumed.
static void testTheTwoCeilingNamesAreOneNumber() {
    CHECK(kEnvTimeMaxSeconds == kGateStageMaxSeconds);
    CHECK(kEnvTimeMaxSeconds == kStageTimeMaxSeconds);
    CHECK(kEnvTimeMaxSeconds == 10.0);
}

// Every domain the binding maps: a stage time through the shared taper, a level, a fraction,
// a normalized filter position, a bipolar depth, and a curve exponent over its log travel.
static void testNormRoundTripsThroughEveryValueDomain() {
    PlaySeconds p;
    setDeckParam(DeckParam::kAttack, p, 0.25, 0);
    CHECK(p.adsr.attackSeconds == timeSecondsFromNorm(0.25));
    // The VALUE round trip is what has to be exact (param_taper.h); the needle returning to the
    // very same norm double is explicitly NOT required of a log map. The residual is bounded by
    // the taper's output quantum read back through the map — under 1e-7 of the travel across the
    // whole domain, which is four orders below one drag pixel.
    CHECK(std::fabs(deckParamNorm(DeckParam::kAttack, p) - 0.25) < 1e-7);
    // The raised ceiling costs the low end nothing: a several-second stage is reachable by hand,
    // AND everything under 100 ms still gets more than 40 % of the knob's travel to itself.
    setDeckParam(DeckParam::kDecay, p, 0.95, 0);
    CHECK(p.adsr.decaySeconds > 5.0 && p.adsr.decaySeconds < kEnvTimeMaxSeconds);
    setDeckParam(DeckParam::kDecay, p, 0.42, 0);
    CHECK(p.adsr.decaySeconds < 0.100);

    setDeckParam(DeckParam::kSustain, p, 0.4, 0);
    CHECK(p.adsr.sustainLevel == 0.4);
    CHECK(deckParamNorm(DeckParam::kSustain, p) == 0.4);

    setDeckParam(DeckParam::kTrigHold, p, 0.75, 0);
    CHECK(p.trigAhd.holdFraction == 0.75);
    CHECK(deckParamNorm(DeckParam::kTrigHold, p) == 0.75);

    // Named field, not just a round trip: cutoff and morph are both normalized positions with
    // the same 1.0 default, so a getter+setter pair that swapped them would round-trip cleanly.
    setDeckParam(DeckParam::kFilterCutoff, p, 0.25, 0);
    CHECK(p.filter.settings.cutoffNorm == 0.25f);
    CHECK(p.filter.settings.morphNorm == 1.0f);
    CHECK(deckParamNorm(DeckParam::kFilterCutoff, p) == 0.25);

    // Bipolar: the centre detent is exact in BOTH directions, so a knob parked at centre
    // persists no depth at all.
    setDeckParam(DeckParam::kFilterModAmt, p, 0.5, 0);
    CHECK(p.filter.modAmount == 0.0);
    CHECK(deckParamNorm(DeckParam::kFilterModAmt, p) == 0.5);
    setDeckParam(DeckParam::kFilterModAmt, p, 1.0, 0);
    CHECK(p.filter.modAmount == 1.0);

    // A curve exponent off neutral survives the round trip; the centre snaps to exactly 1.0.
    setDeckParam(DeckParam::kAttackCurve, p, 1.0, 0);
    CHECK(p.adsr.attackCurve > 1.0);
    CHECK(deckParamNorm(DeckParam::kAttackCurve, p) == 1.0);
    setDeckParam(DeckParam::kAttackCurve, p, 0.5, 0);
    CHECK(p.adsr.attackCurve == 1.0);

    // Out-of-range norms clamp rather than writing an out-of-domain param.
    setDeckParam(DeckParam::kDecay, p, 2.0, 0);
    CHECK(p.adsr.decaySeconds == kEnvTimeMaxSeconds);
    setDeckParam(DeckParam::kDecay, p, -1.0, 0);
    CHECK(p.adsr.decaySeconds == 0.0);
}

// Rate's range is the STRETCHER's, aliased rather than restated, so the knob's two ends and the
// engine's clamp cannot become two opinions. Asserted against the engine constants themselves.
static void testRateKnobEndsAreTheStretchersOwnBounds() {
    CHECK(kRateMinRatio == engine::kStretchRateMin);
    CHECK(kRateMaxRatio == engine::kStretchRateMax);
    PlaySeconds p;
    setDeckParam(DeckParam::kRate, p, 0.0, 0);
    CHECK(p.playRate == engine::kStretchRateMin);
    CHECK(engine::clampStretchRate(p.playRate) == p.playRate);  // the clamp has nothing to do
    setDeckParam(DeckParam::kRate, p, 1.0, 0);
    CHECK(p.playRate == engine::kStretchRateMax);
    CHECK(engine::clampStretchRate(p.playRate) == p.playRate);
    // And nowhere on the travel does the knob produce a rate the engine would move.
    for (int i = 0; i <= 1000; ++i) {
        setDeckParam(DeckParam::kRate, p, static_cast<double>(i) / 1000.0, 0);
        CHECK(engine::clampStretchRate(p.playRate) == p.playRate);
        if (engine::clampStretchRate(p.playRate) != p.playRate) return;
    }
}

// The two new bindings write the two new fields and nothing else — both are doubles on
// PlaySeconds with adjacent homes, so a getter/setter pair that crossed them would still
// round-trip. The centre detent is exact on both, which is what lets an untouched knob persist
// unity rate and zero transposition.
static void testRateAndPitchBindTheirOwnFields() {
    PlaySeconds p;
    setDeckParam(DeckParam::kRate, p, 0.5, 0);
    CHECK(p.playRate == 1.0);
    CHECK(p.pitchOffsetSemitones == 0.0);
    CHECK(deckParamNorm(DeckParam::kRate, p) == 0.5);

    setDeckParam(DeckParam::kPitch, p, 0.5, 0);
    CHECK(p.pitchOffsetSemitones == 0.0);
    CHECK(p.playRate == 1.0);
    CHECK(deckParamNorm(DeckParam::kPitch, p) == 0.5);

    // Pitch rides the SAME centre-expanded depth taper as the pitch envelope's own depth, over
    // the SAME throw — a second constant here would be the defect the spec names.
    setDeckParam(DeckParam::kPitch, p, 1.0, 0);
    CHECK(p.pitchOffsetSemitones == kPitchDepthMaxSemis);
    CHECK(kPitchDepthMaxSemis == kVelocityPitchRangeSemitones);
    setDeckParam(DeckParam::kPitch, p, 0.0, 0);
    CHECK(p.pitchOffsetSemitones == -kPitchDepthMaxSemis);
    CHECK(p.playRate == 1.0);  // untouched by every write above but its own

    // A move on Rate leaves the offset alone, in the other direction.
    setDeckParam(DeckParam::kPitch, p, 0.5, 0);
    setDeckParam(DeckParam::kRate, p, 0.0, 0);
    CHECK(p.pitchOffsetSemitones == 0.0);
}

// Shift's whole unit on BOTH new knobs is the semitone, not the percent their labels read in.
// Asserted through the deck's own snap entry point (the shell calls nothing else), and in
// semitones, which is the unit the rule is stated in.
static void testShiftSnapsBothNewKnobsToWholeSemitones() {
    CHECK(deckParamUnit(DeckParam::kRate) == UnitCategory::Semitones);
    CHECK(deckParamUnit(DeckParam::kPitch) == UnitCategory::Semitones);

    PlaySeconds p;
    // Rate: a norm a third of the way up is 8 semitones below unity — snapping must land on a
    // whole one, and the knob must still be able to reach an octave and a fifth by hand.
    for (double norm : {0.13, 0.37, 0.5, 0.62, 0.88}) {
        setDeckParam(DeckParam::kRate, p, snapDeckParamNorm(DeckParam::kRate, norm), 0);
        const double semis = 12.0 * std::log2(p.playRate);
        CHECK(std::fabs(semis - std::round(semis)) < 1e-9);
        if (!(std::fabs(semis - std::round(semis)) < 1e-9)) return;
    }
    // The two landmarks by name: unity, and a fifth up.
    setDeckParam(DeckParam::kRate, p, snapDeckParamNorm(DeckParam::kRate, 0.5), 0);
    CHECK(p.playRate == 1.0);
    setDeckParam(DeckParam::kRate, p, snapDeckParamNorm(DeckParam::kRate, 0.5 + 7.0 / 24.0), 0);
    CHECK(std::fabs(12.0 * std::log2(p.playRate) - 7.0) < 1e-9);

    // Pitch: whole semitones on the centre-expanded taper, exactly (its taper resolves onto a
    // micro-semitone grid, so a whole semitone is ON that grid).
    for (double norm : {0.17, 0.33, 0.71, 0.94}) {
        setDeckParam(DeckParam::kPitch, p, snapDeckParamNorm(DeckParam::kPitch, norm), 0);
        CHECK(p.pitchOffsetSemitones == std::round(p.pitchOffsetSemitones));
        if (p.pitchOffsetSemitones != std::round(p.pitchOffsetSemitones)) return;
    }
}

// The dual-ring reset contract: the outer ring resets the stage VALUE and the inner dial resets
// the EXPONENT, each leaving the other exactly as it was. Both fields are asserted in both
// directions — checking only the field that changed would pass even if the reset clobbered its
// neighbour.
static void testResetTouchesOnlyItsOwnRingOnADualRingKnob() {
    const PlaySeconds defaults;
    const struct { DeckParam knob; DeckParam curve; } pairs[] = {
        {DeckParam::kAttack, DeckParam::kAttackCurve},
        {DeckParam::kDecay, DeckParam::kDecayCurve},
        {DeckParam::kRelease, DeckParam::kReleaseCurve},
        {DeckParam::kTrigAttack, DeckParam::kTrigAttackCurve},
        {DeckParam::kPitchEnvDecay, DeckParam::kPitchEnvDecayCurve},
        {DeckParam::kFilterEnvRelease, DeckParam::kFilterEnvReleaseCurve},
    };
    for (const auto& pr : pairs) {
        // Dial BOTH rings well away from their defaults.
        PlaySeconds p;
        setDeckParam(pr.knob, p, 0.6, 0);
        setDeckParam(pr.curve, p, 0.9, 0);
        const double dialledValue = deckParamNorm(pr.knob, p);
        const double dialledCurve = deckParamNorm(pr.curve, p);
        CHECK(dialledValue != deckParamNorm(pr.knob, defaults));
        CHECK(dialledCurve != deckParamNorm(pr.curve, defaults));

        // INNER: the exponent goes to exactly the linear neutral, the value does not move.
        PlaySeconds inner = p;
        resetDeckParam(pr.curve, inner);
        CHECK(deckParamNorm(pr.curve, inner) == deckParamNorm(pr.curve, defaults));
        CHECK(deckParamNorm(pr.curve, inner) == 0.5);  // the exponent itself is 1.0
        CHECK(deckParamNorm(pr.knob, inner) == dialledValue);

        // OUTER: the value goes to its default, the exponent does not move.
        PlaySeconds outer = p;
        resetDeckParam(pr.knob, outer);
        CHECK(deckParamNorm(pr.knob, outer) == deckParamNorm(pr.knob, defaults));
        CHECK(deckParamNorm(pr.curve, outer) == dialledCurve);
    }
}

// The exponent reset is specified as EXACTLY 1.0 — the identity curveMap short-circuits on
// (curve_law.h), not merely something that rounds to it.
static void testInnerResetLandsOnTheExactLinearNeutral() {
    PlaySeconds p;
    setDeckParam(DeckParam::kAttackCurve, p, 0.2, 0);
    CHECK(p.adsr.attackCurve < 1.0);
    resetDeckParam(DeckParam::kAttackCurve, p);
    CHECK(p.adsr.attackCurve == 1.0);

    setDeckParam(DeckParam::kFilterTrigDecayCurve, p, 0.95, 0);
    CHECK(p.filter.trigEnv.decayCurve > 1.0);
    resetDeckParam(DeckParam::kFilterTrigDecayCurve, p);
    CHECK(p.filter.trigEnv.decayCurve == 1.0);
}

// A reset lands on the field's own stored default, EXACTLY — the defaults are read off a fresh
// PlaySeconds and COPIED rather than round-tripped, which is what makes the two stage times whose
// defaults are neither 0 nor 1 land bit for bit at a non-power-of-two ceiling.
static void testResetLandsOnTheStoredDefaultOfEachControl() {
    const PlaySeconds defaults;
    PlaySeconds p;
    setDeckParam(DeckParam::kSustain, p, 0.1, 0);
    setDeckParam(DeckParam::kTrigLength, p, 0.3, 0);
    setDeckParam(DeckParam::kFilterKeyTrack, p, 0.9, 0);
    setDeckParam(DeckParam::kPitchEnvDepth, p, 1.0, 0);
    setDeckParam(DeckParam::kAttack, p, 0.5, 0);
    setDeckParam(DeckParam::kRelease, p, 0.5, 0);
    CHECK(p.adsr.attackSeconds != defaults.adsr.attackSeconds);
    CHECK(p.adsr.releaseSeconds != defaults.adsr.releaseSeconds);

    resetDeckParam(DeckParam::kSustain, p);
    resetDeckParam(DeckParam::kTrigLength, p);
    resetDeckParam(DeckParam::kFilterKeyTrack, p);
    resetDeckParam(DeckParam::kPitchEnvDepth, p);
    resetDeckParam(DeckParam::kAttack, p);
    resetDeckParam(DeckParam::kRelease, p);

    CHECK(p.adsr.sustainLevel == defaults.adsr.sustainLevel);
    CHECK(p.trigger.lengthFraction == defaults.trigger.lengthFraction);
    CHECK(p.filter.keyTrack == defaults.filter.keyTrack);
    CHECK(p.pitchEnv.peakSemitones == defaults.pitchEnv.peakSemitones);
    CHECK(p.adsr.attackSeconds == defaults.adsr.attackSeconds);
    CHECK(p.adsr.releaseSeconds == defaults.adsr.releaseSeconds);
}

// EVERY knob resets to its own stored default, not just the six dual-ring pairs above. Swept
// over the whole control-id space so a control added later cannot quietly miss the reset table:
// perturb, reset, and require the control to read exactly what a fresh PlaySeconds reads.
// Compared against the STORED FIELD directly (deckDoubleField/deckFloatField), not the
// normalized read-back: deckParamNorm is not guaranteed injective, so a norm match is weaker
// than the criterion — verification against a default-constructed PlaySeconds.
static void testEveryKnobIdResetsToItsDefault() {
    PlaySeconds defaults;
    for (int i = 0; i < static_cast<int>(DeckParam::kCount); ++i) {
        const DeckParam id = static_cast<DeckParam>(i);
        if (deckParamUnit(id) == UnitCategory::None) continue;  // no reset gesture
        if (id == DeckParam::kMasterGain || id == DeckParam::kKeyTrack) continue;  // not in PlaySeconds
        PlaySeconds p;
        setDeckParam(id, p, 0.37, 0);
        setDeckParam(id, p, 0.83, 0);  // two writes: one of the two is off every default
        if (double* pd = deckDoubleField(id, p)) {
            CHECK(*pd != *deckDoubleField(id, defaults));
            resetDeckParam(id, p);
            CHECK(*pd == *deckDoubleField(id, defaults));
        } else if (float* pf = deckFloatField(id, p)) {
            CHECK(*pf != *deckFloatField(id, defaults));
            resetDeckParam(id, p);
            CHECK(*pf == *deckFloatField(id, defaults));
        } else {
            CHECK(false);  // every non-None, non-excluded id must own a reset field
        }
    }
}

// THE exact-preimage criterion, per unit category, against a default-constructed PlaySeconds and
// against master gain's unity. A host's reset-to-default arrives as toPlain(defaultNorm) with no
// bypass available, so this is the assertion the reset bypass CANNOT stand in for.
static void testEveryDefaultHasAnExactNormalizedPreimage() {
    const PlaySeconds d;
    const struct { DeckParam id; double stored; } msKnobs[] = {
        {DeckParam::kAttack,           d.adsr.attackSeconds},
        {DeckParam::kHold,             d.adsr.holdSeconds},
        {DeckParam::kDecay,            d.adsr.decaySeconds},
        {DeckParam::kRelease,          d.adsr.releaseSeconds},
        {DeckParam::kTrigAttack,       d.trigAhd.attackSeconds},
        {DeckParam::kTrigDecay,        d.trigAhd.decaySeconds},
        {DeckParam::kPitchEnvAttack,   d.pitchEnv.shape.attackSeconds},
        {DeckParam::kPitchEnvDecay,    d.pitchEnv.shape.decaySeconds},
        {DeckParam::kFilterEnvAttack,  d.filter.env.attackSeconds},
        {DeckParam::kFilterEnvHold,    d.filter.env.holdSeconds},
        {DeckParam::kFilterEnvDecay,   d.filter.env.decaySeconds},
        {DeckParam::kFilterEnvRelease, d.filter.env.releaseSeconds},
        {DeckParam::kFilterTrigAttack, d.filter.trigEnv.attackSeconds},
        {DeckParam::kFilterTrigDecay,  d.filter.trigEnv.decaySeconds},
    };
    for (const auto& k : msKnobs) {
        CHECK(timeSecondsFromNorm(deckParamNorm(k.id, d)) == k.stored);
    }
    // The two whose defaults are neither 0 nor the ceiling are the ones that can actually fail.
    CHECK(d.adsr.attackSeconds == 0.003 && d.adsr.releaseSeconds == 0.060);

    CHECK(depthSemitonesFromNorm(deckParamNorm(DeckParam::kPitchEnvDepth, d),
                                 kPitchDepthMaxSemis) == d.pitchEnv.peakSemitones);
    CHECK(deckParamNorm(DeckParam::kSustain, d) == d.adsr.sustainLevel);
    CHECK(deckParamNorm(DeckParam::kTrigLength, d) == d.trigger.lengthFraction);
    CHECK(deckParamNorm(DeckParam::kTrigHold, d) == d.trigAhd.holdFraction);
    CHECK(deckBipolarFromNorm(deckParamNorm(DeckParam::kFilterModAmt, d)) == d.filter.modAmount);
    // The PITCH/RATE pair. Rate's preimage is the taper's unity detent, which sits at true
    // centre only because these bounds are reciprocal; Pitch's is the depth taper's exact zero.
    CHECK(rateRatioFromNorm(deckParamNorm(DeckParam::kRate, d), kRateMinRatio, kRateMaxRatio) ==
          d.playRate);
    CHECK(depthSemitonesFromNorm(deckParamNorm(DeckParam::kPitch, d), kPitchDepthMaxSemis) ==
          d.pitchOffsetSemitones);
    CHECK(util::curveFromKnobNorm(deckParamNorm(DeckParam::kAttackCurve, d)) ==
          d.adsr.attackCurve);
    // Master gain's unity: the case where a hair off is an audible gain error rather than a
    // cosmetic one. Its taper is engine/master_gain's — consumed here, not defined here.
    CHECK(instrument::engine::masterGainLinearFromNorm(instrument::engine::masterGainNormFromLinear(1.0)) == 1.0);
}

// Shift's snap unit is a property of the control's UNIT and lands on a whole unit of what the
// control DISPLAYS — which is why three controls sharing the Percent category take three
// different norm steps.
static void testShiftSnapsToAWholeUnitOfTheDisplayedValue() {
    // Milliseconds: the snapped norm reads back as an exact whole millisecond.
    const double ms = timeSecondsFromNorm(snapDeckParamNorm(DeckParam::kAttack,
                                                            timeNormFromSeconds(0.03472)));
    CHECK(ms == 0.035);
    // Semitones.
    CHECK(depthSemitonesFromNorm(
              snapDeckParamNorm(DeckParam::kPitchEnvDepth,
                                depthNormFromSemitones(6.6, kPitchDepthMaxSemis)),
              kPitchDepthMaxSemis) == 7.0);
    // Percent, 0..100 %: the norm IS the fraction.
    CHECK(snapDeckParamNorm(DeckParam::kSustain, 0.4162) == 0.42);
    // Percent, 0..200 %: a whole DISPLAYED percent is half a norm percent.
    CHECK(snapDeckParamNorm(DeckParam::kFilterKeyTrack, 0.4162) == 0.4150);
    // Percent, +/-100 %: likewise, measured on the bipolar value.
    CHECK(snapDeckParamNorm(DeckParam::kFilterVel, deckNormFromBipolar(-0.4162)) ==
          deckNormFromBipolar(-0.42));
    // Exponent: whole numbers, which puts the linear neutral one snap from centre. Compared as
    // the norm the snap RETURNS — the exponent's own log travel is not an exact round trip.
    CHECK(snapDeckParamNorm(DeckParam::kAttackCurve, util::knobNormFromCurve(2.6)) ==
          util::knobNormFromCurve(3.0));
    CHECK(snapDeckParamNorm(DeckParam::kAttackCurve, util::knobNormFromCurve(1.4)) ==
          util::knobNormFromCurve(util::kCurveNeutral));
    // Decibels, likewise compared as the returned norm.
    CHECK(snapDeckParamNorm(DeckParam::kMasterGain,
                            instrument::engine::masterGainNormFromDb(-6.4)) ==
          instrument::engine::masterGainNormFromDb(-6.0));
    // Already-integer and discrete controls are untouched.
    CHECK(snapDeckParamNorm(DeckParam::kVoiceCount, 0.4162) == 0.4162);
    CHECK(snapDeckParamNorm(DeckParam::kPlayMode, 0.4162) == 0.4162);
    CHECK(deckParamUnit(DeckParam::kVoiceCount) == UnitCategory::None);
    CHECK(deckParamUnit(DeckParam::kAmpVelCurve) == UnitCategory::None);
}

// The taper and the raised ceiling are persistence-neutral BY CONSTRUCTION: the binding only
// READS the stored seconds, so a value dialled under the old 2 s ceiling reloads bit-identical
// and simply sits somewhere else on the knob. Nothing on the load path rewrites it.
static void testAValueStoredUnderTheOldCeilingIsReadNotRewritten() {
    PlaySeconds p;
    p.adsr.decaySeconds = 1.75;  // reachable by hand at the retired 2 s ceiling
    p.adsr.releaseSeconds = 2.0;
    const double normDecay = deckParamNorm(DeckParam::kDecay, p);
    CHECK(p.adsr.decaySeconds == 1.75);   // reading the norm mutated nothing
    CHECK(p.adsr.releaseSeconds == 2.0);
    CHECK(normDecay > 0.0 && normDecay < 1.0);   // still on the knob, just at a new angle
    CHECK(deckParamNorm(DeckParam::kRelease, p) > normDecay);
    // And a no-op touch survives the norm the knob would hand back — for THIS value, which is
    // exactly on the taper's output quantum grid (1.75 s parses to a grid-aligned double). A
    // legacy value off the grid (e.g. 1.2345678912345) WOULD be re-quantized on first touch;
    // that is correct, intended behaviour, not a gap this test is claiming to cover.
    setDeckParam(DeckParam::kDecay, p, normDecay, 0);
    CHECK(p.adsr.decaySeconds == 1.75);
}

// The filter's four tone controls are wire-frozen in the payload: their stored value IS their
// normalized position, and nothing in the taper pass may re-map it. Their snap is display-side
// only, which is what this separates.
static void testTheFilterFourKeepTheirIdentityTaper() {
    PlaySeconds p;
    const double positions[] = {0.0, 0.125, 0.5, 0.73, 1.0};
    for (double n : positions) {
        setDeckParam(DeckParam::kFilterCutoff, p, n, 0);
        setDeckParam(DeckParam::kFilterQ, p, n, 0);
        setDeckParam(DeckParam::kFilterMorph, p, n, 0);
        setDeckParam(DeckParam::kFilterDrive, p, n, 0);
        CHECK(p.filter.settings.cutoffNorm == static_cast<float>(n));
        CHECK(p.filter.settings.resonanceNorm == static_cast<float>(n));
        CHECK(p.filter.settings.morphNorm == static_cast<float>(n));
        CHECK(p.filter.settings.driveNorm == static_cast<float>(n));
        CHECK(deckParamNorm(DeckParam::kFilterCutoff, p) == static_cast<double>(static_cast<float>(n)));
    }
}

int main() {
    testTheTwoCeilingNamesAreOneNumber();
    testNormRoundTripsThroughEveryValueDomain();
    testRateKnobEndsAreTheStretchersOwnBounds();
    testRateAndPitchBindTheirOwnFields();
    testShiftSnapsBothNewKnobsToWholeSemitones();
    testResetTouchesOnlyItsOwnRingOnADualRingKnob();
    testInnerResetLandsOnTheExactLinearNeutral();
    testResetLandsOnTheStoredDefaultOfEachControl();
    testEveryKnobIdResetsToItsDefault();
    testEveryDefaultHasAnExactNormalizedPreimage();
    testShiftSnapsToAWholeUnitOfTheDisplayedValue();
    testAValueStoredUnderTheOldCeilingIsReadNotRewritten();
    testTheFilterFourKeepTheirIdentityTaper();
    if (g_fail) {
        std::printf("%d FAILURE(S)\n", g_fail);
        return 1;
    }
    std::printf("deck_values tests passed\n");
    return 0;
}
