// Host regression for the pure touch reducer and the raw->display transform.
#include "touch_gesture.h"
#include "touch_transform.h"
#include <cassert>
#include <cstdio>
#include <vector>
using namespace wearable;

namespace {
constexpr uint16_t W = 468, H = 466;
constexpr uint16_t RIGHT = 400, LEFT = 40;
constexpr uint16_t MID_Y = 233;

// Drive a contact as a list of (x, y) samples at a fixed cadence.
struct Runner {
    TouchGestureReducer reducer;
    uint64_t now = 1000;
    std::vector<TouchGesture> events;

    void configure(const TouchThresholds& t) { reducer.configure(t); }
    void step(uint16_t x, uint16_t y, uint64_t delta = 25) {
        now += delta;
        const auto g = reducer.update(true, x, y, now);
        if (g != TouchGesture::None) events.push_back(g);
    }
    // Press long enough for the debounce to confirm Down at (x, y).
    void hold(uint16_t x, uint16_t y) { step(x, y, 0); step(x, y, 25); step(x, y, 25); }
    void up() {
        now += 25;
        const auto g = reducer.update(false, 0, 0, now);
        if (g != TouchGesture::None) events.push_back(g);
        reducer.reset();
    }
    bool has(TouchGesture g) const {
        for (auto e : events) if (e == g) return true;
        return false;
    }
    size_t count(TouchGesture g) const {
        size_t n = 0;
        for (auto e : events) if (e == g) ++n;
        return n;
    }
};

void test_tap() {
    Runner r;
    r.step(LEFT, MID_Y, 0);        // raw press; press_min not yet elapsed
    r.step(LEFT, MID_Y, 10);       // still debouncing
    r.step(LEFT, MID_Y, 25);       // still debouncing
    r.step(LEFT, MID_Y, 25);       // Down
    r.step(LEFT, MID_Y + 5);       // small movement inside tap slop
    r.up();
    assert(r.count(TouchGesture::Tap) == 1);
    assert(!r.has(TouchGesture::SwipeLeft) && !r.has(TouchGesture::Long));
    std::puts("tap inside slop resolves exactly one Tap PASS");
}

void test_tap_boundaries() {
    // At the tap slop edge: still a tap.
    {
        Runner r;
        r.hold(LEFT, MID_Y);
        r.step(LEFT + 18, MID_Y, 25);
        r.up();
        assert(r.count(TouchGesture::Tap) == 1);
    }
    // Just beyond the slop but below swipe distance: neither a Tap nor a Swipe,
    // because the contact is no longer a stationary tap.
    {
        Runner r;
        r.hold(LEFT, MID_Y);
        r.step(LEFT + 30, MID_Y, 25);
        r.up();
        assert(r.count(TouchGesture::Tap) == 0);
        assert(!r.has(TouchGesture::SwipeRight));
    }
    std::puts("tap slop boundary 18px is inclusive PASS");
}

void test_swipes() {
    struct Case { int dx, dy; TouchGesture expected; };
    const Case cases[] = {
        {80, 0, TouchGesture::SwipeRight}, {0, 80, TouchGesture::SwipeDown},
        {-80, 0, TouchGesture::SwipeLeft}, {0, -80, TouchGesture::SwipeUp},
    };
    for (const auto& c : cases) {
        Runner r;
        const uint16_t x0 = MID_Y, y0 = MID_Y;
        r.hold(x0, y0);
        // Move in steps so the resolver sees intermediate samples.
        for (int i = 1; i <= 4; ++i) r.step(uint16_t(int(x0) + c.dx * i / 4), uint16_t(int(y0) + c.dy * i / 4));
        r.up();
        assert(r.count(c.expected) == 1);
        assert(!r.has(TouchGesture::Tap));
    }
    std::puts("swipe in four directions resolves the dominant axis PASS");
}

void test_swipe_ratio() {
    // A dominant diagonal is a swipe; an ambiguous diagonal is not.
    {
        Runner r;
        r.hold(LEFT, MID_Y);
        r.step(LEFT + 90, MID_Y + 30, 25);
        r.up();
        assert(r.count(TouchGesture::SwipeRight) == 1);
    }
    {
        Runner r;
        r.hold(LEFT, MID_Y);
        r.step(LEFT + 60, MID_Y + 60, 25);
        r.up();
        assert(!r.has(TouchGesture::SwipeRight) && !r.has(TouchGesture::SwipeDown));
    }
    std::puts("swipe requires >=1.5x dominant axis PASS");
}

void test_long_and_release() {
    Runner r;
    r.hold(LEFT, MID_Y);
    const auto start = r.now;
    while (r.now - start < 600) r.step(LEFT, MID_Y, 25);   // hold inside long slop
    r.step(LEFT, MID_Y, 25);
    assert(r.count(TouchGesture::Long) == 1);
    r.up();
    assert(r.count(TouchGesture::LongRelease) == 1);
    assert(!r.has(TouchGesture::Tap));
    std::puts("long press resolves Long then LongRelease PASS");
}

void test_movement_cancels_long() {
    Runner r;
    r.hold(LEFT, MID_Y);
    const auto start = r.now;
    // Drift beyond the long slop but below swipe distance, for the whole window.
    while (r.now - start < 700) r.step(LEFT + 40, MID_Y, 25);
    r.up();
    assert(!r.has(TouchGesture::Long));
    assert(!r.has(TouchGesture::Tap));         // a drifting contact is not a tap
    assert(!r.has(TouchGesture::SwipeRight));  // and never reached swipe distance
    std::puts("movement cancels Long and never yields Tap PASS");
}

void test_noise_ignored() {
    Runner r;
    r.step(LEFT, MID_Y, 0);
    r.up();                      // shorter than press_min_ms: never a contact
    assert(r.events.empty());
    // A longer hold after the noise still behaves normally.
    r.step(LEFT, MID_Y, 0); r.step(LEFT, MID_Y, 25); r.step(LEFT, MID_Y, 25);
    r.up();
    assert(r.count(TouchGesture::Tap) == 1);
    std::puts("sub-debounce contact is ignored PASS");
}

void test_single_primary_gesture() {
    // Long then release must never also produce Tap or Swipe.
    Runner r;
    r.hold(LEFT, MID_Y);
    const auto start = r.now;
    while (r.now - start < 700) r.step(LEFT, MID_Y, 25);
    r.up();
    assert(r.count(TouchGesture::Long) == 1);
    assert(!r.has(TouchGesture::Tap) && !r.has(TouchGesture::SwipeLeft));
    // Swipe then release must never produce Tap.
    Runner s;
    s.hold(LEFT, MID_Y);
    s.step(LEFT + 90, MID_Y, 25);
    s.up();
    assert(s.count(TouchGesture::SwipeRight) == 1);
    assert(!s.has(TouchGesture::Tap) && !s.has(TouchGesture::Long));
    std::puts("one primary gesture per contact PASS");
}

void test_side_split() {
    Runner r;
    r.hold(LEFT, MID_Y);
    assert(r.reducer.side() == TouchSide::Left);
    r.up();
    Runner r2;
    r2.hold(RIGHT, MID_Y);
    assert(r2.reducer.side() == TouchSide::Right);
    // Exactly at the split boundary the right half owns the contact.
    Runner r3;
    r3.hold(234, MID_Y);
    assert(r3.reducer.side() == TouchSide::Right);
    std::puts("left/right half splits into virtual B/A PASS");
}

void test_transform() {
    TouchTransform t;   // 0..233 raw -> 468x466 display
    uint16_t x = 0, y = 0;
    t.apply(0, 0, x, y);
    assert(x == 0 && y == 0);
    t.apply(233, 233, x, y);
    assert(x >= 460 && x <= 467 && y >= 460 && y <= 465);
    t.apply(116, 116, x, y);
    assert(x > 200 && x < 270 && y > 200 && y < 270);
    // Mirroring and swap are centralized here, not in the UI.
    TouchTransform mirrored;
    mirrored.mirror_x = true;
    mirrored.apply(0, 0, x, y);
    assert(x > 400);
    TouchTransform swapped;
    swapped.swap_xy = true;
    swapped.apply(233, 0, x, y);
    assert(y > 400 && x < 40);
    // Out-of-range raw samples are clamped, never wrapped.
    t.apply(9999, 9999, x, y);
    assert(x <= 467 && y <= 465);
    std::puts("TouchTransform maps, mirrors and clamps in one place PASS");
}

} // namespace

int main() {
    test_tap();
    test_tap_boundaries();
    test_swipes();
    test_swipe_ratio();
    test_long_and_release();
    test_movement_cancels_long();
    test_noise_ignored();
    test_single_primary_gesture();
    test_side_split();
    test_transform();
    std::puts("Raw TouchGesture reducer / transform PASS (host)");
}