"""Tests for Phase 2 document detection + perspective correction."""

from __future__ import annotations

import cv2
import numpy as np
import pytest

from ocr_sprint.pipeline.document_detect import (
    DocumentDetectConfig,
    _find_document_quad,
    _four_point_warp,
    _order_corners,
    _remove_shadow,
    detect_and_correct,
)


def _synthetic_skewed_doc(
    canvas_size: tuple[int, int] = (900, 700),
    page_size: tuple[int, int] = (500, 380),
    skew: tuple[int, int, int, int] = (40, -20, -30, 25),
) -> np.ndarray:
    """Create a dark canvas containing a white quadrilateral 'document'.

    Returns a BGR uint8 image. Useful for testing detection without bringing
    in real photos.
    """
    h, w = canvas_size
    img = np.full((h, w, 3), 30, dtype=np.uint8)  # dark grey background
    page_w, page_h = page_size
    cx, cy = w // 2, h // 2
    # corners offset by `skew` to simulate perspective distortion
    sx_tl, sx_tr, sx_br, sx_bl = skew
    pts = np.array(
        [
            [cx - page_w // 2 + sx_tl, cy - page_h // 2 + sx_tl],  # TL
            [cx + page_w // 2 + sx_tr, cy - page_h // 2 - sx_tr],  # TR
            [cx + page_w // 2 + sx_br, cy + page_h // 2 + sx_br],  # BR
            [cx - page_w // 2 + sx_bl, cy + page_h // 2 - sx_bl],  # BL
        ],
        dtype=np.int32,
    )
    cv2.fillPoly(img, [pts], color=(245, 245, 245))
    # add a fake text line so the page isn't a flat white blob
    cv2.line(img, (cx - 100, cy), (cx + 100, cy), color=(20, 20, 20), thickness=4)
    return img


class TestOrderCorners:
    def test_canonical_order(self) -> None:
        # input pts unordered: BR, TL, TR, BL
        pts = np.array([[100, 100], [10, 10], [110, 8], [9, 95]], dtype=np.float32)
        ordered = _order_corners(pts)
        # TL has the smallest sum
        assert tuple(ordered[0]) == pytest.approx((10.0, 10.0))
        # BR has the largest sum
        assert tuple(ordered[2]) == pytest.approx((100.0, 100.0))


class TestFindDocumentQuad:
    def test_finds_quad_in_synthetic_image(self) -> None:
        img = _synthetic_skewed_doc()
        quad = _find_document_quad(img, min_area_fraction=0.10)
        assert quad is not None
        assert quad.shape == (4, 2)

    def test_returns_none_for_flat_image(self) -> None:
        flat = np.full((400, 400, 3), 200, dtype=np.uint8)
        assert _find_document_quad(flat) is None

    def test_returns_none_for_tiny_image(self) -> None:
        tiny = np.full((20, 20, 3), 200, dtype=np.uint8)
        assert _find_document_quad(tiny) is None


class TestFourPointWarp:
    def test_warp_produces_axis_aligned_rectangle(self) -> None:
        img = _synthetic_skewed_doc()
        quad = _find_document_quad(img, min_area_fraction=0.10)
        assert quad is not None
        warped = _four_point_warp(img, quad)
        # warped output should be smaller than the canvas (it's only the page)
        assert warped.shape[0] < img.shape[0]
        assert warped.shape[1] < img.shape[1]
        # majority of the warped image should be near-white (the page)
        gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
        bright_fraction = float((gray > 200).mean())
        assert bright_fraction > 0.7


class TestRemoveShadow:
    def test_output_shape_matches_input(self) -> None:
        img = np.random.randint(0, 256, (200, 300, 3), dtype=np.uint8)
        out = _remove_shadow(img)
        assert out.shape == img.shape
        assert out.dtype == np.uint8


class TestDetectAndCorrect:
    def test_full_pipeline_on_skewed_doc(self) -> None:
        img = _synthetic_skewed_doc()
        out = detect_and_correct(
            img,
            DocumentDetectConfig(detect_document=True, remove_shadow=True, min_area_fraction=0.1),
        )
        # the output is the cropped+flattened page, so it must be smaller
        assert out.shape[0] < img.shape[0]

    def test_disabled_returns_input_unchanged(self) -> None:
        img = _synthetic_skewed_doc()
        out = detect_and_correct(
            img,
            DocumentDetectConfig(detect_document=False, remove_shadow=False),
        )
        assert out.shape == img.shape
        # disabled both flags → bytes-identical
        assert np.array_equal(out, img)

    def test_failed_detection_falls_back_to_input(self) -> None:
        # flat grey image with no contour-detectable document
        flat = np.full((300, 300, 3), 128, dtype=np.uint8)
        out = detect_and_correct(
            flat,
            DocumentDetectConfig(detect_document=True, remove_shadow=False, min_area_fraction=0.5),
        )
        # detection failed, so the original shape is preserved
        assert out.shape == flat.shape
