// Standalone tests for reasampler::instrument::ui::param_taper — no VST3, no REAPER, no
// framework. The taper is the one map the knob's needle, the AHDSR schematic axis and (later) the
// host's normalization all read, so what is asserted here is what all three obey.
//
// Covers: the modifier truth table (Shift beats Ctrl); the stage-time taper (exact endpoints,
// monotone, the two landmark bands, and the EXACT-PREIMAGE guarantee swept over the whole
// quantum grid rather than sampled at the defaults); the depth taper (exact centre and ends,
// exact symmetry, the +/-7 st landmark, whole-semitone preimages); and the four whole-unit snaps.

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

#include "../src/core/instrument/ui/envelope_overlay.h"  // gateStageSlotPx: finest drag surface
#include "../src/core/instrument/ui/sample_bands.h"      // kEditorMinWidth/kPad: the editor floor

#include <cfenv>
#include <cmath>
#include <cstdio>

using namespace reasampler;
using namespace reasampler::instrument::ui;

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 constexpr double kDepth = 24.0;  // the pitch-depth throw the deck passes in today
// Rate's bounds, as the deck passes them in — the stretcher's own measured range. Written as
// literals HERE on purpose: this is the module's test, and reading the engine constant would
// make the test agree with the taper by construction rather than pin the numbers.
static constexpr double kRateMin = 0.5;
static constexpr double kRateMax = 2.0;

// --- modifiers -----------------------------------------------------------------------------

// Shift+Ctrl is SHIFT: with the output quantized to whole units a finer drag produces the same
// sequence, so Ctrl is ignored there. Asserted rather than left to a comment because the
// "obvious fix" is to compound the two.
static void testShiftBeatsCtrlForTheFineDragRate() {
    CHECK(!fineDrag(DragModifiers{false, false}));
    CHECK(fineDrag(DragModifiers{false, true}));
    CHECK(!fineDrag(DragModifiers{true, false}));
    CHECK(!fineDrag(DragModifiers{true, true}));
    CHECK((DragModifiers{true, false} != DragModifiers{false, false}));
    CHECK((DragModifiers{true, true} == DragModifiers{true, true}));
}

// --- the stage-time taper ------------------------------------------------------------------

// Zero is a REQUIRED value a pure log cannot express, and the ceiling has to be reachable by
// hand — both endpoints are exact, not merely close.
static void testStageTimeEndpointsAreExact() {
    CHECK(timeSecondsFromNorm(0.0) == 0.0);
    CHECK(timeSecondsFromNorm(1.0) == kStageTimeMaxSeconds);
    CHECK(timeNormFromSeconds(0.0) == 0.0);
    CHECK(timeNormFromSeconds(kStageTimeMaxSeconds) == 1.0);
    // Out of domain clamps rather than extrapolating.
    CHECK(timeSecondsFromNorm(-1.0) == 0.0);
    CHECK(timeSecondsFromNorm(2.0) == kStageTimeMaxSeconds);
    CHECK(timeNormFromSeconds(-1.0) == 0.0);
    CHECK(timeNormFromSeconds(1e9) == 1.0);
}

// The ceiling this phase raised it to. Pinned as a literal: this endpoint becomes a frozen host
// normalization, so a silent change to it is exactly what a test has to refuse.
static void testStageTimeCeilingIsTenSeconds() {
    CHECK(kStageTimeMaxSeconds == 10.0);
}

// The two landmarks the taper is fitted to, at the NEW ceiling. They are what make the low end
// dialable at a 10 s range, and they are also the overlay's legibility guarantee.
static void testStageTimeLandmarksLandInTheirBands() {
    const double at10ms = timeNormFromSeconds(0.010);
    const double at100ms = timeNormFromSeconds(0.100);
    CHECK(at10ms >= 0.12 && at10ms <= 0.20);
    CHECK(at100ms >= 0.42 && at100ms <= 0.52);
    // And the two are ordered with real separation, not merely inside their bands.
    CHECK(at100ms > at10ms + 0.2);
}

static void testStageTimeIsMonotone() {
    double prev = -1.0;
    for (int i = 0; i <= 200000; ++i) {
        const double v = timeSecondsFromNorm(static_cast<double>(i) / 200000.0);
        CHECK(v >= prev);
        if (v < prev) return;  // one report is enough
        prev = v;
    }
}

// The FINEST drag a user can make on ANY surface this taper serves — not the knob's own 128 px
// travel, which is coarser than the AHDSR schematic's node drag at the editor floor. Derived from
// the floor constant and the overlay's own slot-width formula, so a later floor change sharpens
// (or coarsens) the step this test exercises automatically instead of leaving a copied number
// silently stale.
static void testEveryFinestDragStepMovesTheValue() {
    const Rect floorArea = Rect::ltrb(0, 0, kEditorMinWidth - 2 * kPad, 100);
    const double slot = gateStageSlotPx(floorArea);
    const int steps = static_cast<int>(slot / kFineDragScale);
    for (int i = 0; i < steps; ++i) {
        const double lo = timeSecondsFromNorm(static_cast<double>(i) / steps);
        const double hi = timeSecondsFromNorm(static_cast<double>(i + 1) / steps);
        CHECK(hi > lo);
        if (!(hi > lo)) return;
    }
}

// THE sharpest requirement in the track. A host's reset-to-default arrives as
// toPlain(defaultNorm) with no bypass available, so the preimage has to be EXACT. Swept over the
// whole quantum grid at the resolution the defaults live at, not sampled at the two the parameter
// set happens to carry today — that is what makes the guarantee structural.
static void testEveryWholeMicrosecondRoundTripsExactly() {
    for (int us = 0; us <= 200000; us += 7) {  // 0 .. 200 ms, a prime stride to avoid alignment
        const double seconds = static_cast<double>(us) / 1e6;
        CHECK(timeSecondsFromNorm(timeNormFromSeconds(seconds)) == seconds);
        if (timeSecondsFromNorm(timeNormFromSeconds(seconds)) != seconds) return;
    }
    // And across the rest of the range, where the map is coarsest.
    for (int ms = 200; ms <= 10000; ms += 13) {
        const double seconds = static_cast<double>(ms) / 1e3;
        CHECK(timeSecondsFromNorm(timeNormFromSeconds(seconds)) == seconds);
        if (timeSecondsFromNorm(timeNormFromSeconds(seconds)) != seconds) return;
    }
}

// MODE-INDEPENDENCE, the whole point of resolveTo's std::round over std::nearbyint. First shows
// the defect directly, generically: under round-toward-zero, the RETIRED std::nearbyint reads
// that mode and truncates a value whose fraction is well past half, while std::round (specified
// to round half-away-from-zero REGARDLESS of the current mode) does not. Then proves the
// production round trip itself — not a stand-in — survives the same hostile mode across the grid.
static void testRoundingSurvivesAHostileFpRoundingMode() {
    const int saved = std::fegetround();
    CHECK(std::fesetround(FE_TOWARDZERO) == 0);

    CHECK(std::nearbyint(12.9) == 12.0);  // the RETIRED behaviour: mode-dependent, wrong here
    CHECK(std::round(12.9) == 13.0);      // the fix: mode-independent, rounds to nearest

    for (int us = 0; us <= 200000; us += 7) {
        const double seconds = static_cast<double>(us) / 1e6;
        CHECK(timeSecondsFromNorm(timeNormFromSeconds(seconds)) == seconds);
        if (timeSecondsFromNorm(timeNormFromSeconds(seconds)) != seconds) break;
    }
    for (int milli = -24000; milli <= 24000; milli += 37) {
        const double d = static_cast<double>(milli) / 1000.0;
        CHECK(depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) == d);
        if (depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) != d) break;
    }

    std::fesetround(saved);  // restore — every other test in this binary assumes the default
}

// The converse round trip is NOT required, but its residual is worth pinning: it is bounded by
// the output quantum read back through the map, which stays four orders below one drag pixel.
// Pinned so a future quantum change cannot make the needle visibly lag the hand unnoticed.
static void testNormRoundTripResidualStaysBelowOneDragPixel() {
    for (int i = 0; i <= 100000; ++i) {
        const double n = static_cast<double>(i) / 100000.0;
        const double back = timeNormFromSeconds(timeSecondsFromNorm(n));
        CHECK(std::fabs(back - n) < 1e-7);
        if (!(std::fabs(back - n) < 1e-7)) return;
    }
}

// The two stage-time defaults the parameter set actually carries, named so a reader can see the
// values the sweep above covers generically.
static void testTheStageTimeDefaultsRoundTripExactly() {
    CHECK(timeSecondsFromNorm(timeNormFromSeconds(0.003)) == 0.003);
    CHECK(timeSecondsFromNorm(timeNormFromSeconds(0.060)) == 0.060);
    CHECK(timeSecondsFromNorm(timeNormFromSeconds(0.0)) == 0.0);
}

// --- the depth taper -----------------------------------------------------------------------

static void testDepthCentreAndEndsAreExact() {
    CHECK(depthNormFromSemitones(0.0, kDepth) == 0.5);
    CHECK(depthSemitonesFromNorm(0.5, kDepth) == 0.0);
    CHECK(depthNormFromSemitones(kDepth, kDepth) == 1.0);
    CHECK(depthNormFromSemitones(-kDepth, kDepth) == 0.0);
    CHECK(depthSemitonesFromNorm(1.0, kDepth) == kDepth);
    CHECK(depthSemitonesFromNorm(0.0, kDepth) == -kDepth);
    // Beyond the throw clamps rather than extrapolating.
    CHECK(depthNormFromSemitones(100.0, kDepth) == 1.0);
    CHECK(depthSemitonesFromNorm(5.0, kDepth) == kDepth);
}

// Symmetric BITWISE, not approximately: a bipolar knob whose two halves disagreed by an ulp
// would read a different depth up than down at the same distance from centre.
static void testDepthIsExactlySymmetric() {
    for (int i = 0; i <= 1000; ++i) {
        const double n = static_cast<double>(i) / 1000.0;
        CHECK(depthSemitonesFromNorm(n, kDepth) == -depthSemitonesFromNorm(1.0 - n, kDepth));
        if (depthSemitonesFromNorm(n, kDepth) != -depthSemitonesFromNorm(1.0 - n, kDepth)) return;
    }
}

// Centre expansion: the musically useful +/-7 st gets more than half of each half-travel.
static void testDepthLandmarkLandsInItsBand() {
    const double halfTravel = (depthNormFromSemitones(7.0, kDepth) - 0.5) * 2.0;
    CHECK(halfTravel >= 0.50 && halfTravel <= 0.58);
    // The negative half is the same distance out. Compared with a tolerance, not bitwise: 0.5+h
    // and 0.5-h round differently, and the mirror that has to be EXACT is the one in the plain
    // direction (testDepthIsExactlySymmetric) — a sub-ulp difference in a needle angle is not.
    CHECK(std::fabs((0.5 - depthNormFromSemitones(-7.0, kDepth)) * 2.0 - halfTravel) < 1e-15);
}

static void testDepthIsMonotone() {
    double prev = -1e9;
    for (int i = 0; i <= 200000; ++i) {
        const double v = depthSemitonesFromNorm(static_cast<double>(i) / 200000.0, kDepth);
        CHECK(v >= prev);
        if (v < prev) return;
        prev = v;
    }
}

// Same exact-preimage guarantee as the time taper: every value on the depth quantum grid comes
// back bitwise. Whole semitones are the case a Shift-snap produces, so they are swept explicitly.
static void testEveryWholeSemitoneRoundTripsExactly() {
    for (int st = -24; st <= 24; ++st) {
        const double d = static_cast<double>(st);
        CHECK(depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) == d);
    }
    for (int milli = -24000; milli <= 24000; milli += 37) {
        const double d = static_cast<double>(milli) / 1000.0;
        CHECK(depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) == d);
        if (depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) != d) return;
    }
}

// A degenerate throw is a caller bug, not a crash: the map collapses to the centre.
static void testDegenerateThrowCollapsesToCentre() {
    CHECK(depthNormFromSemitones(3.0, 0.0) == 0.5);
    CHECK(depthSemitonesFromNorm(0.9, 0.0) == 0.0);
}

// --- the rate taper -------------------------------------------------------------------------

// The three landmarks the range is specified by, all EXACT: half rate at norm 0, double at norm
// 1, and unity at TRUE knob centre — the last is what a detent has to be, and a map that merely
// came close to 1.0 there would persist a hair of transposition on an untouched knob.
static void testRateEndpointsAndCentreAreExact() {
    CHECK(rateRatioFromNorm(0.0, kRateMin, kRateMax) == 0.5);
    CHECK(rateRatioFromNorm(1.0, kRateMin, kRateMax) == 2.0);
    CHECK(rateRatioFromNorm(0.5, kRateMin, kRateMax) == 1.0);
    CHECK(rateNormFromRatio(0.5, kRateMin, kRateMax) == 0.0);
    CHECK(rateNormFromRatio(2.0, kRateMin, kRateMax) == 1.0);
    CHECK(rateNormFromRatio(1.0, kRateMin, kRateMax) == 0.5);
    // Out of domain clamps rather than extrapolating — the map cannot reach a ratio the
    // engine's own clamp would then have to move.
    CHECK(rateRatioFromNorm(-1.0, kRateMin, kRateMax) == 0.5);
    CHECK(rateRatioFromNorm(2.0, kRateMin, kRateMax) == 2.0);
    CHECK(rateNormFromRatio(0.1, kRateMin, kRateMax) == 0.0);
    CHECK(rateNormFromRatio(9.0, kRateMin, kRateMax) == 1.0);
}

// The taper's defining property, and the reason it is the exception to centre expansion: equal
// travel buys equal SEMITONES, everywhere. Checked as a constant ratio-of-ratios across the
// travel rather than at the two ends, which a centre-expanded map would also pass.
static void testRateIsLinearInSemitonesAcrossTheWholeTravel() {
    const double step = 1.0 / 24.0;  // 24 equal steps over 24 semitones
    for (int i = 0; i < 24; ++i) {
        const double lo = rateRatioFromNorm(static_cast<double>(i) * step, kRateMin, kRateMax);
        const double hi = rateRatioFromNorm(static_cast<double>(i + 1) * step, kRateMin, kRateMax);
        CHECK(std::fabs(hi / lo - std::exp2(1.0 / 12.0)) < 1e-12);
        if (!(std::fabs(hi / lo - std::exp2(1.0 / 12.0)) < 1e-12)) return;
    }
    // The named musical landmarks that buys: an octave at each end, a fifth seven steps out.
    CHECK(std::fabs(rateRatioFromNorm(0.5 + 7.0 / 24.0, kRateMin, kRateMax) -
                    std::exp2(7.0 / 12.0)) < 1e-12);
}

static void testRateIsMonotone() {
    double prev = -1.0;
    for (int i = 0; i <= 200000; ++i) {
        const double v = rateRatioFromNorm(static_cast<double>(i) / 200000.0, kRateMin, kRateMax);
        CHECK(v >= prev);
        if (v < prev) return;
        prev = v;
    }
}

// The preimage obligation this control actually carries: its ONE default, bitwise, because a
// host's reset-to-default arrives as toPlain(defaultNorm) with no editor bypass to intercept it.
// Both endpoints are exact for the same reason. Everything between round-trips to within an ulp
// rather than bitwise — the map carries no output quantum, and the header says why.
static void testRateDefaultAndEndpointsRoundTripBitwise() {
    CHECK(rateRatioFromNorm(rateNormFromRatio(1.0, kRateMin, kRateMax), kRateMin, kRateMax) == 1.0);
    CHECK(rateRatioFromNorm(rateNormFromRatio(0.5, kRateMin, kRateMax), kRateMin, kRateMax) == 0.5);
    CHECK(rateRatioFromNorm(rateNormFromRatio(2.0, kRateMin, kRateMax), kRateMin, kRateMax) == 2.0);
    for (int milli = 500; milli <= 2000; milli += 7) {
        const double ratio = static_cast<double>(milli) / 1000.0;
        const double back =
            rateRatioFromNorm(rateNormFromRatio(ratio, kRateMin, kRateMax), kRateMin, kRateMax);
        CHECK(std::fabs(back - ratio) < 1e-14 * ratio);
        if (!(std::fabs(back - ratio) < 1e-14 * ratio)) return;
    }
}

// The exact-unity detent is DERIVED from the bounds, not assumed to sit at centre. The shipped
// bounds are reciprocal so the two agree today, but they are a MEASURED range: re-measure them
// asymmetric and a detent pinned to 0.5 makes the map fold back on itself around centre. Run at
// a deliberately non-reciprocal pair, which is exactly the case the ratio-of-ratios and
// round-trip tests above would still have passed.
static void testRateDetentFollowsAsymmetricBoundsInsteadOfCentre() {
    constexpr double kLo = 0.4;
    constexpr double kHi = 3.0;   // kLo * kHi == 1.2, so unity is NOT at 0.5
    const double unity = rateNormFromRatio(1.0, kLo, kHi);
    CHECK(unity > 0.0 && unity < 1.0);
    CHECK(std::fabs(unity - 0.5) > 0.01);              // the case a 0.5 detent gets wrong
    CHECK(rateRatioFromNorm(unity, kLo, kHi) == 1.0);  // ...and unity is still EXACT there

    double prev = -1.0;
    for (int i = 0; i <= 200000; ++i) {
        const double v = rateRatioFromNorm(static_cast<double>(i) / 200000.0, kLo, kHi);
        CHECK(v >= prev);
        if (v < prev) { std::printf("  asymmetric fold at i=%d\n", i); return; }
        prev = v;
    }
    // That sweep steps OVER the detent rather than onto it, so walk its immediate neighbourhood
    // too — a misplaced exact case shows up there and nowhere else.
    for (int k = -8; k < 8; ++k) {
        const double a = rateRatioFromNorm(unity + static_cast<double>(k) * 1e-9, kLo, kHi);
        const double b = rateRatioFromNorm(unity + static_cast<double>(k + 1) * 1e-9, kLo, kHi);
        CHECK(b >= a);
        if (!(b >= a)) { std::printf("  detent fold at k=%d\n", k); return; }
    }
    // And the shipped reciprocal bounds still put unity at true knob centre: the general rule
    // reproduces the special case rather than replacing it.
    CHECK(rateNormFromRatio(1.0, kRateMin, kRateMax) == 0.5);
}

// Degenerate bounds are a caller bug, not a crash: the map collapses to unity.
static void testDegenerateRateBoundsCollapseToUnity() {
    CHECK(rateRatioFromNorm(0.3, 2.0, 0.5) == 1.0);
    CHECK(rateNormFromRatio(0.9, 2.0, 0.5) == 0.5);
    CHECK(rateRatioFromNorm(0.3, 0.0, 2.0) == 1.0);
}

// --- the whole-unit snaps -------------------------------------------------------------------

static void testMillisecondSnap() {
    CHECK(snapSecondsToWholeMs(0.0124) == 0.012);
    CHECK(snapSecondsToWholeMs(0.0126) == 0.013);
    CHECK(snapSecondsToWholeMs(0.0004) == 0.0);
    CHECK(snapSecondsToWholeMs(-1.0) == 0.0);
    CHECK(snapSecondsToWholeMs(9.9996) == 10.0);
    // The snapped value is itself on the taper's grid, so a snap followed by a round trip holds.
    CHECK(timeSecondsFromNorm(timeNormFromSeconds(snapSecondsToWholeMs(0.0347))) == 0.035);
}

static void testPercentSnap() {
    CHECK(snapFractionToWholePercent(0.514) == 0.51);
    CHECK(snapFractionToWholePercent(0.516) == 0.52);
    CHECK(snapFractionToWholePercent(-0.514) == -0.51);
    CHECK(snapFractionToWholePercent(1.0) == 1.0);
    CHECK(snapFractionToWholePercent(0.0) == 0.0);
}

static void testSemitoneSnap() {
    CHECK(snapSemitonesToWhole(6.6) == 7.0);
    CHECK(snapSemitonesToWhole(-6.6) == -7.0);
    CHECK(snapSemitonesToWhole(0.4) == 0.0);
    CHECK(depthSemitonesFromNorm(depthNormFromSemitones(snapSemitonesToWhole(6.6), kDepth),
                                 kDepth) == 7.0);
}

// Rate's unit is the semitone though it displays as a percent, so Shift lands on the 25 steps
// between the bounds — which is what puts an octave and a fifth under the hand. The detent and
// both ends are reached EXACTLY, so a snap cannot leave the knob a hair off its own endpoint.
static void testRateSemitoneSnap() {
    CHECK(snapRateRatioToWholeSemitone(1.0) == 1.0);
    CHECK(snapRateRatioToWholeSemitone(0.5) == 0.5);
    CHECK(snapRateRatioToWholeSemitone(2.0) == 2.0);
    CHECK(std::fabs(snapRateRatioToWholeSemitone(1.5) - std::exp2(7.0 / 12.0)) < 1e-15);
    // Just off a step in each direction resolves back onto it.
    CHECK(std::fabs(snapRateRatioToWholeSemitone(std::exp2(7.0 / 12.0) * 1.005) -
                    std::exp2(7.0 / 12.0)) < 1e-15);
    CHECK(std::fabs(snapRateRatioToWholeSemitone(std::exp2(7.0 / 12.0) * 0.995) -
                    std::exp2(7.0 / 12.0)) < 1e-15);
    // Within a quarter-semitone of unity snaps to unity, not to a neighbouring step.
    CHECK(snapRateRatioToWholeSemitone(std::exp2(0.25 / 12.0)) == 1.0);
    CHECK(snapRateRatioToWholeSemitone(0.0) == 1.0);   // unusable input parks at unity
    CHECK(snapRateRatioToWholeSemitone(-1.0) == 1.0);
    // What the knob actually stores after a Shift-drag is the snapped norm mapped back through
    // the taper — so the property that matters is that THAT value is still a whole semitone.
    // Measured in semitones, which is the unit the criterion is stated in.
    for (int st = -12; st <= 12; ++st) {
        const double norm =
            rateNormFromRatio(std::exp2(static_cast<double>(st) / 12.0), kRateMin, kRateMax);
        const double stored = rateRatioFromNorm(norm, kRateMin, kRateMax);
        const double semis = 12.0 * std::log2(stored);
        CHECK(std::fabs(semis - static_cast<double>(st)) < 1e-9);
        if (!(std::fabs(semis - static_cast<double>(st)) < 1e-9)) return;
    }
}

// The exponent snap reaches 1.0, the linear neutral — one snap from the dial's centre — and
// clamps into curve_law's own domain rather than rounding to a zero that is not an exponent.
static void testExponentSnap() {
    CHECK(snapExponentToWhole(1.4) == 1.0);
    CHECK(snapExponentToWhole(2.6) == 3.0);
    CHECK(snapExponentToWhole(0.3) == util::kCurveMin);
    CHECK(snapExponentToWhole(0.6) == 1.0);
    CHECK(snapExponentToWhole(1e9) == util::kCurveMax);
}

int main() {
    testShiftBeatsCtrlForTheFineDragRate();

    testStageTimeEndpointsAreExact();
    testStageTimeCeilingIsTenSeconds();
    testStageTimeLandmarksLandInTheirBands();
    testStageTimeIsMonotone();
    testEveryFinestDragStepMovesTheValue();
    testEveryWholeMicrosecondRoundTripsExactly();
    testRoundingSurvivesAHostileFpRoundingMode();
    testNormRoundTripResidualStaysBelowOneDragPixel();
    testTheStageTimeDefaultsRoundTripExactly();

    testDepthCentreAndEndsAreExact();
    testDepthIsExactlySymmetric();
    testDepthLandmarkLandsInItsBand();
    testDepthIsMonotone();
    testEveryWholeSemitoneRoundTripsExactly();
    testDegenerateThrowCollapsesToCentre();

    testRateEndpointsAndCentreAreExact();
    testRateIsLinearInSemitonesAcrossTheWholeTravel();
    testRateIsMonotone();
    testRateDefaultAndEndpointsRoundTripBitwise();
    testRateDetentFollowsAsymmetricBoundsInsteadOfCentre();
    testDegenerateRateBoundsCollapseToUnity();

    testMillisecondSnap();
    testPercentSnap();
    testSemitoneSnap();
    testRateSemitoneSnap();
    testExponentSnap();

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