"""Tests unitaires pour pipeline.deskew.

Tests sans dépendance GPU. Génère des images synthétiques en code + utilise
les images du cache pour les cas réels.
"""
from __future__ import annotations

import math
from pathlib import Path

import numpy as np
import pytest
from PIL import Image

from pipeline.deskew import (
    MAX_ANGLE_DEG,
    MIN_ANGLE_DEG,
    NEAR_HORIZONTAL_BAND,
    deskew_image,
    detect_skew_angle,
)


# ============================================================
# Helpers : fabriquer une image synthétique avec des lignes
# ============================================================

def _make_grid_image(w: int = 800, h: int = 1000,
                     n_lines: int = 30, angle_deg: float = 0.0) -> Image.Image:
    """Crée une image blanche avec `n_lines` lignes horizontales équi-réparties,
    optionnellement tournée d'un angle donné. Parfaite pour tester le détecteur.
    """
    arr = np.ones((h, w), dtype=np.uint8) * 255
    for i in range(1, n_lines + 1):
        y = int(i * h / (n_lines + 1))
        arr[y - 1:y + 1, 50:w - 50] = 0  # ligne horizontale noire de 2 px
    img = Image.fromarray(arr, mode="L")
    if angle_deg != 0.0:
        # PIL.rotate : angle positif = sens trigonométrique (= anti-horaire)
        # On veut tester avec notre convention (positif = horaire) donc
        # on inverse ici pour cohérence avec detect_skew_angle
        img = img.rotate(-angle_deg, resample=Image.Resampling.BICUBIC,
                         expand=False, fillcolor="white")
    return img.convert("RGB")


# ============================================================
# Tests de détection
# ============================================================

class TestDetectSkewAngle:
    def test_image_parfaitement_droite(self):
        img = _make_grid_image()
        angle = detect_skew_angle(img)
        assert abs(angle) < 0.1, f"image droite doit donner ~0°, got {angle}"

    @pytest.mark.parametrize("input_angle", [1.0, 2.0, -3.0, 4.0])
    def test_detecte_angles_modérés(self, input_angle):
        """Sur notre image synthétique (30 lignes), la sensibilité est ~1°.
        Sur de vraies fiches OGC avec 300+ lignes de tableaux, la sensibilité
        descend à 0.3° (cf. test réel sur OGC 1 : +0.91° détecté).
        """
        img = _make_grid_image(angle_deg=input_angle)
        detected = detect_skew_angle(img)
        assert abs(detected - input_angle) < 0.5, \
            f"attendu ~{input_angle}°, détecté {detected}°"

    def test_image_sans_lignes_retourne_zero(self):
        # Image totalement uniforme → aucune ligne détectable
        arr = np.ones((500, 500), dtype=np.uint8) * 255
        img = Image.fromarray(arr, mode="L").convert("RGB")
        assert detect_skew_angle(img) == 0.0

    def test_angle_extrême_rejeté(self):
        # Une rotation de 45° dépasse MAX_ANGLE_DEG → on refuse de corriger
        img = _make_grid_image(angle_deg=45.0)
        detected = detect_skew_angle(img)
        # Soit 0.0 (pas de lignes quasi-horizontales à ±15°), soit borné
        assert abs(detected) < MAX_ANGLE_DEG or detected == 0.0


# ============================================================
# Tests de correction (deskew_image)
# ============================================================

class TestDeskewImage:
    def test_image_droite_inchangée(self):
        img = _make_grid_image()
        rotated, applied = deskew_image(img)
        assert applied == 0.0
        # Identité bit à bit
        assert np.array_equal(np.array(rotated), np.array(img))

    def test_image_inclinée_corrigée(self):
        img = _make_grid_image(angle_deg=2.0)
        rotated, applied = deskew_image(img)
        # On attend qu'on applique un angle proche de 2° (convention positive)
        assert abs(applied) > MIN_ANGLE_DEG, \
            f"devrait corriger, got applied={applied}"
        # Après rotation, l'angle résiduel doit être très faible
        residual = detect_skew_angle(rotated)
        assert abs(residual) < 0.5, \
            f"angle résiduel trop grand après correction : {residual}°"

    def test_seuil_min_angle_respecté(self):
        # Un skew juste sous le seuil ne doit pas être corrigé
        img = _make_grid_image(angle_deg=MIN_ANGLE_DEG / 2)
        _, applied = deskew_image(img)
        assert applied == 0.0

    def test_angle_forcé(self):
        """On peut forcer un angle arbitraire indépendamment de la détection."""
        img = _make_grid_image()  # droit
        rotated, applied = deskew_image(img, angle=5.0)
        assert applied == 5.0
        # Taille conservée
        assert rotated.size == img.size


# ============================================================
# Tests avec fixtures réelles (si cache dispo)
# ============================================================

class TestOnRealCachedPages:
    """Ces tests s'exécutent seulement si le cache d'images existe."""

    @pytest.fixture
    def cached_pages(self):
        paths = sorted(Path(".cache/images").glob("*/page_01.png"))
        if not paths:
            pytest.skip("pas de cache d'images disponible")
        return paths

    def test_detection_ne_crash_pas(self, cached_pages):
        """Sur toutes les pages cachées, detect_skew_angle ne doit pas planter."""
        for p in cached_pages[:5]:  # limite pour la vitesse
            img = Image.open(p)
            angle = detect_skew_angle(img)
            assert isinstance(angle, float)
            assert abs(angle) <= MAX_ANGLE_DEG
