// Standalone tests for reasampler::instrument::ui::curve_tessellate — no VST3, no REAPER, no
// framework. Same fast assert loop as the sibling pure tests.
//
// Every assertion is expressed RELATIVE to the vertices buildEnvelopePolyline returns, never
// against an absolute pixel literal, so a re-scaled overlay axis leaves this file untouched.
// Covers: segmentCurve's node->exponent rule; the neutral exponent emitting today's straight
// vertex list unchanged; knots excluded and node vertices preserved exactly; THE GATE — the
// knot's centre within 1 px of the trace at every exponent, on all three sloped stages of both
// layout policies; the mid-segment level matching curve_law's own curveMidLevel; curvature
// direction; no overshoot past a segment's own endpoint levels; per-pixel-column density that
// scales with the canvas; the x clamp; and the degenerate/empty cases.
//
// Both layout policies here ARE all three envelopes and both play modes: the editor's
// packEnvelope collapses amp/filter/pitch x Gate/Trigger onto exactly these two EnvKinds, and
// paintEnvelopeOverlay is the single paint path over them.

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

#include <cmath>
#include <cstdio>
#include <vector>

#include "../src/core/util/curve_law.h"

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

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 OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; }

// Offset so left/top != 0 (catches origin bugs). Tall enough that a 1 px tolerance is a small
// fraction of the level span, which is what makes the level assertions discriminating.
static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 210); }  // width 1000, height 200

static StageEnvelope ahdsr(double a, double h, double d, double sus, double r) {
    StageEnvelope e;
    e.kind = EnvKind::Ahdsr;
    e.attackSeconds = a;
    e.holdSeconds = h;
    e.decaySeconds = d;
    e.sustainLevel = sus;
    e.releaseSeconds = r;
    return e;
}

static StageEnvelope ahd(double a, double d, double frac, double span) {
    StageEnvelope e;
    e.kind = EnvKind::Ahd;
    e.attackSeconds = a;
    e.decaySeconds = d;
    e.holdFraction = frac;
    e.originSeconds = 0.0;
    e.spanSeconds = span;
    return e;
}

static bool findNode(const std::vector<EnvVertex>& poly, EnvNode node, EnvVertex& out) {
    for (const EnvVertex& v : poly) {
        if (v.node == node) { out = v; return true; }
    }
    return false;
}

// The polyline's y where it crosses `x` — the trace as STROKED, not merely its samples, so a
// knot between two samples is still measured against the line the user sees.
static bool traceYAtX(const std::vector<StrokePoint>& t, double x, double& y) {
    for (std::size_t i = 1; i < t.size(); ++i) {
        const double x0 = t[i - 1].x, x1 = t[i].x;
        if (x1 == x0) {
            if (x == x0) { y = t[i].y; return true; }
            continue;
        }
        const double lo = x0 < x1 ? x0 : x1, hi = x0 < x1 ? x1 : x0;
        if (x < lo || x > hi) continue;
        const double u = (x - x0) / (x1 - x0);
        y = t[i - 1].y + (t[i].y - t[i - 1].y) * u;
        return true;
    }
    return false;
}

// The three sloped stages, each named by the knot that rides it and the two nodes it runs
// between — the same pairing gatePolyline/ahdPolyline place the knot from.
struct Stage {
    EnvNode knot;
    EnvNode from;
    EnvNode to;
};
static const Stage kStages[3] = {
    {EnvNode::AttackCurve,  EnvNode::Origin,       EnvNode::AttackEnd},
    {EnvNode::DecayCurve,   EnvNode::HoldEnd,      EnvNode::DecayEnd},
    {EnvNode::ReleaseCurve, EnvNode::ReleaseStart, EnvNode::ReleaseEnd},
};

static const double kExponents[7] = {util::kCurveMin, 0.25, 0.5, util::kCurveNeutral,
                                     2.0, 4.0, util::kCurveMax};

// ---------------------------------------------------------------------------------------

static void testSegmentCurve() {
    StageEnvelope e = ahdsr(1.0, 0.0, 1.0, 0.5, 1.0);
    e.attackCurve = 0.3;
    e.decayCurve = 3.0;
    e.releaseCurve = 7.0;
    CHECK(segmentCurve(e, EnvNode::AttackEnd) == e.attackCurve);
    CHECK(segmentCurve(e, EnvNode::DecayEnd) == e.decayCurve);
    CHECK(segmentCurve(e, EnvNode::ReleaseEnd) == e.releaseCurve);
    // The plateau-ending nodes carry no exponent of their own.
    CHECK(segmentCurve(e, EnvNode::HoldEnd) == util::kCurveNeutral);
    CHECK(segmentCurve(e, EnvNode::ReleaseStart) == util::kCurveNeutral);
    CHECK(segmentCurve(e, EnvNode::Origin) == util::kCurveNeutral);
}

// The regression guard: at the neutral exponent the trace IS the non-knot vertex list, one
// point per vertex, at the same integer coordinates — the straight stroke drawn before curves.
static void testNeutralIsTodaysStraightLine() {
    const Rect area = wideArea();
    const StageEnvelope envs[2] = {ahdsr(2.0, 0.0, 2.0, 0.4, 2.0), ahd(1.2, 1.6, 0.5, 4.0)};
    for (const StageEnvelope& env : envs) {
        const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
        std::vector<StrokePoint> trace;
        buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);
        std::size_t nodes = 0;
        for (const EnvVertex& v : poly) if (!v.knot) ++nodes;
        CHECK(trace.size() == nodes);
        std::size_t i = 0;
        for (const EnvVertex& v : poly) {
            if (v.knot) continue;
            CHECK(trace[i].x == static_cast<float>(v.x));
            CHECK(trace[i].y == static_cast<float>(v.y));
            ++i;
        }
    }
}

// Knots are handles, not line vertices. A knot sits mid-canvas but is appended AFTER the last
// node, so admitting one would break the trace's x ordering — which is what this catches.
static void testKnotsExcludedAndNodesPreserved() {
    const Rect area = wideArea();
    StageEnvelope env = ahdsr(2.0, 0.0, 2.0, 0.4, 2.0);
    env.attackCurve = 0.3;
    env.decayCurve = 4.0;
    env.releaseCurve = 0.4;
    const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
    std::size_t knots = 0;
    for (const EnvVertex& v : poly) if (v.knot) ++knots;
    CHECK(knots == 3);

    std::vector<StrokePoint> trace;
    buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);
    for (std::size_t i = 1; i < trace.size(); ++i) CHECK(trace[i].x >= trace[i - 1].x);

    // Every node vertex still appears at its exact integer position: the handles are drawn
    // there, and a trace that missed one would sit off its own handle.
    for (const EnvVertex& v : poly) {
        if (v.knot) continue;
        bool found = false;
        for (const StrokePoint& p : trace) {
            if (p.x == static_cast<float>(v.x) && p.y == static_cast<float>(v.y)) found = true;
        }
        CHECK(found);
    }
}

// THE GATE (plan acceptance criterion 1): at every exponent the knot's centre lies on the
// trace within 1 px. Swept over all three sloped stages of the AHDSR schematic.
static void testKnotLiesOnTraceAhdsr() {
    const Rect area = wideArea();
    for (const Stage& st : kStages) {
        for (double pw : kExponents) {
            StageEnvelope env = ahdsr(2.0, 0.0, 2.0, 0.4, 2.0);
            if (st.knot == EnvNode::AttackCurve) env.attackCurve = pw;
            else if (st.knot == EnvNode::DecayCurve) env.decayCurve = pw;
            else env.releaseCurve = pw;

            const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
            EnvVertex knot;
            CHECK(findNode(poly, st.knot, knot));
            std::vector<StrokePoint> trace;
            buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);
            double y = 0.0;
            CHECK(traceYAtX(trace, static_cast<double>(knot.x), y));
            CHECK(std::fabs(y - static_cast<double>(knot.y)) <= 1.0);
        }
    }
}

// The same gate on the AHD policy — its two sloped stages, its own 1:1 time axis.
static void testKnotLiesOnTraceAhd() {
    const Rect area = wideArea();
    for (int stage = 0; stage < 2; ++stage) {
        for (double pw : kExponents) {
            StageEnvelope env = ahd(1.2, 1.6, 0.5, 4.0);
            if (stage == 0) env.attackCurve = pw; else env.decayCurve = pw;
            const EnvNode knotNode = stage == 0 ? EnvNode::AttackCurve : EnvNode::DecayCurve;

            const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
            EnvVertex knot;
            CHECK(findNode(poly, knotNode, knot));
            std::vector<StrokePoint> trace;
            buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);
            double y = 0.0;
            CHECK(traceYAtX(trace, static_cast<double>(knot.x), y));
            CHECK(std::fabs(y - static_cast<double>(knot.y)) <= 1.0);
        }
    }
}

// The trace is the SAME law the audio evaluates: at a segment's midpoint its level is
// curve_law's curveMidLevel of that stage's exponent, composed onto the endpoints the trace
// itself returned.
static void testMidSegmentLevelMatchesTheLaw() {
    const Rect area = wideArea();
    for (const Stage& st : kStages) {
        for (double pw : kExponents) {
            StageEnvelope env = ahdsr(2.0, 0.0, 2.0, 0.4, 2.0);
            if (st.knot == EnvNode::AttackCurve) env.attackCurve = pw;
            else if (st.knot == EnvNode::DecayCurve) env.decayCurve = pw;
            else env.releaseCurve = pw;

            const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
            EnvVertex a, b;
            CHECK(findNode(poly, st.from, a));
            CHECK(findNode(poly, st.to, b));
            std::vector<StrokePoint> trace;
            buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);

            const double xm = 0.5 * (static_cast<double>(a.x) + static_cast<double>(b.x));
            double y = 0.0;
            CHECK(traceYAtX(trace, xm, y));
            const double want = static_cast<double>(a.y) +
                                (static_cast<double>(b.y) - static_cast<double>(a.y)) *
                                    util::curveMidLevel(pw);
            CHECK(std::fabs(y - want) <= 1.0);
        }
    }
}

// A curve must bend, and in the direction the exponent names: phi^p with p > 1 holds the level
// LOW for longer, p < 1 lifts it early. Stated against the chord between the two endpoints the
// trace returned, so it holds whichever way the segment slopes.
static void testCurvatureDirection() {
    const Rect area = wideArea();
    for (const Stage& st : kStages) {
        for (double pw : {0.25, 4.0}) {
            StageEnvelope env = ahdsr(2.0, 0.0, 2.0, 0.4, 2.0);
            if (st.knot == EnvNode::AttackCurve) env.attackCurve = pw;
            else if (st.knot == EnvNode::DecayCurve) env.decayCurve = pw;
            else env.releaseCurve = pw;

            const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
            EnvVertex a, b;
            CHECK(findNode(poly, st.from, a));
            CHECK(findNode(poly, st.to, b));
            std::vector<StrokePoint> trace;
            buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);

            const double xm = 0.5 * (static_cast<double>(a.x) + static_cast<double>(b.x));
            double y = 0.0;
            CHECK(traceYAtX(trace, xm, y));
            const double chord = 0.5 * (static_cast<double>(a.y) + static_cast<double>(b.y));
            // Normalized level at the midpoint, 0 at the start node, 1 at the end node.
            const double dy = static_cast<double>(b.y) - static_cast<double>(a.y);
            const double u = (y - static_cast<double>(a.y)) / dy;
            CHECK(std::fabs(y - chord) > 1.0);          // it actually left the straight line
            if (pw > util::kCurveNeutral) CHECK(u < 0.5);
            else CHECK(u > 0.5);
        }
    }
}

// curveMap maps 0->0 and 1->1 at every positive exponent, so no sample may pass either of its
// own segment's endpoint levels.
static void testNoOvershoot() {
    const Rect area = wideArea();
    for (const Stage& st : kStages) {
        for (double pw : kExponents) {
            StageEnvelope env = ahdsr(2.0, 0.0, 2.0, 0.4, 2.0);
            if (st.knot == EnvNode::AttackCurve) env.attackCurve = pw;
            else if (st.knot == EnvNode::DecayCurve) env.decayCurve = pw;
            else env.releaseCurve = pw;

            const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
            EnvVertex a, b;
            CHECK(findNode(poly, st.from, a));
            CHECK(findNode(poly, st.to, b));
            std::vector<StrokePoint> trace;
            buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);
            const double lo = a.y < b.y ? a.y : b.y;
            const double hi = a.y < b.y ? b.y : a.y;
            for (const StrokePoint& p : trace) {
                if (p.x < static_cast<float>(a.x) || p.x > static_cast<float>(b.x)) continue;
                CHECK(p.y >= static_cast<float>(lo) - 0.001f);
                CHECK(p.y <= static_cast<float>(hi) + 0.001f);
            }
        }
    }
}

// Density follows the canvas: one sample per pixel column, so a wider canvas gets proportionally
// more of them. A fixed count would fail the second half. Uses the AHD policy, whose x axis is
// the waveform's own and so is independent of the AHDSR schematic's scale.
static void testDensityFollowsWidth() {
    std::size_t counts[2] = {0, 0};
    const int widths[2] = {1000, 4000};
    for (int w = 0; w < 2; ++w) {
        const Rect area = Rect::ltrb(20, 10, 20 + widths[w], 210);
        StageEnvelope env = ahd(1.2, 1.6, 0.5, 4.0);
        env.attackCurve = 4.0;
        const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
        EnvVertex a, b;
        CHECK(findNode(poly, EnvNode::Origin, a));
        CHECK(findNode(poly, EnvNode::AttackEnd, b));
        std::vector<StrokePoint> trace;
        buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);

        std::size_t n = 0;
        float prevX = 0.0f;
        for (const StrokePoint& p : trace) {
            if (p.x < static_cast<float>(a.x) || p.x > static_cast<float>(b.x)) continue;
            if (n > 0) CHECK(p.x - prevX <= 1.0f);  // no gap wider than one column
            prevX = p.x;
            ++n;
        }
        CHECK(n > 1);
        counts[w] = n;
    }
    // 4x the canvas, ~4x the samples across the same stage.
    CHECK(counts[1] > 3 * counts[0]);
}

// A plateau is straight because its two endpoints share a level, so a stray exponent on the
// node that ends it must not curve it.
static void testFlatSegmentEmitsNoInterior() {
    const Rect area = wideArea();
    StageEnvelope env = ahdsr(2.0, 0.0, 0.0, 1.0, 2.0);  // sustain == 1: the decay span is flat
    env.attackCurve = util::kCurveNeutral;
    env.decayCurve = 5.0;
    env.releaseCurve = util::kCurveNeutral;
    const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);
    std::vector<StrokePoint> trace;
    buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);
    std::size_t nodes = 0;
    for (const EnvVertex& v : poly) if (!v.knot) ++nodes;
    CHECK(trace.size() == nodes);
}

static void testClampAndDegenerate() {
    const Rect area = wideArea();
    StageEnvelope env = ahdsr(2.0, 0.0, 2.0, 0.4, 2.0);
    env.attackCurve = 0.25;
    env.decayCurve = 4.0;
    const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, overlayOf(area), 4.0);

    // A narrower clamp than the vertices were built for: nothing escapes it.
    std::vector<StrokePoint> trace;
    const int lo = area.x + 100, hi = area.right() - 200;
    buildEnvelopeTrace(poly, env, lo, hi, trace);
    CHECK(!trace.empty());
    for (const StrokePoint& p : trace) {
        CHECK(p.x >= static_cast<float>(lo));
        CHECK(p.x <= static_cast<float>(hi));
    }

    // The out vector is REPLACED, not appended to — a stale trace from the previous paint
    // would otherwise stroke a line across the canvas.
    buildEnvelopeTrace({}, env, area.x, area.right() - 1, trace);
    CHECK(trace.empty());

    // A degenerate surface still yields the flat baseline the painter needs.
    const Rect flat = Rect::ltrb(20, 10, 20, 10);
    const std::vector<EnvVertex> degen = buildEnvelopePolyline(env, overlayOf(flat), 0.0);
    buildEnvelopeTrace(degen, env, flat.x, flat.x, trace);
    CHECK(trace.size() == degen.size());
}

int main() {
    testSegmentCurve();
    testNeutralIsTodaysStraightLine();
    testKnotsExcludedAndNodesPreserved();
    testKnotLiesOnTraceAhdsr();
    testKnotLiesOnTraceAhd();
    testMidSegmentLevelMatchesTheLaw();
    testCurvatureDirection();
    testNoOvershoot();
    testDensityFollowsWidth();
    testFlatSegmentEmitsNoInterior();
    testClampAndDegenerate();
    if (g_fail == 0) std::printf("curve_tessellate: all tests passed\n");
    return g_fail == 0 ? 0 : 1;
}
