"""
Test fundamental UHG measurements: quadrance and spread.
All tests verify that measurements follow UHG principles.
"""

import pytest
import torch
import math
from uhg.projective import ProjectiveUHG

@pytest.fixture
def uhg():
    return ProjectiveUHG()

@pytest.fixture
def sample_points():
    """Generate test points with known properties."""
    return {
        'origin': torch.tensor([0.0, 0.0, 1.0]),
        'unit_x': torch.tensor([1.0, 0.0, 2.0]),  # Normalized to have positive quadrance
        'unit_y': torch.tensor([0.0, 1.0, 2.0]),  # Normalized to have positive quadrance
        'perp_points': (
            torch.tensor([1.0, 0.0, 2.0]),  # Normalized for perpendicularity
            torch.tensor([0.0, 1.0, 2.0])   # x₁x₂ + y₁y₂ - z₁z₂ = 0
        ),
        'same_points': (
            torch.tensor([1.0, 1.0, 2.0]),
            torch.tensor([1.0, 1.0, 2.0])
        ),
        'null_point': torch.tensor([1.0, 1.0, math.sqrt(2.0)]),  # x² + y² - z² = 0
    }

@pytest.fixture
def sample_lines():
    """Generate test lines with known properties."""
    return {
        'x_axis': torch.tensor([0.0, 1.0, 0.0]),  # y = 0
        'y_axis': torch.tensor([1.0, 0.0, 0.0]),  # x = 0
        'diagonal': torch.tensor([1.0, -1.0, 0.0]),  # y = x
        'perp_lines': (
            torch.tensor([1.0, 0.0, 0.0]),  # x = 0
            torch.tensor([0.0, 1.0, 0.0])   # y = 0
        ),
        'parallel_lines': (
            torch.tensor([1.0, 0.0, 0.0]),  # x = 0
            torch.tensor([1.0, 0.0, 1.0])   # x = 1
        ),
    }

def test_quadrance_properties(uhg, sample_points):
    """Test fundamental properties of quadrance."""
    # Property 1: Perpendicular points have quadrance 1
    p1, p2 = sample_points['perp_points']
    assert torch.isclose(uhg.quadrance(p1, p2), torch.tensor(1.0), rtol=1e-5)
    
    # Property 2: Same points have quadrance 0
    p1, p2 = sample_points['same_points']
    assert torch.isclose(uhg.quadrance(p1, p2), torch.tensor(0.0), rtol=1e-5)
    
    # Property 3: Quadrance is symmetric
    p1, p2 = sample_points['perp_points']
    q12 = uhg.quadrance(p1, p2)
    q21 = uhg.quadrance(p2, p1)
    assert torch.isclose(q12, q21, rtol=1e-5)
    
    # Property 4: Null points raise error
    with pytest.raises(ValueError):
        uhg.quadrance(sample_points['null_point'], sample_points['unit_x'])

def test_quadrance_cross_ratio(uhg, sample_points):
    """Test that quadrance equals the cross-ratio with opposite points."""
    p1, p2 = sample_points['perp_points']
    
    # Compute opposite points
    aa = uhg.hyperbolic_dot(p1, p1)
    bb = uhg.hyperbolic_dot(p2, p2)
    ab = uhg.hyperbolic_dot(p1, p2)
    
    o1 = ab * p1 - aa * p2
    o2 = bb * p1 - ab * p2
    
    # Verify q(p1,p2) = (p1,o2:p2,o1)
    q_direct = uhg.quadrance(p1, p2)
    q_cross = uhg.cross_ratio(p1, o2, p2, o1)
    assert torch.isclose(q_direct, q_cross, rtol=1e-5)

def test_spread_properties(uhg, sample_lines):
    """Test fundamental properties of spread."""
    # Property 1: Perpendicular lines have spread 1
    l1, l2 = sample_lines['perp_lines']
    assert torch.isclose(uhg.spread(l1, l2), torch.tensor(1.0), rtol=1e-5)
    
    # Property 2: Parallel lines have spread 0
    l1, l2 = sample_lines['parallel_lines']
    assert torch.isclose(uhg.spread(l1, l2), torch.tensor(0.0), rtol=1e-5)
    
    # Property 3: Spread is symmetric
    l1, l2 = sample_lines['perp_lines']
    s12 = uhg.spread(l1, l2)
    s21 = uhg.spread(l2, l1)
    assert torch.isclose(s12, s21, rtol=1e-5)

def test_projective_invariance(uhg, sample_points, sample_lines):
    """Test that measurements are invariant under projective transformations."""
    p1, p2 = sample_points['perp_points']
    l1, l2 = sample_lines['perp_lines']
    
    # Get random projective transformation (UHG works in RP²)
    matrix = uhg.get_projective_matrix(2)
    
    # Transform points and lines
    p1_trans = uhg.transform(p1, matrix)
    p2_trans = uhg.transform(p2, matrix)
    l1_trans = uhg.transform(l1, matrix.inverse().transpose())
    l2_trans = uhg.transform(l2, matrix.inverse().transpose())
    
    # Verify measurements are preserved
    q_orig = uhg.quadrance(p1, p2)
    q_trans = uhg.quadrance(p1_trans, p2_trans)
    assert torch.isclose(q_orig, q_trans, rtol=1e-5)
    
    s_orig = uhg.spread(l1, l2)
    s_trans = uhg.spread(l1_trans, l2_trans)
    assert torch.isclose(s_orig, s_trans, rtol=1e-5)

def test_null_point_properties(uhg, sample_points):
    """Test properties of null points."""
    # Property 1: A point is null iff x² + y² - z² = 0
    null_point = sample_points['null_point']
    assert uhg.is_null_point(null_point)
    
    # Property 2: Points generated by null_point are always null
    t = torch.tensor(1.0)
    u = torch.tensor(0.5)
    generated_null = uhg.null_point(t, u)
    assert uhg.is_null_point(generated_null)
    
    # Property 3: Non-null points should be identified correctly
    non_null = torch.tensor([1.0, 0.0, 2.0])  # x² + y² - z² = 1 - 4 = -3 ≠ 0
    assert not uhg.is_null_point(non_null)

def test_join_null_points(uhg):
    """Test joining of null points."""
    # Generate two null points
    t1, u1 = torch.tensor(1.0), torch.tensor(0.0)  # [1:0:1]
    t2, u2 = torch.tensor(0.0), torch.tensor(1.0)  # [-1:0:1]
    
    p1 = uhg.null_point(t1, u1)
    p2 = uhg.null_point(t2, u2)
    
    # Verify both points are null
    assert uhg.is_null_point(p1)
    assert uhg.is_null_point(p2)
    
    # Get their join
    join = uhg.join_null_points(t1, u1, t2, u2)
    
    # The join should be a valid line
    # For these parameters, it should be [1:0:0] (the x-axis)
    expected = torch.tensor([1.0, 0.0, 0.0])
    # Lines are only defined up to scale, so normalize before comparing
    join = uhg.normalize_points(join)
    expected = uhg.normalize_points(expected)
    assert torch.allclose(join, expected, rtol=1e-5)

def test_quadrance_with_null_points(uhg, sample_points):
    """Test quadrance behavior with null points."""
    null_point = sample_points['null_point']
    non_null = torch.tensor([1.0, 0.0, 2.0])
    
    # Quadrance should be undefined (raise error) when either point is null
    with pytest.raises(ValueError):
        uhg.quadrance(null_point, non_null)
    
    with pytest.raises(ValueError):
        uhg.quadrance(non_null, null_point)
    
    # Should also raise error when both points are null
    with pytest.raises(ValueError):
        uhg.quadrance(null_point, null_point)
