// test_inversive_distance_functional.cpp
//
// Phase 9a.2 — Inversive-distance functional (Luo 2004) tests.
//
// Validation against three mathematical references:
//
//   [Luo 2004]        ℓ_ij² = exp(2u_i) + exp(2u_j) + 2 I_ij exp(u_i+u_j)
//                     ∂E/∂u_v = Θ_v − Σ α_v   (Lemma 3.1)
//
//   [BS 2004]         I_ij = (ℓ² − r_i² − r_j²) / (2 r_i r_j)
//                     I = 1 ⇒ tangential circles
//                     I = 0 ⇒ orthogonal circles
//
//   [Glickenstein 2011 §5]
//                     correspondence to BPS-2010 face-based CP:
//                     I_ij = cos θ_e on the face-dual mesh
//
// No Java reference exists for this functional in
// de.varylab.discreteconformal.  Cross-validation is done via:
//   1. FD-vs-analytic gradient check (numerical),
//   2. Luo's edge-length identity check (mathematical),
//   3. Tangential-limit identity I=1 ⇒ ℓ = r_i+r_j (geometric).

#include "inversive_distance_functional.hpp"
#include "euclidean_functional.hpp"
#include "mesh_builder.hpp"
#include "conformal_mesh.hpp"

#include <gtest/gtest.h>
#include <vector>
#include <random>
#include <cmath>

using namespace conformallab;

// ════════════════════════════════════════════════════════════════════════════
// 1.  Edge-length formula  (Luo 2004 §3)
//
//   ℓ² = exp(2u_i) + exp(2u_j) + 2 I exp(u_i+u_j)
//      = r_i² + r_j² + 2 I r_i r_j
//
// Special cases:
//   I = 1  ⇒ ℓ² = (r_i + r_j)²  ⇒  ℓ = r_i + r_j     (tangential)
//   I = 0  ⇒ ℓ² = r_i² + r_j²    (orthogonal — circles meet at 90°)
//   I = −1 ⇒ ℓ² = (r_i − r_j)²  ⇒  ℓ = |r_i − r_j|  (inside-tangent)
// ════════════════════════════════════════════════════════════════════════════

TEST(InversiveDistanceFunctional, EdgeLengthFormula_TangentialLimit)
{
    // ui = 0 ⇒ ri = 1;  uj = log(2) ⇒ rj = 2;  I = 1 (tangential):
    //   ℓ² = 1 + 4 + 2·1·1·2 = 9 ⇒ ℓ = 3 = r_i + r_j ✓
    double l2 = id_detail::edge_length_squared(0.0, std::log(2.0), 1.0);
    EXPECT_NEAR(std::sqrt(l2), 3.0, 1e-12);
}

TEST(InversiveDistanceFunctional, EdgeLengthFormula_OrthogonalLimit)
{
    // r_i = 3, r_j = 4, I = 0:  ℓ² = 9 + 16 = 25 ⇒ ℓ = 5  (Pythagorean)
    double l2 = id_detail::edge_length_squared(std::log(3.0), std::log(4.0), 0.0);
    EXPECT_NEAR(std::sqrt(l2), 5.0, 1e-12);
}

TEST(InversiveDistanceFunctional, EdgeLengthFormula_InsideTangentLimit)
{
    // r_i = 2, r_j = 5, I = −1:  ℓ² = (5 − 2)² = 9 ⇒ ℓ = 3
    double l2 = id_detail::edge_length_squared(std::log(2.0), std::log(5.0), -1.0);
    EXPECT_NEAR(std::sqrt(l2), 3.0, 1e-12);
}

TEST(InversiveDistanceFunctional, EdgeLengthFormula_DegenerateReturnsMinusOne)
{
    // r_i = r_j = 1, I = −2:  ℓ² = 1 + 1 − 4 = −2  (impossible packing)
    double l2 = id_detail::edge_length_squared(0.0, 0.0, -2.0);
    EXPECT_EQ(l2, -1.0) << "should signal degenerate packing";
}

// ════════════════════════════════════════════════════════════════════════════
// 2.  Bowers-Stephenson identity round-trip
//
//   Given (ℓ, r_i, r_j), the I_ij that compute_init produces must satisfy
//   Luo's edge-length formula exactly: ℓ²(I_ij, r_i, r_j) = ℓ².
// ════════════════════════════════════════════════════════════════════════════

TEST(InversiveDistanceFunctional, BowersStephensonRoundTrip)
{
    auto mesh = make_triangle();   // (0,0,0)-(1,0,0)-(0,1,0)
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    // At u = 0, exp(u) = r0.  Reconstruct ℓ from (r_i, r_j, I_ij) and compare
    // to the 3-D Euclidean edge length from the mesh.
    for (auto e : mesh.edges()) {
        auto h = mesh.halfedge(e);
        auto p1 = mesh.point(mesh.source(h));
        auto p2 = mesh.point(mesh.target(h));
        double dx = p1.x() - p2.x();
        double dy = p1.y() - p2.y();
        double dz = p1.z() - p2.z();
        double l_3d = std::sqrt(dx*dx + dy*dy + dz*dz);

        double ri = m.r0[mesh.source(h)];
        double rj = m.r0[mesh.target(h)];
        double l2_reconstructed = ri*ri + rj*rj + 2.0 * m.I_e[e] * ri * rj;
        EXPECT_NEAR(std::sqrt(l2_reconstructed), l_3d, 1e-12)
            << "Bowers-Stephenson round-trip failed for an edge";
    }
}

// ════════════════════════════════════════════════════════════════════════════
// 3.  Properties of the init step
// ════════════════════════════════════════════════════════════════════════════

TEST(InversiveDistanceFunctional, InitProducesValidPositiveRadii)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    for (auto v : mesh.vertices()) {
        EXPECT_GT(m.r0[v], 0.0) << "init radius must be positive";
        EXPECT_TRUE(std::isfinite(m.r0[v]));
    }
    for (auto e : mesh.edges()) {
        EXPECT_TRUE(std::isfinite(m.I_e[e]));
        // I > −1 is required for any valid inversive-distance packing.
        EXPECT_GT(m.I_e[e], -1.0);
    }
}

// ════════════════════════════════════════════════════════════════════════════
// 4.  Gradient at the "natural equilibrium" is zero by construction
//
// Same trick as in test_euclidean_functional.cpp:
//   • Set u = 0  ⇒  r = r0  ⇒  ℓ = ℓ_3d  (Bowers-Stephenson round-trip)
//   • Compute G(0) — that's the angle defect Θ − Σ_actual.
//   • Subtract G(0) from Θ → new G(0) is zero.
// This means u = 0 is now the Newton equilibrium of the functional, just
// like in the euclidean functional natural-theta trick.
// ════════════════════════════════════════════════════════════════════════════

TEST(InversiveDistanceFunctional, NaturalThetaGivesZeroGradientAtU0)
{
    auto mesh = make_triangle();
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    // Assign DOFs to all vertices.
    int n = 0;
    for (auto v : mesh.vertices()) m.v_idx[v] = n++;
    std::vector<double> x(static_cast<std::size_t>(n), 0.0);

    auto G0 = inversive_distance_gradient(mesh, x, m);
    for (auto v : mesh.vertices()) {
        int i = m.v_idx[v];
        m.theta_v[v] -= G0[static_cast<std::size_t>(i)];
    }

    auto G_eq = inversive_distance_gradient(mesh, x, m);
    for (double g : G_eq) EXPECT_NEAR(g, 0.0, 1e-13);
}

// ════════════════════════════════════════════════════════════════════════════
// 5.  FD-vs-analytic gradient check (the main acceptance test for the port)
//
// Pattern: identical to test_euclidean_functional.cpp's
// GradientCheck_TriangleVertex (lines 137-149).  The energy is the path
// integral of the gradient (by construction); a consistent FD-vs-analytic
// match validates both energy and gradient implementations together.
// ════════════════════════════════════════════════════════════════════════════

TEST(InversiveDistanceFunctional, FDGradientCheck_Triangle)
{
    auto mesh = make_triangle();
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    int n = 0;
    for (auto v : mesh.vertices()) m.v_idx[v] = n++;

    // Small perturbation u_v ≈ −0.1 keeps every triangle valid.
    std::vector<double> x(static_cast<std::size_t>(n), -0.1);
    EXPECT_TRUE(gradient_check_inversive_distance(mesh, x, m))
        << "FD gradient mismatch on single triangle (u = −0.1)";
}

TEST(InversiveDistanceFunctional, FDGradientCheck_QuadStrip)
{
    auto mesh = make_quad_strip();
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    int n = 0;
    for (auto v : mesh.vertices()) m.v_idx[v] = n++;

    std::vector<double> x(static_cast<std::size_t>(n), -0.15);
    EXPECT_TRUE(gradient_check_inversive_distance(mesh, x, m))
        << "FD gradient mismatch on quad strip";
}

TEST(InversiveDistanceFunctional, FDGradientCheck_Tetrahedron)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    int n = 0;
    for (auto v : mesh.vertices()) m.v_idx[v] = n++;

    std::vector<double> x(static_cast<std::size_t>(n), -0.2);
    EXPECT_TRUE(gradient_check_inversive_distance(mesh, x, m))
        << "FD gradient mismatch on regular tetrahedron";
}

// ════════════════════════════════════════════════════════════════════════════
// 6.  Cross-validation with euclidean_functional.hpp
//
// The two functionals are DIFFERENT geometric models.  At u = 0 with their
// natural inits both produce a valid triangulation, but the per-edge length
// is different:
//   • Euclidean:           ℓ = ℓ_3d                  (exact, by lambda0 init)
//   • Inversive distance:  ℓ = ℓ_3d                  (exact, by BS round-trip)
//
// HOWEVER the GRADIENT at u = 0 differs because the chain rule ∂ℓ/∂u is
// different.  Specifically:
//   • Euclidean:           ∂(2 log ℓ)/∂u_i = 1
//   • Inversive distance:  ∂(2 log ℓ)/∂u_i = (r_i² + I r_i r_j) / ℓ²
//
// This test pins one quantitative consequence: at u = 0 both gradients have
// the SAME angle-defect structure Θ − Σ_actual.  After applying the natural-
// theta trick on each, both must be at equilibrium with G(0) = 0.
// ════════════════════════════════════════════════════════════════════════════

TEST(InversiveDistanceFunctional, AngleDefectAtU0_AgreesWithEuclideanAtU0)
{
    auto mesh = make_quad_strip();

    // ── Inversive distance side ────────────────────────────────────────────
    auto m_id = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m_id);
    int n_id = 0;
    for (auto v : mesh.vertices()) m_id.v_idx[v] = n_id++;
    std::vector<double> x_id(static_cast<std::size_t>(n_id), 0.0);
    auto G_id = inversive_distance_gradient(mesh, x_id, m_id);

    // ── Euclidean side (same mesh, same DOF order) ─────────────────────────
    auto m_eu = setup_euclidean_maps(mesh);
    compute_euclidean_lambda0_from_mesh(mesh, m_eu);
    int n_eu = 0;
    for (auto v : mesh.vertices()) m_eu.v_idx[v] = n_eu++;
    std::vector<double> x_eu(static_cast<std::size_t>(n_eu), 0.0);
    auto G_eu = euclidean_gradient(const_cast<ConformalMesh&>(mesh), x_eu, m_eu);

    // Both should report the same actual angle sum per vertex at u = 0
    // (since both reproduce ℓ = ℓ_3d at u = 0).  Therefore  Θ − Σ_actual
    // is identical for the two functionals (Θ default 2π in both).
    ASSERT_EQ(G_id.size(), G_eu.size());
    for (std::size_t i = 0; i < G_id.size(); ++i) {
        EXPECT_NEAR(G_id[i], G_eu[i], 1e-10)
            << "angle-defect mismatch at u=0, DOF " << i
            << ": id=" << G_id[i] << "  eu=" << G_eu[i];
    }
}
