// Unit tests for fn::tween (cpp/functions/core/tween_curves.{h,cpp}).
//
// Cubre boundaries (t=0, t=1), valor central (t=0.5) y monotonicidad para las
// curvas que deberian ser monotonas (linear, *_quad, *_cubic, *_expo, bounce
// in/out, elastic in/out NO son monotonas — overshoot intencional).

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "core/tween_curves.h"

#include <cmath>

using fn::tween::Ease;

namespace {

constexpr float kEps = 1e-5f;

bool is_monotonic_increasing(Ease e, int samples = 64) {
    float prev = fn::tween::apply(e, 0.0f);
    for (int i = 1; i <= samples; ++i) {
        float t = static_cast<float>(i) / samples;
        float v = fn::tween::apply(e, t);
        if (v < prev - kEps) return false;
        prev = v;
    }
    return true;
}

} // namespace

TEST_CASE("tween: linear is identity", "[tween]") {
    REQUIRE(fn::tween::linear(0.0f) == Catch::Approx(0.0f));
    REQUIRE(fn::tween::linear(0.5f) == Catch::Approx(0.5f));
    REQUIRE(fn::tween::linear(1.0f) == Catch::Approx(1.0f));
}

TEST_CASE("tween: quadratic boundary conditions", "[tween]") {
    REQUIRE(fn::tween::in_quad(0.0f)    == Catch::Approx(0.0f));
    REQUIRE(fn::tween::in_quad(1.0f)    == Catch::Approx(1.0f));
    REQUIRE(fn::tween::out_quad(0.0f)   == Catch::Approx(0.0f));
    REQUIRE(fn::tween::out_quad(1.0f)   == Catch::Approx(1.0f));
    REQUIRE(fn::tween::in_out_quad(0.0f) == Catch::Approx(0.0f));
    REQUIRE(fn::tween::in_out_quad(1.0f) == Catch::Approx(1.0f));
    REQUIRE(fn::tween::in_out_quad(0.5f) == Catch::Approx(0.5f));
}

TEST_CASE("tween: cubic boundary conditions", "[tween]") {
    REQUIRE(fn::tween::in_cubic(0.0f)    == Catch::Approx(0.0f));
    REQUIRE(fn::tween::in_cubic(1.0f)    == Catch::Approx(1.0f));
    REQUIRE(fn::tween::out_cubic(0.0f)   == Catch::Approx(0.0f));
    REQUIRE(fn::tween::out_cubic(1.0f)   == Catch::Approx(1.0f));
    REQUIRE(fn::tween::in_out_cubic(0.0f) == Catch::Approx(0.0f));
    REQUIRE(fn::tween::in_out_cubic(1.0f) == Catch::Approx(1.0f));
    REQUIRE(fn::tween::in_out_cubic(0.5f) == Catch::Approx(0.5f));
}

TEST_CASE("tween: exponential boundary conditions", "[tween]") {
    REQUIRE(fn::tween::in_expo(0.0f)  == Catch::Approx(0.0f));
    REQUIRE(fn::tween::in_expo(1.0f)  == Catch::Approx(1.0f));
    REQUIRE(fn::tween::out_expo(0.0f) == Catch::Approx(0.0f));
    REQUIRE(fn::tween::out_expo(1.0f) == Catch::Approx(1.0f));
    REQUIRE(fn::tween::in_out_expo(0.0f) == Catch::Approx(0.0f));
    REQUIRE(fn::tween::in_out_expo(1.0f) == Catch::Approx(1.0f));
}

TEST_CASE("tween: in_quad < out_quad in mid-range", "[tween]") {
    // Convexity: in_quad starts slow, out_quad starts fast.
    REQUIRE(fn::tween::in_quad(0.5f) < fn::tween::out_quad(0.5f));
    REQUIRE(fn::tween::in_cubic(0.3f) < fn::tween::out_cubic(0.3f));
}

TEST_CASE("tween: monotonic curves are monotonic", "[tween][monotonic]") {
    // Curvas monotonicamente crecientes en [0,1].
    REQUIRE(is_monotonic_increasing(Ease::Linear));
    REQUIRE(is_monotonic_increasing(Ease::InQuad));
    REQUIRE(is_monotonic_increasing(Ease::OutQuad));
    REQUIRE(is_monotonic_increasing(Ease::InOutQuad));
    REQUIRE(is_monotonic_increasing(Ease::InCubic));
    REQUIRE(is_monotonic_increasing(Ease::OutCubic));
    REQUIRE(is_monotonic_increasing(Ease::InOutCubic));
    REQUIRE(is_monotonic_increasing(Ease::InExpo));
    REQUIRE(is_monotonic_increasing(Ease::OutExpo));
    REQUIRE(is_monotonic_increasing(Ease::InOutExpo));
}

TEST_CASE("tween: elastic and bounce hit endpoints exactly", "[tween]") {
    // Elastic / bounce overshoot pero deben pasar por (0,0) y (1,1).
    for (Ease e : {Ease::InElastic, Ease::OutElastic, Ease::InOutElastic,
                   Ease::InBounce, Ease::OutBounce, Ease::InOutBounce}) {
        INFO("ease: " << fn::tween::name(e));
        REQUIRE(fn::tween::apply(e, 0.0f) == Catch::Approx(0.0f).margin(1e-4f));
        REQUIRE(fn::tween::apply(e, 1.0f) == Catch::Approx(1.0f).margin(1e-4f));
    }
}

TEST_CASE("tween: apply dispatch matches direct calls", "[tween]") {
    REQUIRE(fn::tween::apply(Ease::Linear, 0.42f)   == Catch::Approx(fn::tween::linear(0.42f)));
    REQUIRE(fn::tween::apply(Ease::InCubic, 0.3f)   == Catch::Approx(fn::tween::in_cubic(0.3f)));
    REQUIRE(fn::tween::apply(Ease::OutBounce, 0.7f) == Catch::Approx(fn::tween::out_bounce(0.7f)));
}

TEST_CASE("tween: name() returns non-null for every Ease", "[tween]") {
    for (int i = 0; i < fn::tween::ease_count; ++i) {
        const char* n = fn::tween::name(static_cast<Ease>(i));
        REQUIRE(n != nullptr);
        REQUIRE(std::string(n).size() > 0);
    }
}
