// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 Torgeir Børresen <tb@starkad.no>
// Written for this crate (not ported from upstream S2).

//! Differential tests against the Go S2 implementation (github.com/golang/geo).
//!
//! The vectors in `data/go_test_vectors.csv` are generated by `tools/refgen`
//! using golang/geo as an *independent* oracle. Unlike same-implementation unit
//! tests, these catch *wrong answers* — a flipped predicate, a drifted result —
//! by comparing against a second mature S2 port.
//!
//! Coordinates are emitted at full f64 precision and reconstructed here with the
//! non-normalizing [`Point::new`], so the points are bit-identical to the ones
//! the oracle used. The orientation predicate is exact, so the results must
//! match exactly — no tolerance.

use s2rst::r3::Vector;
use s2rst::s2::closest_edge_query::{ClosestEdgeQuery, PointTarget};
use s2rst::s2::edge_crossings::{Crossing, crossing_sign, edge_or_vertex_crossing};
use s2rst::s2::edge_distances::distance_from_segment;
use s2rst::s2::lax_polyline::LaxPolyline;
use s2rst::s2::point_measures::point_area;
use s2rst::s2::polyline::Polyline;
use s2rst::s2::predicates::{Direction, robust_sign};
use s2rst::s2::shape_index::ShapeIndex;
use s2rst::s2::{Loop, Point};

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

/// Lines of a named `# SECTION` (stops at the next `#`/blank line).
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().unwrap()
}

/// Raw (non-normalizing) point so the bytes match the oracle's input exactly.
fn pt(x: f64, y: f64, z: f64) -> Point {
    Point::new(Vector::new(x, y, z))
}

fn dir_to_i32(d: Direction) -> i32 {
    match d {
        Direction::Clockwise => -1,
        Direction::Indeterminate => 0,
        Direction::CounterClockwise => 1,
    }
}

/// `predicates::robust_sign` must agree with golang/geo's `RobustSign` on every
/// vector, including the near-collinear cases that exercise the exact-arithmetic
/// fallback.
#[test]
fn robust_sign_matches_go() {
    let lines = section_lines("ROBUST_SIGN");
    assert!(!lines.is_empty(), "no ROBUST_SIGN vectors found");
    let mut checked = 0usize;
    for line in lines {
        let c: Vec<f64> = line.split(',').take(9).map(parse_f64).collect();
        assert_eq!(c.len(), 9, "short ROBUST_SIGN row: {line}");
        let expected: i32 = line.rsplit(',').next().unwrap().trim().parse().unwrap();

        let a = pt(c[0], c[1], c[2]);
        let b = pt(c[3], c[4], c[5]);
        let cc = pt(c[6], c[7], c[8]);
        let got = dir_to_i32(robust_sign(a, b, cc));
        assert_eq!(
            got, expected,
            "robust_sign disagrees with golang/geo on: {line}"
        );
        checked += 1;
    }
    assert!(
        checked > 10_000,
        "expected the full vector set, only checked {checked}"
    );
}

fn crossing_to_i32(c: Crossing) -> i32 {
    match c {
        Crossing::Cross => 0,
        Crossing::MaybeCross => 1,
        Crossing::DoNotCross => 2,
    }
}

/// `edge_crossings::crossing_sign` and `edge_or_vertex_crossing` must agree with
/// golang/geo's `CrossingSign` / `EdgeOrVertexCrossing` on every edge pair —
/// including constructed crossings, shared-vertex pairs, and near-misses.
#[test]
fn edge_crossing_matches_go() {
    let lines = section_lines("EDGE_CROSSING");
    assert!(!lines.is_empty(), "no EDGE_CROSSING vectors found");
    let mut checked = 0usize;
    for line in lines {
        let v: Vec<f64> = line.split(',').take(12).map(parse_f64).collect();
        assert_eq!(v.len(), 12, "short EDGE_CROSSING row: {line}");
        let mut tail = line.rsplit(',');
        let evc_expected = tail.next().unwrap().trim() == "1";
        let crossing_expected: i32 = tail.next().unwrap().trim().parse().unwrap();

        let a = pt(v[0], v[1], v[2]);
        let b = pt(v[3], v[4], v[5]);
        let c = pt(v[6], v[7], v[8]);
        let d = pt(v[9], v[10], v[11]);

        assert_eq!(
            crossing_to_i32(crossing_sign(a, b, c, d)),
            crossing_expected,
            "crossing_sign disagrees with golang/geo on: {line}"
        );
        assert_eq!(
            edge_or_vertex_crossing(a, b, c, d),
            evc_expected,
            "edge_or_vertex_crossing disagrees with golang/geo on: {line}"
        );
        checked += 1;
    }
    assert!(
        checked > 8_000,
        "expected the full vector set, only checked {checked}"
    );
}

/// Assert `got` is within an absolute+relative tolerance of `exp`. Go and Rust
/// use different libm implementations, so transcendental measures can differ by
/// a few ULPs; this tolerance absorbs that while staying far tighter than any
/// real divergence.
fn close(got: f64, exp: f64, what: &str, line: &str) {
    let tol = 1e-12 + 1e-9 * exp.abs();
    assert!(
        (got - exp).abs() <= tol,
        "{what} disagrees with golang/geo: got {got}, expected {exp} (line: {line})"
    );
}

/// `point_measures::point_area` must agree with golang/geo's `PointArea`.
#[test]
fn point_area_matches_go() {
    let lines = section_lines("POINT_AREA");
    assert!(!lines.is_empty(), "no POINT_AREA vectors found");
    for line in lines {
        let c: Vec<f64> = line.split(',').take(9).map(parse_f64).collect();
        let expected = parse_f64(line.rsplit(',').next().unwrap());
        let a = pt(c[0], c[1], c[2]);
        let b = pt(c[3], c[4], c[5]);
        let cc = pt(c[6], c[7], c[8]);
        close(point_area(a, b, cc), expected, "point_area", line);
    }
}

/// `edge_distances::distance_from_segment` must agree with golang/geo's
/// `DistanceFromSegment`.
#[test]
fn edge_distance_matches_go() {
    let lines = section_lines("EDGE_DISTANCE");
    assert!(!lines.is_empty(), "no EDGE_DISTANCE vectors found");
    for line in lines {
        let c: Vec<f64> = line.split(',').take(9).map(parse_f64).collect();
        let expected = parse_f64(line.rsplit(',').next().unwrap());
        let x = pt(c[0], c[1], c[2]);
        let a = pt(c[3], c[4], c[5]);
        let b = pt(c[6], c[7], c[8]);
        close(
            distance_from_segment(x, a, b).radians(),
            expected,
            "distance_from_segment",
            line,
        );
    }
}

/// `ClosestEdgeQuery` over an indexed polyline must agree with golang/geo's
/// `ClosestEdgeQuery` on the minimum point-to-geometry distance. This exercises
/// the `ShapeIndex` + query layer end to end. Row: N, N points, query, distance.
#[test]
fn closest_edge_matches_go() {
    let lines = section_lines("CLOSEST_EDGE");
    assert!(!lines.is_empty(), "no CLOSEST_EDGE vectors found");
    for line in lines {
        let fields: Vec<&str> = line.split(',').collect();
        let n: usize = fields[0].trim().parse().unwrap();
        assert_eq!(
            fields.len(),
            1 + 3 * n + 3 + 1,
            "bad CLOSEST_EDGE row: {line}"
        );

        let mut k = 1;
        let mut pts = Vec::with_capacity(n);
        for _ in 0..n {
            pts.push(pt(
                parse_f64(fields[k]),
                parse_f64(fields[k + 1]),
                parse_f64(fields[k + 2]),
            ));
            k += 3;
        }
        let query = pt(
            parse_f64(fields[k]),
            parse_f64(fields[k + 1]),
            parse_f64(fields[k + 2]),
        );
        let expected = parse_f64(fields[k + 3]);

        let mut index = ShapeIndex::new();
        index.add(Box::new(LaxPolyline::new(pts)));
        index.build();
        let q = ClosestEdgeQuery::new(&index);
        let got = q.get_distance(&PointTarget::new(query)).radians();
        close(got, expected, "closest_edge", line);
    }
}

/// Parse a row of the form `N, <N points>, value`.
fn parse_points_and_value(line: &str) -> (Vec<Point>, f64) {
    let fields: Vec<&str> = line.split(',').collect();
    let n: usize = fields[0].trim().parse().unwrap();
    assert_eq!(
        fields.len(),
        1 + 3 * n + 1,
        "bad N,points,value row: {line}"
    );
    let mut k = 1;
    let mut pts = Vec::with_capacity(n);
    for _ in 0..n {
        pts.push(pt(
            parse_f64(fields[k]),
            parse_f64(fields[k + 1]),
            parse_f64(fields[k + 2]),
        ));
        k += 3;
    }
    (pts, parse_f64(fields[k]))
}

/// `Polyline::length` must agree with golang/geo's `Polyline.Length`.
#[test]
fn polyline_length_matches_go() {
    let lines = section_lines("POLYLINE_LENGTH");
    assert!(!lines.is_empty(), "no POLYLINE_LENGTH vectors found");
    for line in lines {
        let (pts, expected) = parse_points_and_value(line);
        close(
            Polyline::new(pts).length().radians(),
            expected,
            "polyline_length",
            line,
        );
    }
}

/// `Loop::area` must agree with golang/geo's `Loop.Area`.
#[test]
fn loop_area_matches_go() {
    let lines = section_lines("LOOP_AREA");
    assert!(!lines.is_empty(), "no LOOP_AREA vectors found");
    for line in lines {
        let (pts, expected) = parse_points_and_value(line);
        close(Loop::new(pts).area(), expected, "loop_area", line);
    }
}
