"""Tests for terrain sheet intersection using composition pattern."""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
    from numpy.typing import NDArray

from gri_utils.conversion import lla_to_xyz
from gri_utils.observables import (
    get_aoa_residual,
    get_fdoa_residual,
    get_range_residual,
    get_tdoa_residual,
)
from gri_utils.terrain import (
    intersect_sheet,
    intersect_sheet_scalar,
    lla_to_xyz_sheet,
)


def test_intersect_sheet_with_range_basic() -> None:
    """Test range sphere intersection with terrain."""
    lats = np.linspace(39.0, 40.0, 50)
    lons = np.linspace(-105.0, -104.0, 50)
    alts = np.zeros((50, 50))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    center = sheet[25, 25].copy()
    range_m = 30000.0

    residual = get_range_residual(center, range_m)
    contours = intersect_sheet(sheet, residual)

    assert isinstance(contours, list)
    assert len(contours) >= 1
    for contour in contours:
        assert contour.shape[1] == 3


def test_intersect_sheet_with_range_miss() -> None:
    """Test range sphere that misses terrain."""
    lats = np.linspace(39.0, 40.0, 20)
    lons = np.linspace(-105.0, -104.0, 20)
    alts = np.zeros((20, 20))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    center = np.array([0.0, 0.0, 7e6])
    range_m = 1000.0

    residual = get_range_residual(center, range_m)
    contours = intersect_sheet(sheet, residual)

    assert len(contours) == 0


def test_intersect_sheet_with_tdoa_basic() -> None:
    """Test TDOA hyperboloid intersection with terrain."""
    lats = np.linspace(39.0, 40.0, 50)
    lons = np.linspace(-105.0, -104.0, 50)
    alts = np.zeros((50, 50))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    collector1 = lla_to_xyz(np.array([39.2, -104.7, 1000.0]))
    collector2 = lla_to_xyz(np.array([39.8, -104.3, 1000.0]))

    residual = get_tdoa_residual(collector1, collector2, 0.0)
    contours = intersect_sheet(sheet, residual)

    assert isinstance(contours, list)
    assert len(contours) >= 1


def test_intersect_sheet_with_tdoa_asymmetric() -> None:
    """Test TDOA with non-zero time difference."""
    lats = np.linspace(39.0, 40.0, 50)
    lons = np.linspace(-105.0, -104.0, 50)
    alts = np.zeros((50, 50))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    collector1 = lla_to_xyz(np.array([39.2, -104.7, 1000.0]))
    collector2 = lla_to_xyz(np.array([39.8, -104.3, 1000.0]))

    residual = get_tdoa_residual(collector1, collector2, 0.0001)
    contours = intersect_sheet(sheet, residual)

    assert isinstance(contours, list)


def test_intersect_sheet_with_fdoa_basic() -> None:
    """Test FDOA iso-Doppler surface intersection."""
    lats = np.linspace(39.0, 40.0, 30)
    lons = np.linspace(-105.0, -104.0, 30)
    alts = np.zeros((30, 30))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    collector1 = lla_to_xyz(np.array([39.2, -104.7, 10000.0]))
    collector2 = lla_to_xyz(np.array([39.8, -104.3, 10000.0]))
    vel1 = np.array([100.0, 0.0, 0.0])
    vel2 = np.array([100.0, 0.0, 0.0])

    residual = get_fdoa_residual(collector1, vel1, collector2, vel2, 0.0, 1e9)
    contours = intersect_sheet(sheet, residual)

    assert isinstance(contours, list)


def test_intersect_sheet_with_aoa_basic() -> None:
    """Test AOA bearing intersection."""
    lats = np.linspace(39.0, 40.0, 30)
    lons = np.linspace(-105.0, -104.0, 30)
    alts = np.zeros((30, 30))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    collector = lla_to_xyz(np.array([39.5, -104.5, 10000.0]))
    aoa_cosines = np.array([0.1, 0.1])

    residual = get_aoa_residual(collector, aoa_cosines)
    contours = intersect_sheet(sheet, residual)

    assert isinstance(contours, list)


def test_intersect_sheet_scalar_custom() -> None:
    """Test custom scalar residual function."""
    lats = np.linspace(39.0, 40.0, 20)
    lons = np.linspace(-105.0, -104.0, 20)
    alts = np.zeros((20, 20))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    target = sheet[10, 10]

    def residual_fn(xyz: NDArray[np.floating]) -> float:
        return float(np.linalg.norm(xyz - target) - 50000.0)

    contours = intersect_sheet_scalar(sheet, residual_fn, level=0.0)

    assert isinstance(contours, list)


def test_intersect_sheet_vectorized_custom() -> None:
    """Test custom vectorized residual function."""
    lats = np.linspace(39.0, 40.0, 20)
    lons = np.linspace(-105.0, -104.0, 20)
    alts = np.zeros((20, 20))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    target = sheet[10, 10]

    def residual_fn(points: NDArray[np.floating]) -> NDArray[np.floating]:
        return np.linalg.norm(points - target, axis=-1) - 50000.0

    contours = intersect_sheet(sheet, residual_fn, level=0.0)

    assert isinstance(contours, list)


def test_intersect_contour_points_on_surface() -> None:
    """Test that contour points lie on the iso-surface."""
    lats = np.linspace(39.0, 40.0, 50)
    lons = np.linspace(-105.0, -104.0, 50)
    alts = np.zeros((50, 50))
    sheet = lla_to_xyz_sheet(lats, lons, alts)

    center = sheet[25, 25]
    target_range = 50000.0

    residual = get_range_residual(center, target_range)
    contours = intersect_sheet(sheet, residual)

    if len(contours) > 0:
        for contour in contours:
            for point in contour:
                actual_range = np.linalg.norm(point - center)
                assert abs(actual_range - target_range) < 2000
