import pytest
import math

from wpimath import Quaternion, Rotation3d


def test_init():
    # Identity
    q1 = Quaternion()
    assert q1.w == pytest.approx(1.0)
    assert q1.x == pytest.approx(0.0)
    assert q1.y == pytest.approx(0.0)
    assert q1.z == pytest.approx(0.0)

    # Normalized
    q2 = Quaternion(0.5, 0.5, 0.5, 0.5)
    assert q2.w == pytest.approx(0.5)
    assert q2.x == pytest.approx(0.5)
    assert q2.y == pytest.approx(0.5)
    assert q2.z == pytest.approx(0.5)

    # Unnormalized
    q3 = Quaternion(0.75, 0.3, 0.4, 0.5)
    assert q3.w == pytest.approx(0.75)
    assert q3.x == pytest.approx(0.3)
    assert q3.y == pytest.approx(0.4)
    assert q3.z == pytest.approx(0.5)

    q3 = q3.normalize()
    norm = math.sqrt(0.75 * 0.75 + 0.3 * 0.3 + 0.4 * 0.4 + 0.5 * 0.5)
    assert q3.w == pytest.approx(0.75 / norm)
    assert q3.x == pytest.approx(0.3 / norm)
    assert q3.y == pytest.approx(0.4 / norm)
    assert q3.z == pytest.approx(0.5 / norm)
    assert q3.w * q3.w + q3.x * q3.x + q3.y * q3.y + q3.z * q3.z == pytest.approx(1.0)


def test_addition():
    q = Quaternion(0.1, 0.2, 0.3, 0.4)
    p = Quaternion(0.5, 0.6, 0.7, 0.8)

    sum = q + p

    assert sum.w == pytest.approx(q.w + p.w)
    assert sum.x == pytest.approx(q.x + p.x)
    assert sum.y == pytest.approx(q.y + p.y)
    assert sum.z == pytest.approx(q.z + p.z)


def test_subtraction():
    q = Quaternion(0.1, 0.2, 0.3, 0.4)
    p = Quaternion(0.5, 0.6, 0.7, 0.8)

    difference = q - p

    assert difference.w == pytest.approx(q.w - p.w)
    assert difference.x == pytest.approx(q.x - p.x)
    assert difference.y == pytest.approx(q.y - p.y)
    assert difference.z == pytest.approx(q.z - p.z)


def test_scalar_multiplication():
    q = Quaternion(0.1, 0.2, 0.3, 0.4)
    scalar = 2

    product = q * scalar

    assert product.w == pytest.approx(q.w * scalar)
    assert product.x == pytest.approx(q.x * scalar)
    assert product.y == pytest.approx(q.y * scalar)
    assert product.z == pytest.approx(q.z * scalar)


def test_scalar_division():
    q = Quaternion(0.1, 0.2, 0.3, 0.4)
    scalar = 2

    product = q / scalar

    assert product.w == pytest.approx(q.w / scalar)
    assert product.x == pytest.approx(q.x / scalar)
    assert product.y == pytest.approx(q.y / scalar)
    assert product.z == pytest.approx(q.z / scalar)


def test_multiply():
    # 90° CCW rotations around each axis
    c = math.cos(math.radians(90) / 2.0)
    s = math.sin(math.radians(90) / 2.0)
    x_rot = Quaternion(c, s, 0.0, 0.0)
    y_rot = Quaternion(c, 0.0, s, 0.0)
    z_rot = Quaternion(c, 0.0, 0.0, s)

    # 90° CCW X rotation, 90° CCW Y rotation, and 90° CCW Z rotation should
    # produce a 90° CCW Y rotation
    expected = y_rot
    actual = z_rot * y_rot * x_rot
    assert actual.w == pytest.approx(expected.w, abs=1e-9)
    assert actual.x == pytest.approx(expected.x, abs=1e-9)
    assert actual.y == pytest.approx(expected.y, abs=1e-9)
    assert actual.z == pytest.approx(expected.z, abs=1e-9)

    # Identity
    q = Quaternion(
        0.72760687510899891,
        0.29104275004359953,
        0.38805700005813276,
        0.48507125007266594,
    )
    actual = q * q.inverse()
    assert actual.w == pytest.approx(1.0, abs=1e-9)
    assert actual.x == pytest.approx(0.0, abs=1e-9)
    assert actual.y == pytest.approx(0.0, abs=1e-9)
    assert actual.z == pytest.approx(0.0, abs=1e-9)


def test_conjugate():
    q = Quaternion(
        0.72760687510899891,
        0.29104275004359953,
        0.38805700005813276,
        0.48507125007266594,
    )
    conj = q.conjugate()

    assert conj.w == pytest.approx(q.w)
    assert conj.x == pytest.approx(-q.x)
    assert conj.y == pytest.approx(-q.y)
    assert conj.z == pytest.approx(-q.z)


def test_inverse():
    q = Quaternion(
        0.72760687510899891,
        0.29104275004359953,
        0.38805700005813276,
        0.48507125007266594,
    )
    norm = q.norm()

    inv = q.inverse()

    assert inv.w == pytest.approx(q.w / (norm * norm))
    assert inv.x == pytest.approx(-q.x / (norm * norm))
    assert inv.y == pytest.approx(-q.y / (norm * norm))
    assert inv.z == pytest.approx(-q.z / (norm * norm))


def test_norm():
    q = Quaternion(3, 4, 12, 84)
    norm = q.norm()

    assert norm == pytest.approx(85, abs=1e-9)


def test_exponential():
    q = Quaternion(1.1, 2.2, 3.3, 4.4)
    expect = Quaternion(
        2.81211398529184, -0.392521193481878, -0.588781790222817, -0.785042386963756
    )

    q_exp = q.exp()

    assert q_exp == expect


def test_logarithm():
    q = Quaternion(1.1, 2.2, 3.3, 4.4)
    expect = Quaternion(
        1.7959088706354, 0.515190292664085, 0.772785438996128, 1.03038058532817
    )

    q_log = q.log()

    assert q_log == expect

    zero = Quaternion(0, 0, 0, 0)
    one = Quaternion(1, 0, 0, 0)
    i = Quaternion(0, 1, 0, 0)
    j = Quaternion(0, 0, 1, 0)
    k = Quaternion(0, 0, 0, 1)
    ln_half = Quaternion(math.log(0.5), -math.pi, 0, 0)

    assert one.log() == zero
    assert i.log() == Quaternion(0, math.pi / 2, 0, 0)
    assert j.log() == Quaternion(0, 0, math.pi / 2, 0)
    assert k.log() == Quaternion(0, 0, 0, math.pi / 2)

    assert (one * -1).log() == Quaternion(0, -math.pi, 0, 0)
    assert (one * -0.5).log() == ln_half


def test_logarithm_and_exponential_inverse():
    q = Quaternion(1.1, 2.2, 3.3, 4.4)

    # These operations are order-dependent: ln(exp(q)) is congruent but not
    # necessarily equal to exp(ln(q)) due to the multi-valued nature of the
    # complex logarithm.

    q_log_exp = q.log().exp()

    assert q_log_exp == q

    start = Quaternion(1, 2, 3, 4)
    expect = Quaternion(5, 6, 7, 8)

    twist = (expect * start.inverse()).log()
    actual = twist.exp() * start

    assert actual == expect


def test_dot_product():
    q = Quaternion(1.1, 2.2, 3.3, 4.4)
    p = Quaternion(5.5, 6.6, 7.7, 8.8)

    assert q.dot(p) == pytest.approx(
        q.w * p.w + q.x * p.x + q.y * p.y + q.z * p.z, abs=1e-9
    )


def test_dot_product_as_equality():
    q = Quaternion(1.1, 2.2, 3.3, 4.4)
    q_conj = q.conjugate()

    assert q.dot(q) == pytest.approx(q.norm() * q.norm(), abs=1e-9)
    assert abs(q.dot(q_conj) - q.norm() * q_conj.norm()) > 1e-9
