// Standalone tests for reasampler::instrument::engine::detectPeriod — the offline source-period
// estimate behind Preserve's pitch-synchronous splices. No VST3, no REAPER, no test framework.
//
// Covers:
//   1. accuracy on pure tones across the searched band, at 44.1k and 48k, including the
//      non-integer periods every real capture actually has — the splice jump is n periods, so
//      a fractional-frame error lands multiplied by n.
//   2. the fundamental, not a harmonic: a sawtooth and a missing-fundamental stack must both
//      report the repeat period, which is what a splice has to align on.
//   3. graceful degradation — noise, silence, and a source whose period changes mid-sample all
//      return NONE. That is the contract the shifter's fixed-window fallback rests on: an
//      estimate that is merely wrong would misalign every splice, which is worse than none.
//   4. the band edges and the short-sample path, including the lone-probe accept, the length
//      sweep across every probe-count step, and what counts as evidence against a period.
//   5. the analysis span: a sustain loop stands in for the whole source, but never at the cost
//      of search-band width.
//   6. what the load pays, and that it does not grow with the sample length.

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

#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <tuple>
#include <vector>

using namespace reasampler;
using namespace reasampler::instrument::engine;

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

constexpr double kPi = 3.14159265358979323846;

static std::vector<AudioSample> sineOfPeriod(std::size_t frames, double period,
                                             double phase = 0.0) {
    std::vector<AudioSample> s(frames);
    for (std::size_t i = 0; i < frames; ++i) {
        s[i] = static_cast<AudioSample>(
            std::sin(2.0 * kPi * static_cast<double>(i) / period + phase));
    }
    return s;
}

// --- 1. Accuracy on pure tones -------------------------------------------------------------

static void testPureTonePeriodIsFoundToBetterThanATenthOfAFrame() {
    // Deliberately non-integer periods: an integer-only estimator passes an integer-period
    // sweep and still misaligns every real capture.
    const double periods[] = {23.7, 50.0, 100.25, 200.45, 441.0, 999.9, 1470.0, 2000.3, 2756.0};
    for (double p : periods) {
        const std::vector<AudioSample> src = sineOfPeriod(120000, p);
        const PeriodEstimate est = detectPeriod(src, 44100);
        CHECK(est.valid());
        if (!est.valid()) {
            std::printf("  period %.2f: NOT DETECTED\n", p);
            continue;
        }
        const double errFrames = std::fabs(est.frames - p);
        std::printf("  period %8.2f -> %8.4f (err %.4f fr, conf %.3f)\n", p, est.frames,
                    errFrames, est.confidence);
        CHECK(errFrames < 0.1);
        CHECK(est.confidence > 0.8);
    }
}

static void testTheEstimateIsInSourceFramesSoTheRateOnlyMovesTheBand() {
    // The same 30 Hz tone at two rates: the answer is frames, so it must track the rate. This
    // is what lets the shifter compare it against a window that is also in frames.
    for (int rate : {44100, 48000}) {
        const double p = static_cast<double>(rate) / 30.0;
        const std::vector<AudioSample> src = sineOfPeriod(160000, p);
        const PeriodEstimate est = detectPeriod(src, rate);
        CHECK(est.valid());
        if (est.valid()) {
            std::printf("  30 Hz @ %d: %.3f fr (want %.3f)\n", rate, est.frames, p);
            CHECK(std::fabs(est.frames - p) < 0.5);
        }
    }
}

// --- 2. The fundamental, not a harmonic ----------------------------------------------------

static void testHarmonicRichSourceReportsTheRepeatPeriodNotAPartial() {
    // A sawtooth's strongest correlation dips at EVERY multiple of its period; a global-minimum
    // estimator picks 2P or 3P about as often as P. YIN's first-dip rule is what makes this
    // pass, and a jump quantized to 2P would splice a whole cycle out of phase half the time.
    const double p = 512.0;
    std::vector<AudioSample> src(120000);
    for (std::size_t i = 0; i < src.size(); ++i) {
        double v = 0.0;
        for (int h = 1; h <= 12; ++h) {
            v += std::sin(2.0 * kPi * h * static_cast<double>(i) / p) / h;
        }
        src[i] = static_cast<AudioSample>(0.5 * v);
    }
    const PeriodEstimate est = detectPeriod(src, 44100);
    CHECK(est.valid());
    if (est.valid()) {
        std::printf("  sawtooth P=512 -> %.3f\n", est.frames);
        CHECK(std::fabs(est.frames - p) < 1.0);
    }
}

static void testMissingFundamentalStillReportsTheRepeatPeriod() {
    // Partials 2..6 of a 700-frame period: there is no energy AT the fundamental, but the
    // waveform still repeats every 700 frames — and repetition, not spectral content, is what
    // a splice has to land on.
    const double p = 700.0;
    std::vector<AudioSample> src(120000);
    for (std::size_t i = 0; i < src.size(); ++i) {
        double v = 0.0;
        for (int h = 2; h <= 6; ++h) {
            v += std::sin(2.0 * kPi * h * static_cast<double>(i) / p);
        }
        src[i] = static_cast<AudioSample>(0.2 * v);
    }
    const PeriodEstimate est = detectPeriod(src, 44100);
    CHECK(est.valid());
    if (est.valid()) {
        std::printf("  missing fundamental P=700 -> %.3f\n", est.frames);
        CHECK(std::fabs(est.frames - p) < 2.0);
    }
}

// --- 3. Graceful degradation ---------------------------------------------------------------

static void testNoiseSilenceAndAPeriodChangeAllReportNone() {
    // White noise: no dip below the absolute threshold anywhere.
    {
        std::vector<AudioSample> src(120000);
        std::uint32_t rng = 22222u;
        for (auto& x : src) {
            rng = rng * 1664525u + 1013904223u;
            x = static_cast<AudioSample>((static_cast<double>(rng >> 8) / 8388608.0) - 1.0);
        }
        const PeriodEstimate est = detectPeriod(src, 44100);
        std::printf("  white noise -> %s (%.3f)\n", est.valid() ? "DETECTED" : "none",
                    est.frames);
        CHECK(!est.valid());
    }
    // Digital silence: the difference function is degenerate, not merely inconclusive.
    {
        const std::vector<AudioSample> src(120000, 0.0f);
        CHECK(!detectPeriod(src, 44100).valid());
    }
    // Two halves at genuinely different periods: the probes disagree, so there is no ONE
    // period, and reporting either half's would misalign every splice in the other half.
    {
        std::vector<AudioSample> src(160000);
        double phase = 0.0;
        for (std::size_t i = 0; i < src.size(); ++i) {
            phase += 2.0 * kPi / (i < 80000 ? 300.0 : 700.0);
            src[i] = static_cast<AudioSample>(std::sin(phase));
        }
        const PeriodEstimate est = detectPeriod(src, 44100);
        std::printf("  period change 300->700 -> %s (%.3f)\n", est.valid() ? "DETECTED" : "none",
                    est.frames);
        CHECK(!est.valid());
    }
    // Degenerate inputs.
    CHECK(!detectPeriod({}, 44100).valid());
    CHECK(!detectPeriod(sineOfPeriod(120000, 441.0), 0).valid());
}

static void testAPercussiveDecayIsNotForcedIntoAPeriod() {
    // Filtered noise with a fast decay — the shape of a one-shot drum hit. Nothing repeats, so
    // the answer must be none rather than whatever the envelope's own length looks like.
    std::vector<AudioSample> src(120000);
    std::uint32_t rng = 909090u;
    double lp = 0.0;
    for (std::size_t i = 0; i < src.size(); ++i) {
        rng = rng * 1664525u + 1013904223u;
        const double n = (static_cast<double>(rng >> 8) / 8388608.0) - 1.0;
        lp += 0.25 * (n - lp);
        const double env = std::exp(-static_cast<double>(i % 22050) / 2000.0);
        src[i] = static_cast<AudioSample>(lp * env);
    }
    const PeriodEstimate est = detectPeriod(src, 44100);
    std::printf("  percussive decay -> %s (%.3f)\n", est.valid() ? "DETECTED" : "none",
                est.frames);
    CHECK(!est.valid());
}

// --- 4. Band edges and short sources -------------------------------------------------------

static void testBelowTheBandReportsNoneAndAboveItReportsAWholeMultiple() {
    // Below kPeriodDetectMinHz: none. This is the load-bearing edge — such a period cannot fit
    // the splice jump anyway, so an answer here would only be one the shifter must reject.
    const std::vector<AudioSample> low = sineOfPeriod(200000, 44100.0 / 8.0);   // 8 Hz
    CHECK(!detectPeriod(low, 44100).valid());

    // Above kPeriodDetectMaxHz the search floor sits well above the true period, so what comes
    // back is a WHOLE MULTIPLE of it — which is still an exactly aligned splice target, since
    // every multiple of a period is a period. That is why the high edge needs no special
    // handling: being outside the band costs nothing, because alignment was never in question
    // for a tone this short-period.
    const double p = 44100.0 / 6000.0;  // 7.35 frames
    const PeriodEstimate high = detectPeriod(sineOfPeriod(120000, p), 44100);
    std::printf("  6 kHz (P=%.3f) -> %s (%.3f, = %.3f periods)\n", p,
                high.valid() ? "detected" : "none", high.frames, high.frames / p);
    if (high.valid()) {
        const double n = high.frames / p;
        CHECK(std::fabs(n - std::floor(n + 0.5)) < 0.02);
    }
}

static void testAShortSourceShortensTheSearchRatherThanRefusing() {
    // A 12000-frame one-shot cannot host a full-band probe; the search band shortens to fit and
    // a 200-frame period is still found. Below that the answer is none, not a guess.
    const std::vector<AudioSample> shortSrc = sineOfPeriod(12000, 200.0);
    const PeriodEstimate est = detectPeriod(shortSrc, 44100);
    std::printf("  12000-frame source, P=200 -> %s (%.3f)\n", est.valid() ? "detected" : "none",
                est.frames);
    CHECK(est.valid());
    if (est.valid()) CHECK(std::fabs(est.frames - 200.0) < 0.5);

    // Too short for even the minimum lag: none.
    CHECK(!detectPeriod(sineOfPeriod(40, 20.0), 44100).valid());
}

// A lone piece of evidence is the one case the strict-majority rule cannot rule on, so pin both
// halves of the carve-out: which sources land in it, and that they are accepted rather than
// refused. 30 Hz is first-class material here, and a short low-frequency source is exactly where
// the blunt "require two probes" fix would have silently stopped detecting.
static void testASourceTooShortForASecondProbeIsStillDetectedOnItsOneProbe() {
    const int rate = 44100;
    const std::size_t block = 2 * longestLagFrames(rate);
    // Exactly one probe BLOCK leaves zero room to place a second probe anywhere, whatever
    // separation the placement uses — so this names the lone-probe case independently of the
    // formula, where a length derived from that formula would only re-assert it.
    const double p = static_cast<double>(rate) / 30.0;  // 1470 frames
    const PeriodEstimate lone = detectPeriod(sineOfPeriod(block, p), rate);
    std::printf("  lone probe, %zu frames @ 30 Hz -> %s (%.3f, want %.3f)\n", block,
                lone.valid() ? "detected" : "NONE", lone.frames, p);
    CHECK(lone.valid());
    if (lone.valid()) CHECK(std::fabs(lone.frames - p) < 1.0);

    // Growing the span past the lone case must not turn the answer off, and must not change it:
    // a probe-count boundary is not allowed to be a discontinuity in what a stationary source
    // reports. Asserted over the answer rather than over the count, so it survives the placement
    // rule moving.
    for (std::size_t extra : {std::size_t{1}, block / 4, block / 2, block, 2 * block}) {
        const PeriodEstimate more = detectPeriod(sineOfPeriod(block + extra, p), rate);
        CHECK(more.valid());
        if (more.valid()) CHECK(std::fabs(more.frames - p) < 1.0);
    }
}

// The acceptance property behind the position-placement change: a STATIONARY source must not
// lose detection at any length. The probe count steps at 3x, 4x, 5x and 6x the longest lag and
// the agreement bar steps with it, so a length sweep is the only thing that pins the whole
// band — a single 160 000-frame case sits above every step and cannot see them.
static void testAStationarySourceDetectsAtEveryLengthAcrossTheProbeCountSteps() {
    const int rate = 44100;
    const std::size_t lagHi = longestLagFrames(rate);
    int refused = 0;
    for (double hz : {30.0, 29.0, 55.0}) {
        const double p = static_cast<double>(rate) / hz;
        for (std::size_t n = 2 * lagHi; n <= 12 * lagHi; n += lagHi / 4) {
            const PeriodEstimate est = detectPeriod(sineOfPeriod(n, p), rate);
            if (!est.valid() || std::fabs(est.frames - p) > 1.0) {
                ++refused;
                std::printf("  %.0f Hz at %zu frames (%.2fx lagHi): %s (%.3f)\n", hz, n,
                            static_cast<double>(n) / static_cast<double>(lagHi),
                            est.valid() ? "wrong" : "NONE", est.frames);
            }
        }
    }
    CHECK(refused == 0);
}

// The regime the position-placement change actually moved: probes overlap only while the stride
// is under one block, i.e. below 8x the longest lag. A straddling block finds NO period rather
// than a third one, so counting only the probes that survived turned a genuine two-and-two split
// into a two-of-three accept — a source with two periods reported as having the first one, which
// is the outcome the module calls worse than none.
static void testAPeriodChangeIsRefusedWhereTheProbesOverlapToo() {
    const int rate = 44100;
    const std::size_t lagHi = longestLagFrames(rate);
    for (double mult : {3.0, 4.0, 5.0, 6.0, 7.0}) {
        const std::size_t n = static_cast<std::size_t>(mult * static_cast<double>(lagHi));
        std::vector<AudioSample> src(n);
        double phase = 0.0;
        for (std::size_t i = 0; i < n; ++i) {
            phase += 2.0 * kPi / (i < n / 2 ? 300.0 : 700.0);
            src[i] = static_cast<AudioSample>(std::sin(phase));
        }
        const PeriodEstimate est = detectPeriod(src, rate);
        std::printf("  period change over %.0fx lagHi (%zu frames, %.2f s) -> %s (%.3f)\n", mult,
                    n, static_cast<double>(n) / rate, est.valid() ? "DETECTED" : "none",
                    est.frames);
        CHECK(!est.valid());
    }
}

// Silence and an absent period are NOT the same evidence, and the agreement denominator has to
// tell them apart: a capture with a silent head still detects, while a source that is mostly
// aperiodic with one pitched burst must not be accepted on that burst alone.
static void testSilenceIsNotDissentButAnAbsentPeriodIs() {
    const int rate = 44100;
    const std::size_t lagHi = longestLagFrames(rate);
    const std::size_t n = 10 * lagHi;
    const double p = static_cast<double>(rate) / 30.0;

    for (std::size_t lead : {lagHi, 2 * lagHi, 4 * lagHi}) {
        std::vector<AudioSample> src(n, 0.0f);
        for (std::size_t i = lead; i < n; ++i) {
            src[i] = static_cast<AudioSample>(
                std::sin(2.0 * kPi * static_cast<double>(i - lead) / p));
        }
        const PeriodEstimate est = detectPeriod(src, rate);
        std::printf("  %zu frames of leading silence -> %s (%.3f)\n", lead,
                    est.valid() ? "detected" : "NONE", est.frames);
        CHECK(est.valid());
        if (est.valid()) CHECK(std::fabs(est.frames - p) < 1.0);
    }

    // Noise everywhere but the opening: three probes carry signal and find no period, one finds
    // one. Counting only the survivor accepted this on a single unopposed estimate.
    std::vector<AudioSample> burst(n);
    std::uint32_t rng = 4242u;
    for (auto& x : burst) {
        rng = rng * 1664525u + 1013904223u;
        x = static_cast<AudioSample>((static_cast<double>(rng >> 8) / 8388608.0) - 1.0);
    }
    for (std::size_t i = 0; i < 2 * lagHi; ++i) {
        burst[i] = static_cast<AudioSample>(std::sin(2.0 * kPi * static_cast<double>(i) / p));
    }
    const PeriodEstimate est = detectPeriod(burst, rate);
    std::printf("  noise with one pitched burst -> %s (%.3f)\n", est.valid() ? "DETECTED" : "none",
                est.frames);
    CHECK(!est.valid());
}

static void testTheTwoLowFrequenciesTheShifterWasBuiltForAreDetected() {
    // 30 Hz and 29 Hz — the pair the Preserve geometry work is measured against. 29 Hz is the
    // sharper case: its period does not divide the splice window, so the shifter needs the
    // detected value to be right rather than merely present.
    for (double hz : {30.0, 29.0}) {
        const double p = 44100.0 / hz;
        const PeriodEstimate est = detectPeriod(sineOfPeriod(160000, p), 44100);
        std::printf("  %.0f Hz -> %s (%.3f, want %.3f)\n", hz, est.valid() ? "detected" : "NONE",
                    est.frames, p);
        CHECK(est.valid());
        if (est.valid()) CHECK(std::fabs(est.frames - p) < 0.5);
    }
}

// --- 5. The analysis span -------------------------------------------------------------------

static void testTheLoopStandsInForTheSourceOnlyWhenItCostsNoSearchBand() {
    const int rate = 44100;
    const std::size_t frames = 120000;
    // One full probe block — the span below which detectPeriod starts shortening its own
    // longest lag, which is the only thing the narrower span may never cost.
    const std::size_t minimum = 2 * longestLagFrames(rate);

    // An unusable rate and an empty capture take the same refuse-rather-than-repair path the
    // loop-bounds branches do; nothing downstream may see a span it cannot analyse.
    const AnalysisSpan noRate = periodAnalysisSpan(frames, 0, 90000, true, 0);
    CHECK(noRate.from == 0 && noRate.count == frames);
    const AnalysisSpan negRate = periodAnalysisSpan(frames, 0, 90000, true, -44100);
    CHECK(negRate.from == 0 && negRate.count == frames);
    const AnalysisSpan empty = periodAnalysisSpan(0, 0, 0, true, rate);
    CHECK(empty.from == 0 && empty.count == 0);
    // An empty capture with a loop still reaching past it refuses to the (empty) whole source
    // rather than handing detection a span past the end of the PCM.
    const AnalysisSpan emptyLooped = periodAnalysisSpan(0, 0, 44100, true, rate);
    CHECK(emptyLooped.from == 0 && emptyLooped.count == 0);

    // No loop, an inverted span, and a span reaching past the PCM all yield the whole source.
    for (const auto& [lo, hi, has] : {std::tuple<std::int64_t, std::int64_t, bool>{0, 0, false},
                                      {60000, 120000, false},
                                      {90000, 90000, true},
                                      {90000, 80000, true},
                                      {-1, 90000, true},
                                      {60000, 130000, true}}) {
        const AnalysisSpan s = periodAnalysisSpan(frames, lo, hi, has, rate);
        CHECK(s.from == 0 && s.count == frames);
    }

    // A loop one frame under the minimum falls back to the WIDER span, not to none.
    const AnalysisSpan shortLoop =
        periodAnalysisSpan(frames, 60000, 60000 + static_cast<std::int64_t>(minimum) - 1, true,
                           rate);
    CHECK(shortLoop.from == 0 && shortLoop.count == frames);

    // At the minimum exactly, the loop is taken.
    const AnalysisSpan atMinimum =
        periodAnalysisSpan(frames, 60000, 60000 + static_cast<std::int64_t>(minimum), true, rate);
    CHECK(atMinimum.from == 60000 && atMinimum.count == minimum);

    const double p = static_cast<double>(rate) / 30.0;
    const std::vector<AudioSample> src = sineOfPeriod(frames, p);

    // The span IS taken at the minimum, but that alone doesn't say detection BEHAVES there: at
    // exactly one probe block, detectPeriod takes the lone-probe carve-out (no agreement check
    // at all) — the span choice and the accept rule meet at this exact boundary, and that
    // meeting point is what needs to actually detect, not just be selected.
    const PeriodEstimate atMin = detectPeriod(src, rate, atMinimum.from, atMinimum.count);
    std::printf("  loop at the minimum span -> %s (%.3f, want %.3f)\n",
                atMin.valid() ? "detected" : "NONE", atMin.frames, p);
    CHECK(atMin.valid());
    if (atMin.valid()) CHECK(std::fabs(atMin.frames - p) < 0.5);

    // And the too-short loop still DETECTS through the wider span — refusing there would be a
    // regression against analysing the whole source, and a short sustain loop is common.
    const PeriodEstimate est = detectPeriod(src, rate, shortLoop.from, shortLoop.count);
    std::printf("  short loop -> whole source: %s (%.3f)\n", est.valid() ? "detected" : "NONE",
                est.frames);
    CHECK(est.valid());
    if (est.valid()) CHECK(std::fabs(est.frames - p) < 0.5);
}

static void testAPhraseWhoseLoopIsPitchedDifferentlyFromItsHeadDetectsOverTheLoop() {
    // The case the whole-source analysis cannot answer: the head sustains one pitch, the looped
    // tail another. Analysed whole, two probes land each side and the strict-majority rule
    // correctly refuses — there is no ONE period over the whole source. But under Gate the
    // splicer lives in the loop, whose period is perfectly well defined.
    const int rate = 44100;
    const std::size_t frames = 120000;
    const std::int64_t loopStart = 60000;
    const double headPeriod = 300.0;
    const double loopPeriod = static_cast<double>(rate) / 30.0;  // 1470 frames

    std::vector<AudioSample> src(frames);
    double phase = 0.0;
    for (std::size_t i = 0; i < frames; ++i) {
        phase += 2.0 * kPi /
                 (i < static_cast<std::size_t>(loopStart) ? headPeriod : loopPeriod);
        src[i] = static_cast<AudioSample>(std::sin(phase));
    }

    // BEFORE this rule: the whole source is what was analysed, and it reports none.
    const PeriodEstimate whole = detectPeriod(src, rate);
    std::printf("  phrase analysed whole -> %s (%.3f)\n", whole.valid() ? "DETECTED" : "none",
                whole.frames);
    CHECK(!whole.valid());

    // AFTER: the loop is long enough to host the full band, so it is the analysed span.
    const AnalysisSpan span = periodAnalysisSpan(frames, loopStart,
                                                 static_cast<std::int64_t>(frames), true, rate);
    CHECK(span.from == static_cast<std::size_t>(loopStart));
    const PeriodEstimate looped = detectPeriod(src, rate, span.from, span.count);
    std::printf("  phrase analysed over its loop -> %s (%.3f, want %.3f)\n",
                looped.valid() ? "detected" : "NONE", looped.frames, loopPeriod);
    CHECK(looped.valid());
    if (looped.valid()) CHECK(std::fabs(looped.frames - loopPeriod) < 2.0);

    // The narrowed span must not turn a genuinely aperiodic loop into a period: same geometry,
    // noise in the loop region.
    std::vector<AudioSample> noisyLoop = src;
    std::uint32_t rng = 777u;
    for (std::size_t i = static_cast<std::size_t>(loopStart); i < frames; ++i) {
        rng = rng * 1664525u + 1013904223u;
        noisyLoop[i] = static_cast<AudioSample>((static_cast<double>(rng >> 8) / 8388608.0) - 1.0);
    }
    CHECK(!detectPeriod(noisyLoop, rate, span.from, span.count).valid());
}

static void testAnOutOfRangeSpanEstimatesNothing() {
    const std::vector<AudioSample> src = sineOfPeriod(120000, 441.0);
    CHECK(!detectPeriod(src, 44100, 120001, 10).valid());
    CHECK(!detectPeriod(src, 44100, 119000, 5000).valid());
    CHECK(!detectPeriod(src, 44100, 0, 0).valid());
}

// --- 6. What the load pays ------------------------------------------------------------------

// The whole reason a detector is affordable in a sampler is that it runs ONCE, off the audio
// thread, on a source that is already fully known. This prints what that once costs, and
// asserts the property that makes it safe: the cost does NOT grow with the sample length —
// a fixed number of fixed-size probes is analysed however long the capture is. Meaningful
// only in a Release build; asserted as a RATIO so it holds at either optimization level.
static void testDetectionCostIsBoundedRegardlessOfSampleLength() {
    double shortMs = 0.0, longMs = 0.0;
    for (std::size_t frames : {std::size_t{220500}, std::size_t{4410000}}) {  // 5 s and 100 s
        const std::vector<AudioSample> src = sineOfPeriod(frames, 441.0);
        const int reps = 5;
        const auto t0 = std::chrono::steady_clock::now();
        double guard = 0.0;
        for (int r = 0; r < reps; ++r) guard += detectPeriod(src, 44100).frames;
        const double ms =
            1000.0 * std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count()
            / reps;
        CHECK(guard > 0.0);
        std::printf("  [measure] detectPeriod over %7.1f s of source: %.3f ms\n",
                    static_cast<double>(frames) / 44100.0, ms);
        (frames == 220500 ? shortMs : longMs) = ms;
    }
    // 20x the source for well under 2x the cost — the probes are fixed-size and fixed in
    // number, so the only length dependence left is the cache behaviour of reaching further
    // into the buffer.
    CHECK(longMs < shortMs * 2.0 + 0.5);
}

int main() {
    testPureTonePeriodIsFoundToBetterThanATenthOfAFrame();
    testTheEstimateIsInSourceFramesSoTheRateOnlyMovesTheBand();
    testHarmonicRichSourceReportsTheRepeatPeriodNotAPartial();
    testMissingFundamentalStillReportsTheRepeatPeriod();
    testNoiseSilenceAndAPeriodChangeAllReportNone();
    testAPercussiveDecayIsNotForcedIntoAPeriod();
    testBelowTheBandReportsNoneAndAboveItReportsAWholeMultiple();
    testAShortSourceShortensTheSearchRatherThanRefusing();
    testASourceTooShortForASecondProbeIsStillDetectedOnItsOneProbe();
    testAStationarySourceDetectsAtEveryLengthAcrossTheProbeCountSteps();
    testAPeriodChangeIsRefusedWhereTheProbesOverlapToo();
    testSilenceIsNotDissentButAnAbsentPeriodIs();
    testTheTwoLowFrequenciesTheShifterWasBuiltForAreDetected();
    testTheLoopStandsInForTheSourceOnlyWhenItCostsNoSearchBand();
    testAPhraseWhoseLoopIsPitchedDifferentlyFromItsHeadDetectsOverTheLoop();
    testAnOutOfRangeSpanEstimatesNothing();
    testDetectionCostIsBoundedRegardlessOfSampleLength();

    if (g_fail == 0) {
        std::printf("all period_detect tests passed\n");
        return 0;
    }
    std::printf("%d period_detect check(s) failed\n", g_fail);
    return 1;
}
