// 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 for `s2::CellId` against the C++ reference implementation
//! (google/s2geometry).
//!
//! The vectors in `data/cpp_cellid_vectors.csv` are generated by
//! `tools/refgen-cpp` using the C++ S2 library as an *independent* oracle. They
//! cover `CellId` operations beyond `data/cpp_test_vectors.csv` (edge neighbours,
//! the leaf-cell range, and containment), where "subtly wrong" cell arithmetic
//! would silently corrupt spatial indexing. All values are exact `u64` ids, so
//! the comparison is exact.

use s2rst::s2::CellId;

const CSV: &str = include_str!("data/cpp_cellid_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 u64f(s: &str) -> u64 {
    s.trim().parse().unwrap()
}

/// `CellId::edge_neighbors` must match C++ `S2CellId::GetEdgeNeighbors`.
#[test]
fn edge_neighbors_match_cpp() {
    let lines = section_lines("CELLID_EDGE_NEIGHBORS");
    assert!(!lines.is_empty(), "no CELLID_EDGE_NEIGHBORS vectors");
    let mut checked = 0usize;
    for line in lines {
        let f: Vec<u64> = line.split(',').map(u64f).collect();
        assert_eq!(f.len(), 5, "bad CELLID_EDGE_NEIGHBORS row: {line}");
        let got = CellId(f[0]).edge_neighbors();
        let expected = [CellId(f[1]), CellId(f[2]), CellId(f[3]), CellId(f[4])];
        assert_eq!(
            got, expected,
            "edge_neighbors disagrees with C++ on: {line}"
        );
        checked += 1;
    }
    assert!(
        checked > 4000,
        "expected the full vector set, only checked {checked}"
    );
}

/// `CellId::range_min`/`range_max` must match C++ `S2CellId::range_min/range_max`.
#[test]
fn range_matches_cpp() {
    let lines = section_lines("CELLID_RANGE");
    assert!(!lines.is_empty(), "no CELLID_RANGE vectors");
    for line in lines {
        let f: Vec<u64> = line.split(',').map(u64f).collect();
        assert_eq!(f.len(), 3, "bad CELLID_RANGE row: {line}");
        let c = CellId(f[0]);
        assert_eq!(
            c.range_min(),
            CellId(f[1]),
            "range_min disagrees on: {line}"
        );
        assert_eq!(
            c.range_max(),
            CellId(f[2]),
            "range_max disagrees on: {line}"
        );
    }
}

/// `CellId::contains`/`intersects` must match C++ `S2CellId::contains/intersects`.
#[test]
fn contains_intersects_match_cpp() {
    let lines = section_lines("CELLID_CONTAINS");
    assert!(!lines.is_empty(), "no CELLID_CONTAINS vectors");
    for line in lines {
        let p: Vec<&str> = line.split(',').collect();
        assert_eq!(p.len(), 4, "bad CELLID_CONTAINS row: {line}");
        let a = CellId(u64f(p[0]));
        let b = CellId(u64f(p[1]));
        assert_eq!(
            a.contains(b),
            p[2].trim() == "1",
            "contains disagrees on: {line}"
        );
        assert_eq!(
            a.intersects(b),
            p[3].trim() == "1",
            "intersects disagrees on: {line}"
        );
    }
}

/// `CellId::children` must match C++ `S2CellId::child(0..3)`.
#[test]
fn children_match_cpp() {
    let lines = section_lines("CELLID_CHILDREN");
    assert!(!lines.is_empty(), "no CELLID_CHILDREN vectors");
    for line in lines {
        let f: Vec<u64> = line.split(',').map(u64f).collect();
        assert_eq!(f.len(), 5, "bad CELLID_CHILDREN row: {line}");
        let got = CellId(f[0]).children();
        let expected = [CellId(f[1]), CellId(f[2]), CellId(f[3]), CellId(f[4])];
        assert_eq!(got, expected, "children disagrees with C++ on: {line}");
    }
}

/// `CellId::to_face_ij_orientation` must match C++ `S2CellId::ToFaceIJOrientation`
/// (the core (face, i, j) <-> Hilbert-curve mapping).
#[test]
fn face_ij_match_cpp() {
    let lines = section_lines("CELLID_FACE_IJ");
    assert!(!lines.is_empty(), "no CELLID_FACE_IJ vectors");
    for line in lines {
        let p: Vec<&str> = line.split(',').collect();
        assert_eq!(p.len(), 5, "bad CELLID_FACE_IJ row: {line}");
        let (face, i, j, orientation) = CellId(u64f(p[0])).to_face_ij_orientation();
        let want = |idx: usize| p[idx].trim().parse::<i64>().unwrap();
        assert_eq!(i64::from(u8::from(face)), want(1), "face on: {line}");
        assert_eq!(i64::from(i), want(2), "i on: {line}");
        assert_eq!(i64::from(j), want(3), "j on: {line}");
        assert_eq!(i64::from(orientation), want(4), "orientation on: {line}");
    }
}
