"""Tests for distance metrics.

Functional tests comparing distance algorithms against Vincenty reference values.
"""

import numpy as np
import pytest

from gri_utils.distance import (
    central_angle,
    dist_xyz,
    surface_dist_haversine,
    surface_dist_simple,
    surface_dist_vincenty,
)

# Reference positions with Vincenty distances (from online calculators)
# Format: (lla1, lla2, vincenty_distance_m)
REFERENCE_DISTANCES: list[tuple[tuple, tuple, float]] = [
    ((0, 0, 0), (0, 0, 0), 0),
    ((0, 0, 0), (0, 1, 0), 111319.491),
    ((0, 0, 0), (1, 0, 0), 110574.389),
    ((10.2345, 15.6789, 0), (11.5789, 17.9876, 0), 292937.392),
    ((0, 0, 0), (40, 120, 0), 12521126.888),
    ((0, 0, 0), (40, 60, 0), 7500166.649),
    ((0, 0, 0), (40, -60, 0), 7500166.649),
    ((40, 0, 0), (40, -60, 0), 5020978.634),
    ((-5.123, 1.234, 0), (5.123, -2.345, 0), 1200809.23),
    ((0.1, 0.1, 0), (0.101, 0.1, 0), 110.574),
    ((0.1, 0.1, 0), (0.1, 0.101, 0), 111.319),
    ((40.1, 40.1, 0), (40.101, 40.1, 0), 111.037),
    ((40.1, 40.1, 0), (40.1, 40.101, 0), 85.269),
    ((80.1, 80.1, 0), (80.101, 80.1, 0), 111.661),
    ((80.1, 80.1, 0), (80.1, 80.101, 0), 19.202),
]


# -----------------------------------------------------------------------------
# XYZ Euclidean distance
# -----------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("xyz1", "xyz2", "expected"),
    [
        ((0, 0, 0), (1, 1, 1), 3**0.5),
        ((5, 6, 7), (4, 5, 6), 3**0.5),
        ((0, 0, 0), (3, 4, 0), 5.0),
    ],
)
def test_dist_xyz(
    xyz1: tuple[float, ...],
    xyz2: tuple[float, ...],
    expected: float,
) -> None:
    """Euclidean distance between XYZ points."""
    assert dist_xyz(xyz1, xyz2) == pytest.approx(expected)


def test_dist_xyz_vectorized() -> None:
    """Vectorized pairwise distances."""
    xyz1 = np.array([[0, 0, 0], [5, 6, 7], [0, 0, 0]])
    xyz2 = np.array([[1, 1, 1], [4, 5, 6], [3, 4, 0]])
    result = dist_xyz(xyz1, xyz2)
    expected = np.array([3**0.5, 3**0.5, 5.0])
    assert result == pytest.approx(expected)


# -----------------------------------------------------------------------------
# Central angle
# -----------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("lla1", "lla2", "expected_deg"),
    [
        ((0, 0, 0), (0, 0, 0), 0.0),
        ((0, 0, 0), (0, 1, 0), 1.0),
        ((0, 0, 0), (1, 0, 0), 1.0),
        ((45, 0, 0), (45, 90, 0), 60.0),
    ],
)
def test_central_angle(
    lla1: tuple[float, ...],
    lla2: tuple[float, ...],
    expected_deg: float,
) -> None:
    """Central angle between LLA points."""
    result = central_angle(lla1, lla2)
    assert result == pytest.approx(np.radians(expected_deg), rel=1e-6)


def test_central_angle_vectorized() -> None:
    """Vectorized pairwise central angles."""
    lla1 = np.array([[0, 0, 0], [0, 0, 0], [45, 0, 0]])
    lla2 = np.array([[0, 1, 0], [1, 0, 0], [45, 90, 0]])
    result = central_angle(lla1, lla2)
    expected = np.radians([1.0, 1.0, 60.0])
    assert result == pytest.approx(expected, rel=1e-6)


# -----------------------------------------------------------------------------
# Vincenty (reference algorithm)
# -----------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("lla1", "lla2", "expected"),
    REFERENCE_DISTANCES,
)
def test_vincenty(
    lla1: tuple[float, ...],
    lla2: tuple[float, ...],
    expected: float,
) -> None:
    """Vincenty distance matches reference values."""
    result = surface_dist_vincenty(lla1, lla2)
    assert result == pytest.approx(expected, abs=0.1)


@pytest.mark.parametrize(
    ("lla1", "lla2", "expected"),
    REFERENCE_DISTANCES,
)
def test_vincenty_high_precision(
    lla1: tuple[float, ...],
    lla2: tuple[float, ...],
    expected: float,
) -> None:
    """Vincenty with tight convergence matches reference to 1mm."""
    result = surface_dist_vincenty(lla1, lla2, converge_m=0.001)
    assert result == pytest.approx(expected, abs=0.001)


# -----------------------------------------------------------------------------
# Haversine (spherical approximation)
# -----------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("lla1", "lla2", "expected"),
    REFERENCE_DISTANCES,
)
def test_haversine(
    lla1: tuple[float, ...],
    lla2: tuple[float, ...],
    expected: float,
) -> None:
    """Haversine within 0.6% of Vincenty reference."""
    result = surface_dist_haversine(lla1, lla2)
    if expected == 0:
        assert result == 0
    else:
        assert result == pytest.approx(expected, rel=0.006)


@pytest.mark.parametrize(
    ("lla1", "lla2", "expected"),
    REFERENCE_DISTANCES,
)
def test_haversine_geocentric(
    lla1: tuple[float, ...],
    lla2: tuple[float, ...],
    expected: float,
) -> None:
    """Haversine with geocentric radius within 0.7% of Vincenty."""
    result = surface_dist_haversine(lla1, lla2, use_geocentric_radius=True)
    if expected == 0:
        assert result == 0
    else:
        assert result == pytest.approx(expected, rel=0.007)


def test_haversine_vectorized() -> None:
    """Vectorized haversine matches scalar calls."""
    lla1 = np.array([[0, 0, 0], [0, 0, 0], [40, 0, 0]])
    lla2 = np.array([[0, 1, 0], [1, 0, 0], [40, 1, 0]])
    result = surface_dist_haversine(lla1, lla2)
    expected = np.array(
        [
            surface_dist_haversine([0, 0, 0], [0, 1, 0]),
            surface_dist_haversine([0, 0, 0], [1, 0, 0]),
            surface_dist_haversine([40, 0, 0], [40, 1, 0]),
        ],
    )
    assert result == pytest.approx(expected)


# -----------------------------------------------------------------------------
# Simple (fast approximation)
# -----------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("lla1", "lla2", "expected"),
    REFERENCE_DISTANCES,
)
def test_simple(
    lla1: tuple[float, ...],
    lla2: tuple[float, ...],
    expected: float,
) -> None:
    """Simple distance within 7% of Vincenty reference."""
    result = surface_dist_simple(lla1, lla2)
    if expected == 0:
        assert result == 0
    else:
        assert result == pytest.approx(expected, rel=0.07)


def test_simple_vectorized() -> None:
    """Vectorized simple distance matches scalar calls."""
    lla1 = np.array([[0, 0, 0], [0, 0, 0], [40, 0, 0]])
    lla2 = np.array([[0, 1, 0], [1, 0, 0], [40, 1, 0]])
    result = surface_dist_simple(lla1, lla2)
    expected = np.array(
        [
            surface_dist_simple([0, 0, 0], [0, 1, 0]),
            surface_dist_simple([0, 0, 0], [1, 0, 0]),
            surface_dist_simple([40, 0, 0], [40, 1, 0]),
        ],
    )
    assert result == pytest.approx(expected)
