"""Tests for direction cosine conversion functions."""

import math

import numpy as np
import pytest

from gri_utils.conversion import cosines_to_unit_vector, unit_vector_to_cosines

SQRT2_INV = 1.0 / math.sqrt(2)
SQRT3_INV = 1.0 / math.sqrt(3)


# Tests for cosines_to_unit_vector


@pytest.mark.parametrize(
    ("cosines", "expected"),
    [
        ([1.0, 0.0], [1.0, 0.0, 0.0]),  # +X axis
        ([0.0, 1.0], [0.0, 1.0, 0.0]),  # +Y axis
        ([0.0, 0.0], [0.0, 0.0, 1.0]),  # +Z axis
        ([-1.0, 0.0], [-1.0, 0.0, 0.0]),  # -X axis
        ([0.0, -1.0], [0.0, -1.0, 0.0]),  # -Y axis
        ([SQRT2_INV, SQRT2_INV], [SQRT2_INV, SQRT2_INV, 0.0]),  # XY diagonal (z ~ 1e-8)
        ([SQRT3_INV, SQRT3_INV], [SQRT3_INV, SQRT3_INV, SQRT3_INV]),  # XYZ diagonal
    ],
)
def test_cosines_to_unit_vector_z_positive(
    cosines: list[float],
    expected: list[float],
) -> None:
    result = cosines_to_unit_vector(cosines, z_positive=True)
    assert result.shape == (3,)
    # Relaxed tolerance for XY diagonal case where z has small numerical residual
    assert np.allclose(result, expected, atol=1e-7)


def test_cosines_to_unit_vector_z_negative() -> None:
    result = cosines_to_unit_vector([0.0, 0.0], z_positive=False)
    assert result.shape == (3,)
    assert np.allclose(result, [0.0, 0.0, -1.0], atol=1e-10)


def test_cosines_to_unit_vector_batch() -> None:
    cosines = np.array(
        [
            [1.0, 0.0],
            [0.0, 1.0],
            [0.0, 0.0],
        ],
    )
    expected = np.array(
        [
            [1.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
            [0.0, 0.0, 1.0],
        ],
    )
    result = cosines_to_unit_vector(cosines)
    assert result.shape == (3, 3)
    assert np.allclose(result, expected, atol=1e-10)


def test_cosines_to_unit_vector_clamps_numerical_errors() -> None:
    # Cosines that would give z^2 < 0 due to floating point
    cosines = np.array([0.7071067811865476, 0.7071067811865476])  # sqrt(2)/2 each
    result = cosines_to_unit_vector(cosines)
    # Should not raise, z should be clamped to 0
    assert result.shape == (3,)
    assert np.isfinite(result).all()


# Tests for unit_vector_to_cosines


@pytest.mark.parametrize(
    ("unit_vector", "expected"),
    [
        ([1.0, 0.0, 0.0], [1.0, 0.0]),  # +X axis
        ([0.0, 1.0, 0.0], [0.0, 1.0]),  # +Y axis
        ([0.0, 0.0, 1.0], [0.0, 0.0]),  # +Z axis
        ([0.0, 0.0, -1.0], [0.0, 0.0]),  # -Z axis
        ([-1.0, 0.0, 0.0], [-1.0, 0.0]),  # -X axis
        ([SQRT2_INV, SQRT2_INV, 0.0], [SQRT2_INV, SQRT2_INV]),  # XY diagonal
        ([SQRT3_INV, SQRT3_INV, SQRT3_INV], [SQRT3_INV, SQRT3_INV]),  # XYZ diagonal
    ],
)
def test_unit_vector_to_cosines_single(
    unit_vector: list[float],
    expected: list[float],
) -> None:
    result = unit_vector_to_cosines(unit_vector)
    assert result.shape == (2,)
    assert np.allclose(result, expected, atol=1e-10)


def test_unit_vector_to_cosines_batch() -> None:
    vectors = np.array(
        [
            [1.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
            [0.0, 0.0, 1.0],
        ],
    )
    expected = np.array(
        [
            [1.0, 0.0],
            [0.0, 1.0],
            [0.0, 0.0],
        ],
    )
    result = unit_vector_to_cosines(vectors)
    assert result.shape == (3, 2)
    assert np.allclose(result, expected, atol=1e-10)


def test_unit_vector_to_cosines_normalizes() -> None:
    # Non-unit vector should be normalized
    vector = np.array([3.0, 4.0, 0.0])  # magnitude = 5
    result = unit_vector_to_cosines(vector)
    assert np.allclose(result, [0.6, 0.8], atol=1e-10)


# Round-trip tests


@pytest.mark.parametrize(
    "cosines",
    [
        [1.0, 0.0],
        [0.0, 1.0],
        [0.0, 0.0],
        [0.5, 0.5],
        [-0.6, 0.8],
        [SQRT2_INV, 0.0],
    ],
)
def test_round_trip_cosines_to_vector_to_cosines(cosines: list[float]) -> None:
    vector = cosines_to_unit_vector(cosines, z_positive=True)
    recovered = unit_vector_to_cosines(vector)
    assert np.allclose(recovered, cosines, atol=1e-10)


@pytest.mark.parametrize(
    "unit_vector",
    [
        [1.0, 0.0, 0.0],
        [0.0, 1.0, 0.0],
        [0.0, 0.0, 1.0],
        [SQRT3_INV, SQRT3_INV, SQRT3_INV],
    ],
)
def test_round_trip_vector_to_cosines_to_vector_z_positive(
    unit_vector: list[float],
) -> None:
    cosines = unit_vector_to_cosines(unit_vector)
    recovered = cosines_to_unit_vector(cosines, z_positive=True)
    assert np.allclose(recovered, unit_vector, atol=1e-10)


@pytest.mark.parametrize(
    "unit_vector",
    [
        [0.0, 0.0, -1.0],
        [SQRT3_INV, SQRT3_INV, -SQRT3_INV],
    ],
)
def test_round_trip_vector_to_cosines_to_vector_z_negative(
    unit_vector: list[float],
) -> None:
    cosines = unit_vector_to_cosines(unit_vector)
    recovered = cosines_to_unit_vector(cosines, z_positive=False)
    assert np.allclose(recovered, unit_vector, atol=1e-10)


# Input type flexibility


def test_accepts_list_input() -> None:
    result = cosines_to_unit_vector([0.6, 0.8])
    assert isinstance(result, np.ndarray)
    assert result.shape == (3,)


def test_accepts_tuple_input() -> None:
    result = unit_vector_to_cosines((1.0, 0.0, 0.0))
    assert isinstance(result, np.ndarray)
    assert result.shape == (2,)
