"""
Tests for _filter_border_ghost_words() — removes OCR artefacts from box borders.

When OCR reads a scanned document, box border lines (vertical/horizontal
strokes) are often misrecognised as characters like '|', '1', 'l', '-'.
These phantom words create spurious columns/rows in the grid. The filter
removes them by checking if a word sits on a known box border and looks
like a line artefact.

Lizenz: Apache 2.0
"""

import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from ocr.pipeline.api import _filter_border_ghost_words, _BORDER_GHOST_CHARS
from ocr.types import DetectedBox


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_cell(text: str, x: int, y: int, w: int, h: int,
               col_index: int = 0) -> dict:
    """Create a cell dict with bbox_px matching the word recognition output."""
    return {
        "cell_id": f"c_{x}_{y}",
        "text": text,
        "bbox_px": {"x": x, "y": y, "w": w, "h": h},
        "bbox_pct": {
            "x": x / 12, "y": y / 18,
            "w": w / 12, "h": h / 18,
        },
        "confidence": 80,
        "row_index": 0,
        "col_index": col_index,
    }


def _make_word_result(cells: list, img_w: int = 1200, img_h: int = 1800,
                      columns_used: list = None) -> dict:
    return {
        "cells": cells,
        "image_width": img_w,
        "image_height": img_h,
        "columns_used": columns_used,
        "summary": {
            "total_cells": len(cells),
            "non_empty_cells": sum(1 for c in cells if c.get("text")),
        },
        "grid_shape": {
            "total_cells": len(cells),
            "cols": len(columns_used) if columns_used else 1,
        },
    }


def _make_box(x: int, y: int, w: int, h: int, bt: int = 3) -> DetectedBox:
    return DetectedBox(x=x, y=y, width=w, height=h, confidence=0.9, border_thickness=bt)


# ---------------------------------------------------------------------------
# Basic filtering tests
# ---------------------------------------------------------------------------

class TestBorderGhostFilter:
    """Tests for the _filter_border_ghost_words() function."""

    def test_no_boxes_no_change(self):
        """Without boxes, nothing should be filtered."""
        cells = [_make_cell("hello", 100, 200, 80, 30)]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [])
        assert removed == 0
        assert len(wr["cells"]) == 1

    def test_no_word_result_no_crash(self):
        removed = _filter_border_ghost_words(None, [_make_box(50, 300, 1100, 200)])
        assert removed == 0

    def test_empty_cells_no_crash(self):
        wr = _make_word_result([])
        removed = _filter_border_ghost_words(wr, [_make_box(50, 300, 1100, 200)])
        assert removed == 0

    def test_pipe_on_left_border_removed(self):
        """A '|' character sitting on the left border of a box should be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [
            _make_cell("|", x=48, y=350, w=3, h=25),
            _make_cell("hello", x=200, y=350, w=80, h=25),
        ]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1
        assert wr["cells"][0]["text"] == "hello"

    def test_pipe_on_right_border_removed(self):
        """A '|' character on the right border should be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [
            _make_cell("|", x=1148, y=350, w=4, h=25),
            _make_cell("world", x=600, y=350, w=80, h=25),
        ]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1
        assert wr["cells"][0]["text"] == "world"

    def test_digit_1_on_border_narrow_removed(self):
        """A narrow '1' on a box border should be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [_make_cell("1", x=49, y=400, w=5, h=20)]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1

    def test_dash_on_horizontal_border_removed(self):
        """A '-' on the bottom horizontal border should be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        # Bottom border at y=500, dash at y=498
        cells = [_make_cell("-", x=600, y=498, w=20, h=4)]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1

    def test_real_word_on_border_not_removed(self):
        """A normal word near a border should NOT be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [_make_cell("Tip", x=52, y=350, w=60, h=25)]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 0

    def test_word_far_from_border_not_removed(self):
        """Words far from any border should never be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [_make_cell("|", x=600, y=400, w=3, h=25)]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 0

    def test_multiple_ghosts_on_same_box(self):
        """Multiple ghost words on the same box should all be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [
            _make_cell("|", x=48, y=350, w=3, h=25),
            _make_cell("l", x=1149, y=350, w=4, h=25),
            _make_cell("text", x=400, y=350, w=80, h=25),
        ]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 2
        assert len(wr["cells"]) == 1
        assert wr["cells"][0]["text"] == "text"

    def test_summary_updated_after_removal(self):
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [
            _make_cell("|", x=48, y=350, w=3, h=25),
            _make_cell("hello", x=200, y=350, w=80, h=25),
        ]
        wr = _make_word_result(cells)
        _filter_border_ghost_words(wr, [box])
        assert wr["summary"]["total_cells"] == 1
        assert wr["grid_shape"]["total_cells"] == 1

    def test_ghost_chars_covers_common_artefacts(self):
        """The ghost chars set should include common border-line OCR artefacts."""
        expected = {"|", "1", "l", "I", "!", "[", "]", "-", "—", "_", "/", "\\"}
        assert expected.issubset(_BORDER_GHOST_CHARS)

    def test_multiple_boxes(self):
        box1 = _make_box(x=50, y=300, w=500, h=200, bt=3)
        box2 = _make_box(x=600, y=300, w=500, h=200, bt=3)
        cells = [
            _make_cell("|", x=49, y=350, w=3, h=25),
            _make_cell("I", x=599, y=350, w=4, h=25),
            _make_cell("real", x=300, y=350, w=80, h=25),
        ]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box1, box2])
        assert removed == 2

    def test_uses_bbox_pct_fallback(self):
        """Should work with bbox_pct when bbox_px is not available."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cell = {
            "cell_id": "c_test",
            "text": "|",
            "bbox_pct": {"x": (48 / 1200) * 100, "y": (350 / 1800) * 100,
                         "w": (4 / 1200) * 100, "h": (25 / 1800) * 100},
            "confidence": 80,
            "col_index": 0,
        }
        wr = _make_word_result([cell])
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1

    def test_generous_margin_catches_offset_ghosts(self):
        """Even if OCR word is slightly offset from border, it should be caught."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        # Word 15px away from right border (at x=1135 vs border at x=1150)
        cells = [_make_cell("|", x=1135, y=350, w=4, h=25)]
        wr = _make_word_result(cells)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1


# ---------------------------------------------------------------------------
# Column cleanup tests
# ---------------------------------------------------------------------------

class TestColumnCleanup:
    """Tests for empty column removal after ghost filtering."""

    def test_empty_column_removed(self):
        """After filtering all cells of column 4, it should be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cols = [
            {"index": 0, "type": "column_en", "x": 60, "width": 250},
            {"index": 1, "type": "column_de", "x": 320, "width": 250},
            {"index": 2, "type": "column_3", "x": 580, "width": 250},
            {"index": 3, "type": "column_4", "x": 840, "width": 250},
            {"index": 4, "type": "column_5", "x": 1140, "width": 60},  # ghost column
        ]
        cells = [
            _make_cell("word", x=100, y=350, w=60, h=25, col_index=0),
            _make_cell("Wort", x=360, y=350, w=60, h=25, col_index=1),
            _make_cell("txt", x=620, y=350, w=50, h=25, col_index=2),
            _make_cell("abc", x=880, y=350, w=50, h=25, col_index=3),
            _make_cell("|", x=1148, y=350, w=4, h=25, col_index=4),   # ghost
            _make_cell("l", x=1149, y=400, w=3, h=25, col_index=4),   # ghost
        ]
        wr = _make_word_result(cells, columns_used=cols)
        removed = _filter_border_ghost_words(wr, [box])

        assert removed == 2
        assert len(wr["columns_used"]) == 4  # column 5 removed
        assert wr["grid_shape"]["cols"] == 4

    def test_columns_reindexed_after_removal(self):
        """After removing a middle column, indices should be sequential."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cols = [
            {"index": 0, "type": "column_1", "x": 60, "width": 200},
            {"index": 1, "type": "column_2", "x": 280, "width": 30},  # border col
            {"index": 2, "type": "column_3", "x": 400, "width": 200},
        ]
        # Column 1 only has ghost cells
        cells = [
            _make_cell("hello", x=100, y=350, w=60, h=25, col_index=0),
            # This cell is NOT on a border so it won't be filtered by the ghost filter
            # For this test, put a ghost on the box border
            _make_cell("|", x=49, y=350, w=3, h=25, col_index=1),
            _make_cell("world", x=440, y=350, w=60, h=25, col_index=2),
        ]
        wr = _make_word_result(cells, columns_used=cols)
        _filter_border_ghost_words(wr, [box])

        # Column 1 should be removed, column 2 becomes column 1
        assert len(wr["columns_used"]) == 2
        assert wr["columns_used"][0]["index"] == 0
        assert wr["columns_used"][1]["index"] == 1
        # Remaining cells should have updated col_index
        assert wr["cells"][0]["col_index"] == 0
        assert wr["cells"][1]["col_index"] == 1

    def test_no_columns_used_no_crash(self):
        """If columns_used is None, column cleanup should be skipped."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cells = [_make_cell("|", x=48, y=350, w=3, h=25)]
        wr = _make_word_result(cells, columns_used=None)
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1

    def test_occupied_columns_kept(self):
        """Columns that still have cells after filtering should be kept."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cols = [
            {"index": 0, "type": "column_en", "x": 60, "width": 250},
            {"index": 1, "type": "column_de", "x": 320, "width": 250},
        ]
        cells = [
            _make_cell("word", x=100, y=350, w=60, h=25, col_index=0),
            _make_cell("Wort", x=360, y=350, w=60, h=25, col_index=1),
        ]
        wr = _make_word_result(cells, columns_used=cols)
        removed = _filter_border_ghost_words(wr, [box])

        assert removed == 0
        assert len(wr["columns_used"]) == 2

    def test_single_column_not_removed(self):
        """A single remaining column should never be removed."""
        box = _make_box(x=50, y=300, w=1100, h=200, bt=3)
        cols = [{"index": 0, "type": "column_text", "x": 60, "width": 1000}]
        cells = [_make_cell("|", x=49, y=350, w=3, h=25, col_index=0)]
        wr = _make_word_result(cells, columns_used=cols)
        # Even if the only cell is filtered, we don't remove the last column
        removed = _filter_border_ghost_words(wr, [box])
        assert removed == 1
        # columns_used should still have 1 entry (we skip cleanup for len <= 1)
        assert len(wr["columns_used"]) == 1
