// Standalone tests for reasampler::app_version — no REAPER, no framework. Covers the
// Phase V (V1) pure version-identity core: the exact-string fidelity of the CMake-sourced
// constant (leading-zero preserved), the semver parse/compare arithmetic a within-channel
// forward migration leans on, and the three-way writing-version classification persist reads
// back from ext state (PreVersioning / Unknown / Stamped). The ext-state I/O (persist) and
// the show-version action (main) are DAW-verified shell.

#include "../src/core/version/app_version.h"

// REAPER-free header; pulled in for the ingest action's FOREVER-STABLE id suffixes, so
// the composition assertions below check the strings that actually ship.
#include "../src/shell/actions/ingest.h"

#include <cstdio>
#include <string>

using namespace reasampler;
using namespace reasampler::version;

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

// --- appVersion: exact-string fidelity, leading zero preserved ----------------
//
// The channel is a COMPILE-TIME fact (REASAMPLER_CHANNEL_IS_BETA, threaded through the same
// configure_file'd header): the stable `build` tree compiles these tests with the flag = 0,
// the `build-beta` tree with = 1. So each config's ctest run validates ITS OWN channel's
// derivation. The channel-dependent assertions below branch on isBeta() so the ONE test
// source is correct in both configs — and each branch would fail if the derivation regressed
// (a beta build omitting the suffix, or stable gaining one, trips it).

static void testVersionConstantRenderStructure() {
    // stampVersion() is the CMake-sourced numeric triple on both channels (no suffix).
    // It must be parseable — a malformed version string slipped in at CMake time would
    // silently classify every saved project as Unknown on read-back.
    CHECK(parseVersion(stampVersion()).has_value());

    // appVersion() is the user-visible render: exactly stampVersion() on stable, and
    // stampVersion() + "-beta" on beta. This is the derivation that must hold at ANY
    // version, without naming a literal.
    if (isBeta()) {
        CHECK(appVersion() == stampVersion() + "-beta");
    } else {
        CHECK(appVersion() == stampVersion());
    }
}

static void testLeadingZeroFidelity() {
    // Leading-zero-fidelity is the whole point of sourcing the version STRING (not
    // reconstructing from numeric components): a CMake normalize of "0.9.01" to "0.9.1"
    // is a silent regression. This is tested with a SYNTHETIC fixed example so it pins
    // the fidelity contract independent of any particular live version.
    //   parseVersion("0.9.01") must yield patch == 1 (not 01 as an integer, which is 1,
    //   but the important thing is the STRING "0.9.01" is accepted and produces the right
    //   triple — the string fidelity is the caller's job, the integer parse just rounds the
    //   patch correctly). The live version may or may not have a zero-padded patch; this
    //   synthetic pin catches a parse regression that would misclassify such a stamp.
    auto v = parseVersion("0.9.01");
    CHECK(v.has_value());
    CHECK(v && v->major == 0 && v->minor == 9 && v->patch == 1);  // "01" -> 1, not rejected
}

// --- channel-derived rendering (V4) -------------------------------------------

static void testChannelDerivedRendering() {
    // The DISPLAY render. Stable: exactly the numeric string. Beta: numeric + "-beta"
    // (a plain suffix, V2). This is the show-version + panel-readout value. Each branch is
    // the assertion the OTHER config's build would need to fail — i.e. a stable build that
    // wrongly rendered "-beta", or a beta build that dropped it, is caught here.
    if (isBeta()) {
        CHECK(channel() == Channel::Beta);
        CHECK(appVersion() == stampVersion() + "-beta");
    } else {
        CHECK(channel() == Channel::Stable);
        CHECK(appVersion() == stampVersion());
    }
    // The STAMP value is the numeric triple on BOTH channels — never suffixed — so it stays
    // classifiable (see the stamp-classifiability test) and byte-identical to stable.
    // Verified against stampVersion() rather than a literal so a version bump is not a
    // test failure: the real invariant is that the stamp is parseable (checked in
    // testVersionConstantRenderStructure) and that it has no suffix here.
    CHECK(appVersion().substr(0, stampVersion().size()) == stampVersion());
    CHECK(isBeta() ? appVersion().size() > stampVersion().size()
                   : appVersion().size() == stampVersion().size());
}

static void testChannelDerivedIdentityStrings() {
    // Namespace, command-id prefix, action-name prefix, binary + dock idents all fork from
    // the one channel bit. Stable values are BYTE-IDENTICAL to the pre-V4 build — any drift
    // in the stable branch is a shipped-identity defect.
    if (isBeta()) {
        CHECK(extStateNamespace() == "reasampler_beta");
        CHECK(commandIdPrefix() == "CEREBELLUM_REASAMPLER_BETA_");
        CHECK(actionDisplayPrefix() == "ReaSampler beta: ");
        CHECK(binaryName() == "reaper_reasampler_beta");
        CHECK(dockTitle() == "ReaSampler Bank beta");
        CHECK(dockIdent() == "reasampler_bank_panel_beta");
    } else {
        CHECK(extStateNamespace() == "reasampler");
        CHECK(commandIdPrefix() == "CEREBELLUM_REASAMPLER_");
        CHECK(actionDisplayPrefix() == "ReaSampler: ");
        CHECK(binaryName() == "reaper_reasampler");
        CHECK(dockTitle() == "ReaSampler Bank");
        CHECK(dockIdent() == "reasampler_bank_panel");
    }
}

static void testVstIdentityStringsForkByChannel() {
    // S18: the VST3 instrument's on-disk name and display name fork from the SAME one channel
    // bit as the extension's idents above. Stable values are BYTE-IDENTICAL to pre-S18 — any
    // drift in the stable branch orphans a saved instance's on-disk reference / mislabels the
    // FX browser. These are the accessors vst_entry.cpp's factory, the editor title, and the
    // embed label all source; each branch is the assertion the OTHER config's build would fail.
    if (isBeta()) {
        CHECK(vstOutputName() == "reasampler_9000_beta");
        CHECK(vstPluginName() == "ReaSampler 9000 beta");
    } else {
        CHECK(vstOutputName() == "reasampler_9000");
        CHECK(vstPluginName() == "ReaSampler 9000");
    }
    // The on-disk name must match the CMake OUTPUT_NAME fork (REASAMPLER_VST_OUTPUT_NAME): a
    // divergence between this accessor and the artifact name would ship a binary whose
    // self-identification disagrees with its filename. (The CMake side is the authoritative
    // artifact name; this pins the in-binary derivation to the same two literals.)
    CHECK(vstOutputName() == (isBeta() ? "reasampler_9000_beta" : "reasampler_9000"));
}

static void testVstIdentityAndDataNamespaceShareOneChannel() {
    // The S18 pairing invariant at the SEAM: the VST's plugin identity (its display/output
    // names) and the DATA namespace its bridge reads (extStateNamespace(), what ext_keys
    // delegates to) must resolve to the SAME channel — a beta-named plugin reading the stable
    // namespace, or vice versa, is precisely the split the invariant forbids. Both forks fan
    // out from the one isBeta() bit, so this assertion fails if EITHER fork regressed
    // independently (a beta output name paired with the stable namespace trips the beta arm).
    const bool identityIsBeta =
        (vstPluginName() == "ReaSampler 9000 beta") && (vstOutputName() == "reasampler_9000_beta");
    const bool dataIsBeta = (extStateNamespace() == "reasampler_beta");
    CHECK(identityIsBeta == dataIsBeta);          // identity and data agree on the channel
    CHECK(identityIsBeta == isBeta());            // and both agree with the compiled bit
}

static void testChannelQualifiedIdAndNameComposition() {
    // The two composition helpers the shells funnel through. A representative shipped id
    // (CAPTURE_TRACK) and phrase must compose to the exact channel-qualified strings — this
    // is what guarantees stable rebuilds its shipped id and beta gets the isolated one.
    if (isBeta()) {
        CHECK(channelCommandId("CAPTURE_TRACK") == "CEREBELLUM_REASAMPLER_BETA_CAPTURE_TRACK");
        CHECK(channelActionName("capture selected track(s)") ==
              "ReaSampler beta: capture selected track(s)");
    } else {
        CHECK(channelCommandId("CAPTURE_TRACK") == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK");
        CHECK(channelActionName("capture selected track(s)") ==
              "ReaSampler: capture selected track(s)");
    }
}

static void testMediaExplorerImportIdsAreDistinctAndChannelIsolated() {
    // The Media-Explorer import publishes into TWO action sections, and a custom_action
    // idStr must be unique across all sections — so the two entries carry two suffixes.
    // Both are FOREVER-STABLE per channel. Composed from the SHIPPED constants and checked
    // against spelled-out literals, so a suffix edit in ingest.h fails here.
    const std::string mainId = channelCommandId(kIngestImportMediaExplorerId);
    const std::string mxId   = channelCommandId(kIngestImportMediaExplorerMxId);

    if (isBeta()) {
        CHECK(mainId == "CEREBELLUM_REASAMPLER_BETA_INGEST_IMPORT_MEDIA_EXPLORER");
        CHECK(mxId   == "CEREBELLUM_REASAMPLER_BETA_INGEST_IMPORT_MEDIA_EXPLORER_MX");
        CHECK(channelActionName("import Media Explorer file into selected track") ==
              "ReaSampler beta: import Media Explorer file into selected track");
    } else {
        CHECK(mainId == "CEREBELLUM_REASAMPLER_INGEST_IMPORT_MEDIA_EXPLORER");
        CHECK(mxId   == "CEREBELLUM_REASAMPLER_INGEST_IMPORT_MEDIA_EXPLORER_MX");
        CHECK(channelActionName("import Media Explorer file into selected track") ==
              "ReaSampler: import Media Explorer file into selected track");
    }
}

static void testStampClassifiesAsStampedOnOwnChannel() {
    // The V4 stamp-classifiability requirement: the value a channel WRITES (stampVersion())
    // must classify as Stamped when that same channel reads it back — on BOTH channels. A
    // "-beta"-suffixed stamp would classify as Unknown, so this fails if a build ever stamped
    // appVersion() instead of stampVersion().
    WritingVersion wv = classifyWritingVersion(stampVersion());
    CHECK(wv.kind == WritingVersion::Kind::Stamped);
    // raw must be the exact stamp string (no truncation, no suffix added by classify).
    CHECK(wv.raw == stampVersion());
    // parsed must round-trip: re-parsing the stamp string must yield the same triple
    // classify stored, without naming a literal version.
    auto reparsed = parseVersion(stampVersion());
    CHECK(reparsed.has_value());
    CHECK(reparsed && wv.parsed.major == reparsed->major
                   && wv.parsed.minor == reparsed->minor
                   && wv.parsed.patch == reparsed->patch);

    // The display string with the "-beta" suffix is intentionally NOT the stamp value —
    // confirm it classifies Unknown, proving why the stamp must stay the numeric triple.
    // This uses a synthetic fixed string (not the live version) because the intent is to
    // show that ANY "-beta"-suffixed string fails the semver parse used by classify.
    CHECK(classifyWritingVersion("0.9.01-beta").kind == WritingVersion::Kind::Unknown);
}

// --- parseVersion: well-formed, leading zeros, and rejection ------------------

static void testParseWellFormed() {
    auto v = parseVersion("0.9.01");
    CHECK(v.has_value());
    CHECK(v && v->major == 0 && v->minor == 9 && v->patch == 1);  // "01" -> 1
    auto w = parseVersion("1.10.2");
    CHECK(w.has_value());
    CHECK(w && w->major == 1 && w->minor == 10 && w->patch == 2);
}

static void testParseRejectsMalformed() {
    CHECK(!parseVersion("").has_value());          // empty
    CHECK(!parseVersion("1.2").has_value());       // too few components
    CHECK(!parseVersion("1.2.3.4").has_value());   // too many components
    CHECK(!parseVersion("1..2").has_value());      // empty middle component
    CHECK(!parseVersion(".1.2").has_value());      // empty leading component
    CHECK(!parseVersion("1.2.").has_value());      // empty trailing component
    CHECK(!parseVersion("1.2.x").has_value());     // non-digit
    CHECK(!parseVersion("-1.2.3").has_value());    // sign
    CHECK(!parseVersion("1.2.3-beta").has_value()); // trailing garbage
    CHECK(!parseVersion("v1.2.3").has_value());    // prefix
    CHECK(!parseVersion(" 1.2.3").has_value());    // leading whitespace
}

// --- versionLess: NUMERIC ordering (10 > 9, not lexicographic) ----------------

static void testOrderingByPatch() {
    // 0.9.02 > 0.9.01 (the per-test-build patch increment the spec calls out).
    auto a = parseVersion("0.9.01");
    auto b = parseVersion("0.9.02");
    CHECK(a && b);
    CHECK(a && b && versionLess(*a, *b));
    CHECK(a && b && !versionLess(*b, *a));
}

static void testOrderingIsNumericNotLexicographic() {
    // 0.10.01 > 0.9.02 — the case a string compare would get WRONG ("10" < "9"
    // lexicographically). This assertion fails if compare regressed to string order.
    auto a = parseVersion("0.9.02");
    auto b = parseVersion("0.10.01");
    CHECK(a && b);
    CHECK(a && b && versionLess(*a, *b));
    // And a major bump outranks a large minor: 1.0.0 > 0.99.99.
    auto c = parseVersion("0.99.99");
    auto d = parseVersion("1.0.0");
    CHECK(c && d && versionLess(*c, *d));
}

static void testOrderingIrreflexive() {
    // Equal versions are not less-than either way.
    auto a = parseVersion("0.9.01");
    auto b = parseVersion("0.9.01");
    CHECK(a && b && !versionLess(*a, *b));
    CHECK(a && b && !versionLess(*b, *a));
}

// --- classifyWritingVersion: the three-way stamp classification ----------------

static void testAbsentStampIsPreVersioning() {
    // An absent key -> getProjExtStateString returns "" -> PreVersioning (a project
    // saved before this feature). Silent, not an error, no throw.
    WritingVersion wv = classifyWritingVersion("");
    CHECK(wv.kind == WritingVersion::Kind::PreVersioning);
    CHECK(wv.raw.empty());
}

static void testMalformedStampIsUnknown() {
    // A present-but-unparseable stamp -> Unknown (ignored, never a throw). Keeps the raw
    // value for diagnostics but does not pretend it is a version.
    WritingVersion wv = classifyWritingVersion("garbage-not-a-version");
    CHECK(wv.kind == WritingVersion::Kind::Unknown);
    CHECK(wv.raw == "garbage-not-a-version");
}

static void testWellFormedStampIsStamped() {
    // A well-formed stamp -> Stamped, carrying the EXACT stored string plus the parsed
    // triple for comparison. Round-trips the current version through classify.
    WritingVersion wv = classifyWritingVersion("0.9.01");
    CHECK(wv.kind == WritingVersion::Kind::Stamped);
    CHECK(wv.raw == "0.9.01");
    CHECK(wv.parsed.major == 0 && wv.parsed.minor == 9 && wv.parsed.patch == 1);
}

static void testStampedStampIsOrderableAgainstCurrent() {
    // The migration use case: a stamp read back is comparable to the running build. An
    // older stamp (0.9.01) precedes a newer one (0.9.05) numerically.
    WritingVersion older = classifyWritingVersion("0.9.01");
    WritingVersion newer = classifyWritingVersion("0.9.05");
    CHECK(older.kind == WritingVersion::Kind::Stamped);
    CHECK(newer.kind == WritingVersion::Kind::Stamped);
    CHECK(versionLess(older.parsed, newer.parsed));
}

int main() {
    testVersionConstantRenderStructure();
    testLeadingZeroFidelity();
    testChannelDerivedRendering();
    testChannelDerivedIdentityStrings();
    testVstIdentityStringsForkByChannel();
    testVstIdentityAndDataNamespaceShareOneChannel();
    testChannelQualifiedIdAndNameComposition();
    testMediaExplorerImportIdsAreDistinctAndChannelIsolated();
    testStampClassifiesAsStampedOnOwnChannel();
    testParseWellFormed();
    testParseRejectsMalformed();
    testOrderingByPatch();
    testOrderingIsNumericNotLexicographic();
    testOrderingIrreflexive();
    testAbsentStampIsPreVersioning();
    testMalformedStampIsUnknown();
    testWellFormedStampIsStamped();
    testStampedStampIsOrderableAgainstCurrent();

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