// Standalone tests for reasampler::card_meta — no REAPER, no test framework. Asserts the
// L7 decorative overlay formatters: bars.beats.subdivisions (F1 tempo+meter stamp) and
// seconds.milliseconds.
//
// Covers: bar-1 origin (zero length), sub-bar, exact bar boundary rollover, multi-bar,
// long capture, non-4/4 meters (3/4 and 6/8), unstamped meter -> blank, unknown tempo ->
// blank (s.ms still derivable); s.ms zero / sub-second / multi-second / ms carry / negative.

#include "../src/core/ui/card_meta.h"

#include <cstdio>
#include <string>

using namespace reasampler;
using namespace reasampler::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)

// Beat length in 4/4 at 120 BPM: (60/120)*(4/4) = 0.5 s/beat, so a 4/4 bar = 2.0 s.

// --- bars.beats.subdivisions -------------------------------------------------

static void testZeroLengthIsBarOneOrigin() {
    // Zero length -> the musical origin, "1.1.00".
    CHECK(formatBarsBeats(MusicalLength{0.0, 120.0, 4, 4}) == "1.1.00");
}

static void testSubBeat() {
    // 0.25 s at 120 BPM 4/4 = 0.5 beats -> bar 1, beat 1, .50.
    CHECK(formatBarsBeats(MusicalLength{0.25, 120.0, 4, 4}) == "1.1.50");
}

static void testWholeBeatWithinBar() {
    // 0.5 s = exactly 1 beat -> bar 1, beat 2, .00.
    CHECK(formatBarsBeats(MusicalLength{0.5, 120.0, 4, 4}) == "1.2.00");
}

static void testExactBarBoundaryRollsOver() {
    // 2.0 s = exactly 4 beats = 1 bar in 4/4 -> rolls to bar 2, beat 1 (NOT "1.5.00").
    CHECK(formatBarsBeats(MusicalLength{2.0, 120.0, 4, 4}) == "2.1.00");
}

static void testMultiBar() {
    // 5.0 s at 120 4/4 = 10 beats = 2 bars + 2 beats -> "3.3.00".
    CHECK(formatBarsBeats(MusicalLength{5.0, 120.0, 4, 4}) == "3.3.00");
}

static void testLongCaptureNoBarCap() {
    // 256 s at 120 4/4 = 512 beats = 128 bars exactly -> bar 129, beat 1.
    CHECK(formatBarsBeats(MusicalLength{256.0, 120.0, 4, 4}) == "129.1.00");
}

static void testThreeFourMeter() {
    // 3/4 at 120 BPM: beat = (60/120)*(4/4)=0.5 s, bar = 3 beats = 1.5 s. 1.5 s -> bar 2,1.
    CHECK(formatBarsBeats(MusicalLength{1.5, 120.0, 3, 4}) == "2.1.00");
    // 1.0 s = 2 beats -> bar 1, beat 3.
    CHECK(formatBarsBeats(MusicalLength{1.0, 120.0, 3, 4}) == "1.3.00");
}

static void testSixEightMeter() {
    // 6/8 at 120 BPM: an eighth-beat = (60/120)*(4/8) = 0.25 s; bar = 6 beats = 1.5 s.
    // 1.5 s -> bar 2, beat 1.
    CHECK(formatBarsBeats(MusicalLength{1.5, 120.0, 6, 8}) == "2.1.00");
    // 0.25 s = exactly 1 eighth-beat -> bar 1, beat 2.
    CHECK(formatBarsBeats(MusicalLength{0.25, 120.0, 6, 8}) == "1.2.00");
}

static void testUnstampedMeterIsBlank() {
    // 0/0 (pre-L7 sample) -> no musical read-out.
    CHECK(formatBarsBeats(MusicalLength{3.0, 120.0, 0, 0}).empty());
    CHECK(formatBarsBeats(MusicalLength{3.0, 120.0, 4, 0}).empty());  // partial stamp also blank
    CHECK(formatBarsBeats(MusicalLength{3.0, 120.0, 0, 4}).empty());
}

static void testUnknownTempoIsBlank() {
    // Tempo 0 -> no musical read-out even with a meter (cannot derive beats).
    CHECK(formatBarsBeats(MusicalLength{3.0, 0.0, 4, 4}).empty());
}

static void testNegativeLengthClampsToOrigin() {
    // Defensive: a negative length reads as the origin, not garbage.
    CHECK(formatBarsBeats(MusicalLength{-5.0, 120.0, 4, 4}) == "1.1.00");
}

// --- seconds.milliseconds ----------------------------------------------------

static void testSecondsMsZero() {
    CHECK(formatSecondsMs(0.0) == "0.000");
}

static void testSecondsMsSubSecond() {
    CHECK(formatSecondsMs(0.5) == "0.500");
}

static void testSecondsMsMultiSecond() {
    CHECK(formatSecondsMs(1.5) == "1.500");
    CHECK(formatSecondsMs(62.037) == "62.037");
}

static void testSecondsMsNegativeClamps() {
    CHECK(formatSecondsMs(-1.0) == "0.000");
}

static void testSecondsMsNearWholeSecondCarry() {
    // 0.9999 s: floor to ms could hit 999 or 1000; the carry keeps it well-formed.
    const std::string r = formatSecondsMs(0.9999);
    CHECK(r == "0.999" || r == "1.000");
}

// --- the card's name strip ----------------------------------------------------

// The shipping cell size (panel_state.h's kGrid).
static const Rect kCell{40, 100, 140, 84};

static void testNameStripSitsAcrossTheTopOfTheCell() {
    const Rect s = cardNameStrip(kCell);
    CHECK(s.x == kCell.x + kCardStripPad);
    CHECK(s.y == kCell.y + 1);                       // clear of the selection border
    CHECK(s.width == kCell.width - 2 * kCardStripPad);
    CHECK(s.height == kCardStripHeight);
}

static void testNameStripNeverOverlapsTheLengthReadOut() {
    // The read-out occupies the bottom kCardStripHeight of the same cell.
    const Rect s = cardNameStrip(kCell);
    CHECK(s.bottom() <= kCell.bottom() - kCardStripHeight);
}

static void testNameStripStaysInsideTheCell() {
    const Rect s = cardNameStrip(kCell);
    CHECK(s.x >= kCell.x);
    CHECK(s.right() <= kCell.right());
    CHECK(s.y >= kCell.y);
    CHECK(s.bottom() <= kCell.bottom());
}

static void testNameStripSuppressedOnATooShortCell() {
    // Below three strip-heights the card would be text with a sliver of waveform — draw
    // no name rather than bury the thumbnail.
    CHECK(cardNameStrip(Rect{0, 0, 140, 3 * kCardStripHeight - 1}).empty());
    CHECK(!cardNameStrip(Rect{0, 0, 140, 3 * kCardStripHeight}).empty());
}

static void testNameStripSuppressedOnATooNarrowCell() {
    CHECK(cardNameStrip(Rect{0, 0, 2 * kCardStripPad, 84}).empty());
    CHECK(cardNameStrip(Rect{0, 0, 0, 84}).empty());
}

int main() {
    testNameStripSitsAcrossTheTopOfTheCell();
    testNameStripNeverOverlapsTheLengthReadOut();
    testNameStripStaysInsideTheCell();
    testNameStripSuppressedOnATooShortCell();
    testNameStripSuppressedOnATooNarrowCell();
    testZeroLengthIsBarOneOrigin();
    testSubBeat();
    testWholeBeatWithinBar();
    testExactBarBoundaryRollsOver();
    testMultiBar();
    testLongCaptureNoBarCap();
    testThreeFourMeter();
    testSixEightMeter();
    testUnstampedMeterIsBlank();
    testUnknownTempoIsBlank();
    testNegativeLengthClampsToOrigin();
    testSecondsMsZero();
    testSecondsMsSubSecond();
    testSecondsMsMultiSecond();
    testSecondsMsNegativeClamps();
    testSecondsMsNearWholeSecondCarry();

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