// Copyright (c) 2024-2026 Tarik Moussa.
// SPDX-License-Identifier: MIT

// test_stereographic_layout.cpp
//
// Tests for stereographic_layout.hpp (Phase 9d.3).
// Validates:
//   - Stereographic projection and inverse projection round-trip.
//   - North pole projects to infinity.
//   - South pole projects to origin.
//   - Stereographic layout from a spherical layout.

#include <gtest/gtest.h>
#include "conformal_mesh.hpp"
#include "layout.hpp"
#include "stereographic_layout.hpp"
#include <Eigen/Dense>

namespace cl = conformallab;

// ────────────────────────────────────────────────────────────────────────────
// Tests: Stereographic Projection
// ────────────────────────────────────────────────────────────────────────────

TEST(StereographicProjection, SouthPoleProjectsToOrigin)
{
    // South pole: (0, 0, -1).
    auto z = cl::stereographic_project(0.0, 0.0, -1.0);

    EXPECT_NEAR(z.real(), 0.0, 1e-10)
        << "South pole should project to (0,0) in ℂ";
    EXPECT_NEAR(z.imag(), 0.0, 1e-10)
        << "South pole should project to (0,0) in ℂ";
}

TEST(StereographicProjection, NorthPoleProjectsToInfinity)
{
    // North pole: (0, 0, 1).
    auto z = cl::stereographic_project(0.0, 0.0, 1.0);

    // Returns NaN to signal infinity.
    EXPECT_TRUE(std::isnan(z.real()))
        << "North pole should project to ∞ (NaN)";
    EXPECT_TRUE(std::isnan(z.imag()))
        << "North pole should project to ∞ (NaN)";
}

TEST(StereographicProjection, EquatorProjectsToUnitInComplex)
{
    // Equator point: (1, 0, 0).
    auto z = cl::stereographic_project(1.0, 0.0, 0.0);

    // Formula: (1 + 0i) / (1 - 0) = 1.
    EXPECT_NEAR(z.real(), 1.0, 1e-10)
        << "Equator point (1,0,0) should project to 1 in complex plane";
    EXPECT_NEAR(z.imag(), 0.0, 1e-10);
}

TEST(StereographicProjection, AnotherEquatorPoint)
{
    // Equator point: (0, 1, 0).
    auto z = cl::stereographic_project(0.0, 1.0, 0.0);

    // Formula: (0 + 1i) / (1 - 0) = i.
    EXPECT_NEAR(z.real(), 0.0, 1e-10)
        << "Equator point (0,1,0) should project to i in ℂ";
    EXPECT_NEAR(z.imag(), 1.0, 1e-10);
}

// ────────────────────────────────────────────────────────────────────────────
// Tests: Inverse Stereographic Projection
// ────────────────────────────────────────────────────────────────────────────

TEST(InverseStereographicProjection, OriginMapsToSouthPole)
{
    auto z = std::complex<double>(0.0, 0.0);
    auto p = cl::inverse_stereographic_project(z);

    EXPECT_NEAR(p.x(), 0.0, 1e-10)
        << "Origin should map to (0,0,-1)";
    EXPECT_NEAR(p.y(), 0.0, 1e-10);
    EXPECT_NEAR(p.z(), -1.0, 1e-10);
}

TEST(InverseStereographicProjection, OneMapsToEquatorPoint)
{
    auto z = std::complex<double>(1.0, 0.0);
    auto p = cl::inverse_stereographic_project(z);

    EXPECT_NEAR(p.x(), 1.0, 1e-10)
        << "1 in complex plane should map to (1,0,0)";
    EXPECT_NEAR(p.y(), 0.0, 1e-10);
    EXPECT_NEAR(p.z(), 0.0, 1e-10);
}

TEST(InverseStereographicProjection, ImaginaryUnitMapsToEquator)
{
    auto z = std::complex<double>(0.0, 1.0);
    auto p = cl::inverse_stereographic_project(z);

    EXPECT_NEAR(p.x(), 0.0, 1e-10)
        << "i in complex plane should map to (0,1,0)";
    EXPECT_NEAR(p.y(), 1.0, 1e-10);
    EXPECT_NEAR(p.z(), 0.0, 1e-10);
}

// ────────────────────────────────────────────────────────────────────────────
// Tests: Round-Trip Consistency
// ────────────────────────────────────────────────────────────────────────────

TEST(StereographicRoundTrip, ProjectAndInvert_South)
{
    cl::Point3 south(0.0, 0.0, -1.0);
    double error = cl::stereographic_roundtrip_error(south);

    EXPECT_LT(error, 1e-10)
        << "South pole round-trip should be accurate";
}

TEST(StereographicRoundTrip, ProjectAndInvert_Equator)
{
    cl::Point3 eq1(1.0, 0.0, 0.0);
    double error1 = cl::stereographic_roundtrip_error(eq1);
    EXPECT_LT(error1, 1e-10)
        << "Equator point round-trip should be accurate";

    cl::Point3 eq2(0.0, 1.0, 0.0);
    double error2 = cl::stereographic_roundtrip_error(eq2);
    EXPECT_LT(error2, 1e-10)
        << "Another equator point round-trip should be accurate";
}

TEST(StereographicRoundTrip, ProjectAndInvert_RandomSphericalPoint)
{
    // Arbitrary point on the unit sphere: normalize (1, 2, 3).
    double norm = std::sqrt(1.0*1.0 + 2.0*2.0 + 3.0*3.0);
    cl::Point3 p(1.0/norm, 2.0/norm, 3.0/norm);

    double error = cl::stereographic_roundtrip_error(p);
    EXPECT_LT(error, 1e-10)
        << "Arbitrary spherical point round-trip should be accurate";
}

TEST(StereographicRoundTrip, ProjectAndInvert_NearNorthPole)
{
    // Point very close to the north pole: (0, 0, 0.99999).
    cl::Point3 close_to_north(0.0, 0.0, 0.99999);
    double error = cl::stereographic_roundtrip_error(close_to_north);

    // Near the north pole, the projection maps to a very large complex number.
    // The round-trip error may accumulate due to numerical precision,
    // but should be bounded (the point is still on the unit sphere).
    EXPECT_LT(error, 2.1)
        << "Point near north pole should have reasonable error";
}

// ────────────────────────────────────────────────────────────────────────────
// Tests: Stereographic Layout Conversion
// ────────────────────────────────────────────────────────────────────────────

TEST(StereographicLayout, ConvertsSphericalLayoutTo2D)
{
    // Create a simple tetrahedron mesh (all vertices roughly on a sphere).
    cl::ConformalMesh mesh;
    auto v0 = mesh.add_vertex(cl::Point3(1.0, 0.0, 0.0));
    auto v1 = mesh.add_vertex(cl::Point3(0.0, 1.0, 0.0));
    auto v2 = mesh.add_vertex(cl::Point3(0.0, 0.0, 1.0));
    mesh.add_face(v0, v1, v2);

    // Create a corresponding 3-D spherical layout
    // (place vertices on the unit sphere).
    cl::Layout3D spherical_layout;
    spherical_layout.pos.resize(3);
    spherical_layout.pos[0] = Eigen::Vector3d(1.0, 0.0, 0.0);
    spherical_layout.pos[1] = Eigen::Vector3d(0.0, 1.0, 0.0);
    spherical_layout.pos[2] = Eigen::Vector3d(0.0, 0.0, 1.0);

    // Convert to stereographic layout.
    auto planar_layout = cl::stereographic_layout(mesh, spherical_layout);

    // Check that the output is 2-D (uv coordinates).
    EXPECT_EQ(planar_layout.uv.size(), 3)
        << "Output layout should have 3 vertices";

    // South pole (0,0,-1) would project to (0,0);
    // Equator points project to unit circle.
    // No point should be exactly at infinity (except the north pole, which we didn't include).
    for (const auto& uv : planar_layout.uv) {
        EXPECT_TRUE(std::isfinite(uv[0]) || std::isnan(uv[0]))
            << "Output coordinates should be finite or NaN";
        EXPECT_TRUE(std::isfinite(uv[1]) || std::isnan(uv[1]));
    }
}

TEST(StereographicLayout, CentresLayout)
{
    cl::ConformalMesh mesh;
    auto v0 = mesh.add_vertex(cl::Point3(1.0, 0.0, 0.0));
    auto v1 = mesh.add_vertex(cl::Point3(0.0, 1.0, 0.0));
    auto v2 = mesh.add_vertex(cl::Point3(-1.0, 0.0, 0.0));
    mesh.add_face(v0, v1, v2);

    cl::Layout3D spherical_layout;
    spherical_layout.pos.resize(3);
    spherical_layout.pos[0] = Eigen::Vector3d(1.0, 0.0, 0.0);
    spherical_layout.pos[1] = Eigen::Vector3d(0.0, 1.0, 0.0);
    spherical_layout.pos[2] = Eigen::Vector3d(-1.0, 0.0, 0.0);

    auto planar_layout = cl::stereographic_layout(mesh, spherical_layout);

    // Compute centroid of valid points.
    double cx = 0.0, cy = 0.0;
    int n_valid = 0;
    for (const auto& uv : planar_layout.uv) {
        if (std::isfinite(uv[0]) && std::isfinite(uv[1])) {
            cx += uv[0];
            cy += uv[1];
            n_valid++;
        }
    }
    if (n_valid > 0) {
        cx /= n_valid;
        cy /= n_valid;
    }

    // After centring, centroid should be close to (0,0).
    EXPECT_LT(std::abs(cx), 0.5)
        << "Centroid x should be small after centring";
    EXPECT_LT(std::abs(cy), 0.5)
        << "Centroid y should be small after centring";
}

// ────────────────────────────────────────────────────────────────────────────
// Sanity Tests
// ────────────────────────────────────────────────────────────────────────────

TEST(StereographicLayout_Sanity, ProjectionIsConformal)
{
    // Stereographic projection is conformal (angle-preserving).
    // Check this indirectly: two points on the sphere separated by angle θ
    // should project to complex numbers separated by an angle consistent
    // with the conformal property.

    // Two points on the equator: (1,0,0) and (0,1,0), 90° apart.
    auto z1 = cl::stereographic_project(1.0, 0.0, 0.0);
    auto z2 = cl::stereographic_project(0.0, 1.0, 0.0);

    // In the complex plane, their argument difference should be ~90°.
    double arg1 = std::arg(z1);  // atan2(0, 1) = 0
    double arg2 = std::arg(z2);  // atan2(1, 0) = π/2

    double arg_diff = std::abs(arg2 - arg1);
    EXPECT_NEAR(arg_diff, M_PI / 2.0, 1e-10)
        << "Stereographic projection should preserve angles";
}

