"""Cropping a photograph down to the document in it.

Everything here is drawn, not photographed. The real corpus is client CNICs and
bank statements — Level 3 under ADR-0002, which never leave the installation and
certainly never enter a repository. Synthetic images also fail the same way
every time, which a photograph does not.

## What these are really guarding

The module's whole safety property is that it never makes a document worse: on
any doubt it keeps the original. So the tests that matter most are the REFUSALS
— that a blurred crop, a smear of clutter, or a document running off the edge of
the shot is declined rather than filed.
"""

from __future__ import annotations

from pathlib import Path

import numpy as np
import pytest

cv2 = pytest.importorskip("cv2")

from app.images.prepare import (  # noqa: E402
    Thresholds,
    _as_quad,
    detect_document,
    fills_frame,
    prepare,
)


def photograph(
    *,
    width: int = 1200,
    height: int = 900,
    margin: int = 120,
    background: int = 40,
    document: int = 235,
) -> np.ndarray:
    """A light document on a dark surface, seen straight on."""
    frame = np.full((height, width, 3), background, np.uint8)
    frame[margin:height - margin, margin:width - margin] = document

    # Some texture, or the "document" is a flat rectangle with no detail and
    # sharpness measures zero.
    for y in range(margin + 40, height - margin - 40, 45):
        cv2.line(frame, (margin + 40, y), (width - margin - 40, y), (90, 90, 90), 3)

    return frame


class TestFindingTheDocument:
    def test_a_document_on_a_contrasting_surface_is_found(self):
        corners = detect_document(photograph())

        assert corners is not None
        assert len(corners) == 4

    def test_a_photograph_of_nothing_in_particular_finds_nothing(self):
        noise = np.random.default_rng(0).integers(0, 255, (600, 800, 3), dtype=np.uint8)

        assert detect_document(noise) is None


class TestTheShapeItWillAccept:
    """_as_quad, which decides whether a contour is document-shaped.

    The tolerance is swept rather than fixed at 2% of the perimeter. That number
    suits a card lying flat with four crisp corners and is too tight for most of
    what arrives — a bent CNIC, a lifted corner, an edge softened by shadow, all
    of which simplify to five or six points and were discarded as "not a
    document" while being plainly a document.
    """

    def _contour_of(self, mask: np.ndarray):
        contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        return max(contours, key=cv2.contourArea)

    def test_a_clean_rectangle_gives_four_corners(self):
        mask = np.zeros((400, 600), np.uint8)
        cv2.rectangle(mask, (80, 60), (520, 340), 255, -1)
        contour = self._contour_of(mask)

        quad = _as_quad(contour, cv2.contourArea(contour), Thresholds())

        assert quad is not None
        assert len(quad) == 4

    def test_a_rectangle_with_a_softened_corner_is_still_accepted(self):
        # The case the fixed 2% tolerance threw away.
        mask = np.zeros((400, 600), np.uint8)
        points = np.array([[80, 60], [500, 62], [520, 300], [300, 342], [82, 338]])
        cv2.fillPoly(mask, [points], 255)
        contour = self._contour_of(mask)

        quad = _as_quad(contour, cv2.contourArea(contour), Thresholds())

        assert quad is not None, 'a five-sided document is still a document'
        assert len(quad) == 4

    def test_a_shape_that_is_not_rectangular_is_refused(self):
        # An L, which fills barely half its own bounding box. Accepting this is
        # how a smear of clutter becomes somebody's tax document.
        mask = np.zeros((400, 600), np.uint8)
        cv2.fillPoly(mask, [np.array([[60, 60], [260, 60], [260, 200], [520, 200],
                                      [520, 340], [60, 340]])], 255)
        contour = self._contour_of(mask)

        assert _as_quad(contour, cv2.contourArea(contour), Thresholds()) is None


class TestAPhotographThatIsAlreadyTheDocument:
    """A close-up held to the camera, with no surface visible around it.

    There is nothing to crop and no quadrilateral to find, so detection declines
    and the original is kept — which was always right. What was wrong was
    calling it "no document was found in the picture", which made a correct
    outcome indistinguishable from a miss in the logs and in every measurement
    taken from them.
    """

    @staticmethod
    def close_up(width: int = 1200, height: int = 900) -> np.ndarray:
        """A document photographed so close that it reaches every edge.

        The texture has to run right to the border. An earlier version of this
        fixture left a clean white margin, which gave the frame's edge a
        different mean from its middle — the very signal that says a document
        does NOT fill the frame — and the fixture failed the check it was
        written to demonstrate.
        """
        frame = np.full((height, width, 3), 225, np.uint8)

        for y in range(6, height, 26):
            cv2.line(frame, (0, y), (width, y), (95, 95, 95), 3)

        return frame

    def test_it_is_recognised_as_filling_the_frame(self):
        assert fills_frame(self.close_up()) is True

    def test_a_document_with_a_visible_surface_around_it_is_not(self):
        assert fills_frame(photograph()) is False

    def test_prepare_says_so_rather_than_reporting_a_miss(self, tmp_path: Path):
        path = tmp_path / 'close-up.jpg'
        cv2.imwrite(str(path), self.close_up())

        result = prepare(path)

        assert result.cropped is False
        assert result.path == path
        assert 'fills the frame' in result.reason
        assert result.metadata.get('already_cropped') is True


class TestItNeverCostsADocument:
    """The property the whole module exists to preserve."""

    def test_an_unreadable_file_returns_the_original(self, tmp_path: Path):
        path = tmp_path / 'broken.jpg'
        path.write_bytes(b'this is not an image')

        result = prepare(path)

        assert result.cropped is False
        assert result.path == path

    def test_something_that_is_not_an_image_at_all_returns_the_original(self, tmp_path: Path):
        path = tmp_path / 'statement.pdf'
        path.write_bytes(b'%PDF-1.4')

        result = prepare(path)

        assert result.cropped is False
        assert result.path == path

    def test_a_blurred_photograph_is_refused_rather_than_filed(self, tmp_path: Path):
        blurred = cv2.GaussianBlur(photograph(), (51, 51), 0)
        path = tmp_path / 'blurred.jpg'
        cv2.imwrite(str(path), blurred)

        result = prepare(path)

        assert result.cropped is False, 'OCR on a blurred crop produces confident nonsense'

    def test_a_good_photograph_is_cropped_and_the_crop_is_written(self, tmp_path: Path):
        path = tmp_path / 'clean.jpg'
        cv2.imwrite(str(path), photograph())

        result = prepare(path)

        assert result.cropped is True
        assert result.path.exists()
        assert result.path != path
        assert result.metadata['crop_success'] is True
