"""Tests for the two-flow extractor (no LLM, no real Camelot).

Covers the contract that matters for the parallel flows: the Camelot flow
tags every surviving grid with an is-table verdict but never drops on it
(only content-empty shells are dropped), the vision flow maps located
tables to per-page VisualTables, and the DualResult page-count helpers.
"""

import asyncio
from pathlib import Path
from typing import List

import pytest

from quber.agents.classifier import ClassifierResult, MockClassifier
from quber.agents.grid_locator import LocatedTable, MockGridLocator
from quber.core.extractors.camelot.acquire import CamelotCandidate
from quber.core.extractors.dual import camelot as camelot_flow
from quber.core.extractors.dual import vision as vision_flow
from quber.core.extractors.dual.models import CamelotTable, DualResult, VisualTable


def _candidate(cid: str, page: int, markdown: str, cells: List[List[str]]) -> CamelotCandidate:
    return CamelotCandidate(
        candidate_id=cid, flavor="stream", page=page, markdown=markdown, cells=cells, accuracy=99.0
    )


def test_camelot_flow_tags_verdict_and_never_drops_on_it(monkeypatch: pytest.MonkeyPatch):
    real = _candidate("stream-p1-0", 1, "| a | b |\n| --- | --- |\n| 1 | 2 |", [["a", "b"], ["1", "2"]])
    shell = _candidate("stream-p1-1", 1, "|  |\n| --- |", [[""]])  # content-empty lattice shell
    monkeypatch.setattr(camelot_flow, "run_camelot_flavors_parallel", lambda source: [real, shell])

    # Classifier says NOT a table; the grid must still survive (verdict is metadata).
    classifier = MockClassifier(ClassifierResult(is_table=False, reason="mock-reject"))
    out = asyncio.run(camelot_flow.run_camelot_flow(Path("x.pdf"), classifier))

    assert len(out) == 1  # only the content-empty shell was dropped
    assert out[0].candidate.candidate_id == "stream-p1-0"
    assert out[0].classification is not None
    assert out[0].classification.is_table is False  # tagged, not dropped


def test_camelot_flow_without_classifier_leaves_untagged(monkeypatch: pytest.MonkeyPatch):
    real = _candidate("stream-p2-0", 2, "| a | b |\n| --- | --- |\n| 1 | 2 |", [["a", "b"], ["1", "2"]])
    monkeypatch.setattr(camelot_flow, "run_camelot_flavors_parallel", lambda source: [real])
    out = asyncio.run(camelot_flow.run_camelot_flow(Path("x.pdf"), classifier=None))
    assert len(out) == 1
    assert out[0].classification is None


def test_vision_flow_maps_located_to_per_page_tables(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
    p1 = tmp_path / "page-0001.png"
    p2 = tmp_path / "page-0002.png"
    p1.write_bytes(b"x")
    p2.write_bytes(b"x")
    monkeypatch.setattr(vision_flow, "render_pages", lambda source, dpi, out_dir: [p1, p2])

    located = [
        LocatedTable(
            ordinal=1,
            title="t",
            region=(0.1, 0.1, 0.9, 0.5),
            grid_rows=(1, 2),
            grid_cols=(0, 1),
            tightened=True,
        )
    ]
    out = asyncio.run(vision_flow.run_vision_flow(Path("x.pdf"), MockGridLocator(located)))

    assert len(out) == 2  # one canned table per page
    assert {v.page for v in out} == {1, 2}
    assert all(v.title == "t" and v.tightened for v in out)


def test_dualresult_page_counts():
    result = DualResult(
        source="x.pdf",
        visual=[
            VisualTable(page=1, ordinal=1, title="", region=(0, 0, 1, 1), tightened=True),
            VisualTable(page=1, ordinal=2, title="", region=(0, 0, 1, 1), tightened=True),
            VisualTable(page=2, ordinal=1, title="", region=(0, 0, 1, 1), tightened=True),
        ],
        camelot=[CamelotTable(candidate=_candidate("a", 1, "m", []))],
    )
    assert result.visual_count_by_page() == {1: 2, 2: 1}
    assert result.camelot_count_by_page() == {1: 1}
