// test_spherical_functional.cpp  (Phase 3c + 3e)
//
// Phase 3c — SphericalFunctional ported to ConformalMesh.
//
// Corresponds to de.varylab.discreteconformal.functional.SphericalFunctionalTest.
//
// Test map (Java → C++)
// ──────────────────────
//   testHessian (Ignored)         → GradientCheck_Hessian     (ported)
//   testGradientWithHyperIdeal…   → GradientCheck_OctaFaceVertex   (ported)
//   testGradientInExtendedDomain  → GradientCheck_SpherTetVertex    (ported)
//   testGradientWithHyperelliptic → GradientCheck_SpherTetAllDofs   (ported)
//   testFunctionalAtNaNValue      → AnglesFiniteAtKnownPoint         (ported)
//
// Energy model
// ────────────
//   The energy is computed as the Schläfli path integral E(x) = ∫₀¹⟨G(tx),x⟩dt
//   using 10-point Gauss-Legendre quadrature. The gradient check therefore
//   verifies that G is curl-free (the integrability / exactness condition of
//   the spherical discrete conformal functional). This is equivalent to the
//   Java FunctionalTest gradient check.

#include "conformal_mesh.hpp"
#include "mesh_builder.hpp"
#include "spherical_functional.hpp"
#include "spherical_hessian.hpp"
#include <gtest/gtest.h>
#include <cmath>
#include <vector>

using namespace conformallab;

// ════════════════════════════════════════════════════════════════════════════
// Cross-module Hessian check: spherical_gradient() ↔ spherical_hessian()
//
// Java @Ignore reason: "no Hessian implemented" — the Java functional test
// was written before the Hessian existed.  In C++ the analytic spherical
// Hessian (spherical_hessian.hpp, Phase 3f) is complete.
//
// This test verifies cross-module consistency between the functional and
// the Hessian module.  The spherical Hessian is NSD (negative semi-definite)
// because the spherical energy is concave — hessian_check_spherical() uses
// the sign-corrected FD check appropriate for the spherical case.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, GradientCheck_Hessian)
{
    auto mesh = make_spherical_tetrahedron();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_vertex_dof_indices(mesh, maps);

    std::vector<double> x(static_cast<std::size_t>(n), -0.2);

    EXPECT_TRUE(hessian_check_spherical(mesh, x, maps))
        << "Cross-module: spherical_gradient() and spherical_hessian() are inconsistent";
}

// ════════════════════════════════════════════════════════════════════════════
// Angle formula: octahedron-face triangle has all angles = π/2
//
// The triangle (1,0,0)–(0,1,0)–(0,0,1) has l_ij = π/2 for all edges.
// Half-angle formula: s = 3π/4, s_ij = π/4 for all three.
// All angles = π/2 (right-angled spherical triangle).
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, OctaFaceAnglesAreRightAngles)
{
    // l_ij = π/2 for all edges (octahedron face on unit sphere)
    const double l = PI_SPHER / 2.0;
    auto fa = spherical_angles(l, l, l);

    ASSERT_TRUE(fa.valid) << "Equilateral spherical triangle must be valid";
    EXPECT_NEAR(PI_SPHER / 2.0, fa.alpha1, 1e-12);
    EXPECT_NEAR(PI_SPHER / 2.0, fa.alpha2, 1e-12);
    EXPECT_NEAR(PI_SPHER / 2.0, fa.alpha3, 1e-12);
}

// ════════════════════════════════════════════════════════════════════════════
// Angle sum of a spherical triangle exceeds π (positive curvature)
//
// For the spherical tetrahedron face (arccos(−1/3) ≈ 1.9106 per edge):
// The dihedral angle = arccos(1/3) ≈ 70.53°; by symmetry the face angles
// (vertex angles of the spherical triangle) are all equal.
// Angle sum must be > π and equal 3·arccos(1/3) ≈ 3·1.2310 ≈ 3.693 rad.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, SpherTetAngleSumExceedsPi)
{
    // Edge length of spherical tetrahedron face: arccos(−1/3)
    const double l = std::acos(-1.0 / 3.0);
    auto fa = spherical_angles(l, l, l);

    ASSERT_TRUE(fa.valid);
    EXPECT_GT(fa.alpha1 + fa.alpha2 + fa.alpha3, PI_SPHER)
        << "Angle sum of spherical triangle must exceed π";

    // By symmetry all three angles must be equal
    EXPECT_NEAR(fa.alpha1, fa.alpha2, 1e-12);
    EXPECT_NEAR(fa.alpha2, fa.alpha3, 1e-12);

    // For a regular spherical tetrahedron with edge arccos(−1/3):
    // half-angle: tan(α/2) = √(sin(l/2)/sin(3l/2)) = √3 → α/2 = π/3 → α = 2π/3.
    // (arccos(1/3) ≈ 1.231 is the 3D dihedral angle of a Euclidean tetrahedron, not this.)
    double expected = 2.0 * PI_SPHER / 3.0;   // 120°
    EXPECT_NEAR(fa.alpha1, expected, 1e-10);
}

// ════════════════════════════════════════════════════════════════════════════
// Gradient check: octahedron-face triangle, vertex DOFs only
//
// Sets λ° from mesh geometry (unit sphere), all u_i = −0.3 (slightly smaller).
// Mirrors Java testGradientWithHyperIdeal… on a single-triangle mesh.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, GradientCheck_OctaFaceVertex)
{
    auto mesh = make_octahedron_face();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_vertex_dof_indices(mesh, maps);

    // Small uniform conformal factor: shrink the triangle slightly.
    std::vector<double> x(static_cast<std::size_t>(n), -0.3);

    EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
        << "Gradient check failed on octahedron-face triangle (vertex DOFs)";
}

// ════════════════════════════════════════════════════════════════════════════
// Gradient check: spherical tetrahedron (4 faces), vertex DOFs only
//
// Closed surface; exercises accumulation over multiple faces per vertex.
// Mirrors Java testGradientInTheExtendedDomain.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, GradientCheck_SpherTetVertex)
{
    auto mesh = make_spherical_tetrahedron();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_vertex_dof_indices(mesh, maps);

    std::vector<double> x(static_cast<std::size_t>(n), -0.2);

    EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
        << "Gradient check failed on spherical tetrahedron (vertex DOFs)";
}

// ════════════════════════════════════════════════════════════════════════════
// Gradient check: spherical tetrahedron, all DOFs (vertex + edge)
//
// Exercises the edge-gradient branch: G_e = α_opp⁺ + α_opp⁻ − π.
// Mirrors Java testGradientWithHyperellipticCurve.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, GradientCheck_SpherTetAllDofs)
{
    auto mesh = make_spherical_tetrahedron();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_all_spherical_dof_indices(mesh, maps);

    // Small but non-zero values; vertex DOFs negative, edge DOFs zero.
    // Edge DOF adjusts the effective log-length Λ_ij = λ°_ij + u_i + u_j + λ_e.
    std::vector<double> x(static_cast<std::size_t>(n), 0.0);
    // Set vertex DOFs (indices 0..3) to -0.2 to keep triangle well-formed.
    for (int i = 0; i < 4; ++i) x[static_cast<std::size_t>(i)] = -0.2;

    EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
        << "Gradient check failed on spherical tetrahedron (all DOFs)";
}

// ════════════════════════════════════════════════════════════════════════════
// Angles are finite at a known interior point
//
// Mirrors Java testFunctionalAtNaNValue: choose DOFs that could hit
// a degenerate branch (l_ij → 0 or triangle inequality fails) and check
// the gradient vector is free of NaN/Inf.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, AnglesFiniteAtKnownPoint)
{
    auto mesh = make_spherical_tetrahedron();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_vertex_dof_indices(mesh, maps);

    // u_i = -1.5: contracts the triangle heavily but stays non-degenerate.
    std::vector<double> x(static_cast<std::size_t>(n), -1.5);
    auto G = spherical_gradient(mesh, x, maps);

    for (std::size_t i = 0; i < G.size(); ++i) {
        EXPECT_FALSE(std::isnan(G[i])) << "Gradient component " << i << " is NaN";
        EXPECT_FALSE(std::isinf(G[i])) << "Gradient component " << i << " is Inf";
    }
}

// ════════════════════════════════════════════════════════════════════════════
// Gradient check: fan-4 mesh on unit sphere, vertex DOFs only
//
// Make a fan of 4 triangles around the north pole (0,0,1);
// rim vertices projected onto the equator.
// Exercises high-valence vertex gradient accumulation.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, GradientCheck_SpherFan4Vertex)
{
    // Build a fan with 4 spherical triangles manually (can't use make_fan
    // directly because those vertices are not on the unit sphere).
    ConformalMesh mesh;
    auto center = mesh.add_vertex(Point3(0, 0, 1));  // north pole
    const int n_rim = 4;
    std::vector<Vertex_index> rim(n_rim);
    const double dtheta = 2.0 * PI_SPHER / n_rim;
    const double phi    = PI_SPHER / 4.0;  // 45° colatitude
    for (int i = 0; i < n_rim; ++i) {
        double theta = i * dtheta;
        rim[i] = mesh.add_vertex(Point3(
            std::sin(phi) * std::cos(theta),
            std::sin(phi) * std::sin(theta),
            std::cos(phi)));
    }
    for (int i = 0; i < n_rim; ++i)
        mesh.add_face(center, rim[i], rim[(i + 1) % n_rim]);

    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int ndof = assign_vertex_dof_indices(mesh, maps);

    std::vector<double> x(static_cast<std::size_t>(ndof), -0.3);

    EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
        << "Gradient check failed on spherical fan-4 mesh";
}

// ════════════════════════════════════════════════════════════════════════════
// Gradient check: mixed pinned/variable vertices
//
// One vertex pinned (u_v = 0 fixed), others variable.
// Verifies that the gradient accumulation skips pinned vertices correctly.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, GradientCheck_MixedPinnedVertices)
{
    auto mesh = make_octahedron_face();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);

    // Pin v0, make v1 and v2 variable.
    auto vit = mesh.vertices().begin();
    Vertex_index v0 = *vit++;
    Vertex_index v1 = *vit++;
    Vertex_index v2 = *vit;

    maps.v_idx[v0] = -1;   // pinned
    maps.v_idx[v1] = 0;
    maps.v_idx[v2] = 1;

    std::vector<double> x = {-0.2, -0.4};

    EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
        << "Gradient check failed for mixed pinned/variable vertices";
}

// ════════════════════════════════════════════════════════════════════════════
// Phase 3e — Gauge-fix for closed spherical surfaces
//
// On a closed spherical surface, the functional has a gauge mode:
//   E(u + t·1)  is maximised at some t*.
//   At t*, the sum of all vertex gradients equals zero: Σ G_v = 0.
//
// Test: start from a point with non-zero ΣG_v, apply the gauge shift,
// and verify ΣG_v(x + t*·1) ≈ 0.
// ════════════════════════════════════════════════════════════════════════════

TEST(SphericalFunctional, GaugeFix_SpherTetVertexZerosSumGv)
{
    auto mesh = make_spherical_tetrahedron();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_vertex_dof_indices(mesh, maps);

    // Off-gauge starting point: all u_i = -0.5
    std::vector<double> x(static_cast<std::size_t>(n), -0.5);

    // Compute ΣG_v before gauge shift.
    {
        auto G = spherical_gradient(mesh, x, maps);
        double sum = 0.0;
        for (auto v : mesh.vertices()) {
            int iv = maps.v_idx[v];
            if (iv >= 0) sum += G[static_cast<std::size_t>(iv)];
        }
        // At -0.5 the surface is compressed; ΣG_v should be non-zero.
        EXPECT_NE(sum, 0.0) << "Pre-gauge ΣG_v should be non-zero";
    }

    // Compute gauge shift and apply.
    double t = spherical_gauge_shift(mesh, x, maps);
    std::vector<double> x_fixed = x;
    for (auto v : mesh.vertices()) {
        int iv = maps.v_idx[v];
        if (iv >= 0)
            x_fixed[static_cast<std::size_t>(iv)] += t;
    }

    // Verify ΣG_v ≈ 0 at the gauge-fixed point.
    {
        auto G = spherical_gradient(mesh, x_fixed, maps);
        double sum = 0.0;
        for (auto v : mesh.vertices()) {
            int iv = maps.v_idx[v];
            if (iv >= 0) sum += G[static_cast<std::size_t>(iv)];
        }
        EXPECT_NEAR(sum, 0.0, 1e-6)
            << "After gauge fix, Σ G_v should vanish; t* = " << t;
    }
}

TEST(SphericalFunctional, GaugeFix_ApplyInPlace)
{
    auto mesh = make_spherical_tetrahedron();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_vertex_dof_indices(mesh, maps);

    // x = -0.3: compressed but inside the valid spherical domain.
    std::vector<double> x(static_cast<std::size_t>(n), -0.3);

    apply_spherical_gauge(mesh, x, maps);

    // After in-place gauge fix, ΣG_v must be near 0.
    auto G = spherical_gradient(mesh, x, maps);
    double sum = 0.0;
    for (auto v : mesh.vertices()) {
        int iv = maps.v_idx[v];
        if (iv >= 0) sum += G[static_cast<std::size_t>(iv)];
    }
    EXPECT_NEAR(sum, 0.0, 1e-6)
        << "apply_spherical_gauge must drive Σ G_v to zero";
}

TEST(SphericalFunctional, GaugeFix_AlreadyAtGaugeReturnsTNearZero)
{
    // A symmetric, equilateral spherical tetrahedron at x=0 is already
    // at the gauge maximum (by symmetry, ΣG_v = 0).
    auto mesh = make_spherical_tetrahedron();
    auto maps = setup_spherical_maps(mesh);
    compute_lambda0_from_mesh(mesh, maps);
    int n = assign_vertex_dof_indices(mesh, maps);

    std::vector<double> x(static_cast<std::size_t>(n), 0.0);
    double t = spherical_gauge_shift(mesh, x, maps);
    // Symmetric starting point → t* should be very close to 0.
    EXPECT_NEAR(t, 0.0, 1e-5)
        << "Gauge shift from the symmetric point should be ~0; got " << t;
}
