// test_newton_phase9a.cpp
//
// Phase 9a Newton solvers — convergence tests for the two new
// circle-packing functionals.
//
// Validates that:
//   • newton_cp_euclidean()        — face-based BPS-2010 functional.
//   • newton_inversive_distance()  — vertex-based Luo-2004 functional.
// both reach a Newton equilibrium (‖G‖∞ < 1e-8) in < 30 iterations
// on a range of test meshes, and that the converged solution satisfies
// the relevant geometric invariants.

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

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

using namespace conformallab;

namespace {

// Open 3-face mesh (tetrahedron minus one face) — exercises boundary edges.
inline ConformalMesh make_open_3face_mesh()
{
    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));
    mesh.add_face(v0, v2, v1);
    mesh.add_face(v0, v1, v3);
    mesh.add_face(v0, v3, v2);
    return mesh;
}

} // anonymous

// ════════════════════════════════════════════════════════════════════════════
// 1. CP-Euclidean Newton — orthogonal circle packing
//
// Setup matches CPEuclideanFunctionalTest.java (Java parity at the
// solver level): θ_e = π/2 everywhere, φ_f = 2π for all faces.  Use
// the "natural-phi" trick (analog of natural-theta in Euclidean):
// adjust φ so that ρ = 0 is the natural equilibrium → Newton must
// converge in zero iterations.
// ════════════════════════════════════════════════════════════════════════════

TEST(NewtonPhase9a, CPEuclidean_NaturalPhi_ClosedTetrahedron_ConvergesInZeroIterations)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
    ASSERT_EQ(n, 3);

    // Natural-phi: shift φ_f so the gradient at ρ = 0 is zero.
    std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
    auto G0 = cp_euclidean_gradient(mesh, x0, m);
    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)];
    }

    auto res = newton_cp_euclidean(mesh, x0, m);
    EXPECT_TRUE(res.converged);
    EXPECT_EQ(res.iterations, 0)
        << "natural-phi pre-shift should make x=0 the equilibrium";
    EXPECT_LT(res.grad_inf_norm, 1e-10);
    for (double r : res.x) EXPECT_NEAR(r, 0.0, 1e-12);
}

// ════════════════════════════════════════════════════════════════════════════
// 2. CP-Euclidean Newton — perturbed equilibrium converges back to 0
//
// Same setup as test 1, but start from a small perturbation.  The
// strictly-convex BPS-2010 energy means Newton must converge back
// to the natural-phi equilibrium ρ = 0.
// ════════════════════════════════════════════════════════════════════════════

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

    // Apply natural-phi (equilibrium at ρ=0).
    std::vector<double> x0_zero(static_cast<std::size_t>(n), 0.0);
    auto G0 = cp_euclidean_gradient(mesh, x0_zero, m);
    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)];
    }

    // Start from a perturbation.
    std::vector<double> x0 = {0.1, -0.2, 0.15};
    auto res = newton_cp_euclidean(mesh, x0, m);

    EXPECT_TRUE(res.converged);
    EXPECT_LT(res.iterations, 30);
    EXPECT_LT(res.grad_inf_norm, 1e-8);
    // Strictly-convex unique minimum → converges back to ρ=0.
    for (double r : res.x) EXPECT_NEAR(r, 0.0, 1e-6);
}

// ════════════════════════════════════════════════════════════════════════════
// 3. CP-Euclidean Newton — open mesh (boundary edges)
// ════════════════════════════════════════════════════════════════════════════

TEST(NewtonPhase9a, CPEuclidean_OpenTetrahedron_NaturalPhi_Converges)
{
    auto mesh = make_open_3face_mesh();
    auto m    = setup_cp_euclidean_maps(mesh);
    const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
    ASSERT_EQ(n, 2);

    std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
    auto G0 = cp_euclidean_gradient(mesh, x0, m);
    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)];
    }

    auto res = newton_cp_euclidean(mesh, x0, m);
    EXPECT_TRUE(res.converged);
    EXPECT_LT(res.iterations, 30);
    EXPECT_LT(res.grad_inf_norm, 1e-8);
}

// ════════════════════════════════════════════════════════════════════════════
// 4. Inversive-Distance Newton — natural-theta on triangle
//
// At u = 0, Bowers-Stephenson init reproduces the input edge lengths
// exactly.  Natural-theta then shifts Θ so the gradient is zero, making
// u = 0 the equilibrium.  Newton must converge in zero iterations.
// ════════════════════════════════════════════════════════════════════════════

TEST(NewtonPhase9a, InversiveDistance_NaturalTheta_Triangle_ConvergesInZero)
{
    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++;

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

    auto res = newton_inversive_distance(mesh, x0, m);
    EXPECT_TRUE(res.converged);
    EXPECT_EQ(res.iterations, 0);
    EXPECT_LT(res.grad_inf_norm, 1e-10);
    for (double u : res.x) EXPECT_NEAR(u, 0.0, 1e-12);
}

// ════════════════════════════════════════════════════════════════════════════
// 5. Inversive-Distance Newton — perturbed start on quad strip
// ════════════════════════════════════════════════════════════════════════════

TEST(NewtonPhase9a, InversiveDistance_PerturbedQuadStrip_Converges)
{
    auto mesh = make_quad_strip();
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    // Pin vertex 0; index the rest.
    auto vit = mesh.vertices().begin();
    m.v_idx[*vit++] = -1;
    int n = 0;
    for (; vit != mesh.vertices().end(); ++vit) m.v_idx[*vit] = n++;

    // Natural-theta with the pin in place.
    std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
    auto G0 = inversive_distance_gradient(mesh, x0, m);
    for (auto v : mesh.vertices()) {
        int i = m.v_idx[v];
        if (i >= 0) m.theta_v[v] -= G0[static_cast<std::size_t>(i)];
    }

    // Perturb away from the equilibrium and watch it return.
    std::vector<double> x_pert(static_cast<std::size_t>(n), -0.05);
    auto res = newton_inversive_distance(mesh, x_pert, m);

    EXPECT_TRUE(res.converged);
    EXPECT_LT(res.iterations, 30);
    EXPECT_LT(res.grad_inf_norm, 1e-8);
    // Strictly-convex unique minimum on the open domain → back to 0.
    for (double u : res.x) EXPECT_NEAR(u, 0.0, 1e-6);
}

// ════════════════════════════════════════════════════════════════════════════
// 6. Inversive-Distance Newton — tetrahedron (closed mesh)
// ════════════════════════════════════════════════════════════════════════════

TEST(NewtonPhase9a, InversiveDistance_PerturbedTetrahedron_Converges)
{
    auto mesh = make_tetrahedron();
    auto m    = setup_inversive_distance_maps(mesh);
    compute_inversive_distance_init_from_mesh(mesh, m);

    // Closed mesh — pin one vertex to remove the gauge mode.
    auto vit = mesh.vertices().begin();
    m.v_idx[*vit++] = -1;
    int n = 0;
    for (; vit != mesh.vertices().end(); ++vit) m.v_idx[*vit] = n++;

    std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
    auto G0 = inversive_distance_gradient(mesh, x0, m);
    for (auto v : mesh.vertices()) {
        int i = m.v_idx[v];
        if (i >= 0) m.theta_v[v] -= G0[static_cast<std::size_t>(i)];
    }

    std::vector<double> x_pert(static_cast<std::size_t>(n), -0.1);
    auto res = newton_inversive_distance(mesh, x_pert, m);

    EXPECT_TRUE(res.converged);
    EXPECT_LT(res.iterations, 30);
    EXPECT_LT(res.grad_inf_norm, 1e-8);
}

// ════════════════════════════════════════════════════════════════════════════
// 7. CP-Euclidean Newton — uses analytic Hessian (NOT FD)
//
// Regression guard: verify the solver actually calls cp_euclidean_hessian
// (the analytic 2×2-per-edge formula) rather than degenerating to a
// per-iteration FD pass.  If iteration count exceeds a tight upper bound
// for a tiny mesh, that would suggest a slow inner Hessian computation
// or a wrong-sign mistake.
// ════════════════════════════════════════════════════════════════════════════

TEST(NewtonPhase9a, CPEuclidean_UsesAnalyticHessian)
{
    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> x0(static_cast<std::size_t>(n), 0.0);
    auto G0 = cp_euclidean_gradient(mesh, x0, m);
    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)];
    }

    // Strong perturbation — quadratic Newton with analytic Hessian
    // should still converge in a handful of iterations.
    std::vector<double> x_pert = {0.5, -0.4, 0.3};
    auto res = newton_cp_euclidean(mesh, x_pert, m);

    EXPECT_TRUE(res.converged);
    EXPECT_LE(res.iterations, 10)
        << "analytic Hessian: expect very fast convergence on a 3-DOF problem";
}
