"""Tests for the LLMClient Protocol and its implementations."""

from pathlib import Path

import pytest

from quber.agents.llm_client import (
    FootnoteDef,
    LLMClient,
    LLMTableCorrection,
    MockLLMClient,
    get_llm_client,
)


def test_mock_client_satisfies_protocol():
    assert isinstance(MockLLMClient(), LLMClient)


def test_mock_count_tables_default():
    assert MockLLMClient().count_tables_sync(Path("nope.png")) == 1


def test_mock_count_tables_configurable():
    assert MockLLMClient(table_count=4).count_tables_sync(Path("nope.png")) == 4


def test_mock_correct_structure_default_passthrough():
    client = MockLLMClient()
    out = client.correct_structure_sync(Path("nope.png"), markdown="| a |\n|---|\n| 1 |")
    assert isinstance(out, LLMTableCorrection)
    assert out.markdown == "| a |\n|---|\n| 1 |"
    assert out.footnotes == []


def test_mock_correct_structure_injected():
    forced = LLMTableCorrection(
        title="T", caption="S", markdown="MD", footnotes=[FootnoteDef(text="fn")], header_rows=1
    )
    client = MockLLMClient(correction=forced)
    out = client.correct_structure_sync(Path("nope.png"), markdown="other")
    assert out is forced


def test_get_llm_client_mock():
    assert isinstance(get_llm_client("mock"), MockLLMClient)


def test_get_llm_client_unknown_raises():
    with pytest.raises(ValueError):
        get_llm_client("bogus")  # type: ignore[arg-type]


def test_compound_marker_stacks_split_at_the_schema():
    # '(b)(c)(d)' printed as one stack arrives as one string when prompt
    # compliance slips; the schema splits it — refs into distinct markers,
    # a mark into one entry per marker at the same cell. A parenthesized
    # negative value is not a stack and passes through untouched.
    from quber.agents.llm_client import FootnoteMark

    out = LLMTableCorrection(
        markdown="| a |",
        footnote_refs=["(b)(c)(d)", "(a)", "(84)"],
        footnote_marks=[FootnoteMark(marker="(b)(c)", cell_text="Net write-off rate(b)(c)")],
        header_rows=1,
    )
    assert out.footnote_refs == ["(b)", "(c)", "(d)", "(a)", "(84)"]
    assert [(m.marker, m.cell_text) for m in out.footnote_marks] == [
        ("(b)", "Net write-off rate(b)(c)"),
        ("(c)", "Net write-off rate(b)(c)"),
    ]


def test_footnote_def_leading_symbol_lifts_into_marker():
    # A symbol legend arriving with the symbol folded into the text splits
    # into marker + text; an already-markered def and a plain unmarked note
    # pass through untouched.
    lifted = FootnoteDef(text="# Denotes a variance of 100 percent or more")
    assert (lifted.marker, lifted.text) == ("#", "Denotes a variance of 100 percent or more")

    kept = FootnoteDef(marker="(a)", text="Represents net income.")
    assert (kept.marker, kept.text) == ("(a)", "Represents net income.")

    unmarked = FootnoteDef(text="Amounts are presented on a consolidated basis.")
    assert (unmarked.marker, unmarked.text) == ("", "Amounts are presented on a consolidated basis.")


def test_correction_separates_running_text_from_footnotes():
    # The agent names what it saw below the table: footnotes and table
    # qualifications go in `footnotes`; the document's running text is listed
    # by its opening words in `body_text` and never enters the table.
    out = LLMTableCorrection(
        markdown="| A | B |\n| --- | --- |\n| 1 | 2 |",
        footnotes=[
            FootnoteDef(marker="(1)", text="Includes the impact of interest rate floors."),
            FootnoteDef(marker="", text="Amounts are presented on a consolidated basis."),
        ],
        body_text=["As of June 30, 2026, $2.5 billion, or 88.9%, of the outstanding"],
        header_rows=1,
    )
    assert [f.marker for f in out.footnotes] == ["(1)", ""]
    assert out.body_text == ["As of June 30, 2026, $2.5 billion, or 88.9%, of the outstanding"]
    assert "88.9" not in out.markdown and all("88.9" not in f.text for f in out.footnotes)
    # Older artifacts and agents that omit the field still validate.
    assert LLMTableCorrection(markdown="| A |\n| --- |\n| 1 |", header_rows=1).body_text == []
