"""Tests for CoordinateTransformer (F13) — Real coordinate chain (ESKF-06)."""

import numpy as np
import pytest

from gps_denied.core.coordinates import (
    CoordinateTransformer,
    OriginNotSetError,
    _build_intrinsic_matrix,
    _cam_to_body_rotation,
    _quat_to_rotation_matrix,
)
from gps_denied.schemas import CameraParameters, GPSPoint


@pytest.fixture
def transformer():
    return CoordinateTransformer()


def test_enu_origin_management(transformer):
    fid = "flight_123"
    origin = GPSPoint(lat=48.0, lon=37.0)

    # Before setting
    with pytest.raises(OriginNotSetError):
        transformer.get_enu_origin(fid)

    # After setting
    transformer.set_enu_origin(fid, origin)
    assert transformer.get_enu_origin(fid).lat == 48.0


def test_gps_to_enu(transformer):
    fid = "flight_123"
    origin = GPSPoint(lat=48.0, lon=37.0)
    transformer.set_enu_origin(fid, origin)

    # Same point -> 0, 0, 0
    enu = transformer.gps_to_enu(fid, origin)
    assert enu == (0.0, 0.0, 0.0)

    # Point north
    target = GPSPoint(lat=48.01, lon=37.0)
    enu_n = transformer.gps_to_enu(fid, target)
    assert enu_n[0] == 0.0
    assert enu_n[1] > 1000.0  # 0.01 deg lat is > 1km
    assert enu_n[2] == 0.0


def test_enu_roundtrip(transformer):
    fid = "flight_123"
    origin = GPSPoint(lat=48.0, lon=37.0)
    transformer.set_enu_origin(fid, origin)

    test_gps = GPSPoint(lat=48.056, lon=37.123)
    enu = transformer.gps_to_enu(fid, test_gps)

    recovered = transformer.enu_to_gps(fid, enu)
    assert pytest.approx(recovered.lat, abs=1e-6) == test_gps.lat
    assert pytest.approx(recovered.lon, abs=1e-6) == test_gps.lon


def test_pixel_to_gps_flow(transformer):
    fid = "flight_123"
    origin = GPSPoint(lat=48.0, lon=37.0)
    transformer.set_enu_origin(fid, origin)

    cam = CameraParameters(
        focal_length=25.0,
        sensor_width=23.5,
        sensor_height=15.6,
        resolution_width=4000,
        resolution_height=3000,
    )

    # Image center should yield approximately the frame center
    cx = cam.resolution_width / 2.0
    cy = cam.resolution_height / 2.0
    pixel = (cx, cy)
    pose = {"position": [0, 0, 0]}
    q_identity = np.array([1.0, 0.0, 0.0, 0.0])

    gps_res = transformer.pixel_to_gps(fid, pixel, pose, cam, 100.0, quaternion=q_identity)
    assert pytest.approx(gps_res.lat, abs=1e-4) == origin.lat
    assert pytest.approx(gps_res.lon, abs=1e-4) == origin.lon

    # Inverse must match pixel
    pix_res = transformer.gps_to_pixel(fid, gps_res, pose, cam, 100.0, quaternion=q_identity)
    assert abs(pix_res[0] - pixel[0]) < 1.0
    assert abs(pix_res[1] - pixel[1]) < 1.0

    # And image_object_to_gps should work
    obj_gps = transformer.image_object_to_gps(
        fid, 1, pixel, frame_pose=pose, camera_params=cam, altitude=100.0, quaternion=q_identity
    )
    assert pytest.approx(obj_gps.lat, abs=1e-4) == origin.lat


# ============================================================================
# ESKF-06: Real Coordinate Chain Tests
# ============================================================================


class TestIntrinsicMatrix:
    """Tests for camera intrinsic matrix construction."""

    def test_build_intrinsic_matrix_adti_20l_v1(self):
        """Test K matrix for ADTI 20L V1 camera with 16mm lens."""
        cam = CameraParameters(
            focal_length=16.0,
            sensor_width=23.2,
            sensor_height=15.4,
            resolution_width=5456,
            resolution_height=3632,
        )
        K = _build_intrinsic_matrix(cam)

        # Expected focal lengths
        fx_expected = 16.0 * 5456 / 23.2
        fy_expected = 16.0 * 3632 / 15.4

        assert K.shape == (3, 3)
        assert abs(K[0, 0] - fx_expected) < 1.0  # fx
        assert abs(K[1, 1] - fy_expected) < 1.0  # fy
        assert abs(K[0, 2] - 2728.0) < 1.0  # cx (center)
        assert abs(K[1, 2] - 1816.0) < 1.0  # cy (center)
        assert K[2, 2] == 1.0

    def test_build_intrinsic_matrix_custom_principal_point(self):
        """Test K matrix with custom principal point."""
        cam = CameraParameters(
            focal_length=16.0,
            sensor_width=23.2,
            sensor_height=15.4,
            resolution_width=5456,
            resolution_height=3632,
            principal_point=(2700.0, 1800.0),
        )
        K = _build_intrinsic_matrix(cam)

        assert abs(K[0, 2] - 2700.0) < 0.1  # cx
        assert abs(K[1, 2] - 1800.0) < 0.1  # cy


class TestCameraToBodyRotation:
    """Tests for camera-to-body rotation matrix."""

    def test_cam_to_body_is_180_x_rotation(self):
        """Test that camera-to-body rotation is Rx(180deg)."""
        R_cam_body = _cam_to_body_rotation()

        # Should flip Y and Z axes, keep X
        assert R_cam_body.shape == (3, 3)
        assert R_cam_body[0, 0] == 1.0
        assert R_cam_body[1, 1] == -1.0
        assert R_cam_body[2, 2] == -1.0
        assert np.allclose(np.linalg.det(R_cam_body), 1.0)  # det = 1 (proper rotation)


class TestQuaternionRotation:
    """Tests for quaternion to rotation matrix conversion."""

    def test_identity_quaternion(self):
        """Test identity quaternion [1, 0, 0, 0] produces identity matrix."""
        q = np.array([1.0, 0.0, 0.0, 0.0])
        R = _quat_to_rotation_matrix(q)

        assert np.allclose(R, np.eye(3))

    def test_90_degree_z_rotation(self):
        """Test 90-degree rotation around Z axis."""
        # q = [cos(45deg), 0, 0, sin(45deg)] for 90-degree rotation around Z
        angle = np.pi / 4  # 45 degrees
        q = np.array([np.cos(angle), 0.0, 0.0, np.sin(angle)])
        R = _quat_to_rotation_matrix(q)

        # Should rotate [1, 0, 0] to approximately [0, 1, 0]
        x_axis = np.array([1.0, 0.0, 0.0])
        rotated = R @ x_axis
        expected = np.array([0.0, 1.0, 0.0])
        assert np.allclose(rotated, expected, atol=0.01)


class TestPixelToGPSChain:
    """Tests for full pixel-to-GPS projection chain (ESKF-06)."""

    def test_image_center_nadir_projection(self):
        """Test that image center pixel projects to UAV nadir position."""
        transformer = CoordinateTransformer()
        origin = GPSPoint(lat=48.0, lon=37.0)
        transformer.set_enu_origin("f1", origin)

        cam = CameraParameters(
            focal_length=16.0,
            sensor_width=23.2,
            sensor_height=15.4,
            resolution_width=5456,
            resolution_height=3632,
        )

        # Center pixel with identity quaternion (level flight)
        pixel = (2728.0, 1816.0)
        pose = {"position": [0.0, 0.0, 0.0]}
        q_identity = np.array([1.0, 0.0, 0.0, 0.0])

        gps = transformer.pixel_to_gps(
            "f1", pixel, pose, cam, altitude=600.0, quaternion=q_identity
        )

        # Center pixel should project to UAV nadir (origin)
        assert abs(gps.lat - 48.0) < 0.001
        assert abs(gps.lon - 37.0) < 0.001

    def test_pixel_offset_produces_ground_offset(self):
        """Test that off-center pixel offsets project to corresponding ground distance."""
        transformer = CoordinateTransformer()
        origin = GPSPoint(lat=48.0, lon=37.0)
        transformer.set_enu_origin("f1", origin)

        cam = CameraParameters(
            focal_length=16.0,
            sensor_width=23.2,
            sensor_height=15.4,
            resolution_width=5456,
            resolution_height=3632,
        )

        pose = {"position": [0.0, 0.0, 0.0]}
        q_identity = np.array([1.0, 0.0, 0.0, 0.0])
        altitude = 600.0

        # Center pixel
        center_pixel = (2728.0, 1816.0)
        center_gps = transformer.pixel_to_gps(
            "f1", center_pixel, pose, cam, altitude=altitude, quaternion=q_identity
        )

        # Offset pixel (100 pixels to the right)
        offset_pixel = (2828.0, 1816.0)
        offset_gps = transformer.pixel_to_gps(
            "f1", offset_pixel, pose, cam, altitude=altitude, quaternion=q_identity
        )

        # Longitude difference should be non-zero (right = east = longer)
        assert offset_gps.lon != center_gps.lon

    def test_altitude_scaling_is_linear(self):
        """Test that doubling altitude doubles ground distance from nadir."""
        transformer = CoordinateTransformer()
        origin = GPSPoint(lat=48.0, lon=37.0)
        transformer.set_enu_origin("f1", origin)

        cam = CameraParameters(
            focal_length=16.0,
            sensor_width=23.2,
            sensor_height=15.4,
            resolution_width=5456,
            resolution_height=3632,
        )

        pose = {"position": [0.0, 0.0, 0.0]}
        q_identity = np.array([1.0, 0.0, 0.0, 0.0])
        pixel = (2728.0 + 100, 1816.0)  # Offset pixel

        # Project at altitude 300m
        gps_300 = transformer.pixel_to_gps(
            "f1", pixel, pose, cam, altitude=300.0, quaternion=q_identity
        )

        # Project at altitude 600m
        gps_600 = transformer.pixel_to_gps(
            "f1", pixel, pose, cam, altitude=600.0, quaternion=q_identity
        )

        # Distance from origin should scale with altitude (roughly)
        # At 600m, ground distance should be ~2x that at 300m
        dist_300 = abs(gps_300.lon - 37.0)
        dist_600 = abs(gps_600.lon - 37.0)
        ratio = dist_600 / dist_300 if dist_300 > 0 else 1.0
        assert 1.8 < ratio < 2.2  # Allow some numerical tolerance


class TestGPSToPixelInverse:
    """Tests for GPS-to-pixel projection (inverse of pixel-to-GPS)."""

    def test_gps_to_pixel_is_inverse(self):
        """Test that gps_to_pixel is the exact inverse of pixel_to_gps."""
        transformer = CoordinateTransformer()
        origin = GPSPoint(lat=48.0, lon=37.0)
        transformer.set_enu_origin("f1", origin)

        cam = CameraParameters(
            focal_length=16.0,
            sensor_width=23.2,
            sensor_height=15.4,
            resolution_width=5456,
            resolution_height=3632,
        )

        pose = {"position": [0.0, 0.0, 0.0]}
        q_identity = np.array([1.0, 0.0, 0.0, 0.0])
        original_pixel = (2728.0 + 50, 1816.0 - 30)

        # pixel -> GPS
        gps = transformer.pixel_to_gps(
            "f1", original_pixel, pose, cam, altitude=600.0, quaternion=q_identity
        )

        # GPS -> pixel
        recovered_pixel = transformer.gps_to_pixel(
            "f1", gps, pose, cam, altitude=600.0, quaternion=q_identity
        )

        # Should recover original pixel
        assert abs(recovered_pixel[0] - original_pixel[0]) < 1.0
        assert abs(recovered_pixel[1] - original_pixel[1]) < 1.0

    def test_roundtrip_with_quaternion(self):
        """Test pixel-GPS-pixel roundtrip with non-identity quaternion."""
        transformer = CoordinateTransformer()
        origin = GPSPoint(lat=48.0, lon=37.0)
        transformer.set_enu_origin("f1", origin)

        cam = CameraParameters(
            focal_length=16.0,
            sensor_width=23.2,
            sensor_height=15.4,
            resolution_width=5456,
            resolution_height=3632,
        )

        pose = {"position": [100.0, 50.0, 0.0]}
        # Small 30-degree rotation around Z
        angle = np.radians(30)
        q = np.array([np.cos(angle / 2), 0, 0, np.sin(angle / 2)])
        original_pixel = (2728.0, 1816.0)  # Center

        gps = transformer.pixel_to_gps(
            "f1", original_pixel, pose, cam, altitude=600.0, quaternion=q
        )

        recovered_pixel = transformer.gps_to_pixel(
            "f1", gps, pose, cam, altitude=600.0, quaternion=q
        )

        # Center pixel should still recover to center
        assert abs(recovered_pixel[0] - 2728.0) < 10.0
        assert abs(recovered_pixel[1] - 1816.0) < 10.0


class TestTransformPoints:
    """Tests for homography transform via cv2.perspectiveTransform."""

    def test_transform_points_identity(self):
        """Test homography transform with identity matrix."""
        transformer = CoordinateTransformer()

        H = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]  # Identity
        points = [(10.0, 20.0), (30.0, 40.0)]

        result = transformer.transform_points(points, H)

        assert len(result) == 2
        assert abs(result[0][0] - 10.0) < 0.1
        assert abs(result[0][1] - 20.0) < 0.1
        assert abs(result[1][0] - 30.0) < 0.1
        assert abs(result[1][1] - 40.0) < 0.1

    def test_transform_points_translation(self):
        """Test homography with translation."""
        transformer = CoordinateTransformer()

        # Translate by (100, 50)
        H = [[1, 0, 100], [0, 1, 50], [0, 0, 1]]
        points = [(0.0, 0.0)]

        result = transformer.transform_points(points, H)

        assert len(result) == 1
        assert abs(result[0][0] - 100.0) < 0.1
        assert abs(result[0][1] - 50.0) < 0.1

    def test_transform_points_empty(self):
        """Test transform_points with empty point list."""
        transformer = CoordinateTransformer()
        H = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
        result = transformer.transform_points([], H)
        assert result == []
