// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 Torgeir Børresen <tb@starkad.no>
// Part of a Rust port of Google's S2 Geometry library. The upstream
// implementations — C++ (google/s2geometry), Go (golang/geo), and Java
// (google/s2-geometry-library-java) — are licensed under the Apache License,
// Version 2.0, and are Copyright Google Inc. See LICENSE.

//! Cross-validation tests: compare Rust S2 types against C++ reference output.
//!
//! The CSV test vectors were generated by the C++ s2geometry library
//! (`s2geometry/doc/examples/generate_test_vectors.cc`). Each section starts
//! with a `# SECTION_NAME` header line followed by CSV data rows.

use s2rst::r1;
use s2rst::s1;
use s2rst::s2::{Cell, CellId, LatLng, Loop, Point, Rect};

const CSV: &str = include_str!("data/cpp_test_vectors.csv");

// ---------------------------------------------------------------------------
// CSV parsing helpers
// ---------------------------------------------------------------------------

/// Extract lines belonging to a named section (e.g. "`CELLID_FROM_POINT`").
fn section_lines(name: &str) -> Vec<&'static str> {
    let header = format!("# {name}");
    let mut in_section = false;
    let mut lines = Vec::new();
    for line in CSV.lines() {
        if line == header {
            in_section = true;
            continue;
        }
        if in_section {
            if line.starts_with('#') || line.is_empty() {
                break;
            }
            lines.push(line);
        }
    }
    lines
}

fn parse_f64(s: &str) -> f64 {
    s.trim().parse::<f64>().unwrap()
}

fn parse_u64(s: &str) -> u64 {
    s.trim().parse::<u64>().unwrap()
}

fn parse_i32(s: &str) -> i32 {
    s.trim().parse::<i32>().unwrap()
}

fn parse_bool(s: &str) -> bool {
    s.trim() == "1"
}

/// Assert two f64 values are within absolute tolerance.
fn assert_close(a: f64, b: f64, tol: f64, context: &str) {
    let diff = (a - b).abs();
    assert!(diff <= tol, "{context}: {a} vs {b}, diff={diff}, tol={tol}");
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[test]
fn test_cpp_cellid_from_point() {
    for line in section_lines("CELLID_FROM_POINT") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 8, "bad line: {line}");
        let x = parse_f64(f[0]);
        let y = parse_f64(f[1]);
        let z = parse_f64(f[2]);
        let expected_id = parse_u64(f[3]);
        let expected_face = s2rst::s2::Face::from_u8(parse_i32(f[4]) as u8);
        let expected_level = parse_i32(f[5]) as u8;
        let expected_valid = parse_bool(f[6]);
        let expected_leaf = parse_bool(f[7]);

        let p = Point::from_coords(x, y, z);
        let id = CellId::from_point(&p);

        assert_eq!(
            id.id(),
            expected_id,
            "cell_id mismatch for point ({x},{y},{z})"
        );
        assert_eq!(
            id.face(),
            expected_face,
            "face mismatch for point ({x},{y},{z})"
        );
        assert_eq!(
            id.level(),
            expected_level,
            "level mismatch for point ({x},{y},{z})"
        );
        assert_eq!(
            id.is_valid(),
            expected_valid,
            "is_valid mismatch for point ({x},{y},{z})"
        );
        assert_eq!(
            id.is_leaf(),
            expected_leaf,
            "is_leaf mismatch for point ({x},{y},{z})"
        );
    }
}

#[test]
fn test_cpp_cellid_from_lat_lng() {
    for line in section_lines("CELLID_FROM_LAT_LNG") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 4, "bad line: {line}");
        let lat_deg = parse_f64(f[0]);
        let lng_deg = parse_f64(f[1]);
        let expected_id = parse_u64(f[2]);
        let expected_face = s2rst::s2::Face::from_u8(parse_i32(f[3]) as u8);

        let ll = LatLng::from_degrees(lat_deg, lng_deg);
        let id = CellId::from_lat_lng(&ll);

        assert_eq!(
            id.id(),
            expected_id,
            "cell_id mismatch for ({lat_deg},{lng_deg})"
        );
        assert_eq!(
            id.face(),
            expected_face,
            "face mismatch for ({lat_deg},{lng_deg})"
        );
    }
}

#[test]
fn test_cpp_cellid_to_point() {
    for line in section_lines("CELLID_TO_POINT") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 4, "bad line: {line}");
        let cell_id = parse_u64(f[0]);
        let expected_x = parse_f64(f[1]);
        let expected_y = parse_f64(f[2]);
        let expected_z = parse_f64(f[3]);

        let id = CellId(cell_id);
        let p = id.to_point();

        assert_close(p.x(), expected_x, 1e-15, &format!("x for cell {cell_id}"));
        assert_close(p.y(), expected_y, 1e-15, &format!("y for cell {cell_id}"));
        assert_close(p.z(), expected_z, 1e-15, &format!("z for cell {cell_id}"));
    }
}

#[test]
fn test_cpp_cellid_hierarchy() {
    for line in section_lines("CELLID_HIERARCHY") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 9, "bad line: {line}");
        let cell_id = parse_u64(f[0]);
        let level = parse_i32(f[1]) as u8;
        let expected_parent = parse_u64(f[2]);
        let expected_child0 = parse_u64(f[3]);
        let expected_child1 = parse_u64(f[4]);
        let expected_child2 = parse_u64(f[5]);
        let expected_child3 = parse_u64(f[6]);
        let expected_range_min = parse_u64(f[7]);
        let expected_range_max = parse_u64(f[8]);

        let id = CellId(cell_id);
        assert_eq!(id.level(), level, "level mismatch for cell {cell_id}");

        // Parent
        if level > 0 {
            assert_eq!(
                id.parent().id(),
                expected_parent,
                "parent mismatch for cell {cell_id}"
            );
        }

        // Children
        if !id.is_leaf() {
            let children = id.children();
            assert_eq!(
                children[0].id(),
                expected_child0,
                "child0 mismatch for cell {cell_id}"
            );
            assert_eq!(
                children[1].id(),
                expected_child1,
                "child1 mismatch for cell {cell_id}"
            );
            assert_eq!(
                children[2].id(),
                expected_child2,
                "child2 mismatch for cell {cell_id}"
            );
            assert_eq!(
                children[3].id(),
                expected_child3,
                "child3 mismatch for cell {cell_id}"
            );
        }

        // Range
        assert_eq!(
            id.range_min().id(),
            expected_range_min,
            "range_min mismatch for cell {cell_id}"
        );
        assert_eq!(
            id.range_max().id(),
            expected_range_max,
            "range_max mismatch for cell {cell_id}"
        );
    }
}

#[test]
fn test_cpp_cellid_tokens() {
    for line in section_lines("CELLID_TOKENS") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 2, "bad line: {line}");
        let cell_id = parse_u64(f[0]);
        let expected_token = f[1].trim();

        let id = CellId(cell_id);
        let token = id.to_token();
        assert_eq!(
            token, expected_token,
            "to_token mismatch for cell {cell_id}"
        );

        // Roundtrip
        let back = CellId::from_token(&token);
        assert_eq!(
            back.id(),
            cell_id,
            "from_token roundtrip mismatch for token '{token}'"
        );
    }
}

#[test]
fn test_cpp_latlng_conversion() {
    for line in section_lines("LATLNG_CONVERSION") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 7, "bad line: {line}");
        let lat_deg = parse_f64(f[0]);
        let lng_deg = parse_f64(f[1]);
        let expected_px = parse_f64(f[2]);
        let expected_py = parse_f64(f[3]);
        let expected_pz = parse_f64(f[4]);
        let expected_lat_rad = parse_f64(f[5]);
        let expected_lng_rad = parse_f64(f[6]);

        let ll = LatLng::from_degrees(lat_deg, lng_deg);
        let p = ll.to_point();
        let ctx = format!("({lat_deg},{lng_deg})");

        assert_close(p.x(), expected_px, 1e-15, &format!("px for {ctx}"));
        assert_close(p.y(), expected_py, 1e-15, &format!("py for {ctx}"));
        assert_close(p.z(), expected_pz, 1e-15, &format!("pz for {ctx}"));

        let back = LatLng::from_point(p);
        assert_close(
            back.lat.radians(),
            expected_lat_rad,
            1e-15,
            &format!("lat_rad for {ctx}"),
        );
        assert_close(
            back.lng.radians(),
            expected_lng_rad,
            1e-15,
            &format!("lng_rad for {ctx}"),
        );
    }
}

#[test]
fn test_cpp_latlng_distance() {
    for line in section_lines("LATLNG_DISTANCE") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 5, "bad line: {line}");
        let lat1 = parse_f64(f[0]);
        let lng1 = parse_f64(f[1]);
        let lat2 = parse_f64(f[2]);
        let lng2 = parse_f64(f[3]);
        let expected_dist = parse_f64(f[4]);

        let a = LatLng::from_degrees(lat1, lng1);
        let b = LatLng::from_degrees(lat2, lng2);
        let dist = a.get_distance(b).radians();

        assert_close(
            dist,
            expected_dist,
            1e-13,
            &format!("distance ({lat1},{lng1})->({lat2},{lng2})"),
        );
    }
}

#[test]
fn test_cpp_point_distance() {
    for line in section_lines("POINT_DISTANCE") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 7, "bad line: {line}");
        let x1 = parse_f64(f[0]);
        let y1 = parse_f64(f[1]);
        let z1 = parse_f64(f[2]);
        let x2 = parse_f64(f[3]);
        let y2 = parse_f64(f[4]);
        let z2 = parse_f64(f[5]);
        let expected_angle = parse_f64(f[6]);

        let a = Point::from_coords(x1, y1, z1);
        let b = Point::from_coords(x2, y2, z2);
        let angle = a.distance(b).radians();

        assert_close(
            angle,
            expected_angle,
            1e-15,
            &format!("angle ({x1},{y1},{z1})->({x2},{y2},{z2})"),
        );
    }
}

#[test]
fn test_cpp_cell_area() {
    for line in section_lines("CELL_AREA") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 4, "bad line: {line}");
        let cell_id = parse_u64(f[0]);
        let _level = parse_i32(f[1]) as u8;
        let expected_approx = parse_f64(f[2]);
        let expected_avg = parse_f64(f[3]);

        let cell = Cell::from_cell_id(CellId(cell_id));
        let approx = cell.approx_area();
        let avg = cell.average_area();

        assert_close(
            approx,
            expected_approx,
            1e-13,
            &format!("approx_area for cell {cell_id}"),
        );
        assert_close(
            avg,
            expected_avg,
            1e-13,
            &format!("average_area for cell {cell_id}"),
        );
    }
}

#[test]
fn test_cpp_cell_vertices() {
    for line in section_lines("CELL_VERTICES") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 13, "bad line: {line}");
        let cell_id = parse_u64(f[0]);

        let cell = Cell::from_cell_id(CellId(cell_id));

        for k in 0..4 {
            let ex = parse_f64(f[1 + k * 3]);
            let ey = parse_f64(f[2 + k * 3]);
            let ez = parse_f64(f[3 + k * 3]);

            let v = cell.vertex(k);
            assert_close(
                v.x(),
                ex,
                1e-15,
                &format!("vertex {k} x for cell {cell_id}"),
            );
            assert_close(
                v.y(),
                ey,
                1e-15,
                &format!("vertex {k} y for cell {cell_id}"),
            );
            assert_close(
                v.z(),
                ez,
                1e-15,
                &format!("vertex {k} z for cell {cell_id}"),
            );
        }
    }
}

#[test]
fn test_cpp_rect_operations() {
    let deg2rad = |d: f64| d * std::f64::consts::PI / 180.0;
    for line in section_lines("RECT_OPERATIONS") {
        let f: Vec<&str> = line.split(',').collect();
        assert_eq!(f.len(), 20, "bad line: {line}");
        let lat_lo1 = parse_f64(f[0]);
        let lat_hi1 = parse_f64(f[1]);
        let lng_lo1 = parse_f64(f[2]);
        let lng_hi1 = parse_f64(f[3]);
        let lat_lo2 = parse_f64(f[4]);
        let lat_hi2 = parse_f64(f[5]);
        let lng_lo2 = parse_f64(f[6]);
        let lng_hi2 = parse_f64(f[7]);
        let expected_area1 = parse_f64(f[8]);
        let expected_area2 = parse_f64(f[9]);
        let expected_contains = parse_bool(f[10]);
        let expected_intersects = parse_bool(f[11]);
        let expected_u_lat_lo = parse_f64(f[12]);
        let expected_u_lat_hi = parse_f64(f[13]);
        let expected_u_lng_lo = parse_f64(f[14]);
        let expected_u_lng_hi = parse_f64(f[15]);
        let expected_i_lat_lo = parse_f64(f[16]);
        let expected_i_lat_hi = parse_f64(f[17]);
        let expected_i_lng_lo = parse_f64(f[18]);
        let expected_i_lng_hi = parse_f64(f[19]);

        let r1 = Rect::new(
            r1::Interval::new(deg2rad(lat_lo1), deg2rad(lat_hi1)),
            s1::Interval::new(deg2rad(lng_lo1), deg2rad(lng_hi1)),
        );
        let r2 = Rect::new(
            r1::Interval::new(deg2rad(lat_lo2), deg2rad(lat_hi2)),
            s1::Interval::new(deg2rad(lng_lo2), deg2rad(lng_hi2)),
        );

        let ctx = format!(
            "rects [{lat_lo1},{lat_hi1},{lng_lo1},{lng_hi1}] vs [{lat_lo2},{lat_hi2},{lng_lo2},{lng_hi2}]"
        );

        assert_close(r1.area(), expected_area1, 1e-13, &format!("area1 {ctx}"));
        assert_close(r2.area(), expected_area2, 1e-13, &format!("area2 {ctx}"));
        assert_eq!(
            r1.contains(r2),
            expected_contains,
            "contains mismatch: {ctx}"
        );
        assert_eq!(
            r1.intersects(r2),
            expected_intersects,
            "intersects mismatch: {ctx}"
        );

        let u = r1.union(r2);
        assert_close(
            u.lat.lo,
            deg2rad(expected_u_lat_lo),
            1e-13,
            &format!("union lat_lo {ctx}"),
        );
        assert_close(
            u.lat.hi,
            deg2rad(expected_u_lat_hi),
            1e-13,
            &format!("union lat_hi {ctx}"),
        );
        assert_close(
            u.lng.lo,
            deg2rad(expected_u_lng_lo),
            1e-13,
            &format!("union lng_lo {ctx}"),
        );
        assert_close(
            u.lng.hi,
            deg2rad(expected_u_lng_hi),
            1e-13,
            &format!("union lng_hi {ctx}"),
        );

        let i = r1.intersection(r2);
        // For disjoint rects, intersection is empty — check emptiness via lat
        if expected_i_lat_lo > expected_i_lat_hi {
            assert!(i.is_empty(), "expected empty intersection: {ctx}");
        } else {
            assert_close(
                i.lat.lo,
                deg2rad(expected_i_lat_lo),
                1e-13,
                &format!("isect lat_lo {ctx}"),
            );
            assert_close(
                i.lat.hi,
                deg2rad(expected_i_lat_hi),
                1e-13,
                &format!("isect lat_hi {ctx}"),
            );
            assert_close(
                i.lng.lo,
                deg2rad(expected_i_lng_lo),
                1e-13,
                &format!("isect lng_lo {ctx}"),
            );
            assert_close(
                i.lng.hi,
                deg2rad(expected_i_lng_hi),
                1e-13,
                &format!("isect lng_hi {ctx}"),
            );
        }
    }
}

#[test]
fn test_cpp_loop_area() {
    for line in section_lines("LOOP_AREA") {
        // Format: vertices,area,turning_angle,cx,cy,cz,is_normalized
        // Vertices are semicolon-separated "x,y,z;x,y,z;..."
        // But vertices themselves contain commas, so we need to find
        // the boundary between vertex data and the trailing fields.
        // The last 6 fields are: area, turning_angle, cx, cy, cz, is_normalized
        // Work backwards from the end.
        let parts: Vec<&str> = line.rsplitn(7, ',').collect();
        // rsplitn(7, ',') gives: [is_normalized, cz, cy, cx, turning_angle, area, vertices_part]
        assert_eq!(parts.len(), 7, "bad line: {line}");
        let is_normalized = parse_bool(parts[0]);
        let cz = parse_f64(parts[1]);
        let cy = parse_f64(parts[2]);
        let cx = parse_f64(parts[3]);
        let turning_angle = parse_f64(parts[4]);
        let area = parse_f64(parts[5]);
        let vertices_str = parts[6];

        // Parse vertices: semicolon-separated "x,y,z" triples
        let vertex_strs: Vec<&str> = vertices_str.split(';').collect();
        let mut vertices = Vec::new();
        for vs in &vertex_strs {
            let coords: Vec<&str> = vs.split(',').collect();
            assert_eq!(coords.len(), 3, "bad vertex: {vs}");
            let x = parse_f64(coords[0]);
            let y = parse_f64(coords[1]);
            let z = parse_f64(coords[2]);
            vertices.push(Point::from_coords(x, y, z));
        }

        let l = Loop::new(vertices);

        assert_close(
            l.area(),
            area,
            1e-13,
            &format!("area for loop with {} vertices", vertex_strs.len()),
        );
        assert_close(
            l.turning_angle(),
            turning_angle,
            1e-13,
            &format!("turning_angle for loop with {} vertices", vertex_strs.len()),
        );

        let centroid = l.centroid();
        assert_close(
            centroid.x(),
            cx,
            1e-13,
            &format!("centroid x for loop with {} vertices", vertex_strs.len()),
        );
        assert_close(
            centroid.y(),
            cy,
            1e-13,
            &format!("centroid y for loop with {} vertices", vertex_strs.len()),
        );
        assert_close(
            centroid.z(),
            cz,
            1e-13,
            &format!("centroid z for loop with {} vertices", vertex_strs.len()),
        );
        assert_eq!(
            l.is_normalized(),
            is_normalized,
            "is_normalized mismatch for loop with {} vertices",
            vertex_strs.len()
        );
    }
}
