"""Integration tests for compound (multi-column) cursor pagination."""

import base64
import json

import pytest
import pytest_asyncio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker

from sqlalchemy_relay_pagination import PaginationConfig, paginate
from tests.conftest import User


@pytest_asyncio.fixture
async def dup_name_users(async_engine: AsyncEngine) -> list[User]:
    """20 users: 10 named 'Alice' (ids 1-10) and 10 named 'Bob' (ids 11-20)."""
    from datetime import datetime, timezone

    start = datetime(2024, 1, 1, tzinfo=timezone.utc)
    users = [User(id=i, name="Alice" if i <= 10 else "Bob", created_at=start) for i in range(1, 21)]
    factory = async_sessionmaker(async_engine, expire_on_commit=False)
    async with factory() as session:
        session.add_all(users)
        await session.commit()
    return users


@pytest_asyncio.fixture
async def compound_session(async_engine: AsyncEngine, dup_name_users: list[User]):  # type: ignore[return]
    factory = async_sessionmaker(async_engine, expire_on_commit=False)
    async with factory() as session:
        yield session
        await session.rollback()


async def _collect_all(session, order_by, page_size=7) -> list[User]:
    """Collect all users via forward pagination."""
    nodes: list[User] = []
    cursor = None
    while True:
        conn = await paginate(session, select(User), order_by=order_by, first=page_size, after=cursor)
        nodes.extend(e.node for e in conn.edges)
        if not conn.pageInfo.hasNextPage:
            break
        cursor = conn.pageInfo.endCursor
    return nodes


@pytest.mark.asyncio
async def test_compound_first_page(compound_session):
    conn = await paginate(
        compound_session,
        select(User),
        order_by=[User.name, User.id],
        first=5,
    )
    assert len(conn.edges) == 5
    nodes = [e.node for e in conn.edges]
    # All first 5 should be Alice (ids 1-5)
    assert all(n.name == "Alice" for n in nodes)
    assert [n.id for n in nodes] == [1, 2, 3, 4, 5]


@pytest.mark.asyncio
async def test_compound_cursor_is_base64(compound_session):
    conn = await paginate(
        compound_session,
        select(User),
        order_by=[User.name, User.id],
        first=5,
    )
    cursor = conn.pageInfo.endCursor
    assert cursor is not None
    # Should not be a plain integer
    with pytest.raises(ValueError):  # noqa: PT011
        int(cursor)
    # Should be decodable as base64 JSON
    decoded = json.loads(base64.urlsafe_b64decode(cursor.encode()))
    assert "name" in decoded
    assert "id" in decoded


@pytest.mark.asyncio
async def test_compound_no_duplicates_no_gaps(compound_session):
    nodes = await _collect_all(compound_session, order_by=[User.name, User.id], page_size=7)
    assert len(nodes) == 20
    ids = [n.id for n in nodes]
    assert len(set(ids)) == 20  # no duplicates
    # Order: Alice (1-10) then Bob (11-20)
    assert [n.name for n in nodes] == ["Alice"] * 10 + ["Bob"] * 10
    assert [n.id for n in nodes[:10]] == list(range(1, 11))
    assert [n.id for n in nodes[10:]] == list(range(11, 21))


@pytest.mark.asyncio
async def test_compound_backward_pagination(compound_session):
    # Get first page forward
    conn_fwd = await paginate(
        compound_session,
        select(User),
        order_by=[User.name, User.id],
        first=5,
    )
    end_cursor = conn_fwd.pageInfo.endCursor

    # Use end_cursor as 'before' for backward pagination
    conn_bwd = await paginate(
        compound_session,
        select(User),
        order_by=[User.name, User.id],
        last=5,
        before=end_cursor,
    )
    # Should return the 4 rows before the cursor (Alice ids 1-4)
    bwd_ids = [e.node.id for e in conn_bwd.edges]
    assert bwd_ids == [1, 2, 3, 4]


@pytest.mark.asyncio
async def test_compound_desc_secondary(compound_session):
    conn = await paginate(
        compound_session,
        select(User),
        order_by=[User.name.asc(), User.id.desc()],
        first=5,
    )
    nodes = [e.node for e in conn.edges]
    # All Alice, ids in descending order: 10, 9, 8, 7, 6
    assert all(n.name == "Alice" for n in nodes)
    assert [n.id for n in nodes] == [10, 9, 8, 7, 6]


@pytest.mark.asyncio
async def test_single_col_cursor_not_base64(compound_session):
    conn = await paginate(
        compound_session,
        select(User),
        order_by=User.id,
        first=5,
    )
    for edge in conn.edges:
        # Single-col cursor must equal str(node.id)
        assert edge.cursor == str(edge.node.id)


@pytest.mark.asyncio
async def test_custom_codec(compound_session):
    class SimpleCodec:
        """Pipe-delimited key=value codec for testing."""

        def encode(self, values: dict) -> str:
            return "|".join(f"{k}={v}" for k, v in sorted(values.items()))

        def decode(self, cursor: str) -> dict:
            return dict(pair.split("=", 1) for pair in cursor.split("|"))

    cfg = PaginationConfig(cursor_codec=SimpleCodec())

    conn = await paginate(
        compound_session,
        select(User),
        order_by=[User.name, User.id],
        first=3,
        config=cfg,
    )
    end_cursor = conn.pageInfo.endCursor
    assert end_cursor is not None
    # Custom codec produces pipe-delimited format
    assert "|" in end_cursor
    assert "id=" in end_cursor
    assert "name=" in end_cursor

    # Can continue pagination with the same codec
    conn2 = await paginate(
        compound_session,
        select(User),
        order_by=[User.name, User.id],
        first=3,
        after=end_cursor,
        config=cfg,
    )
    assert len(conn2.edges) == 3
    # No overlap
    ids1 = {e.node.id for e in conn.edges}
    ids2 = {e.node.id for e in conn2.edges}
    assert ids1.isdisjoint(ids2)
