// Standalone tests for the FOREVER-FROZEN VST3 parameter id table and the exposed set derived
// from the commit predicate — no VST3, no REAPER, no framework.
//
// The asserted numbers are LITERALS on purpose. A test that recomputed them from cellIds, from
// the enum's position, or from the table itself would defeat the freeze it exists to hold: the
// point is that changing any id has to break this file.

#include "../src/core/instrument/param/param_id.h"

#include <cstdio>
#include <set>
#include <string>

using namespace reasampler;
using namespace reasampler::instrument::param;
using reasampler::instrument::ui::DeckParam;
using reasampler::instrument::ui::LiveCommit;
using reasampler::instrument::ui::deckParamCommit;

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 void testEveryIdHoldsTheNumberItShippedWith() {
    // docs/product/parameter-automation.md 6.2, transcribed. If this list and the table
    // disagree, the table moved and every automation lane recorded against it now means
    // something else.
    struct Expect { ParamId id; DeckParam deck; };
    const Expect kExpected[] = {
        {1000, DeckParam::kKeyTrack},
        {1010, DeckParam::kRate},
        {1020, DeckParam::kPitch},
        {1100, DeckParam::kPitchEnvAttack},
        {1101, DeckParam::kPitchEnvAttackCurve},
        {1110, DeckParam::kPitchEnvHold},
        {1120, DeckParam::kPitchEnvDecay},
        {1121, DeckParam::kPitchEnvDecayCurve},
        {1130, DeckParam::kPitchEnvDepth},
        {1200, DeckParam::kFilterMorph},
        {1210, DeckParam::kFilterCutoff},
        {1220, DeckParam::kFilterQ},
        {1230, DeckParam::kFilterDrive},
        {1240, DeckParam::kFilterModAmt},
        {1250, DeckParam::kFilterVel},
        {1260, DeckParam::kFilterKeyTrack},
        {1300, DeckParam::kFilterEnvAttack},
        {1301, DeckParam::kFilterEnvAttackCurve},
        {1310, DeckParam::kFilterEnvHold},
        {1320, DeckParam::kFilterEnvDecay},
        {1321, DeckParam::kFilterEnvDecayCurve},
        {1330, DeckParam::kFilterEnvSustain},
        {1340, DeckParam::kFilterEnvRelease},
        {1341, DeckParam::kFilterEnvReleaseCurve},
        {1350, DeckParam::kFilterTrigAttack},
        {1351, DeckParam::kFilterTrigAttackCurve},
        {1360, DeckParam::kFilterTrigHold},
        {1370, DeckParam::kFilterTrigDecay},
        {1371, DeckParam::kFilterTrigDecayCurve},
        {1400, DeckParam::kAttack},
        {1401, DeckParam::kAttackCurve},
        {1410, DeckParam::kHold},
        {1420, DeckParam::kDecay},
        {1421, DeckParam::kDecayCurve},
        {1430, DeckParam::kSustain},
        {1440, DeckParam::kRelease},
        {1441, DeckParam::kReleaseCurve},
        {1450, DeckParam::kTrigLength},
        {1460, DeckParam::kTrigAttack},
        {1461, DeckParam::kTrigAttackCurve},
        {1470, DeckParam::kTrigHold},
        {1480, DeckParam::kTrigDecay},
        {1481, DeckParam::kTrigDecayCurve},
        {1700, DeckParam::kMasterGain},
    };
    const std::size_t expectedCount = sizeof(kExpected) / sizeof(kExpected[0]);
    CHECK(expectedCount == 44);
    CHECK(paramTable().size() == expectedCount);
    if (paramTable().size() != expectedCount) return;
    for (std::size_t i = 0; i < expectedCount; ++i) {
        CHECK(paramTable()[i].id == kExpected[i].id);
        CHECK(paramTable()[i].deck == kExpected[i].deck);
        CHECK(paramIdFor(kExpected[i].deck) == kExpected[i].id);
    }
}

static void testIdsAreUniqueAscendingInBlockAndOnStep() {
    std::set<ParamId> seen;
    ParamId previous = 0;
    for (const ParamRow& row : paramTable()) {
        CHECK(seen.insert(row.id).second);          // unique
        CHECK(row.id > previous);                   // ascending == presentation order
        previous = row.id;
        CHECK(row.id >= 1000);                      // 0 is a plausible accident, never an id
        const ParamId withinBlock = row.id % 100;
        // On the step, or one past it — a curve dial takes its outer knob's id + 1, and only a
        // curve dial may.
        const bool onStep = withinBlock % 10 == 0;
        const bool innerDial = withinBlock % 10 == 1;
        CHECK(onStep || innerDial);
    }
}

static void testAnInnerDialSitsBesideTheKnobItShapes() {
    for (const ParamRow& row : paramTable()) {
        if (row.id % 10 != 1) continue;
        // Its outer knob is the row numbered one below it, and curveParamFor must agree that the
        // dial belongs to that knob.
        const ParamRow* outer = nullptr;
        for (const ParamRow& candidate : paramTable()) {
            if (candidate.id == row.id - 1) outer = &candidate;
        }
        CHECK(outer != nullptr);
        if (outer) CHECK(reasampler::instrument::ui::curveParamFor(outer->deck) == row.deck);
    }
}

static void testABlockCarriesOnlyItsOwnGroup() {
    for (const ParamRow& row : paramTable()) {
        const ParamId block = row.id / 100 * 100;
        switch (block) {
            case 1000: CHECK(row.unit == kUnitPitch); break;
            case 1100: CHECK(row.unit == kUnitPitchEnv); break;
            case 1200: CHECK(row.unit == kUnitFilter); break;
            case 1300: CHECK(row.unit == kUnitFilterEnv); break;
            case 1400: CHECK(row.unit == kUnitAmp); break;
            case 1700: CHECK(row.unit == kUnitMaster); break;
            default:   CHECK(false); break;  // 1500/1600 are reserved and must stay empty
        }
    }
}

static void testTheExposedSetIsExactlyThePredicateAnswer() {
    // Asserted against deckParamCommit, never against a literal count: the list follows the
    // predicate, and the predicate is never bent to fill the list.
    for (const ParamRow& row : paramTable()) {
        const bool live = deckParamCommit(row.deck) != LiveCommit::Reload;
        CHECK(isExposed(row.deck) == live);
        const bool listed = exposedRowFor(row.id) != nullptr;
        CHECK(listed == live);
    }
    for (const ParamRow& row : exposedParams()) {
        CHECK(deckParamCommit(row.deck) != LiveCommit::Reload);
    }
    // Every control the predicate calls live must HAVE a row — a live control with no number is
    // a parameter the host can never be told about.
    for (int i = 0; i < static_cast<int>(DeckParam::kCount); ++i) {
        const auto deck = static_cast<DeckParam>(i);
        if (deckParamCommit(deck) == LiveCommit::Reload) continue;
        CHECK(paramIdFor(deck) != 0);
    }
}

static void testEveryNumberedRowIsIssued() {
    // Key-track (pitch) and Trigger length were reserved-but-unissued while they routed through
    // the reload tier; both are note-on-latched now, so both are issued under the numbers that
    // were held for them and no other id moved — which is what the block-and-step scheme bought.
    CHECK(paramIdFor(DeckParam::kKeyTrack) == 1000);
    CHECK(paramIdFor(DeckParam::kTrigLength) == 1450);
    CHECK(isExposed(DeckParam::kKeyTrack));
    CHECK(isExposed(DeckParam::kTrigLength));
    CHECK(exposedRowFor(1000) != nullptr);
    CHECK(exposedRowFor(1450) != nullptr);
    // Master gain IS issued: one atomic store the audio thread picks up next block is the live
    // tier by that tier's own definition.
    CHECK(isExposed(DeckParam::kMasterGain));
    CHECK(exposedRowFor(1700) != nullptr);
    // The whole table is issued today — 44 of 44.
    CHECK(exposedParams().size() == paramTable().size());
    CHECK(exposedParams().size() == 44);
}

static void testEveryRowCarriesADistinctTitleAndShortTitle() {
    std::set<std::string> titles;
    std::set<std::string> shortTitles;
    for (const ParamRow& row : paramTable()) {
        CHECK(row.title && row.title[0] != '\0');
        CHECK(row.shortTitle && row.shortTitle[0] != '\0');
        CHECK(std::string(row.title) != std::string(row.shortTitle));
        CHECK(titles.insert(row.title).second);
        CHECK(shortTitles.insert(row.shortTitle).second);
    }
}

int main() {
    testEveryIdHoldsTheNumberItShippedWith();
    testIdsAreUniqueAscendingInBlockAndOnStep();
    testAnInnerDialSitsBesideTheKnobItShapes();
    testABlockCarriesOnlyItsOwnGroup();
    testTheExposedSetIsExactlyThePredicateAnswer();
    testEveryNumberedRowIsIssued();
    testEveryRowCarriesADistinctTitleAndShortTitle();
    if (g_fail == 0) std::printf("param_id: all tests passed\n");
    return g_fail == 0 ? 0 : 1;
}
