"""Tests for LSC Optimizer core algorithms."""

from __future__ import annotations

import numpy as np
import pytest
from scipy.optimize import OptimizeResult

from pytola_sim.lscopt.cli import LSCCurve, LSCOptimizer


class TestLSCCurve:
    """Tests for LSCCurve class implementing least squares curve optimization.

    Tests cover initialization, mathematical computations, constraint handling,
    and edge case robustness.
    """

    def test_curve_initialization(self) -> None:
        """Test LSCCurve initialization with default values."""
        curve = LSCCurve()

        # Test default parameter values
        assert curve.m == pytest.approx(-1.3)
        assert curve.m1 == pytest.approx(-2.4)
        assert curve.s == pytest.approx(1.2183)
        assert pytest.approx(0.5) == curve.H
        assert pytest.approx(80.0) == curve.J

        # Test that cached properties are computed correctly
        assert isinstance(curve.n, float)
        assert isinstance(curve.t, float)
        assert curve.n > 0  # cotangent of angle should be positive
        assert curve.t > 0

    def test_curve_with_custom_parameters(self) -> None:
        """Test LSCCurve with custom parameter values."""
        curve = LSCCurve(m=-2.0, m1=-3.0, s=2.0, H=1.0, J=90.0)

        assert curve.m == pytest.approx(-2.0)
        assert curve.m1 == pytest.approx(-3.0)
        assert curve.s == pytest.approx(2.0)
        assert pytest.approx(1.0) == curve.H
        assert pytest.approx(90.0) == curve.J

    def test_cached_properties_computation(self) -> None:
        """Test that cached properties are computed correctly."""
        curve = LSCCurve()

        # Test mathematical properties
        assert curve.ms == curve.m**2
        assert curve.mc == curve.m**3
        assert curve.ms4 == curve.m**4
        assert curve.m1s == curve.m1**2
        assert curve.m1c == curve.m1**3
        assert curve.m1s4 == curve.m1**4

        # Test trigonometric properties
        expected_n = 1 / np.tan(np.radians(curve.J))
        expected_t = 1 / np.tan(np.radians(curve.J1))
        assert abs(curve.n - expected_n) < 1e-10
        assert abs(curve.t - expected_t) < 1e-10

    def test_matrix_dimensions(self) -> None:
        """Test that constraint matrices have correct dimensions."""
        curve = LSCCurve()

        # Test C matrix (16x8)
        assert curve.C.shape == (8, 16)

        # Test constraint vectors
        assert curve.d.shape == (8,)
        assert curve.A_ineq.shape[1] == 16  # Should match solution vector size
        assert curve.A_eq.shape[1] == 16  # Should match solution vector size
        assert len(curve.b_ineq) == curve.A_ineq.shape[0]
        assert len(curve.b_eq) == curve.A_eq.shape[0]

    def test_solution_computation(self) -> None:
        """Test that optimization problem can be solved."""
        curve = LSCCurve()

        # Test that result is computed
        assert isinstance(curve.R, OptimizeResult)
        assert hasattr(curve.R, "x")
        assert hasattr(curve.R, "cost")

        # Test solution vector
        assert isinstance(curve.x, np.ndarray)
        assert len(curve.x) == 16  # 16 coefficients

        # Test that cost is reasonable
        assert curve.R.cost >= 0
        assert not np.isnan(curve.R.cost)
        assert not np.isinf(curve.R.cost)

    def test_plot_method_exists(self) -> None:
        """Test that plot method exists and can be called."""
        curve = LSCCurve()

        # Should not raise exception when called without axis
        try:
            curve.plot()
        except Exception as e:
            if "matplotlib" not in str(e).lower():
                raise  # Re-raise if not matplotlib related

    def test_edge_cases(self) -> None:
        """Test edge cases and boundary conditions.

        Tests various extreme parameter combinations and boundary conditions
        to ensure robustness of the algorithm.
        """
        # Test with zero angles (should handle division by zero)
        curve = LSCCurve(J=90.0, J1=90.0)  # cot(90°) = 0
        assert abs(curve.n) < 1e-15  # Very close to zero due to floating point precision
        assert abs(curve.t) < 1e-15  # Very close to zero due to floating point precision

        # Test with extreme parameter values
        curve_extreme = LSCCurve(m=-10.0, m1=-20.0, s=100.0, H=10.0)
        # Should still compute without errors
        result = curve_extreme.R
        assert isinstance(result, OptimizeResult)

        # Test with negative heights (boundary case)
        curve_negative = LSCCurve(H=-0.1, H1=-0.05, H2=-0.15)
        result_negative = curve_negative.R
        assert isinstance(result_negative, OptimizeResult)

        # Test with very small values
        curve_small = LSCCurve(m=-0.001, m1=-0.002, s=0.001)
        result_small = curve_small.R
        assert isinstance(result_small, OptimizeResult)


class TestLSCOptimizer:
    """Tests for LSCOptimizer class providing high-level optimization interface.

    Tests cover initialization, parameter handling, consistency, and
    sensitivity analysis.
    """

    def test_optimizer_initialization(self) -> None:
        """Test LSCOptimizer initialization."""
        optimizer = LSCOptimizer()
        assert optimizer is not None

    def test_optimize_method(self) -> None:
        """Test the optimize method."""
        optimizer = LSCOptimizer()

        # Test with default parameters
        x, cost = optimizer.optimize()
        assert isinstance(x, np.ndarray)
        assert isinstance(cost, float)
        assert len(x) == 16
        assert cost >= 0
        assert not np.isnan(cost)

    def test_optimize_with_custom_params(self) -> None:
        """Test optimization with custom parameters."""
        optimizer = LSCOptimizer()

        # Test with specific parameter values
        x, cost = optimizer.optimize(m=-2.0, H=1.0, s=2.0)

        assert isinstance(x, np.ndarray)
        assert isinstance(cost, float)
        assert len(x) == 16
        assert cost >= 0

    def test_consistency(self) -> None:
        """Test that repeated calls with same parameters give consistent results.

        Ensures deterministic behavior of the optimization algorithm.
        """
        optimizer = LSCOptimizer()

        # Run optimization twice with same parameters
        x1, cost1 = optimizer.optimize(m=-1.5, H=0.8)
        x2, cost2 = optimizer.optimize(m=-1.5, H=0.8)

        # Results should be identical
        np.testing.assert_array_almost_equal(x1, x2)
        assert abs(cost1 - cost2) < 1e-10

    def test_parameter_sensitivity(self) -> None:
        """Test sensitivity to parameter changes.

        Verifies that changing parameters produces different results.
        Uses larger parameter differences to ensure measurable changes.
        """
        optimizer = LSCOptimizer()

        # Get baseline results
        x_baseline, cost_baseline = optimizer.optimize(m=-1.3, H=0.5)

        # Change parameters significantly
        x_modified, cost_modified = optimizer.optimize(m=-2.0, H=1.0)

        # Results should be different
        assert not np.allclose(x_baseline, x_modified, rtol=1e-2)
        # Cost difference should be detectable (relaxed threshold)
        cost_diff = abs(cost_baseline - cost_modified)
        assert cost_diff > 1e-10 or cost_diff < 1e-20  # Either significant difference or numerical precision limit


def test_import_star() -> None:
    """Test that __all__ exports work correctly.

    Verifies that module exports are properly defined and accessible.
    """
    # Test that we can import from module
    try:
        from pytola_sim.lscopt.cli import __version__

        assert isinstance(__version__, str)
        assert __version__
    except ImportError:
        pass  # Skip if import fails


def test_version_format() -> None:
    """Test that version string follows semantic versioning format.

    Validates that the version string is properly formatted.
    """
    from pytola_sim.lscopt.cli import __version__

    # Should be in format X.Y.Z
    parts = __version__.split(".")
    assert len(parts) == 3
    assert all(part.isdigit() for part in parts)


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