// test_cp_euclidean_functional.cpp
//
// Phase 9a.1 — CPEuclideanFunctional (BPS 2010) tests.
//
// Replicates de.varylab.discreteconformal.functional.CPEuclideanFunctionalTest
// (88 lines) and adds boundary-edge coverage plus a closed-mesh case.
//
// Java test pattern (lines 50-87):
//   1. Build dodecahedron via HalfEdgeUtils.addDodecahedron.
//   2. Remove face 0 to produce an open mesh.
//   3. theta_e = π/2 for every edge.       (orthogonal circle packing)
//   4. phi_f   = 2π for every face.        (flat target)
//   5. Random ρ ∈ [−0.5, 0.5] (seed 1).
//   6. FunctionalTest.setXGradient(ρ) → FD-vs-analytic gradient check.
//   7. FunctionalTest.setXHessian(ρ)  → FD-vs-analytic Hessian  check.
//
// C++ port uses the tetrahedron (4 faces) instead of the dodecahedron (12 faces)
// because the analytic structure is identical and the smaller mesh keeps the
// test fast and human-inspectable.  We exercise the boundary-edge code path
// by additionally testing a tetrahedron with one face removed (3 faces, 3
// boundary edges, 3 interior edges).

#include "cp_euclidean_functional.hpp"
#include "mesh_builder.hpp"
#include "conformal_mesh.hpp"

#include <Eigen/Eigenvalues>
#include <gtest/gtest.h>
#include <vector>
#include <random>

using namespace conformallab;

// ════════════════════════════════════════════════════════════════════════════
// 1.  Helper: explicit values for p(θ*, Δρ) at known inputs
//
// p(θ*, 0) = 0          (tanh 0 = 0)
// p(π, Δρ) = π·sign(Δρ) (tan(π/2) = ∞, atan saturates to ±π/2)
// p(0, Δρ) = 0          (tan(0) = 0)
// p odd in Δρ (tanh is odd).
// ════════════════════════════════════════════════════════════════════════════

TEST(CPEuclideanFunctional, PFunctionKnownValues)
{
    using cp_detail::p_function;
    constexpr double PI_ = 3.14159265358979323846;

    // p(any, 0) = 0
    EXPECT_NEAR(p_function(PI_ / 4,  0.0), 0.0, 1e-15);
    EXPECT_NEAR(p_function(PI_ / 2,  0.0), 0.0, 1e-15);

    // Odd in Δρ
    const double thStar = PI_ / 3;
    for (double dr : {0.1, 0.5, 1.0, 2.0}) {
        EXPECT_NEAR(p_function(thStar, dr) + p_function(thStar, -dr), 0.0, 1e-12)
            << "p(θ*, Δρ) should be odd in Δρ";
    }
}

// ════════════════════════════════════════════════════════════════════════════
// 2.  Property-map setup defaults
// ════════════════════════════════════════════════════════════════════════════

TEST(CPEuclideanFunctional, SetupDefaults)
{
    auto mesh = make_tetrahedron();
    auto m = setup_cp_euclidean_maps(mesh);

    constexpr double PI_ = 3.14159265358979323846;
    for (auto e : mesh.edges()) EXPECT_NEAR(m.theta_e[e], PI_ / 2, 1e-15);
    for (auto f : mesh.faces()) EXPECT_NEAR(m.phi_f[f],   2.0 * PI_, 1e-15);
    for (auto f : mesh.faces()) EXPECT_EQ(m.f_idx[f], -1) << "all faces start pinned";
}

TEST(CPEuclideanFunctional, AssignDofIndices_PinsOneFace)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    EXPECT_EQ(n, 3) << "tetrahedron has 4 faces; 1 pinned ⇒ 3 free DOFs";

    int pinned_count = 0;
    int max_idx = -1;
    for (auto f : mesh.faces()) {
        if (m.f_idx[f] == -1) ++pinned_count;
        else max_idx = std::max(max_idx, m.f_idx[f]);
    }
    EXPECT_EQ(pinned_count, 1);
    EXPECT_EQ(max_idx, 2);
}

// ════════════════════════════════════════════════════════════════════════════
// 3.  Tangential limit (θ = 0): p = 0, energy collapses, gradient = φ_f
// ════════════════════════════════════════════════════════════════════════════

TEST(CPEuclideanFunctional, TangentialLimitGradientEqualsPhi)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    for (auto e : mesh.edges()) m.theta_e[e] = 0.0;   // tangential limit
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    // At θ = 0: θ* = π.  Interior edge contribution: −(p+θ*) where p = π·sign(Δρ).
    // Boundary contribution: −2π.  At ρ = 0, Δρ = 0 so p = 0; each interior face
    // contributes −π per incident interior halfedge; for a tetrahedron each face
    // has 3 interior halfedges ⇒ −3π.  Net gradient: 2π − 3π = −π per free face.
    std::vector<double> x(static_cast<std::size_t>(n), 0.0);
    auto G = cp_euclidean_gradient(mesh, x, m);

    constexpr double PI_ = 3.14159265358979323846;
    for (double g : G) EXPECT_NEAR(g, -PI_, 1e-10);
}

// ════════════════════════════════════════════════════════════════════════════
// 4.  FD gradient check on closed tetrahedron at random ρ
//
// Java parity: this is exactly the structure of CPEuclideanFunctionalTest.
// ════════════════════════════════════════════════════════════════════════════

TEST(CPEuclideanFunctional, FDGradientCheck_ClosedTetrahedron_RandomRho)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    // Java: rnd.setSeed(1);  rho_i = rnd.nextDouble() − 0.5
    std::mt19937 rng(1);
    std::uniform_real_distribution<double> u(-0.5, 0.5);
    std::vector<double> rho(static_cast<std::size_t>(n));
    for (auto& r : rho) r = u(rng);

    EXPECT_TRUE(gradient_check_cp_euclidean(mesh, rho, m))
        << "FD vs analytic gradient mismatch on closed tetrahedron";
}

// ════════════════════════════════════════════════════════════════════════════
// 5.  FD Hessian check on closed tetrahedron at random ρ
// ════════════════════════════════════════════════════════════════════════════

TEST(CPEuclideanFunctional, FDHessianCheck_ClosedTetrahedron_RandomRho)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    std::mt19937 rng(1);
    std::uniform_real_distribution<double> u(-0.5, 0.5);
    std::vector<double> rho(static_cast<std::size_t>(n));
    for (auto& r : rho) r = u(rng);

    EXPECT_TRUE(hessian_check_cp_euclidean(mesh, rho, m))
        << "FD vs analytic Hessian mismatch on closed tetrahedron";
}

// ════════════════════════════════════════════════════════════════════════════
// 6.  Boundary-edge coverage: open mesh (tetrahedron with one face removed)
//
// Java test does this via `hds.removeFace(hds.getFace(0))`.  In CGAL we get
// an equivalent open mesh by skipping the construction of one face.
// ════════════════════════════════════════════════════════════════════════════

inline ConformalMesh make_open_tetrahedron()
{
    ConformalMesh mesh;
    auto v0 = mesh.add_vertex(Point3( 1,  1,  1));
    auto v1 = mesh.add_vertex(Point3( 1, -1, -1));
    auto v2 = mesh.add_vertex(Point3(-1,  1, -1));
    auto v3 = mesh.add_vertex(Point3(-1, -1,  1));
    // Three faces (omit the one opposite v0):
    mesh.add_face(v0, v2, v1);
    mesh.add_face(v0, v1, v3);
    mesh.add_face(v0, v3, v2);
    return mesh;
}

TEST(CPEuclideanFunctional, FDGradientCheck_OpenTetrahedron_RandomRho)
{
    auto mesh = make_open_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    EXPECT_EQ(n, 2);   // 3 faces, 1 pinned ⇒ 2 free DOFs

    std::mt19937 rng(1);
    std::uniform_real_distribution<double> u(-0.5, 0.5);
    std::vector<double> rho(static_cast<std::size_t>(n));
    for (auto& r : rho) r = u(rng);

    EXPECT_TRUE(gradient_check_cp_euclidean(mesh, rho, m))
        << "FD vs analytic gradient mismatch on open tetrahedron";
}

TEST(CPEuclideanFunctional, FDHessianCheck_OpenTetrahedron_RandomRho)
{
    auto mesh = make_open_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    std::mt19937 rng(1);
    std::uniform_real_distribution<double> u(-0.5, 0.5);
    std::vector<double> rho(static_cast<std::size_t>(n));
    for (auto& r : rho) r = u(rng);

    EXPECT_TRUE(hessian_check_cp_euclidean(mesh, rho, m))
        << "FD vs analytic Hessian mismatch on open tetrahedron";
}

// ════════════════════════════════════════════════════════════════════════════
// 7.  Hessian is symmetric positive-semidefinite (BPS-2010 §6 convexity)
//
// The energy is convex in ρ on its domain of validity.  Hence H is PSD with
// a 1-dim null space (constant shift of all ρ, removed by gauge pin).
// ════════════════════════════════════════════════════════════════════════════

TEST(CPEuclideanFunctional, HessianIsPSD)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    std::vector<double> rho(static_cast<std::size_t>(n), 0.1);
    auto H = cp_euclidean_hessian(mesh, rho, m);

    // Symmetry
    Eigen::MatrixXd Hd(H);
    EXPECT_NEAR((Hd - Hd.transpose()).cwiseAbs().maxCoeff(), 0.0, 1e-15);

    // Smallest eigenvalue ≥ 0 (PSD)
    Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(Hd);
    EXPECT_GE(es.eigenvalues().minCoeff(), -1e-12)
        << "Hessian must be PSD (BPS-2010 §6)";
}

// ════════════════════════════════════════════════════════════════════════════
// 8.  At equilibrium (Newton-converged ρ*), the gradient is zero by construction
//
// We do not run a full Newton solver here; we set up the "natural-theta" trick:
// adjust φ_f so that ρ = 0 is the equilibrium.  This is the analog of the
// natural-theta convention already used in euclidean_functional tests
// (see test_euclidean_functional.cpp lines 159-189).
// ════════════════════════════════════════════════════════════════════════════

TEST(CPEuclideanFunctional, NaturalPhiMakesZeroTheEquilibrium)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);

    std::vector<double> rho(static_cast<std::size_t>(n), 0.0);

    // Step 1: gradient at ρ = 0 with default φ.
    auto G0 = cp_euclidean_gradient(mesh, rho, m);

    // Step 2: adjust φ_f so the new gradient at ρ = 0 is zero.
    //   ∂E/∂ρ_f = φ_f − (sum of edge contributions)
    //   To zero G_f: subtract G_f from φ_f.
    for (auto f : mesh.faces()) {
        int i = m.f_idx[f];
        if (i < 0) continue;
        m.phi_f[f] -= G0[static_cast<std::size_t>(i)];
    }

    // Step 3: gradient at ρ = 0 should now be ~zero.
    auto G_eq = cp_euclidean_gradient(mesh, rho, m);
    for (double g : G_eq) EXPECT_NEAR(g, 0.0, 1e-13);
}
