// Copyright (c) 2024-2026 Tarik Moussa.
// SPDX-License-Identifier: MIT

// test_pn_geometry.cpp
//
// Verifies pn_geometry.hpp against:
//   (a) closed-form analytic golden values, and
//   (b) the already-verified projective_math.hpp::hyperbolicDistance
//       (regression anchor for the HYPERBOLIC sign convention).
//
// Mirrored Java source: de.jreality.math.Pn

#include "pn_geometry.hpp"
#include "projective_math.hpp"
#include <gtest/gtest.h>
#include <Eigen/Core>
#include <cmath>

using namespace conformallab;

namespace {
Eigen::VectorXd V(std::initializer_list<double> xs) {
    Eigen::VectorXd v(static_cast<int>(xs.size()));
    int i = 0;
    for (double x : xs) v(i++) = x;
    return v;
}
constexpr double PN_PI = 3.14159265358979323846;
} // namespace

// ── Inner product per signature ──────────────────────────────────────────────
TEST(PnGeometry, InnerProductSignatures)
{
    auto u = V({1.0, 2.0, 3.0});   // spatial=(1,2), last=3
    auto v = V({4.0, 5.0, 6.0});   // spatial=(4,5), last=6
    // spat = 1·4 + 2·5 = 14,  last = 3·6 = 18
    EXPECT_NEAR(pn_inner_product(u, v, PN_EUCLIDEAN),   14.0,      1e-12);
    EXPECT_NEAR(pn_inner_product(u, v, PN_ELLIPTIC),    14.0+18.0, 1e-12);
    EXPECT_NEAR(pn_inner_product(u, v, PN_HYPERBOLIC),  18.0-14.0, 1e-12);
}

// ── Euclidean distance ────────────────────────────────────────────────────────
TEST(PnGeometry, EuclideanDistance)
{
    // 3-4-5 right triangle in the plane (homogeneous w=1).
    auto p  = V({0.0, 0.0, 1.0});
    auto q  = V({3.0, 4.0, 1.0});
    EXPECT_NEAR(pn_distance_between(p, q, PN_EUCLIDEAN), 5.0, 1e-12);

    // Homogeneous scaling must not change the distance.
    auto q2 = V({6.0, 8.0, 2.0});  // same point as q after dehomogenize
    EXPECT_NEAR(pn_distance_between(p, q2, PN_EUCLIDEAN), 5.0, 1e-12);
}

// ── Elliptic distance = spherical angle ───────────────────────────────────────
TEST(PnGeometry, EllipticDistanceIsAngle)
{
    auto e1 = V({1.0, 0.0, 0.0});
    auto e2 = V({0.0, 1.0, 0.0});
    auto d  = V({1.0, 1.0, 0.0});   // 45° between e1 and e2

    EXPECT_NEAR(pn_distance_between(e1, e2, PN_ELLIPTIC), PN_PI / 2.0, 1e-12);
    EXPECT_NEAR(pn_distance_between(e1, d,  PN_ELLIPTIC), PN_PI / 4.0, 1e-12);
    EXPECT_NEAR(pn_distance_between(e1, e1, PN_ELLIPTIC), 0.0,      1e-12);
}

// ── Hyperbolic distance: closed form + projective_math.hpp anchor ─────────────
TEST(PnGeometry, HyperbolicDistanceClosedFormAndAnchor)
{
    // Upper hyperboloid: p = apex (0,0,1); q = (sinh r, 0, cosh r) at distance r.
    const double r = 0.873;
    auto p  = V({0.0, 0.0, 1.0});
    auto q  = V({std::sinh(r), 0.0, std::cosh(r)});

    EXPECT_NEAR(pn_distance_between(p, q, PN_HYPERBOLIC), r, 1e-10);

    // Regression anchor: identical to projective_math.hpp::hyperbolicDistance.
    EXPECT_NEAR(pn_distance_between(p, q, PN_HYPERBOLIC),
                hyperbolicDistance(p, q), 1e-12);

    // Scale-invariance: homogeneous rescaling must not change distance.
    auto q2 = (2.5 * q).eval();
    EXPECT_NEAR(pn_distance_between(p, q2, PN_HYPERBOLIC), r, 1e-10);
}

// ── Norm / setToLength / normalize ───────────────────────────────────────────
TEST(PnGeometry, NormAndScaling)
{
    auto p = V({3.0, 4.0, 0.0});
    EXPECT_NEAR(pn_norm(p, PN_ELLIPTIC), 5.0, 1e-12);

    auto s = pn_set_to_length(p, 2.0, PN_ELLIPTIC);
    EXPECT_NEAR(pn_norm(s, PN_ELLIPTIC), 2.0, 1e-12);

    auto u = pn_normalize(p, PN_ELLIPTIC);
    EXPECT_NEAR(pn_norm(u, PN_ELLIPTIC), 1.0, 1e-12);

    // A unit hyperboloid sheet point already has hyperbolic norm 1.
    auto h = V({std::sinh(0.6), 0.0, std::cosh(0.6)});
    EXPECT_NEAR(pn_norm(h, PN_HYPERBOLIC), 1.0, 1e-12);
}

// ── Geodesic interpolation ────────────────────────────────────────────────────
TEST(PnGeometry, LinearInterpolationGeodesic)
{
    // EUCLIDEAN: affine midpoint.
    {
        auto p = V({0.0, 0.0, 1.0});
        auto q = V({4.0, 0.0, 1.0});
        auto m = pn_linear_interpolation(p, q, 0.5, PN_EUCLIDEAN);
        EXPECT_NEAR(m(0), 2.0, 1e-12);
    }

    // ELLIPTIC: midpoint of e1,e2 is at half the π/2 arc = π/4 from each.
    {
        auto e1 = V({1.0, 0.0, 0.0});
        auto e2 = V({0.0, 1.0, 0.0});
        auto m  = pn_linear_interpolation(e1, e2, 0.5, PN_ELLIPTIC);
        EXPECT_NEAR(pn_distance_between(e1, m, PN_ELLIPTIC), PN_PI / 4.0, 1e-10);
        EXPECT_NEAR(pn_distance_between(e2, m, PN_ELLIPTIC), PN_PI / 4.0, 1e-10);
        // Endpoint recovery at t=0.
        auto m0 = pn_linear_interpolation(e1, e2, 0.0, PN_ELLIPTIC);
        EXPECT_NEAR(pn_distance_between(e1, m0, PN_ELLIPTIC), 0.0, 1e-10);
    }

    // HYPERBOLIC: midpoint is at exactly half the geodesic distance from each end.
    {
        const double r = 1.2;
        auto p = V({0.0, 0.0, 1.0});
        auto q = V({std::sinh(r), 0.0, std::cosh(r)});
        auto m = pn_linear_interpolation(p, q, 0.5, PN_HYPERBOLIC);
        EXPECT_NEAR(pn_distance_between(p, m, PN_HYPERBOLIC), r / 2.0, 1e-9);
        EXPECT_NEAR(pn_distance_between(q, m, PN_HYPERBOLIC), r / 2.0, 1e-9);
    }
}
