// Standalone tests for reasampler::theme — no REAPER, no LICE, no test framework. Same
// fast assert loop as the sibling pure tests.
//
// The load-bearing test: EVERY text-on-surface pair the kit actually draws clears its WCAG
// floor (AA 4.5:1 body / 3:1 large + state indicators) — the "punch to the floor, not past
// it" rule made testable (brief §1, CONTEXT.md §palette). Plus: the direction constants are
// the single point of change (structural — roleColor is the only color source), the
// interaction-state transform behaves, the WCAG math is correct against known anchors, and
// the Direction C spectral ramp interpolates its endpoints.

#include "../src/theme.h"

#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <initializer_list>

using namespace reasampler;

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

// --- WCAG math against known anchors ------------------------------------------

static void testContrastKnownAnchors() {
    const KitColor white{255, 255, 255, 255};
    const KitColor black{0, 0, 0, 255};
    // Black-on-white is the canonical 21:1.
    CHECK(std::fabs(contrastRatio(white, black) - 21.0) < 0.05);
    // Contrast is symmetric (order-independent).
    CHECK(std::fabs(contrastRatio(white, black) - contrastRatio(black, white)) < 1e-9);
    // A color against itself is 1:1 (no contrast).
    CHECK(std::fabs(contrastRatio(white, white) - 1.0) < 1e-9);
    // Luminance ordering: white brightest, black darkest.
    CHECK(relativeLuminance(white) > relativeLuminance(black));
    CHECK(std::fabs(relativeLuminance(black)) < 1e-9);
    CHECK(std::fabs(relativeLuminance(white) - 1.0) < 1e-9);
}

// --- THE load-bearing test: every drawn text-on-surface pair clears its floor -

// The surfaces text lands on (DS-2 revised REAPER-grey elevation stack).
static void testTextPrimaryClearsBodyFloorOnSurfaces() {
    const KitColor tp = roleColor(Role::TextPrimary);
    // Primary text is body text -> 4.5:1 on every surface it is drawn on.
    CHECK(contrastRatio(tp, roleColor(Role::BgBase))  >= textFloor(TextClass::Body));
    CHECK(contrastRatio(tp, roleColor(Role::BgPanel)) >= textFloor(TextClass::Body));
    CHECK(contrastRatio(tp, roleColor(Role::BgCell))  >= textFloor(TextClass::Body));
}

static void testTextDimClearsItsFloorOnSurfaces() {
    const KitColor td = roleColor(Role::TextDim);
    // DS-2 revised (grey re-read): text/dim is used for keybinding sub-labels / units on
    // the mid-grey surfaces, and mid-grey-on-mid-grey is the classic AA failure. It is
    // LOCKED to the lightest grey that still reads dim while clearing AA 4.5:1 BODY on the
    // GREYEST surface it draws body text on (bg/cell). This is the load-bearing check: the
    // spec-start #a0a0a0 is UNDER floor on grey by design — the value was lifted until this
    // passes. Body floor on all three surfaces (bg/cell is the tight one).
    CHECK(contrastRatio(td, roleColor(Role::BgBase))  >= textFloor(TextClass::Body));
    CHECK(contrastRatio(td, roleColor(Role::BgPanel)) >= textFloor(TextClass::Body));
    CHECK(contrastRatio(td, roleColor(Role::BgCell))  >= textFloor(TextClass::Body));
}

static void testAccentsClearIndicatorFloorOnCell() {
    // DS-2 revised (grey re-read): each of the three pastel accents is used as a state
    // indicator / active fill on bg/cell (selection border, active segment, active tab).
    // The greyer surface shrank the cushion from ~15:1 (near-black) to ~6-7:1 (grey), so
    // this is re-verified on bg/cell (NOT the old near-black) at the 3:1 large/indicator
    // floor. If any pastel dropped below, the fix is to nudge that hue deeper (hue stays
    // lime/teal/purple) — the value the test locks proves it did not need it here.
    for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary,
                   Role::AccentHot}) {
        CHECK(contrastRatio(roleColor(a), roleColor(Role::BgCell))
              >= textFloor(TextClass::Large));
    }
    // The primary accent also leads the mode-switch/tab active fill drawn over bg/base.
    CHECK(contrastRatio(roleColor(Role::AccentPrimary), roleColor(Role::BgBase))
          >= textFloor(TextClass::Large));
    // bank_panel draws categorical accents as text on bg/panel (region-title band):
    // AccentSecondary/Tertiary as region-title text; AccentPrimary as the "Active:" readout.
    // Verify all three clear the 3:1 large/indicator floor on bg/panel (the ACTUAL draw surface).
    for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary}) {
        CHECK(contrastRatio(roleColor(a), roleColor(Role::BgPanel))
              >= textFloor(TextClass::Large));
    }
}

// Text drawn ON a pastel accent fill (retained tight pair, re-verified for the pastels):
// active buttons/segments/tabs fill with the primary accent and draw their label in
// bg/base (near-black-ish grey). That near-black-on-pastel pair must clear AA 4.5:1 body,
// or active controls would be unreadable. Verified for EACH pastel that can be a text fill.
static void testTextOnPastelFillClearsBodyFloor() {
    const KitColor label = roleColor(Role::BgBase);  // what drawButton/drawListRow use
    for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary}) {
        CHECK(contrastRatio(label, roleColor(a)) >= textFloor(TextClass::Body));
    }
}

// Secondary and tertiary are the CATEGORICAL pair (region titles, spectral bands). They
// must read as DISTINCT categories, not two greys. Two pastels can be near-equal in
// LUMINANCE yet clearly distinct in HUE (teal vs purple differ mainly by channel balance,
// not brightness) — so distinguishability is a hue-separation check, not a luminance-
// contrast one. Assert a meaningful per-channel difference: teal is green/blue-leaning,
// purple is red/blue-leaning, so their channel BALANCE diverges substantially.
static void testSecondaryTertiaryAreDistinguishable() {
    const KitColor sec = roleColor(Role::AccentSecondary);   // teal: G,B high, R low
    const KitColor ter = roleColor(Role::AccentTertiary);    // purple: R,B high, G lower
    CHECK(!(sec == ter));
    CHECK(!(roleColor(Role::AccentPrimary) == sec));
    // Hue divergence: teal's green dominates its red; purple's red dominates its green.
    // A muddying that collapsed them toward a common grey would break this ordering.
    CHECK(sec.g > sec.r);   // teal leans green over red
    CHECK(ter.r > ter.g);   // purple leans red over green
    // And the total channel-balance gap is large (sum of absolute channel deltas), so the
    // two are far apart in color space even though close in luminance.
    const int delta = std::abs(int(sec.r) - int(ter.r)) +
                      std::abs(int(sec.g) - int(ter.g)) +
                      std::abs(int(sec.b) - int(ter.b));
    CHECK(delta >= 60);
}

static void testWarnClearsStateFloorOnBackground() {
    // warn (destructive) must be unmistakable -> clears the state floor on the base.
    CHECK(contrastRatio(roleColor(Role::Warn), roleColor(Role::BgBase))
          >= textFloor(TextClass::Large));
}

// Label drawn on an ACTIVE (accent-filled) surface: the shell draws it in bg/base. That
// pairing must also clear the body floor, or active buttons would be unreadable.
static void testLabelOnActiveSurfaceClearsFloor() {
    const KitColor activeFill = roleColorState(Role::BgCell, InteractionState::Active);
    const KitColor labelOnActive = roleColor(Role::BgBase);  // what drawButton uses
    CHECK(contrastRatio(labelOnActive, activeFill) >= textFloor(TextClass::Body));
}

// Text drawn on a HOVER-state surface (Phase L, L3): the VST editor + embed restyle lights
// hover on inactive segments/tabs/cards by lightening bg/cell toward accent/hot (~10%).
// Lightening the surface REDUCES contrast against light text, so re-verify the pairs on the
// hover surface, not just on rest bg/cell. (A distinct drawn pair the L3 shells introduce,
// not covered by the rest-surface tests above.)
//   - text/PRIMARY (card name, segment/tab labels) is BODY -> must clear AA 4.5:1 even hovered.
//   - text/DIM is used ONLY as a compact UI LABEL on a hoverable surface (the card's root/key
//     BADGE — the one dim-on-hover pair the restyle draws), so it clears at the 3:1 large/
//     UI-label floor, per §2.1's "confine dim to large/UI-label use where the 3:1 floor
//     applies." A lightened surface pulls a mid-grey dim toward the body floor; the badge is
//     deliberately a label, not body, so 3:1 is the correct floor and this locks it there.
static void testTextOnHoverSurfaceClearsFloor() {
    const KitColor hover = roleColorState(Role::BgCell, InteractionState::Hover);
    CHECK(contrastRatio(roleColor(Role::TextPrimary), hover) >= textFloor(TextClass::Body));
    CHECK(contrastRatio(roleColor(Role::TextDim), hover) >= textFloor(TextClass::Large));
}

// --- Single point of change (structural guarantee) ----------------------------
//
// roleColor is the ONLY color source; there is no other public accessor that yields a
// palette color, so re-picking the direction is editing the one constants block roleColor
// reads. We assert the roles are DISTINCT (the block actually differentiates them — a
// collapsed/duplicated palette would betray a broken edit) and that the elevation stack
// is monotonic (base darkest -> cell lightest), the invariant the direction must preserve.
static void testRolesAreDistinctAndElevationMonotonic() {
    CHECK(!(roleColor(Role::BgBase) == roleColor(Role::BgPanel)));
    CHECK(!(roleColor(Role::BgPanel) == roleColor(Role::BgCell)));
    CHECK(!(roleColor(Role::TextPrimary) == roleColor(Role::TextDim)));
    CHECK(!(roleColor(Role::AccentPrimary) == roleColor(Role::Warn)));
    // Elevation reads as increasing luminance base < panel < cell.
    CHECK(relativeLuminance(roleColor(Role::BgBase)) <
          relativeLuminance(roleColor(Role::BgPanel)));
    CHECK(relativeLuminance(roleColor(Role::BgPanel)) <
          relativeLuminance(roleColor(Role::BgCell)));
    // Primary text is brighter than dim text (the type hierarchy).
    CHECK(relativeLuminance(roleColor(Role::TextPrimary)) >
          relativeLuminance(roleColor(Role::TextDim)));
}

// --- Interaction-state transform ----------------------------------------------

static void testStateRestIsIdentity() {
    for (Role r : {Role::BgBase, Role::BgPanel, Role::BgCell, Role::AccentPrimary}) {
        CHECK(roleColorState(r, InteractionState::Rest) == roleColor(r));
    }
}

static void testHoverLightensPressedDarkens() {
    const KitColor rest = roleColor(Role::BgCell);
    const KitColor hover = roleColorState(Role::BgCell, InteractionState::Hover);
    const KitColor pressed = roleColorState(Role::BgCell, InteractionState::Pressed);
    // Hover lightens toward the hot accent; pressed darkens.
    CHECK(relativeLuminance(hover) > relativeLuminance(rest));
    CHECK(relativeLuminance(pressed) < relativeLuminance(rest));
}

static void testActiveIsPrimaryAccent() {
    // DS-2 revised: the Active state is ALWAYS the primary accent ("what's live" leads).
    CHECK(roleColorState(Role::BgCell, InteractionState::Active)
          == roleColor(Role::AccentPrimary));
}

static void testDisabledDropsAlphaAndDesaturates() {
    const KitColor rest = roleColor(Role::AccentPrimary);
    const KitColor dis = roleColorState(Role::AccentPrimary, InteractionState::Disabled);
    // Alpha drops to ~40%.
    CHECK(dis.a < rest.a);
    CHECK(dis.a >= 90 && dis.a <= 110);  // 255 * 0.4 ~= 102
}

// --- Direction C spectral ramp ------------------------------------------------

static void testSpectralIsPastelRampAnchoredOnAccents() {
    // DS-2 revised Direction C: the spectral ramp is a PASTEL sweep anchored on the three
    // accents — lime (low) -> teal (mid) -> purple (high) — NOT the old neon cool->hot.
    const KitColor lo = spectralColor(0.0);
    const KitColor mid = spectralColor(0.5);
    const KitColor hi = spectralColor(1.0);
    // The three stops ARE the three accent constants (single source — the strip belongs to
    // the accent system). This is the load-bearing identity of the pastel ramp.
    CHECK(lo == roleColor(Role::AccentPrimary));    // low  = pastel lime
    CHECK(mid == roleColor(Role::AccentSecondary)); // mid  = pastel teal
    CHECK(hi == roleColor(Role::AccentTertiary));   // high = pastel purple
    // Low is lime (green-dominant); high is purple (red+blue over green) — distinct hues.
    CHECK(lo.g > lo.r && lo.g > lo.b);
    CHECK(hi.b > hi.g && hi.r > hi.g);
    // A quarter-point interpolates within the lime->teal half (not off the family).
    const KitColor q = spectralColor(0.25);
    CHECK(q.b > lo.b && q.b < mid.b);  // blue rises lime -> teal
    // Clamps out of range to the endpoints.
    CHECK(spectralColor(-1.0) == lo);
    CHECK(spectralColor(2.0) == hi);
}

int main() {
    testContrastKnownAnchors();
    testTextPrimaryClearsBodyFloorOnSurfaces();
    testTextDimClearsItsFloorOnSurfaces();
    testAccentsClearIndicatorFloorOnCell();
    testTextOnPastelFillClearsBodyFloor();
    testTextOnHoverSurfaceClearsFloor();
    testSecondaryTertiaryAreDistinguishable();
    testWarnClearsStateFloorOnBackground();
    testLabelOnActiveSurfaceClearsFloor();
    testRolesAreDistinctAndElevationMonotonic();
    testStateRestIsIdentity();
    testHoverLightensPressedDarkens();
    testActiveIsPrimaryAccent();
    testDisabledDropsAlphaAndDesaturates();
    testSpectralIsPastelRampAnchoredOnAccents();

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