// test_phase7.cpp
//
// Phase 7 — Tests for Java-parity layout features:
//   - MobiusMap            : identity, inverse, compose, from_three, is_identity
//   - best_root_face       : selects a valid face; interior bonus
//   - halfedge_uv          : size, non-seam consistency, seam divergence
//   - Priority BFS         : vertex ordering / depth correctness
//   - normalise_euclidean  : halfedge_uv centroid at origin
//   - period_matrix.hpp    : τ in upper half-plane, SL(2,ℤ) reduction
//   - fundamental_domain.hpp: parallelogram CCW, generators, tiling_copy

#include "conformal_mesh.hpp"
#include "mesh_builder.hpp"
#include "euclidean_functional.hpp"
#include "hyper_ideal_functional.hpp"
#include "newton_solver.hpp"
#include "layout.hpp"
#include "period_matrix.hpp"
#include "fundamental_domain.hpp"
#include <gtest/gtest.h>
#include <cmath>
#include <complex>
#include <vector>
#include <limits>

using namespace conformallab;
using C = std::complex<double>;

// ════════════════════════════════════════════════════════════════════════════
// MobiusMap
// ════════════════════════════════════════════════════════════════════════════

TEST(MobiusMap, Identity_AppliesAsIdentity)
{
    MobiusMap id = MobiusMap::identity();
    C z(0.3, 0.7);
    C w = id.apply(z);
    EXPECT_NEAR(w.real(), z.real(), 1e-12);
    EXPECT_NEAR(w.imag(), z.imag(), 1e-12);
}

TEST(MobiusMap, Identity_IsIdentity)
{
    EXPECT_TRUE(MobiusMap::identity().is_identity());
}

TEST(MobiusMap, NonIdentity_IsNotIdentity)
{
    // T(z) = z + 1 — translation, clearly not identity
    MobiusMap T{ C(1), C(1), C(0), C(1) };
    EXPECT_FALSE(T.is_identity());
}

TEST(MobiusMap, Inverse_ComposeIsIdentity)
{
    // T(z) = (2z + 1) / (z + 3)
    MobiusMap T{ C(2), C(1), C(1), C(3) };
    MobiusMap TinvT = T.inverse().compose(T);
    EXPECT_TRUE(TinvT.is_identity(1e-9));
}

TEST(MobiusMap, Compose_OrderCorrect)
{
    // S: z ↦ z + 1,   T: z ↦ 2z
    // S.compose(T) means S applied after T: z ↦ 2z + 1
    MobiusMap S{ C(1), C(1), C(0), C(1) };   // z + 1
    MobiusMap T{ C(2), C(0), C(0), C(1) };   // 2z
    MobiusMap ST = S.compose(T);
    C z(1.0, 0.0);
    // S(T(z)) = S(2) = 3
    EXPECT_NEAR(ST.apply(z).real(), 3.0, 1e-12);
    EXPECT_NEAR(ST.apply(z).imag(), 0.0, 1e-12);
}

TEST(MobiusMap, FromThree_RecoversMap)
{
    // Known map T(z) = (z + i) / (1 + 0·z) — translation by i
    C w1 = C(0, 1) + C(0, 1);   // T(i) = 2i
    C w2 = C(1, 0) + C(0, 1);   // T(1) = 1 + i
    C w3 = C(-1, 0) + C(0, 1);  // T(-1) = -1 + i
    MobiusMap T = MobiusMap::from_three(C(0, 1), w1, C(1, 0), w2, C(-1, 0), w3);
    // Verify T maps a fourth point correctly: T(0) = i
    C result = T.apply(C(0, 0));
    EXPECT_NEAR(result.real(), 0.0, 1e-9);
    EXPECT_NEAR(result.imag(), 1.0, 1e-9);
}

TEST(MobiusMap, FromThree_DegenerateReturnsIdentity)
{
    // Three coincident points → singular system → identity fallback
    C z(0.5, 0.5);
    MobiusMap T = MobiusMap::from_three(z, z, z, z, z, z);
    // Should not crash; returns identity (or at least a valid map)
    // We just check the result is finite
    C w = T.apply(C(0.1, 0.2));
    EXPECT_FALSE(std::isnan(w.real()));
    EXPECT_FALSE(std::isnan(w.imag()));
}

TEST(MobiusMap, Apply_Vector2d)
{
    MobiusMap id = MobiusMap::identity();
    Eigen::Vector2d p(0.4, 0.6);
    Eigen::Vector2d q = id.apply(p);
    EXPECT_NEAR(q.x(), p.x(), 1e-12);
    EXPECT_NEAR(q.y(), p.y(), 1e-12);
}

// ════════════════════════════════════════════════════════════════════════════
// best_root_face
// ════════════════════════════════════════════════════════════════════════════

TEST(BestRootFace, ReturnsValidFace_Triangle)
{
    auto mesh = make_triangle();
    Face_index f = detail::best_root_face(mesh);
    EXPECT_NE(f, Face_index());
    EXPECT_GE(f.idx(), 0);
}

TEST(BestRootFace, ReturnsValidFace_Tetrahedron)
{
    auto mesh = make_tetrahedron();
    Face_index f = detail::best_root_face(mesh);
    EXPECT_NE(f, Face_index());
    // Tetrahedron has 4 faces — best is one of them
    EXPECT_LT(static_cast<std::size_t>(f.idx()), mesh.number_of_faces());
}

// ════════════════════════════════════════════════════════════════════════════
// halfedge_uv — size and non-seam consistency
// ════════════════════════════════════════════════════════════════════════════

// Helper: build equilibrium Euclidean layout for a given mesh.
// Uses x = 0 (identity scale factor) which is the equilibrium for natural edge lengths.
static Layout2D make_euclidean_layout(ConformalMesh& mesh)
{
    EuclideanMaps maps = setup_euclidean_maps(mesh);
    compute_euclidean_lambda0_from_mesh(mesh, maps);
    // Pin first vertex (DOF = -1); assign sequential indices to the rest.
    auto vit = mesh.vertices().begin();
    maps.v_idx[*vit++] = -1;
    int idx = 0;
    for (; vit != mesh.vertices().end(); ++vit) maps.v_idx[*vit] = idx++;
    std::vector<double> x(static_cast<std::size_t>(idx), 0.0);
    return euclidean_layout(mesh, x, maps);
}

TEST(HalfedgeUV, Size_EqualsNumberOfHalfedges_Triangle)
{
    auto mesh = make_triangle();
    auto lay   = make_euclidean_layout(mesh);
    EXPECT_EQ(lay.halfedge_uv.size(), mesh.number_of_halfedges());
}

TEST(HalfedgeUV, Size_EqualsNumberOfHalfedges_QuadStrip)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    EXPECT_EQ(lay.halfedge_uv.size(), mesh.number_of_halfedges());
}

TEST(HalfedgeUV, NonBorderHalfedges_MatchUV)
{
    // For an open mesh with no cut graph the layout has no seams.
    // Every non-border halfedge h must satisfy:
    //   halfedge_uv[h] == uv[source(h)]
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    for (auto h : mesh.halfedges()) {
        if (mesh.is_border(h)) continue;
        std::size_t hi  = static_cast<std::size_t>(h.idx());
        std::size_t vi  = static_cast<std::size_t>(mesh.source(h).idx());
        EXPECT_NEAR(lay.halfedge_uv[hi].x(), lay.uv[vi].x(), 1e-10)
            << "halfedge " << hi << " source vertex " << vi;
        EXPECT_NEAR(lay.halfedge_uv[hi].y(), lay.uv[vi].y(), 1e-10)
            << "halfedge " << hi << " source vertex " << vi;
    }
}

TEST(HalfedgeUV, BorderHalfedges_AreZero)
{
    auto mesh = make_triangle();
    auto lay   = make_euclidean_layout(mesh);
    bool found_border = false;
    for (auto h : mesh.halfedges()) {
        if (!mesh.is_border(h)) continue;
        std::size_t hi = static_cast<std::size_t>(h.idx());
        EXPECT_NEAR(lay.halfedge_uv[hi].x(), 0.0, 1e-12);
        EXPECT_NEAR(lay.halfedge_uv[hi].y(), 0.0, 1e-12);
        found_border = true;
    }
    EXPECT_TRUE(found_border);
}

// ════════════════════════════════════════════════════════════════════════════
// Priority BFS — depth ordering
// ════════════════════════════════════════════════════════════════════════════

TEST(PriorityBFS, Layout_SucceedsOnOpenMesh)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    EXPECT_TRUE(lay.success);
}

TEST(PriorityBFS, Layout_NoSeamOnOpenMesh)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    EXPECT_FALSE(lay.has_seam);
}

TEST(PriorityBFS, AllVerticesPlaced)
{
    auto mesh = make_tetrahedron();
    // Tetrahedron is closed; layout without cut graph will have a seam
    EuclideanMaps maps = setup_euclidean_maps(mesh);
    compute_euclidean_lambda0_from_mesh(mesh, maps);
    auto vit = mesh.vertices().begin();
    maps.v_idx[*vit++] = -1;
    int idx = 0;
    for (; vit != mesh.vertices().end(); ++vit) maps.v_idx[*vit] = idx++;
    std::vector<double> x(static_cast<std::size_t>(idx), 0.0);
    auto lay = euclidean_layout(mesh, x, maps);
    EXPECT_TRUE(lay.success);
    // All UVs must be finite
    for (auto& p : lay.uv) {
        EXPECT_FALSE(std::isnan(p.x()));
        EXPECT_FALSE(std::isnan(p.y()));
    }
}

// ════════════════════════════════════════════════════════════════════════════
// normalise_euclidean — centroid + PCA applied to both uv and halfedge_uv
// ════════════════════════════════════════════════════════════════════════════

TEST(NormaliseEuclidean, UVCentroidAtOrigin)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    normalise_euclidean(lay);

    Eigen::Vector2d mean = Eigen::Vector2d::Zero();
    for (auto& p : lay.uv) mean += p;
    mean /= static_cast<double>(lay.uv.size());
    EXPECT_NEAR(mean.x(), 0.0, 1e-10);
    EXPECT_NEAR(mean.y(), 0.0, 1e-10);
}

TEST(NormaliseEuclidean, HalfedgeUVCentroidAlsoShifted)
{
    // After normalisation: the non-border halfedge_uv entries should also be
    // centred (since they are shifted by the same mean as uv).
    // We verify that the mean of non-border halfedge_uv is near (0,0).
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    normalise_euclidean(lay);

    Eigen::Vector2d mean = Eigen::Vector2d::Zero();
    int count = 0;
    for (auto h : mesh.halfedges()) {
        if (mesh.is_border(h)) continue;
        mean += lay.halfedge_uv[static_cast<std::size_t>(h.idx())];
        ++count;
    }
    if (count > 0) mean /= static_cast<double>(count);
    EXPECT_NEAR(mean.x(), 0.0, 1e-9);
    EXPECT_NEAR(mean.y(), 0.0, 1e-9);
}

// ════════════════════════════════════════════════════════════════════════════
// PeriodMatrix — reduce_to_fundamental_domain
// ════════════════════════════════════════════════════════════════════════════

TEST(PeriodMatrix, ReduceToFD_AlreadyInFD)
{
    // τ = i is in F  (|i|=1, Re(i)=0, Im(i)=1>0)
    C tau(0.0, 1.0);
    C reduced = reduce_to_fundamental_domain(tau);
    EXPECT_TRUE(is_in_fundamental_domain(reduced));
    EXPECT_NEAR(reduced.real(), 0.0, 1e-10);
    EXPECT_NEAR(reduced.imag(), 1.0, 1e-10);
}

TEST(PeriodMatrix, ReduceToFD_ShiftsRealPart)
{
    // τ = 2 + 3i  →  T step: τ -= 2  →  3i  (|3i|=3≥1, Re=0)
    C tau(2.0, 3.0);
    C reduced = reduce_to_fundamental_domain(tau);
    EXPECT_TRUE(is_in_fundamental_domain(reduced, 1e-9));
    EXPECT_NEAR(reduced.real(), 0.0, 1e-10);
    EXPECT_NEAR(reduced.imag(), 3.0, 1e-10);
}

TEST(PeriodMatrix, ReduceToFD_InvertsSmallTau)
{
    // τ = 0.5i  →  |0.5i|=0.5<1  →  S: τ↦-1/(0.5i) = 2i
    C tau(0.0, 0.5);
    C reduced = reduce_to_fundamental_domain(tau);
    EXPECT_TRUE(is_in_fundamental_domain(reduced, 1e-9));
    EXPECT_NEAR(reduced.real(), 0.0, 1e-10);
    EXPECT_NEAR(reduced.imag(), 2.0, 1e-10);
}

TEST(PeriodMatrix, ReduceToFD_ThrowsForNonUpperHalfPlane)
{
    C tau(0.5, -1.0);  // Im < 0 → not in upper half-plane
    EXPECT_THROW(reduce_to_fundamental_domain(tau), std::domain_error);
}

TEST(PeriodMatrix, IsInFundamentalDomain_Square)
{
    EXPECT_TRUE(is_in_fundamental_domain(C(0.0, 1.0)));        // i
    EXPECT_TRUE(is_in_fundamental_domain(C(0.3, 1.5)));        // inside
    EXPECT_FALSE(is_in_fundamental_domain(C(0.6, 1.5)));       // Re > 1/2
    EXPECT_FALSE(is_in_fundamental_domain(C(0.0, 0.5)));       // |τ| < 1
}

TEST(PeriodMatrix, ComputePeriodMatrix_UnitSquare)
{
    // ω_1 = (1, 0), ω_2 = (0, 1)  →  τ = i
    HolonomyData hol;
    hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) };
    PeriodData pd = compute_period_matrix(hol, /*reduce=*/false);
    EXPECT_EQ(pd.genus(), 1);
    EXPECT_GT(pd.tau.imag(), 0.0);
    EXPECT_NEAR(pd.tau.real(), 0.0, 1e-10);
    EXPECT_NEAR(pd.tau.imag(), 1.0, 1e-10);
}

TEST(PeriodMatrix, ComputePeriodMatrix_ReducedTau_InFD)
{
    // ω_1 = (1, 0), ω_2 = (0.5, 0.25)  →  τ = 0.5 + 0.25i
    // |τ| = sqrt(0.25 + 0.0625) ≈ 0.559 < 1 → needs S step
    HolonomyData hol;
    hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.5, 0.25) };
    PeriodData pd = compute_period_matrix(hol, /*reduce=*/true);
    EXPECT_TRUE(pd.in_fundamental_domain);
    EXPECT_TRUE(is_in_fundamental_domain(pd.tau, 1e-9));
}

// ════════════════════════════════════════════════════════════════════════════
// FundamentalDomain — genus-1 parallelogram
// ════════════════════════════════════════════════════════════════════════════

TEST(FundamentalDomain, Genus1_HasFourVertices)
{
    HolonomyData hol;
    hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) };
    FundamentalDomain fd = compute_fundamental_domain_genus1(hol);
    EXPECT_EQ(fd.vertices.size(), 4u);
    EXPECT_TRUE(fd.is_valid());
}

TEST(FundamentalDomain, Genus1_VerticesMatchGenerators_UnitSquare)
{
    Eigen::Vector2d w1(1.0, 0.0), w2(0.0, 1.0);
    HolonomyData hol;
    hol.translations = { w1, w2 };
    FundamentalDomain fd = compute_fundamental_domain_genus1(hol);
    // Expected (CCW): origin, w1, w1+w2, w2
    EXPECT_NEAR(fd.vertices[0].x(), 0.0, 1e-12);
    EXPECT_NEAR(fd.vertices[0].y(), 0.0, 1e-12);
    EXPECT_NEAR(fd.vertices[1].x(), w1.x(), 1e-12);
    EXPECT_NEAR(fd.vertices[1].y(), w1.y(), 1e-12);
    EXPECT_NEAR(fd.vertices[2].x(), (w1 + w2).x(), 1e-12);
    EXPECT_NEAR(fd.vertices[2].y(), (w1 + w2).y(), 1e-12);
    EXPECT_NEAR(fd.vertices[3].x(), w2.x(), 1e-12);
    EXPECT_NEAR(fd.vertices[3].y(), w2.y(), 1e-12);
}

TEST(FundamentalDomain, Genus1_CCWOrientation)
{
    // After possible swap, the signed area = cross(v1-v0, v3-v0) > 0 (CCW)
    HolonomyData hol;
    hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) };
    FundamentalDomain fd = compute_fundamental_domain_genus1(hol);
    Eigen::Vector2d v0 = fd.vertices[0], v1 = fd.vertices[1], v3 = fd.vertices[3];
    double cross = (v1 - v0).x() * (v3 - v0).y() - (v1 - v0).y() * (v3 - v0).x();
    EXPECT_GT(cross, 0.0);
}

TEST(FundamentalDomain, Genus1_CCWEnforced_WhenInputIsCW)
{
    // If we give CW generators (w2 × w1 < 0), the polygon must still be CCW.
    // w1 = (0,1), w2 = (1,0):  cross w1×w2 = 0*0 - 1*1 = -1 < 0  →  should swap
    HolonomyData hol;
    hol.translations = { Eigen::Vector2d(0.0, 1.0), Eigen::Vector2d(1.0, 0.0) };
    FundamentalDomain fd = compute_fundamental_domain_genus1(hol);
    Eigen::Vector2d v0 = fd.vertices[0], v1 = fd.vertices[1], v3 = fd.vertices[3];
    double cross = (v1 - v0).x() * (v3 - v0).y() - (v1 - v0).y() * (v3 - v0).x();
    EXPECT_GT(cross, 0.0);
}

TEST(FundamentalDomain, Genus1_EdgeIdentifications)
{
    HolonomyData hol;
    hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) };
    FundamentalDomain fd = compute_fundamental_domain_genus1(hol);
    EXPECT_EQ(fd.edge_identifications.size(), 2u);
    // bottom ≡ top: (0,2)
    EXPECT_EQ(fd.edge_identifications[0].first,  0);
    EXPECT_EQ(fd.edge_identifications[0].second, 2);
    // right ≡ left: (1,3)
    EXPECT_EQ(fd.edge_identifications[1].first,  1);
    EXPECT_EQ(fd.edge_identifications[1].second, 3);
}

TEST(FundamentalDomain, Genus1_GeneratorsStored)
{
    Eigen::Vector2d w1(2.0, 1.0), w2(-1.0, 3.0);
    HolonomyData hol;
    hol.translations = { w1, w2 };
    FundamentalDomain fd = compute_fundamental_domain_genus1(hol);
    EXPECT_EQ(fd.generators.size(), 2u);
    // Generators are w1 and w2 (possibly swapped to ensure CCW)
    // Their sum of norms matches the originals
    double norm_gen = fd.generators[0].norm() + fd.generators[1].norm();
    double norm_in  = w1.norm() + w2.norm();
    EXPECT_NEAR(norm_gen, norm_in, 1e-10);
}

TEST(FundamentalDomain, HigherGenus_ReturnsEmpty)
{
    HolonomyData hol;
    hol.translations = {
        Eigen::Vector2d(1, 0), Eigen::Vector2d(0, 1),
        Eigen::Vector2d(2, 0), Eigen::Vector2d(0, 2)   // g=2, 4 generators
    };
    FundamentalDomain fd = compute_fundamental_domain(hol);
    // g > 1 returns empty (TODO Phase 8)
    EXPECT_FALSE(fd.is_valid());
}

// ════════════════════════════════════════════════════════════════════════════
// tiling_copy / tiling_neighbourhood
// ════════════════════════════════════════════════════════════════════════════

TEST(TilingCopy, ShiftAppliedToAllUV)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);

    Eigen::Vector2d w1(3.0, 0.0), w2(0.0, 2.0);
    // m=1, n=2 → expected shift = w1 + 2*w2 = (3, 4)
    Layout2D copy = tiling_copy(lay, w1, w2, 1, 2);
    Eigen::Vector2d expected_shift(3.0, 4.0);
    for (std::size_t i = 0; i < lay.uv.size(); ++i) {
        EXPECT_NEAR(copy.uv[i].x(), lay.uv[i].x() + expected_shift.x(), 1e-12);
        EXPECT_NEAR(copy.uv[i].y(), lay.uv[i].y() + expected_shift.y(), 1e-12);
    }
}

TEST(TilingCopy, ZeroShift_IsSameAsCopy)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    Eigen::Vector2d w1(1, 0), w2(0, 1);
    Layout2D copy = tiling_copy(lay, w1, w2, 0, 0);
    for (std::size_t i = 0; i < lay.uv.size(); ++i) {
        EXPECT_NEAR(copy.uv[i].x(), lay.uv[i].x(), 1e-12);
        EXPECT_NEAR(copy.uv[i].y(), lay.uv[i].y(), 1e-12);
    }
}

TEST(TilingNeighbourhood, CorrectCount)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    HolonomyData hol;
    hol.translations = { Eigen::Vector2d(1, 0), Eigen::Vector2d(0, 1) };
    // m_max=1, n_max=1 → (2*1+1) * (2*1+1) = 9 tiles
    auto tiles = tiling_neighbourhood(lay, hol, 1, 1);
    EXPECT_EQ(tiles.size(), 9u);
}

TEST(TilingNeighbourhood, EmptyHolonomy_ReturnsSingleTile)
{
    auto mesh = make_quad_strip();
    auto lay   = make_euclidean_layout(mesh);
    HolonomyData hol;  // no translations
    auto tiles = tiling_neighbourhood(lay, hol);
    EXPECT_EQ(tiles.size(), 1u);
}
