// test_cgal_traits_mvp.cpp
//
// Phase 8 MVP — first tests for the new CGAL-style public API.
//
// Validates:
//   1. Default_conformal_map_traits<Surface_mesh, K> compiles and
//      provides all advertised types and property-map accessors.
//   2. discrete_conformal_map_euclidean() runs end-to-end on a small mesh.
//   3. Named-parameter overrides (gradient_tolerance, max_iterations)
//      change the Newton behaviour as expected.
//   4. The result agrees with the legacy newton_euclidean() at the
//      same DOF assignment — proving the wrapper is non-destructive.
//
// These tests are the Phase 8 MVP acceptance probe.  Phase 9a
// (Inversive-Distance) will become the next, deeper validation by
// implementing a new functional against this same trait API.

#include <CGAL/Conformal_map_traits.h>
#include <CGAL/Discrete_conformal_map.h>
#include <CGAL/Kernel_traits.h>

#include "mesh_builder.hpp"   // make_triangle, make_quad_strip, make_tetrahedron
#include "euclidean_functional.hpp"
#include "newton_solver.hpp"

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

using namespace conformallab;

// ════════════════════════════════════════════════════════════════════════════
// 1.  Traits class: compile-time type sanity
// ════════════════════════════════════════════════════════════════════════════

TEST(CGALConformalTraits, DefaultTraitsTypes)
{
    using K     = CGAL::Simple_cartesian<double>;
    using Mesh  = CGAL::Surface_mesh<K::Point_3>;
    using Tr    = CGAL::Default_conformal_map_traits<Mesh, K>;

    // FT comes from the kernel.
    static_assert(std::is_same_v<typename Tr::FT, double>);

    // Descriptors come from boost::graph_traits, not from Surface_mesh directly.
    static_assert(std::is_same_v<typename Tr::Triangle_mesh, Mesh>);
    static_assert(std::is_same_v<
        typename Tr::Vertex_descriptor,
        typename boost::graph_traits<Mesh>::vertex_descriptor>);

    // Property-map types should match Surface_mesh::Property_map for the
    // appropriate key.
    static_assert(std::is_same_v<
        typename Tr::Theta_pmap,
        typename Mesh::template Property_map<typename Tr::Vertex_descriptor, double>>);
    static_assert(std::is_same_v<
        typename Tr::Vertex_index_pmap,
        typename Mesh::template Property_map<typename Tr::Vertex_descriptor, int>>);
    static_assert(std::is_same_v<
        typename Tr::Lambda0_pmap,
        typename Mesh::template Property_map<typename Tr::Edge_descriptor, double>>);

    // Default kernel: Simple_cartesian<double>.
    using TrDefault = CGAL::Default_conformal_map_traits<Mesh>;
    static_assert(std::is_same_v<typename TrDefault::Kernel,
                                 CGAL::Simple_cartesian<double>>);
}

// ════════════════════════════════════════════════════════════════════════════
// 2.  Traits property-map accessors are non-destructive
//
// Setting up via the trait helpers and via setup_euclidean_maps() must yield
// the same property map (Surface_mesh deduplicates by name).
// ════════════════════════════════════════════════════════════════════════════

TEST(CGALConformalTraits, AccessorsReuseExistingMaps)
{
    using K   = CGAL::Simple_cartesian<double>;
    using Mesh = CGAL::Surface_mesh<K::Point_3>;
    using Tr  = CGAL::Default_conformal_map_traits<Mesh, K>;

    auto mesh = make_triangle();
    auto maps = setup_euclidean_maps(mesh);

    auto theta_via_traits   = Tr::theta_map(mesh);
    auto idx_via_traits     = Tr::vertex_index_map(mesh);
    auto lambda0_via_traits = Tr::lambda0_map(mesh);

    // Surface_mesh property maps with the same key type are equality-comparable
    // by name lookup — accessing through the traits class must return the
    // same map that setup_euclidean_maps() created.
    EXPECT_EQ(theta_via_traits,   maps.theta_v);
    EXPECT_EQ(idx_via_traits,     maps.v_idx);
    EXPECT_EQ(lambda0_via_traits, maps.lambda0);
}

// ════════════════════════════════════════════════════════════════════════════
// 3.  End-to-end: discrete_conformal_map_euclidean() on a small open mesh
// ════════════════════════════════════════════════════════════════════════════

TEST(CGALDiscreteConformalMap, SingleTriangleConverges)
{
    auto mesh = make_triangle();
    auto result = CGAL::discrete_conformal_map_euclidean(mesh);

    EXPECT_TRUE(result.converged)
        << "Newton did not converge on a single triangle";
    EXPECT_LT(result.gradient_norm, 1e-8);
    EXPECT_GE(result.iterations, 0);
    EXPECT_EQ(result.u_per_vertex.size(), num_vertices(mesh));

    // With the default flat-disc target curvature and the first vertex pinned,
    // the natural-theta equilibrium is at u = 0 — Newton should accept x0=0.
    for (double u : result.u_per_vertex)
        EXPECT_NEAR(u, 0.0, 1e-8);
}

TEST(CGALDiscreteConformalMap, QuadStripConverges)
{
    auto mesh = make_quad_strip();
    auto result = CGAL::discrete_conformal_map_euclidean(mesh);

    EXPECT_TRUE(result.converged);
    EXPECT_LT(result.gradient_norm, 1e-8);
    EXPECT_EQ(result.u_per_vertex.size(), num_vertices(mesh));
}

// ════════════════════════════════════════════════════════════════════════════
// 4.  Named-parameter overrides take effect
// ════════════════════════════════════════════════════════════════════════════

TEST(CGALDiscreteConformalMap, MaxIterationsTakesEffect)
{
    auto mesh = make_triangle();

    // max_iterations(0) forces Newton to give up immediately.
    auto result = CGAL::discrete_conformal_map_euclidean(
        mesh,
        CGAL::parameters::max_iterations(0));

    EXPECT_EQ(result.iterations, 0);
    // Trivial natural-theta case: gradient is already zero at x=0,
    // so even 0 iterations may report "converged" depending on the
    // initial gradient check.  The point is just that the parameter
    // was *read* — verified by EXPECT_EQ on iterations above.
}

TEST(CGALDiscreteConformalMap, GradientToleranceTakesEffect)
{
    auto mesh = make_quad_strip();

    // Loose tolerance — must still converge, but possibly in fewer steps.
    auto result_loose = CGAL::discrete_conformal_map_euclidean(
        mesh,
        CGAL::parameters::gradient_tolerance(1e-4));
    EXPECT_TRUE(result_loose.converged);

    // Strict tolerance — also must converge, gradient norm must be tighter.
    auto result_strict = CGAL::discrete_conformal_map_euclidean(
        mesh,
        CGAL::parameters::gradient_tolerance(1e-12));
    EXPECT_TRUE(result_strict.converged);
    EXPECT_LT(result_strict.gradient_norm, 1e-10);
}

// ════════════════════════════════════════════════════════════════════════════
// 5.  Wrapper agrees with the legacy newton_euclidean() at the same setup
//
// This is the cross-API consistency check: same mesh, same default settings
// (first vertex pinned, x0=0) — the u-vector returned by the wrapper must
// match what newton_euclidean produces directly.
// ════════════════════════════════════════════════════════════════════════════

// ════════════════════════════════════════════════════════════════════════════
// 6.  Kernel deduction: the wrapper must NOT hard-code Simple_cartesian
//
// Regression guard: the wrapper deduces its kernel from the mesh point type
// via `CGAL::Kernel_traits`.  If anyone re-introduces a hard-coded
// `Simple_cartesian<double>` in the wrapper, the static_asserts here still
// pass (the legacy ConformalMesh uses that kernel) — but Phase 9a or any
// user with a different kernel-backed Surface_mesh would fail to compile.
// This test pins the deduction *contract* explicitly.
// ════════════════════════════════════════════════════════════════════════════

TEST(CGALDiscreteConformalMap, KernelIsDeducedFromMeshPointType)
{
    using Mesh = ConformalMesh;
    using P    = typename Mesh::Point;

    using DeducedKernel = typename CGAL::Kernel_traits<P>::Kernel;
    using DeducedTraits = CGAL::Default_conformal_map_traits<Mesh, DeducedKernel>;

    static_assert(std::is_same_v<DeducedKernel, CGAL::Simple_cartesian<double>>,
                  "ConformalMesh point type must deduce to Simple_cartesian<double>");
    static_assert(std::is_same_v<typename DeducedTraits::FT, double>);
    static_assert(std::is_same_v<typename DeducedTraits::Triangle_mesh, Mesh>);

    // Run-time sanity: the wrapper accepts the deduced-kernel mesh end-to-end.
    auto mesh = make_quad_strip();
    auto result = CGAL::discrete_conformal_map_euclidean(mesh);
    EXPECT_TRUE(result.converged);
}

TEST(CGALDiscreteConformalMap, WrapperMatchesLegacyAPI)
{
    auto mesh = make_quad_strip();

    // ── New API: applies natural-theta automatically ───────────────────────
    auto result_new = CGAL::discrete_conformal_map_euclidean(mesh);

    // ── Legacy API on a fresh mesh — must replicate the *same* preparation
    //    that the wrapper performs internally (pin first vertex, assign
    //    DOFs, apply natural-theta).  Otherwise the comparison is unfair
    //    (Newton would diverge without natural-theta on these meshes). ────
    auto mesh_legacy = make_quad_strip();
    auto maps = setup_euclidean_maps(mesh_legacy);
    compute_euclidean_lambda0_from_mesh(mesh_legacy, maps);

    // Pin first vertex (gauge), assign sequential DOFs to the rest.
    auto vit = mesh_legacy.vertices().begin();
    maps.v_idx[*vit++] = -1;
    int idx = 0;
    for (; vit != mesh_legacy.vertices().end(); ++vit)
        maps.v_idx[*vit] = idx++;

    // Natural-theta: shift Θ so that x = 0 is the natural equilibrium.
    std::vector<double> x0(idx, 0.0);
    auto G0 = euclidean_gradient(mesh_legacy, x0, maps);
    for (auto v : mesh_legacy.vertices()) {
        int j = maps.v_idx[v];
        if (j >= 0) maps.theta_v[v] -= G0[static_cast<std::size_t>(j)];
    }

    auto nr = newton_euclidean(mesh_legacy, x0, maps, 1e-10, 200);

    // ── Compare ────────────────────────────────────────────────────────────
    EXPECT_EQ(result_new.converged, nr.converged);
    EXPECT_NEAR(result_new.gradient_norm, nr.grad_inf_norm, 1e-12);

    // Pinned vertex u is 0 in both; for the rest the values agree.
    for (auto v : mesh_legacy.vertices()) {
        int j = maps.v_idx[v];
        double u_legacy = (j >= 0) ? nr.x[static_cast<std::size_t>(j)] : 0.0;
        EXPECT_NEAR(result_new.u_per_vertex[v.idx()], u_legacy, 1e-10)
            << "Wrapper diverges from legacy for vertex " << v.idx();
    }
}
