"""OCR plumbing for `petctl read` — engine selection and output cleanup.

Only the pure half is covered, per the testing conventions: capture and the
OCR call itself need a real screen and a real engine. Engine probes are
injected so these pass on a machine with a different set installed (or none).
"""

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from bolt_pet import screen_text
from bolt_pet.monitors import Monitor


def probes(modules=(), binaries=()):
    return (lambda name: name in modules), (
        lambda name: f"/usr/bin/{name}" if name in binaries else None
    )


def test_prefers_tesseract_when_fully_installed():
    has_module, which = probes({"pytesseract"}, {"tesseract"})
    assert screen_text.resolve_engine(has_module, which) == ("pytesseract", "")


def test_falls_back_to_rapidocr_when_tesseract_binary_is_absent():
    has_module, which = probes({"pytesseract", "rapidocr_onnxruntime"}, set())
    engine, reason = screen_text.resolve_engine(has_module, which)
    assert engine == "rapidocr"
    assert reason == ""


def test_pytesseract_without_the_binary_says_which_half_is_missing():
    """The commonest broken setup: `pip install pytesseract` and stop, not
    realising the actual engine is a system package."""
    has_module, which = probes({"pytesseract"}, set())
    engine, reason = screen_text.resolve_engine(has_module, which)
    assert engine is None
    assert "tesseract binary" in reason
    assert "apt install tesseract-ocr" in reason


def test_nothing_installed_explains_how_to_fix_it():
    has_module, which = probes(set(), set())
    engine, reason = screen_text.resolve_engine(has_module, which)
    assert engine is None
    assert "pip install" in reason


def test_capture_availability_follows_mss():
    assert screen_text.capture_available(lambda name: name == "mss")
    assert not screen_text.capture_available(lambda name: False)


def test_clean_drops_ocr_noise_and_blank_runs():
    raw = "Firefox\n\n\n |\n  .\nBuild failed\n~\n"
    assert screen_text.clean_ocr_text(raw) == "Firefox\nBuild failed"


def test_clean_collapses_whitespace_but_keeps_line_structure():
    raw = "  File     edit   view  \nline\ttwo  "
    assert screen_text.clean_ocr_text(raw) == "File edit view\nline two"


def test_clean_drops_consecutive_duplicates_only():
    raw = "Terminal\nTerminal\nEditor\nTerminal"
    assert screen_text.clean_ocr_text(raw) == "Terminal\nEditor\nTerminal"


def test_clean_keeps_short_but_real_tokens():
    # two alphanumerics is the bar — "ok" and "42" survive, "-" doesn't
    assert screen_text.clean_ocr_text("ok\n-\n42") == "ok\n42"


def test_clean_truncates_and_says_so():
    out = screen_text.clean_ocr_text("word " * 500, max_chars=100)
    assert out.endswith("[truncated]")
    # the cap applies to the text, before the marker is appended
    assert len(out.split("\n[truncated]")[0]) <= 100


def test_clean_handles_empty_input():
    assert screen_text.clean_ocr_text("") == ""
    assert screen_text.clean_ocr_text(None) == ""


def test_format_reading_names_the_monitor():
    monitor = Monitor(1, "HDMI-1", 1920, 0, 1920, 1080)
    out = screen_text.format_reading(monitor, "Build failed")
    assert "monitor 2 (HDMI-1)" in out
    assert out.endswith("Build failed")


def test_format_reading_when_nothing_was_recognised():
    monitor = Monitor(0, "DP-0", 0, 0, 1920, 1080)
    assert "no text recognised" in screen_text.format_reading(monitor, "   ")


def test_read_monitor_never_raises_without_an_engine(monkeypatch):
    """Its return value goes back to the server as command output, so every
    failure has to come back as a sentence rather than an exception."""
    monkeypatch.setattr(screen_text, "capture_available", lambda *a, **k: True)
    monkeypatch.setattr(
        screen_text, "resolve_engine", lambda *a, **k: (None, "no engine here")
    )
    out = screen_text.read_monitor(Monitor(0, "DP-0", 0, 0, 1920, 1080))
    assert out.startswith("[pet]")
    assert "no engine here" in out


def test_read_monitor_reports_a_failed_capture(monkeypatch):
    monkeypatch.setattr(screen_text, "capture_available", lambda *a, **k: True)
    monkeypatch.setattr(screen_text, "resolve_engine", lambda *a, **k: ("pytesseract", ""))
    monkeypatch.setattr(screen_text, "capture", lambda monitor: None)
    out = screen_text.read_monitor(Monitor(0, "DP-0", 0, 0, 1920, 1080))
    assert "couldn't capture" in out and "Wayland" in out


def test_read_monitor_survives_an_exploding_engine(monkeypatch):
    monkeypatch.setattr(screen_text, "capture_available", lambda *a, **k: True)
    monkeypatch.setattr(screen_text, "resolve_engine", lambda *a, **k: ("pytesseract", ""))
    monkeypatch.setattr(screen_text, "capture", lambda monitor: object())
    monkeypatch.setattr(
        screen_text, "_ocr", lambda image, engine: (_ for _ in ()).throw(RuntimeError("boom"))
    )
    out = screen_text.read_monitor(Monitor(0, "DP-0", 0, 0, 1920, 1080))
    assert "OCR failed" in out and "boom" in out


def test_read_monitors_splits_the_budget(monkeypatch):
    seen = []

    def fake_read(monitor, max_chars):
        seen.append((monitor.number, max_chars))
        return f"screen {monitor.number}"

    monkeypatch.setattr(screen_text, "read_monitor", fake_read)
    screens = [
        Monitor(0, "A", 0, 0, 100, 100),
        Monitor(1, "B", 100, 0, 100, 100),
        Monitor(2, "C", 200, 0, 100, 100),
    ]
    out = screen_text.read_monitors(screens, 3000)
    assert [n for n, _ in seen] == [1, 2, 3]
    assert all(limit == 1000 for _, limit in seen)
    assert out.count("screen ") == 3


def test_read_monitors_keeps_a_floor_on_the_budget(monkeypatch):
    monkeypatch.setattr(
        screen_text, "read_monitor", lambda monitor, max_chars: str(max_chars)
    )
    screens = [Monitor(i, str(i), 0, 0, 10, 10) for i in range(20)]
    # 100/20 would be 5 characters per screen, which is useless — floor wins
    assert "400" in screen_text.read_monitors(screens, 100)


def test_read_monitors_with_one_screen_uses_the_whole_budget(monkeypatch):
    monkeypatch.setattr(
        screen_text, "read_monitor", lambda monitor, max_chars: str(max_chars)
    )
    assert screen_text.read_monitors([Monitor(0, "A", 0, 0, 10, 10)], 4000) == "4000"


def test_read_monitors_with_no_screens():
    assert "no monitor information" in screen_text.read_monitors([], 4000)
