"""Unit tests for the Hyperplane class.

Run with:
    python3 -m pytest tests/test_hyperplane.py -v

Or simply:
    python3 tests/test_hyperplane.py
"""

import sys
import os

import numpy as np
import pytest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from ellipsoidal_toolbox.core.hyperplane import Hyperplane
from ellipsoidal_toolbox.core.config import ABS_TOL


# ======================================================================
# 1. CONSTRUCTION & VALIDATION
# ======================================================================

class TestConstruction:
    """Test that hyperplanes are built correctly."""

    def test_basic_construction(self):
        """H([1, 0], 3) defines the vertical line x=3 in 2D.

        The set { x : <[1,0], x> = 3 } = { x : x_1 = 3 }.
        Normal is stored as float64, shift as float.
        """
        H = Hyperplane([1, 0], 3.0)
        np.testing.assert_array_almost_equal(H.normal, [1.0, 0.0])
        assert H.shift == pytest.approx(3.0)

    def test_default_shift_is_zero(self):
        """If no shift is given, it defaults to 0.

        H([1, 0]) defines { x : x_1 = 0 } — the y-axis.
        """
        H = Hyperplane([1, 0])
        assert H.shift == pytest.approx(0.0)

    def test_empty_construction(self):
        """Hyperplane() with no arguments creates an empty hyperplane."""
        H = Hyperplane()
        assert H.is_empty()
        assert H.normal is None

    def test_3d_hyperplane(self):
        """A 3D hyperplane H([0,0,1], 5) is the plane z=5."""
        H = Hyperplane([0, 0, 1], 5.0)
        assert H.dimension() == 3
        np.testing.assert_array_almost_equal(H.normal, [0, 0, 1])
        assert H.shift == pytest.approx(5.0)

    def test_zero_normal_with_nonzero_shift_becomes_trivial(self):
        """If the normal is (near-)zero but shift is nonzero, the
        hyperplane { <0, x> = c } with c != 0 is the empty set — no
        point can satisfy it.

        The MATLAB code handles this by setting both to zero.  We
        preserve that behavior.
        """
        H = Hyperplane([0.0, 0.0], 5.0)
        np.testing.assert_array_almost_equal(H.normal, [0, 0])
        assert H.shift == pytest.approx(0.0)

    def test_zero_normal_with_zero_shift_is_accepted(self):
        """Normal = 0, shift = 0 is technically { <0, x> = 0 } = R^n.

        It's a degenerate but valid hyperplane.
        """
        H = Hyperplane([0.0, 0.0], 0.0)
        assert not H.is_empty()


# ======================================================================
# 2. BASIC QUERIES
# ======================================================================

class TestBasicQueries:
    """Test dimension, parameters, is_empty."""

    def test_dimension(self):
        """Dimension should match the length of the normal vector."""
        H = Hyperplane([1, 2, 3])
        assert H.dimension() == 3

    def test_dimension_empty(self):
        """Empty hyperplane has dimension 0."""
        assert Hyperplane().dimension() == 0

    def test_parameters(self):
        """parameters() should return (normal, shift) as originally set."""
        H = Hyperplane([1, -1], 2.5)
        v, c = H.parameters()
        np.testing.assert_array_almost_equal(v, [1, -1])
        assert c == pytest.approx(2.5)

    def test_is_empty_false(self):
        """A hyperplane with a normal vector is not empty."""
        assert not Hyperplane([1, 0]).is_empty()


# ======================================================================
# 3. NEGATION
# ======================================================================

class TestNegation:
    """Test the __neg__ operator which flips the halfspace.

    If H defines <v, x> <= c, then -H defines <-v, x> <= -c,
    which is equivalent to <v, x> >= c — the OTHER side of the
    hyperplane.  This is used extensively in intersection
    computations.
    """

    def test_negate_normal_and_shift(self):
        """-(H([1,0], 3)) should give H([-1,0], -3)."""
        H = Hyperplane([1, 0], 3.0)
        neg_H = -H
        np.testing.assert_array_almost_equal(neg_H.normal, [-1, 0])
        assert neg_H.shift == pytest.approx(-3.0)

    def test_negate_empty(self):
        """Negating an empty hyperplane gives another empty hyperplane."""
        H = -Hyperplane()
        assert H.is_empty()

    def test_double_negate(self):
        """Negating twice should give back the original."""
        H = Hyperplane([2, -3], 7.0)
        H2 = -(-H)
        assert H == H2


# ======================================================================
# 4. EQUALITY & DISPLAY
# ======================================================================

class TestEqualityAndDisplay:
    """Test __eq__, __ne__, __repr__, __str__."""

    def test_equal_hyperplanes(self):
        """Same normal and shift => equal."""
        H1 = Hyperplane([1, 0], 3.0)
        H2 = Hyperplane([1, 0], 3.0)
        assert H1 == H2

    def test_different_normals(self):
        """Different normals => not equal."""
        H1 = Hyperplane([1, 0], 3.0)
        H2 = Hyperplane([0, 1], 3.0)
        assert H1 != H2

    def test_different_shifts(self):
        """Different shifts => not equal."""
        H1 = Hyperplane([1, 0], 3.0)
        H2 = Hyperplane([1, 0], 5.0)
        assert H1 != H2

    def test_both_empty_equal(self):
        """Two empty hyperplanes should be equal."""
        assert Hyperplane() == Hyperplane()

    def test_nearly_equal(self):
        """Hyperplanes within ABS_TOL should be equal."""
        H1 = Hyperplane([1, 0], 3.0)
        H2 = Hyperplane([1 + 1e-10, 0], 3.0 - 1e-10)
        assert H1 == H2

    def test_repr_nonempty(self):
        """repr should show dimension."""
        H = Hyperplane([1, 0, 0])
        assert "dim=3" in repr(H)

    def test_repr_empty(self):
        """repr of empty should say 'empty'."""
        assert "empty" in repr(Hyperplane())

    def test_str_contains_info(self):
        """str should mention normal and shift."""
        H = Hyperplane([1, 0], 3.0)
        s = str(H)
        assert "normal" in s
        assert "shift" in s


# ======================================================================
# Main entry point
# ======================================================================

if __name__ == "__main__":
    pytest.main([__file__, "-v", "--tb=short"])
