import numpy as np
import pytest

from gri_utils.conversion import ccr_to_xyz, get_ccr, translate_ccr, xyz_to_ccr

# Type aliases for readability
XYZ = tuple[float, float, float]
CCR = tuple[float, float, float]


@pytest.mark.parametrize(
    ("xyz_pos", "xyz_pos2", "expected"),
    [
        ((6378000, 0, 0), (6378100, 0, 0), (0, -90, 100)),
        ((6378000, 0, 0), (6378000, 100, 0), (90, 0, 100)),
        ((6378000, 0, 0), (6378000, 0, 100), (0, 0, 100)),
    ],
)
def test_get_ccr(xyz_pos: XYZ, xyz_pos2: XYZ, expected: CCR) -> None:
    """Test get_ccr with array and sequence inputs."""
    # Array input
    ccr = get_ccr(np.array(xyz_pos), np.array(xyz_pos2))
    assert np.allclose(np.array(expected), ccr), f"{ccr}"

    # Sequence input
    ccr_seq = get_ccr(xyz_pos, xyz_pos2)
    assert ccr_seq == pytest.approx(expected), f"{ccr_seq}"


@pytest.mark.parametrize(
    ("xyz_pos", "ccr_vec", "expected"),
    [
        ((6378000, 0, 0), (0, -90, 100), (6378100, 0, 0)),
        ((6378000, 0, 0), (90, 0, 100), (6378000, 100, 0)),
        ((6378000, 0, 0), (0, 0, 100), (6378000, 0, 100)),
    ],
)
def test_translate_ccr(xyz_pos: XYZ, ccr_vec: CCR, expected: XYZ) -> None:
    """Test translate_ccr with array and sequence inputs."""
    # Array input
    xyz = translate_ccr(np.array(xyz_pos), np.array(ccr_vec))
    assert np.allclose(np.array(expected), xyz), f"{xyz}"

    # Sequence input
    xyz_seq = translate_ccr(xyz_pos, ccr_vec)
    assert xyz_seq == pytest.approx(expected), f"{xyz_seq}"


@pytest.mark.parametrize(
    ("xyz_pos", "xyz_vec", "expected"),
    [
        ((6378000, 0, 0), (100, 0, 0), (0, -90, 100)),
        ((6378000, 0, 0), (0, 100, 0), (90, 0, 100)),
        ((6378000, 0, 0), (0, 0, 100), (0, 0, 100)),
    ],
)
def test_xyz_to_ccr(xyz_pos: XYZ, xyz_vec: XYZ, expected: CCR) -> None:
    """Test xyz_to_ccr with array and sequence inputs."""
    # Array input
    ccr = xyz_to_ccr(np.array(xyz_pos), np.array(xyz_vec))
    assert np.allclose(np.array(expected), ccr), f"{ccr}"

    # Sequence input
    ccr_seq = xyz_to_ccr(xyz_pos, xyz_vec)
    assert ccr_seq == pytest.approx(expected), f"{ccr_seq}"


@pytest.mark.parametrize(
    ("xyz_pos", "ccr_vec", "expected"),
    [
        ((6378000, 0, 0), (0, -90, 100), (100, 0, 0)),
        ((6378000, 0, 0), (90, 0, 100), (0, 100, 0)),
        ((6378000, 0, 0), (0, 0, 100), (0, 0, 100)),
    ],
)
def test_ccr_to_xyz(xyz_pos: XYZ, ccr_vec: CCR, expected: XYZ) -> None:
    """Test ccr_to_xyz with array and sequence inputs."""
    # Array input
    xyz = ccr_to_xyz(np.array(xyz_pos), np.array(ccr_vec))
    assert np.allclose(np.array(expected), xyz), f"{xyz}"

    # Sequence input
    xyz_seq = ccr_to_xyz(xyz_pos, ccr_vec)
    assert xyz_seq == pytest.approx(expected), f"{xyz_seq}"


# Clock wrapping tests (0->360, same as azimuth)
@pytest.mark.parametrize(
    ("xyz_pos2", "expected"),
    [
        ((6378000, 0, -100), (180, 0, 100)),
        ((6378000, -100, 0), (270, 0, 100)),
    ],
)
def test_clock_wrap(xyz_pos2: XYZ, expected: CCR) -> None:
    """Test clock wrapping to 0-360 range."""
    xyz_pos = (6378000, 0, 0)
    ccr = get_ccr(xyz_pos, xyz_pos2)
    assert ccr == pytest.approx(expected), f"{ccr}"


def test_clock_near_360() -> None:
    """Test clock near 360 degrees doesn't wrap to 0."""
    xyz_pos = (6378000, 0, 0)
    ccr = get_ccr(xyz_pos, (6378000, -1, 100))
    assert 359 < ccr[0] < 360


# Cone wrapping tests (-90=zenith, 0=horizon, 90=nadir, inverse of elev)
@pytest.mark.parametrize(
    ("xyz_pos2", "expected"),
    [
        ((6378100, 0, 0), (0, -90, 100)),
        ((6377900, 0, 0), (0, 90, 100)),
        ((6377900, 0, -100), (180, 45, (100**2 * 2) ** 0.5)),
        ((6378100, 0, -100), (180, -45, (100**2 * 2) ** 0.5)),
    ],
)
def test_cone_wrap(xyz_pos2: XYZ, expected: CCR) -> None:
    """Test cone wrapping (-90=zenith, 0=horizon, 90=nadir)."""
    xyz_pos = (6378000, 0, 0)
    ccr = get_ccr(xyz_pos, xyz_pos2)
    assert ccr == pytest.approx(expected), f"{ccr}"


# Input clock handling tests
@pytest.mark.parametrize(
    ("ccr_vec", "expected"),
    [
        ((360, 0, 100), (6378000, 0, 100)),
        ((450, 0, 100), (6378000, 100, 0)),
        ((-270, 0, 100), (6378000, 100, 0)),
        ((270, 0, 100), (6378000, -100, 0)),
    ],
)
def test_input_clock(ccr_vec: CCR, expected: XYZ) -> None:
    """Test input clock values outside 0-360 range."""
    xyz_pos = (6378000, 0, 0)
    xyz = translate_ccr(xyz_pos, ccr_vec)
    assert xyz == pytest.approx(expected)


# Input cone handling tests (cone is inverse of elev)
@pytest.mark.parametrize(
    ("ccr_vec", "expected"),
    [
        ((0, -360, 100), (6378000, 0, 100)),
        ((0, -180, 100), (6378000, 0, -100)),
        ((0, 180, 100), (6378000, 0, -100)),
        ((0, 90, 100), (6377900, 0, 0)),
    ],
)
def test_input_cone(ccr_vec: CCR, expected: XYZ) -> None:
    """Test input cone values outside -90 to 90 range."""
    xyz_pos = (6378000, 0, 0)
    xyz = translate_ccr(xyz_pos, ccr_vec)
    assert xyz == pytest.approx(expected)


# Vectorized tests


def test_get_ccr_vectorized() -> None:
    """Test get_ccr with multiple target positions."""
    # Reference position on equator at prime meridian
    xyz_ref: XYZ = (6378000, 0, 0)

    # Multiple target positions: +100m in each direction
    xyz_targets = np.array(
        [
            (6378100, 0, 0),  # +100m radially out (Up, cone=-90)
            (6378000, 100, 0),  # +100m in Y (East, clock=90)
            (6378000, 0, 100),  # +100m in Z (North, clock=0)
        ],
    )
    expected = np.array(
        [
            (0, -90, 100),  # Up: clock=0, cone=-90
            (90, 0, 100),  # East: clock=90, cone=0
            (0, 0, 100),  # North: clock=0, cone=0
        ],
    )

    ccr_vecs = get_ccr(xyz_ref, xyz_targets)

    assert ccr_vecs.shape == (3, 3)
    assert np.allclose(ccr_vecs, expected)


def test_translate_ccr_vectorized() -> None:
    """Test translate_ccr with multiple CCR vectors."""
    xyz_ref: XYZ = (6378000, 0, 0)

    # Multiple CCR vectors
    ccr_vecs = np.array(
        [
            (0, -90, 100),  # Up -> radially out
            (90, 0, 100),  # East
            (0, 0, 100),  # North
        ],
    )
    expected = np.array(
        [
            (6378100, 0, 0),
            (6378000, 100, 0),
            (6378000, 0, 100),
        ],
    )

    xyzs = translate_ccr(xyz_ref, ccr_vecs)

    assert xyzs.shape == (3, 3)
    assert np.allclose(xyzs, expected)


def test_xyz_to_ccr_vectorized() -> None:
    """Test xyz_to_ccr with multiple XYZ vectors."""
    xyz_ref: XYZ = (6378000, 0, 0)

    # Use 4 vectors to avoid (3,3) matrix interpretation in ENU layer
    xyz_vecs = np.array(
        [
            (100, 0, 0),  # Radial out -> Up (cone=-90)
            (0, 100, 0),  # Y -> East (clock=90)
            (0, 0, 100),  # Z -> North (clock=0)
            (0, 0, -100),  # -Z -> South (clock=180)
        ],
    )
    expected = np.array(
        [
            (0, -90, 100),
            (90, 0, 100),
            (0, 0, 100),
            (180, 0, 100),
        ],
    )

    ccr_vecs = xyz_to_ccr(xyz_ref, xyz_vecs)

    assert ccr_vecs.shape == (4, 3)
    assert np.allclose(ccr_vecs, expected)


def test_ccr_to_xyz_vectorized() -> None:
    """Test ccr_to_xyz with multiple CCR vectors."""
    xyz_ref: XYZ = (6378000, 0, 0)

    # Use 4 vectors to avoid (3,3) matrix interpretation in ENU layer
    ccr_vecs = np.array(
        [
            (0, -90, 100),  # Up -> X (radial)
            (90, 0, 100),  # East -> Y
            (0, 0, 100),  # North -> Z
            (180, 0, 100),  # South -> -Z
        ],
    )
    expected = np.array(
        [
            (100, 0, 0),
            (0, 100, 0),
            (0, 0, 100),
            (0, 0, -100),
        ],
    )

    xyz_vecs = ccr_to_xyz(xyz_ref, ccr_vecs)

    assert xyz_vecs.shape == (4, 3)
    assert np.allclose(xyz_vecs, expected)


def test_ccr_xyz_vectorized_round_trip() -> None:
    """Test round-trip CCR<->XYZ conversion with batched inputs."""
    xyz_ref: XYZ = (6378000, 0, 0)

    ccr_vecs = np.array(
        [
            (45, -30, 100),
            (90, 0, 100),
            (0, -45, 100),
            (180, 30, 100),
            (270, -60, 100),
        ],
    )

    xyz_vecs = ccr_to_xyz(xyz_ref, ccr_vecs)
    ccr_back = xyz_to_ccr(xyz_ref, xyz_vecs)

    assert ccr_back.shape == ccr_vecs.shape
    assert np.allclose(ccr_back, ccr_vecs)
