import pytest
from sqlalchemy import Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

from sqlalchemy_relay_pagination.exceptions import InvalidArgumentsError
from sqlalchemy_relay_pagination.keyset import (
    build_compound_keyset_filter,
    build_keyset_filter,
    extract_column,
    reverse_order_by,
)


class Base(DeclarativeBase):
    pass


class Item(Base):
    __tablename__ = "items"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String)
    score: Mapped[int] = mapped_column(Integer)


def test_extract_column_plain():
    col, is_desc = extract_column(Item.id)
    assert col is Item.id
    assert is_desc is False


def test_extract_column_asc():
    col, is_desc = extract_column(Item.id.asc())
    assert col.key == "id"
    assert is_desc is False


def test_extract_column_desc():
    col, is_desc = extract_column(Item.id.desc())
    assert col.key == "id"
    assert is_desc is True


def test_extract_column_invalid():
    with pytest.raises(InvalidArgumentsError):
        extract_column("not_a_column")  # type: ignore[arg-type]


def test_build_keyset_asc_forward():
    clause = build_keyset_filter(Item.id, 5, is_desc=False, is_forward=True)
    sql = str(clause.compile(compile_kwargs={"literal_binds": True}))
    assert "items.id > 5" in sql


def test_build_keyset_desc_forward():
    clause = build_keyset_filter(Item.id, 5, is_desc=True, is_forward=True)
    sql = str(clause.compile(compile_kwargs={"literal_binds": True}))
    assert "items.id < 5" in sql


def test_build_keyset_asc_backward():
    clause = build_keyset_filter(Item.id, 5, is_desc=False, is_forward=False)
    sql = str(clause.compile(compile_kwargs={"literal_binds": True}))
    assert "items.id < 5" in sql


def test_build_keyset_desc_backward():
    clause = build_keyset_filter(Item.id, 5, is_desc=True, is_forward=False)
    sql = str(clause.compile(compile_kwargs={"literal_binds": True}))
    assert "items.id > 5" in sql


def test_reverse_order_by_asc():
    expr = reverse_order_by(Item.id, is_desc=False)
    sql = str(expr.compile())
    assert "DESC" in sql.upper()


def test_reverse_order_by_desc():
    expr = reverse_order_by(Item.id, is_desc=True)
    sql = str(expr.compile())
    assert "ASC" in sql.upper() or "DESC" not in sql.upper()


# --- compound keyset filter tests ---


def _sql(clause) -> str:
    return str(clause.compile(compile_kwargs={"literal_binds": True}))


def test_compound_two_cols_asc_asc_forward():
    cols = [(Item.id, False), (Item.score, False)]
    clause = build_compound_keyset_filter(cols, [5, 10], is_forward=True)
    sql = _sql(clause)
    # First branch: id > 5
    assert "items.id > 5" in sql
    # Second branch: id = 5 AND score > 10
    assert "items.id = 5" in sql
    assert "items.score > 10" in sql
    assert "OR" in sql.upper()


def test_compound_asc_desc_forward():
    cols = [(Item.id, False), (Item.score, True)]
    clause = build_compound_keyset_filter(cols, [5, 10], is_forward=True)
    sql = _sql(clause)
    # id ASC forward → >; score DESC forward → <
    assert "items.id > 5" in sql
    assert "items.score < 10" in sql


def test_compound_backward():
    cols = [(Item.id, False), (Item.score, False)]
    clause = build_compound_keyset_filter(cols, [5, 10], is_forward=False)
    sql = _sql(clause)
    # ASC backward → <
    assert "items.id < 5" in sql
    assert "items.score < 10" in sql


def test_compound_three_cols():
    cols = [(Item.id, False), (Item.score, False), (Item.id, False)]
    clause = build_compound_keyset_filter(cols, [1, 2, 3], is_forward=True)
    sql = _sql(clause)
    # Three OR branches
    assert sql.count("OR") >= 2
