// Standalone tests for reasampler::tail_control — no REAPER, no framework. Covers
// the pure pieces behind the docked panel's tail-mode toggle: the cycle order
// (None -> Auto -> Manual -> None), the manual-length clamp to the 8 s cap, and the
// toggle label text. The drawing / click hit-testing is DAW-verified in bank_panel.

#include "../src/core/capture/tail_control.h"

#include <cstdio>
#include <string>

using namespace reasampler;
using namespace reasampler::capture;

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

// --- cycleTailMode: the toggle order ------------------------------------------

static void testCycleOrderIsNoneAutoManualNone() {
    // None -> Auto -> Manual -> None, wrapping. The click handler relies on exactly
    // this order; a reorder (e.g. skipping Manual) would fail here.
    CHECK(cycleTailMode(TailMode::None)   == TailMode::Auto);
    CHECK(cycleTailMode(TailMode::Auto)   == TailMode::Manual);
    CHECK(cycleTailMode(TailMode::Manual) == TailMode::None);
}

static void testCycleThreeStepsReturnsToStart() {
    // Three cycles from any state land back on that state (a full lap of the 3-cycle).
    TailMode m = TailMode::None;
    m = cycleTailMode(m);
    m = cycleTailMode(m);
    m = cycleTailMode(m);
    CHECK(m == TailMode::None);
}

// --- clampManualMs: the runaway guard -----------------------------------------

static void testManualClampInRangeIsUnchanged() {
    // A value inside [0, kMaxTailMs] passes through untouched.
    CHECK(clampManualMs(kDefaultManualTailMs) == kDefaultManualTailMs);
    CHECK(clampManualMs(0.0) == 0.0);
    CHECK(clampManualMs(kMaxTailMs) == kMaxTailMs);
}

static void testManualClampCapsAtEightSeconds() {
    // Over the 8 s cap clamps to kMaxTailMs; negative floors to 0. This mirrors the
    // clamp in tailRenderSettingsFor, so the panel and the render agree on the bound.
    CHECK(clampManualMs(kMaxTailMs + 5000.0) == kMaxTailMs);
    CHECK(clampManualMs(-100.0) == 0.0);
}

// --- adjustManualMs: the scroll-wheel fine-adjust arithmetic ------------------

static void testAdjustUpAndDownBySteps() {
    // Positive notches lengthen, negative shorten, in whole kManualStepMs increments.
    CHECK(adjustManualMs(2000.0, 1, kManualStepMs)  == 2250.0);
    CHECK(adjustManualMs(2000.0, -1, kManualStepMs) == 1750.0);
    CHECK(adjustManualMs(2000.0, 4, kManualStepMs)  == 3000.0);  // 4 * 250
    CHECK(adjustManualMs(2000.0, 0, kManualStepMs)  == 2000.0);  // no notch, no move
}

static void testAdjustClampsAtUpperBound() {
    // Scrolling up past the 8 s cap saturates AT the cap, never beyond.
    CHECK(adjustManualMs(kMaxTailMs, 1, kManualStepMs)         == kMaxTailMs);
    CHECK(adjustManualMs(kMaxTailMs - 100.0, 10, kManualStepMs) == kMaxTailMs);
}

static void testAdjustClampsAtLowerBound() {
    // Scrolling down past 0 floors at 0, never negative.
    CHECK(adjustManualMs(0.0, -1, kManualStepMs)    == 0.0);
    CHECK(adjustManualMs(100.0, -10, kManualStepMs) == 0.0);
}

// --- tailToggleLabel: the exact strings the panel draws -----------------------

static void testLabelStringsPerMode() {
    TailSetting off;   off.mode   = TailMode::None;
    TailSetting autoM; autoM.mode = TailMode::Auto;
    // Off/Auto carry NO length regardless of manualMs.
    off.manualMs   = 5000.0;
    autoM.manualMs = 5000.0;
    CHECK(tailToggleLabel(off)   == "Tail: Off");
    CHECK(tailToggleLabel(autoM) == "Tail: Auto");
}

static void testManualLabelRendersLengthInSeconds() {
    // Manual appends the length in seconds to one decimal — pin the format and the
    // boundary values (0.0s, the 2 s default, the 8 s cap).
    TailSetting man; man.mode = TailMode::Manual;
    man.manualMs = 0.0;
    CHECK(tailToggleLabel(man) == "Tail: Manual 0.0s");
    man.manualMs = kDefaultManualTailMs;  // 2000 ms
    CHECK(tailToggleLabel(man) == "Tail: Manual 2.0s");
    man.manualMs = kMaxTailMs;            // 8000 ms
    CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s");
    // An over-cap stored value renders at the CLAMPED length, never past the cap.
    man.manualMs = kMaxTailMs + 3000.0;
    CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s");
}

static void testDefaultSettingIsOff() {
    // The zero-value setting is None (Off) with the 2 s manual default — the safe
    // default the panel starts in so captures stay exact-bounds until opt-in.
    TailSetting s;
    CHECK(s.mode == TailMode::None);
    CHECK(s.manualMs == kDefaultManualTailMs);
    CHECK(tailToggleLabel(s) == "Tail: Off");
}

// --- serialize/deserialize: per-project persistence round-trip ----------------

static bool settingsEqual(const TailSetting& a, const TailSetting& b) {
    return a.mode == b.mode && a.manualMs == b.manualMs;
}

static void testRoundTripNoneDefault() {
    TailSetting s;  // None + 2 s default
    auto back = deserializeTailSetting(serializeTailSetting(s));
    CHECK(back.has_value());
    CHECK(back && settingsEqual(*back, s));
}

static void testRoundTripManualArbitraryMs() {
    // A non-round manual length must round-trip bit-for-bit (17-sig-digit emit).
    TailSetting s; s.mode = TailMode::Manual; s.manualMs = 3141.592653589793;
    auto back = deserializeTailSetting(serializeTailSetting(s));
    CHECK(back.has_value());
    CHECK(back && settingsEqual(*back, s));
}

static void testRoundTripAuto() {
    TailSetting s; s.mode = TailMode::Auto; s.manualMs = 500.0;
    auto back = deserializeTailSetting(serializeTailSetting(s));
    CHECK(back.has_value());
    CHECK(back && settingsEqual(*back, s));
}

static void testSerializeByteIdentity() {
    // Q-W1 structural-dedupe guard: the core/json-backed writer must emit the
    // EXACT bytes the former snprintf writer produced ({"mode":%d,"manualMs":%.17g})
    // and re-serializing a round-tripped setting must be byte-identical — the blob
    // lives in the .rpp, so a byte shift would dirty every saved project.
    TailSetting s;  // None + 2000.0 default
    CHECK(serializeTailSetting(s) == "{\"mode\":0,\"manualMs\":2000}");
    TailSetting man; man.mode = TailMode::Manual; man.manualMs = 3141.592653589793;
    const std::string json = serializeTailSetting(man);
    auto back = deserializeTailSetting(json);
    CHECK(back.has_value());
    CHECK(back && serializeTailSetting(*back) == json);  // stable second round-trip
}

static void testDeserializeEmptyIsDefault() {
    // An absent/empty stored value (older project) -> nullopt, so the caller falls
    // back to the default. This is the graceful-old-project path the brief requires.
    CHECK(!deserializeTailSetting("").has_value());
}

static void testDeserializeMalformedIsDefault() {
    // Garbage, a missing key, or an unknown mode enumerant -> nullopt (no crash).
    CHECK(!deserializeTailSetting("not json at all").has_value());
    CHECK(!deserializeTailSetting("{\"mode\":1}").has_value());        // manualMs missing
    CHECK(!deserializeTailSetting("{\"manualMs\":2000}").has_value()); // mode missing
    CHECK(!deserializeTailSetting("{\"mode\":9,\"manualMs\":2000}").has_value()); // bad enum
    CHECK(!deserializeTailSetting("{\"mode\":x,\"manualMs\":2000}").has_value()); // non-numeric
}

int main() {
    testCycleOrderIsNoneAutoManualNone();
    testCycleThreeStepsReturnsToStart();
    testManualClampInRangeIsUnchanged();
    testManualClampCapsAtEightSeconds();
    testAdjustUpAndDownBySteps();
    testAdjustClampsAtUpperBound();
    testAdjustClampsAtLowerBound();
    testLabelStringsPerMode();
    testManualLabelRendersLengthInSeconds();
    testDefaultSettingIsOff();
    testRoundTripNoneDefault();
    testRoundTripManualArbitraryMs();
    testRoundTripAuto();
    testSerializeByteIdentity();
    testDeserializeEmptyIsDefault();
    testDeserializeMalformedIsDefault();

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