// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 lacklustr@protonmail.com https://github.com/eadf

use super::*;
use crate::common::sealed::ScalarType;
use vector_traits::glam::Vec3;

fn make_triangle<S: ScalarType>(v0: S::Vec3, v1: S::Vec3, v2: S::Vec3) -> Triangle<S> {
    let edge0 = v1 - v0;
    let edge1 = v2 - v0;
    let normal = edge0.cross(edge1).normalize();

    Triangle { v0, v1, v2, normal }
}

#[test]
fn test_project_inside_triangle() {
    // Triangle in XY plane at z=0
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point above center of triangle
    let p = Vec3::new(0.25, 0.25, 1.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        (result.x - 0.25).abs() < 0.001,
        "x should be 0.25, got {}",
        result.x
    );
    assert!(
        (result.y - 0.25).abs() < 0.001,
        "y should be 0.25, got {}",
        result.y
    );
    assert!(
        (result.z).abs() < 0.001,
        "z should be 0.0, got {}",
        result.z
    );
}

#[test]
fn test_project_below_triangle() {
    let tri = make_triangle(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point below triangle
    let p = Vec3::new(0.3, 0.3, -2.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        (result.x - 0.3).abs() < 0.001,
        "x should be 0.3, got {}",
        result.x
    );
    assert!(
        (result.y - 0.3).abs() < 0.001,
        "y should be 0.3, got {}",
        result.y
    );
    assert!(
        (result.z).abs() < 0.001,
        "z should be 0.0, got {}",
        result.z
    );
}

#[test]
fn test_project_to_vertex_a() {
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point near vertex A
    let p = Vec3::new(-1.0, -1.0, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        result.distance(tri.v0) < 0.001,
        "Should project to vertex A (0,0,0), got {:?}",
        result
    );
}

#[test]
fn test_project_to_vertex_b() {
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point near vertex B
    let p = Vec3::new(2.0, -1.0, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        result.distance(tri.v1) < 0.001,
        "Should project to vertex B (1,0,0), got {:?}",
        result
    );
}

#[test]
fn test_project_to_vertex_c() {
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point near vertex C
    let p = Vec3::new(-1.0, 2.0, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        result.distance(tri.v2) < 0.001,
        "Should project to vertex C (0,1,0), got {:?}",
        result
    );
}

#[test]
fn test_project_to_edge_ab() {
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point below edge AB
    let p = Vec3::new(0.5, -1.0, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        (result.x - 0.5).abs() < 0.001,
        "x should be 0.5, got {}",
        result.x
    );
    assert!(
        (result.y).abs() < 0.001,
        "y should be 0.0, got {}",
        result.y
    );
    assert!(
        (result.z).abs() < 0.001,
        "z should be 0.0, got {}",
        result.z
    );
}

#[test]
fn test_project_to_edge_ac() {
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point to the left of edge AC
    let p = Vec3::new(-1.0, 0.5, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        (result.x).abs() < 0.001,
        "x should be 0.0, got {}",
        result.x
    );
    assert!(
        (result.y - 0.5).abs() < 0.001,
        "y should be 0.5, got {}",
        result.y
    );
    assert!(
        (result.z).abs() < 0.001,
        "z should be 0.0, got {}",
        result.z
    );
}

#[test]
fn test_project_to_edge_bc() {
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point outside, nearest to edge BC (hypotenuse)
    let p = Vec3::new(1.0, 1.0, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    // Should project to midpoint of BC edge
    assert!(
        (result.x - 0.5).abs() < 0.001,
        "x should be ~0.5, got {}",
        result.x
    );
    assert!(
        (result.y - 0.5).abs() < 0.001,
        "y should be ~0.5, got {}",
        result.y
    );
    assert!(
        (result.z).abs() < 0.001,
        "z should be 0.0, got {}",
        result.z
    );
}

#[test]
fn test_project_on_triangle_plane() {
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    // Point already on the triangle
    let p = Vec3::new(0.3, 0.3, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        result.distance(p) < 0.001,
        "Point on triangle should project to itself, got {:?}",
        result
    );
}

#[test]
fn test_project_tilted_triangle() {
    // Triangle not aligned with any axis
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 1.0),
        Vec3::new(0.0, 1.0, 1.0),
    );

    // Point above the tilted triangle
    let p = Vec3::new(0.3, 0.3, 2.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    // Check that result is on the triangle plane
    let edge0 = tri.v1 - tri.v0;
    let edge1 = tri.v2 - tri.v0;
    let normal = edge0.cross(edge1).normalize();

    let v0_to_result = result - tri.v0;
    let dist_to_plane = v0_to_result.dot(normal).abs();

    assert!(
        dist_to_plane < 0.001,
        "Result should be on triangle plane, distance: {}",
        dist_to_plane
    );
}

#[test]
fn test_project_large_triangle() {
    // Very large triangle
    let tri = make_triangle::<f32>(
        Vec3::new(-100.0, -100.0, 0.0),
        Vec3::new(100.0, -100.0, 0.0),
        Vec3::new(0.0, 100.0, 0.0),
    );

    // Small point inside
    let p = Vec3::new(1.0, 1.0, 5.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        (result.x - 1.0).abs() < 0.001,
        "x should be 1.0, got {}",
        result.x
    );
    assert!(
        (result.y - 1.0).abs() < 0.001,
        "y should be 1.0, got {}",
        result.y
    );
    assert!(
        (result.z).abs() < 0.001,
        "z should be 0.0, got {}",
        result.z
    );
}

#[test]
fn test_project_equilateral_triangle() {
    // Equilateral triangle centered at origin
    let h = (3.0_f32).sqrt() / 2.0; // height of equilateral triangle with side 1
    let tri = make_triangle::<f32>(
        Vec3::new(-0.5, -h / 3.0, 0.0),
        Vec3::new(0.5, -h / 3.0, 0.0),
        Vec3::new(0.0, 2.0 * h / 3.0, 0.0),
    );

    // Point directly above center
    let p = Vec3::new(0.0, 0.0, 1.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!("Point: {:?}, Projected: {:?}", p, result);

    assert!(
        (result.x).abs() < 0.001,
        "x should be ~0.0, got {}",
        result.x
    );
    assert!(
        (result.y).abs() < 0.001,
        "y should be ~0.0, got {}",
        result.y
    );
    assert!(
        (result.z).abs() < 0.001,
        "z should be 0.0, got {}",
        result.z
    );
}

#[should_panic]
#[test]
fn test_project_degenerate_triangle() {
    // Degenerate triangle (all points collinear)
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(2.0, 0.0, 0.0),
    );

    let p = Vec3::new(0.5, 1.0, 0.0);
    let result = project_point_to_triangle::<f32>(p, &tri);

    println!(
        "Degenerate triangle - Point: {:?}, Projected: {:?}",
        p, result
    );

    // Should project to one of the vertices (likely the closest one)
    let dist_a = result.distance(tri.v0);
    let dist_b = result.distance(tri.v1);
    let dist_c = result.distance(tri.v2);

    let on_vertex = dist_a < 0.001 || dist_b < 0.001 || dist_c < 0.001;
    assert!(
        on_vertex,
        "Should project to a vertex for degenerate triangle, got {:?}",
        result
    );
}

#[test]
fn test_project_consistency() {
    // Test that projecting the same point multiple times gives the same result
    let tri = make_triangle::<f32>(
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(1.0, 0.0, 0.0),
        Vec3::new(0.0, 1.0, 0.0),
    );

    let p = Vec3::new(0.4, 0.4, 3.0);

    let result1 = project_point_to_triangle::<f32>(p, &tri);
    let result2 = project_point_to_triangle::<f32>(p, &tri);
    let result3 = project_point_to_triangle::<f32>(p, &tri);

    assert_eq!(result1, result2);
    assert_eq!(result2, result3);
}
