import numpy as np
import pytest

from uniface import compute_similarity, face_alignment


@pytest.fixture
def mock_image():
    """
    Create a mock 640x640 BGR image.
    """
    return np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8)


@pytest.fixture
def mock_landmarks():
    """
    Create mock 5-point facial landmarks.
    Standard positions for a face roughly centered at (112/2, 112/2).
    """
    return np.array(
        [
            [38.2946, 51.6963],  # Left eye
            [73.5318, 51.5014],  # Right eye
            [56.0252, 71.7366],  # Nose
            [41.5493, 92.3655],  # Left mouth corner
            [70.7299, 92.2041],  # Right mouth corner
        ],
        dtype=np.float32,
    )


# compute_similarity tests
def test_compute_similarity_same_embedding():
    """
    Test that similarity of an embedding with itself is 1.0.
    """
    embedding = np.random.randn(1, 512).astype(np.float32)
    embedding = embedding / np.linalg.norm(embedding)  # Normalize

    similarity = compute_similarity(embedding, embedding)
    assert np.isclose(similarity, 1.0, atol=1e-5), f'Self-similarity should be 1.0, got {similarity}'


def test_compute_similarity_range():
    """
    Test that similarity is always in the range [-1, 1].
    """
    # Test with multiple random embeddings
    for _ in range(10):
        emb1 = np.random.randn(1, 512).astype(np.float32)
        emb2 = np.random.randn(1, 512).astype(np.float32)

        # Normalize
        emb1 = emb1 / np.linalg.norm(emb1)
        emb2 = emb2 / np.linalg.norm(emb2)

        similarity = compute_similarity(emb1, emb2)
        assert -1.0 <= similarity <= 1.0, f'Similarity should be in [-1, 1], got {similarity}'


def test_compute_similarity_orthogonal():
    """
    Test that orthogonal embeddings have similarity close to 0.
    """
    # Create orthogonal embeddings
    emb1 = np.zeros((1, 512), dtype=np.float32)
    emb1[0, 0] = 1.0  # [1, 0, 0, ..., 0]

    emb2 = np.zeros((1, 512), dtype=np.float32)
    emb2[0, 1] = 1.0  # [0, 1, 0, ..., 0]

    similarity = compute_similarity(emb1, emb2)
    assert np.isclose(similarity, 0.0, atol=1e-5), f'Orthogonal embeddings should have similarity 0.0, got {similarity}'


def test_compute_similarity_opposite():
    """
    Test that opposite embeddings have similarity close to -1.
    """
    emb1 = np.ones((1, 512), dtype=np.float32)
    emb1 = emb1 / np.linalg.norm(emb1)

    emb2 = -emb1  # Opposite direction

    similarity = compute_similarity(emb1, emb2)
    assert np.isclose(similarity, -1.0, atol=1e-5), f'Opposite embeddings should have similarity -1.0, got {similarity}'


def test_compute_similarity_symmetry():
    """
    Test that similarity(A, B) == similarity(B, A).
    """
    emb1 = np.random.randn(1, 512).astype(np.float32)
    emb2 = np.random.randn(1, 512).astype(np.float32)

    # Normalize
    emb1 = emb1 / np.linalg.norm(emb1)
    emb2 = emb2 / np.linalg.norm(emb2)

    sim_12 = compute_similarity(emb1, emb2)
    sim_21 = compute_similarity(emb2, emb1)

    assert np.isclose(sim_12, sim_21), 'Similarity should be symmetric'


def test_compute_similarity_dtype():
    """
    Test that compute_similarity returns a float.
    """
    emb1 = np.random.randn(1, 512).astype(np.float32)
    emb2 = np.random.randn(1, 512).astype(np.float32)

    # Normalize
    emb1 = emb1 / np.linalg.norm(emb1)
    emb2 = emb2 / np.linalg.norm(emb2)

    similarity = compute_similarity(emb1, emb2)
    assert isinstance(similarity, (float, np.floating)), f'Similarity should be float, got {type(similarity)}'


# face_alignment tests
def test_face_alignment_output_shape(mock_image, mock_landmarks):
    """
    Test that face_alignment produces output with the correct shape.
    """
    aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))

    assert aligned.shape == (112, 112, 3), f'Expected shape (112, 112, 3), got {aligned.shape}'


def test_face_alignment_dtype(mock_image, mock_landmarks):
    """
    Test that aligned face has the correct data type.
    """
    aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))

    assert aligned.dtype == np.uint8, f'Expected uint8, got {aligned.dtype}'


def test_face_alignment_different_sizes(mock_image, mock_landmarks):
    """
    Test face alignment with different output sizes.
    """
    # Only test sizes that are multiples of 112 or 128 as required by the function
    test_sizes = [(112, 112), (128, 128), (224, 224)]

    for size in test_sizes:
        aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=size)
        assert aligned.shape == (*size, 3), f'Failed for size {size}'


def test_face_alignment_consistency(mock_image, mock_landmarks):
    """
    Test that the same input produces the same aligned face.
    """
    aligned1, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))
    aligned2, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))

    assert np.allclose(aligned1, aligned2), 'Same input should produce same aligned face'


def test_face_alignment_landmarks_as_list(mock_image):
    """
    Test that landmarks can be passed as a list of lists (converted to array).
    """
    landmarks_list = [
        [38.2946, 51.6963],
        [73.5318, 51.5014],
        [56.0252, 71.7366],
        [41.5493, 92.3655],
        [70.7299, 92.2041],
    ]

    # Convert list to numpy array before passing to face_alignment
    landmarks_array = np.array(landmarks_list, dtype=np.float32)
    aligned, _ = face_alignment(mock_image, landmarks_array, image_size=(112, 112))
    assert aligned.shape == (112, 112, 3), 'Should work with landmarks as array'


def test_face_alignment_value_range(mock_image, mock_landmarks):
    """
    Test that aligned face pixel values are in valid range [0, 255].
    """
    aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))

    assert np.all(aligned >= 0), 'Pixel values should be >= 0'
    assert np.all(aligned <= 255), 'Pixel values should be <= 255'


def test_face_alignment_not_all_zeros(mock_image, mock_landmarks):
    """
    Test that aligned face is not all zeros (actual transformation occurred).
    """
    aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))

    # At least some pixels should be non-zero
    assert np.any(aligned > 0), 'Aligned face should have some non-zero pixels'


def test_face_alignment_from_different_positions(mock_image):
    """
    Test alignment with landmarks at different positions in the image.
    """
    # Landmarks at different positions
    positions = [
        np.array(
            [[100, 100], [150, 100], [125, 130], [110, 150], [140, 150]],
            dtype=np.float32,
        ),
        np.array(
            [[300, 200], [350, 200], [325, 230], [310, 250], [340, 250]],
            dtype=np.float32,
        ),
        np.array(
            [[500, 400], [550, 400], [525, 430], [510, 450], [540, 450]],
            dtype=np.float32,
        ),
    ]

    for landmarks in positions:
        aligned, _ = face_alignment(mock_image, landmarks, image_size=(112, 112))
        assert aligned.shape == (112, 112, 3), f'Failed for landmarks at {landmarks[0]}'


def test_face_alignment_landmark_count(mock_image):
    """
    Test that face_alignment works specifically with 5-point landmarks.
    """
    # Standard 5-point landmarks
    landmarks_5pt = np.array(
        [
            [38.2946, 51.6963],
            [73.5318, 51.5014],
            [56.0252, 71.7366],
            [41.5493, 92.3655],
            [70.7299, 92.2041],
        ],
        dtype=np.float32,
    )

    aligned, _ = face_alignment(mock_image, landmarks_5pt, image_size=(112, 112))
    assert aligned.shape == (112, 112, 3), 'Should work with 5-point landmarks'


def test_compute_similarity_with_recognition_embeddings():
    """
    Test compute_similarity with realistic embedding dimensions.
    """
    # Simulate ArcFace/MobileFace/SphereFace embeddings (512-dim)
    emb1 = np.random.randn(1, 512).astype(np.float32)
    emb2 = np.random.randn(1, 512).astype(np.float32)

    # Normalize (as done in get_normalized_embedding)
    emb1 = emb1 / np.linalg.norm(emb1)
    emb2 = emb2 / np.linalg.norm(emb2)

    similarity = compute_similarity(emb1, emb2)

    # Should be a valid similarity score
    assert -1.0 <= similarity <= 1.0
    assert isinstance(similarity, (float, np.floating))
