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

from sqlalchemy_relay_pagination.codec import Base64JsonCodec
from sqlalchemy_relay_pagination.cursor import decode_cursor, encode_cursor
from sqlalchemy_relay_pagination.exceptions import InvalidCursorError, NullOrderingColumnError


class Base(DeclarativeBase):
    pass


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


class FakeRow:
    def __init__(self, **kwargs: object) -> None:
        for k, v in kwargs.items():
            setattr(self, k, v)


_codec = Base64JsonCodec()
_single_id = [(Thing.id, False)]  # type: ignore[list-item]
_single_name = [(Thing.name, False)]  # type: ignore[list-item]
_compound = [(Thing.id, False), (Thing.name, False)]  # type: ignore[list-item]


def test_roundtrip_int():
    row = FakeRow(id=42)
    cursor = encode_cursor(_single_id, row, _codec)
    assert cursor == "42"
    assert decode_cursor(cursor, _single_id, _codec) == [42]


def test_roundtrip_str():
    row = FakeRow(name="hello")
    cursor = encode_cursor(_single_name, row, _codec)
    assert cursor == "hello"
    assert decode_cursor(cursor, _single_name, _codec) == ["hello"]


def test_null_raises():
    row = FakeRow(id=None)
    with pytest.raises(NullOrderingColumnError):
        encode_cursor(_single_id, row, _codec)


def test_invalid_cursor_raises():
    with pytest.raises(InvalidCursorError):
        decode_cursor("not-an-int", _single_id, _codec)


def test_single_col_cursor_is_plain_str():
    row = FakeRow(id=42)
    cursor = encode_cursor(_single_id, row, _codec)
    assert cursor == "42"
    # Must not be base64 — plain integer string
    assert not cursor.endswith("=") and "+" not in cursor and "/" not in cursor
    # Parseable as int directly
    assert int(cursor) == 42


def test_compound_encode_is_base64():
    row = FakeRow(id=5, name="Alice")
    cursor = encode_cursor(_compound, row, _codec)
    # Not a plain integer
    with pytest.raises(ValueError):  # noqa: PT011
        int(cursor)
    # URL-safe base64: no + or /
    assert "+" not in cursor
    assert "/" not in cursor


def test_compound_roundtrip():
    row = FakeRow(id=7, name="Bob")
    cursor = encode_cursor(_compound, row, _codec)
    values = decode_cursor(cursor, _compound, _codec)
    assert values == [7, "Bob"]


def test_compound_null_raises():
    row = FakeRow(id=None, name="Carol")
    with pytest.raises(NullOrderingColumnError):
        encode_cursor(_compound, row, _codec)


def test_compound_missing_key_raises():
    # Manually create a cursor that is missing the 'name' key
    import base64
    import json

    payload = json.dumps({"id": "1"}, separators=(",", ":")).encode()
    cursor = base64.urlsafe_b64encode(payload).decode()
    with pytest.raises(InvalidCursorError):
        decode_cursor(cursor, _compound, _codec)
