"""Deterministic logic of figure-value reconciliation.

The two readers are agents and are exercised by the measurement runs, not
here; these tests pin the pure logic between them: fragment normalization,
scoping by overlap, value normalization, and the tie that assigns statuses.
"""

from __future__ import annotations

from typing import Optional

from quber.core.extractors.base import CELL_STATUS_REFERENCE
from quber.core.figures.models import Box, FigureRecord
from quber.core.figures.values import (
    PageFragment,
    ParsedValue,
    ReadValue,
    fragments_in_box,
    norm_value,
    page_fragments,
    picture_for_value,
    tie_page,
)


def figure(picture_ref: str, fig_box: Optional[Box] = None, text: str = "") -> FigureRecord:
    return FigureRecord(page=1, text=text, box=fig_box, picture_ref=picture_ref)


def box(left: float, top: float, right: float, bottom: float) -> Box:
    return {"left": left, "top": top, "right": right, "bottom": bottom}


def fragment(fid: str, text: str, frag_box: Optional[Box] = None) -> PageFragment:
    return PageFragment(id=fid, text=text, box=frag_box or box(0.1, 0.1, 0.2, 0.12))


def test_page_fragments_normalizes_and_skips_blanks() -> None:
    cells_page = {
        "page_no": 3,
        "width": 612.0,
        "height": 792.0,
        "cells": [
            {"text": "  ", "box": [0, 0, 10, 10]},
            {"text": "$1,108.1", "box": [61.2, 79.2, 122.4, 158.4], "from_ocr": True, "confidence": 0.8},
            {"text": "Revenue", "box": [0, 0, 306, 396]},
        ],
    }
    fragments = page_fragments(cells_page)
    assert len(fragments) == 2
    # Ids keep the cell's index in the source list, so a blank cell never
    # shifts the id of the cells after it.
    assert [f.id for f in fragments] == ["p3.c1", "p3.c2"]
    first = fragments[0]
    assert first.text == "$1,108.1"
    assert first.box == {"left": 0.1, "top": 0.1, "right": 0.2, "bottom": 0.2}
    assert first.from_ocr is True
    assert first.confidence == 0.8
    second = fragments[1]
    assert second.box == {"left": 0.0, "top": 0.0, "right": 0.5, "bottom": 0.5}
    assert second.from_ocr is False


def test_fragments_in_box_selects_by_overlap() -> None:
    fragments = [
        fragment("inside", "a", box(0.2, 0.2, 0.3, 0.25)),
        fragment("outside", "b", box(0.8, 0.8, 0.9, 0.85)),
        fragment("straddling-mostly-in", "c", box(0.45, 0.2, 0.55, 0.25)),
        fragment("straddling-mostly-out", "d", box(0.48, 0.2, 0.9, 0.25)),
    ]
    scope = box(0.1, 0.1, 0.52, 0.6)
    assert [f.id for f in fragments_in_box(fragments, scope)] == ["inside", "straddling-mostly-in"]


def test_norm_value_forms() -> None:
    assert norm_value("$1,108.1") == "1108.1"
    assert norm_value("1108.1") == "1108.1"
    assert norm_value("($46.8)") == "-46.8"
    assert norm_value("$–") == "DASH"
    assert norm_value("-") == "DASH"
    assert norm_value("21.8%") == "21.8"
    assert norm_value("N/A") == "n/a"


def test_tie_agreement_with_containing_fragment_reconciles() -> None:
    frag = fragment("p1.c0", "$1,108.1", box(0.3, 0.4, 0.4, 0.42))
    out = tie_page(
        page=1,
        figures=[figure("#/pictures/0", box(0.0, 0.0, 0.5, 0.5))],
        scan_values=[ParsedValue(label="Revenue", value="1108.1")],
        local_values=[ReadValue(label="Revenue", value="$1,108.1", fragment_ids=["p1.c0"])],
        fragments=[frag],
    )
    assert len(out) == 1
    value = out[0]
    assert value.status == "reconciled"
    assert value.note is None
    assert value.box == frag.box
    assert value.value == "$1,108.1"
    assert value.picture_ref == "#/pictures/0"


def test_tie_agreed_value_over_wrong_fragment_is_misread() -> None:
    frag = fragment("p1.c0", "$46.8", box(0.3, 0.4, 0.4, 0.42))
    out = tie_page(
        page=1,
        figures=[],
        scan_values=[ParsedValue(label="FFO", value="47.9")],
        local_values=[ReadValue(label="FFO", value="$47.9", fragment_ids=["p1.c0"])],
        fragments=[frag],
    )
    assert len(out) == 1
    value = out[0]
    assert value.status == "value_misread"
    assert value.box == frag.box
    assert value.note is not None and "'$46.8'" in value.note


def test_tie_scan_only_value_is_unreconciled() -> None:
    out = tie_page(
        page=1,
        figures=[],
        scan_values=[ParsedValue(label="NOI", value="12.3")],
        local_values=[],
        fragments=[],
    )
    assert len(out) == 1
    value = out[0]
    assert value.status == "value_unreconciled"
    assert (
        value.note
        == "one measurement, from the figure description; uncontradicted, not corroborated on the printed page"
    )
    assert value.value == "12.3"


def test_tie_local_only_value_is_unreconciled() -> None:
    frag = fragment("p1.c0", "$12.3")
    out = tie_page(
        page=1,
        figures=[],
        scan_values=[],
        local_values=[ReadValue(label="NOI", value="$12.3", fragment_ids=["p1.c0"])],
        fragments=[frag],
    )
    assert len(out) == 1
    value = out[0]
    assert value.status == "value_unreconciled"
    assert value.note == "one measurement, read from the page; uncontradicted, not independently corroborated"


def test_tie_repeated_values_resolve_by_count() -> None:
    frag_a = fragment("p1.c0", "$47.9", box(0.2, 0.3, 0.3, 0.32))
    frag_b = fragment("p1.c1", "$47.9", box(0.6, 0.3, 0.7, 0.32))
    out = tie_page(
        page=1,
        figures=[],
        scan_values=[
            ParsedValue(label="Revenue", value="$47.9"),
            ParsedValue(label="Expenses", value="$47.9"),
        ],
        local_values=[
            ReadValue(label="Revenue", value="$47.9", fragment_ids=["p1.c0"]),
            ReadValue(label="Expenses", value="$47.9", fragment_ids=["p1.c1"]),
        ],
        fragments=[frag_a, frag_b],
    )
    assert len(out) == 2
    assert [v.status for v in out] == ["reconciled", "reconciled"]
    assert out[0].box == frag_a.box
    assert out[1].box == frag_b.box


def test_tie_match_without_cited_fragments_is_unreconciled() -> None:
    out = tie_page(
        page=1,
        figures=[],
        scan_values=[ParsedValue(label="FFO", value="47.9")],
        local_values=[ReadValue(label="FFO", value="$47.9", fragment_ids=[])],
        fragments=[],
    )
    assert len(out) == 1
    value = out[0]
    assert value.status == "value_unreconciled"
    assert value.note == "corroborated by both readings, but not anchored to printed text on the page"
    assert value.box is None


def test_picture_for_value_prefers_the_containing_figure() -> None:
    figures = [
        figure("#/pictures/1", box(0.0, 0.0, 0.45, 1.0), text="Hotel 16% of $46.0 total"),
        figure("#/pictures/2", box(0.5, 0.0, 1.0, 1.0), text="South 40%, Industrial 16%"),
    ]
    # A boxed value belongs to the figure containing its center, not the first.
    assert picture_for_value(figures, box(0.85, 0.3, 0.9, 0.33), "40%") == "#/pictures/2"
    # An unboxed value belongs to the one figure whose scan text prints it.
    assert picture_for_value(figures, None, "$46.0") == "#/pictures/1"
    # A value printed by both figures or by neither anchors to no picture.
    assert picture_for_value(figures, None, "16%") is None
    assert picture_for_value(figures, None, "$99.9") is None


def test_picture_for_value_nested_figures_take_the_smallest() -> None:
    figures = [
        figure("#/pictures/1", box(0.0, 0.0, 1.0, 1.0)),
        figure("#/pictures/2", box(0.4, 0.4, 0.6, 0.6)),
    ]
    assert picture_for_value(figures, box(0.48, 0.48, 0.52, 0.52), "7%") == "#/pictures/2"


def test_figure_value_statuses_are_registered() -> None:
    by_code = {s.code: s for s in CELL_STATUS_REFERENCE}
    for code in ("value_misread", "value_unreconciled"):
        assert code in by_code
        assert by_code[code].inspect is True
