"""FC-3f: ArtistDirectoryService unit tests.

Mirrors TagDirectoryService's test shape — covers cursor encode/decode,
ordering by (name ASC, id ASC), substring q filter, platform EXISTS
filter (with the row-duplication trap), pagination, image_count via
LEFT JOIN, and the window-function preview helper.
"""
import hashlib

import pytest

from backend.app.models import Artist, ImageRecord, Source
from backend.app.services.artist_directory_service import (
    ArtistDirectoryService,
    _decode,
    _encode,
)

pytestmark = pytest.mark.integration


# --- Cursor round-trip ----------------------------------------------------


def test_cursor_round_trip():
    c = _encode("alice", 5)
    assert _decode(c) == ("alice", 5)


def test_decode_cursor_rejects_garbage():
    with pytest.raises(ValueError):
        _decode("not-base64!!!")


def test_cursor_round_trip_handles_pipes_in_name():
    # Names can theoretically contain '|'; rsplit on the LAST '|' must
    # still recover (name, id) correctly.
    c = _encode("a|b|c", 7)
    assert _decode(c) == ("a|b|c", 7)


# --- Fixture builders -----------------------------------------------------


async def _seed_artist(db, name: str, *, is_subscription: bool = False):
    a = Artist(
        name=name, slug=name.lower().replace(" ", "-"),
        is_subscription=is_subscription,
    )
    db.add(a)
    await db.flush()
    return a


async def _seed_source(db, artist_id: int, platform: str, url: str):
    s = Source(artist_id=artist_id, platform=platform, url=url, enabled=True)
    db.add(s)
    await db.flush()
    return s


async def _seed_image(db, artist_id: int, suffix: str):
    # Derive a stable, collision-free digest from the suffix. The old
    # `abs(hash(suffix)) % 10000` used PYTHONHASHSEED-randomized hash() over only
    # 10k buckets, so two suffixes in one test could birthday-collide and violate
    # uq_image_record_sha256 — flaky per process seed (CI run 1182).
    db.add(ImageRecord(
        path=f"/images/{suffix}.jpg",
        sha256=hashlib.sha256(suffix.encode()).hexdigest(),
        size_bytes=10, mime="image/jpeg", width=10, height=10,
        origin="downloaded", artist_id=artist_id,
    ))
    await db.flush()


# --- Ordering + pagination -----------------------------------------------


@pytest.mark.asyncio
async def test_list_orders_by_name_asc(db):
    await _seed_artist(db, "zara-order")
    await _seed_artist(db, "alice-order")
    await _seed_artist(db, "miles-order")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-order", platform=None, cursor=None, limit=60,
    )
    names = [c["name"] for c in page.cards]
    assert names == ["alice-order", "miles-order", "zara-order"]


@pytest.mark.asyncio
async def test_list_pagination_across_two_pages(db):
    artists = []
    for letter in "abcde":
        a = await _seed_artist(db, f"{letter}-page")
        artists.append(a)
    await db.commit()

    page1 = await ArtistDirectoryService(db).list_artists(
        q="-page", platform=None, cursor=None, limit=2,
    )
    assert [c["name"] for c in page1.cards] == ["a-page", "b-page"]
    assert page1.next_cursor is not None

    page2 = await ArtistDirectoryService(db).list_artists(
        q="-page", platform=None, cursor=page1.next_cursor, limit=2,
    )
    assert [c["name"] for c in page2.cards] == ["c-page", "d-page"]
    assert page2.next_cursor is not None

    page3 = await ArtistDirectoryService(db).list_artists(
        q="-page", platform=None, cursor=page2.next_cursor, limit=2,
    )
    assert [c["name"] for c in page3.cards] == ["e-page"]
    assert page3.next_cursor is None


# --- q filter ------------------------------------------------------------


@pytest.mark.asyncio
async def test_list_q_filter_case_insensitive_substring(db):
    await _seed_artist(db, "Alice-Q")
    await _seed_artist(db, "Bob-Q")
    await _seed_artist(db, "Charlie-Q")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="ALICE", platform=None, cursor=None, limit=60,
    )
    names = [c["name"] for c in page.cards]
    assert names == ["Alice-Q"]


# --- platform filter (correlated EXISTS, not JOIN) -----------------------


@pytest.mark.asyncio
async def test_list_platform_filter_excludes_no_source(db):
    a = await _seed_artist(db, "alice-noplat")
    await _seed_source(db, a.id, "patreon", "https://p/alice-np")
    await _seed_artist(db, "bob-noplat")  # no sources
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-noplat", platform="patreon", cursor=None, limit=60,
    )
    names = [c["name"] for c in page.cards]
    assert names == ["alice-noplat"]


@pytest.mark.asyncio
async def test_list_platform_filter_excludes_wrong_platform(db):
    a = await _seed_artist(db, "alice-wplat")
    await _seed_source(db, a.id, "discord", "https://d/alice-wp")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-wplat", platform="patreon", cursor=None, limit=60,
    )
    assert page.cards == []


@pytest.mark.asyncio
async def test_list_platform_filter_does_not_duplicate_artist_with_multiple_sources(db):
    """The EXISTS subquery prevents JOIN-style row duplication when an
    artist has multiple sources on the requested platform."""
    a = await _seed_artist(db, "alice-multi")
    await _seed_source(db, a.id, "patreon", "https://p/alice-m1")
    await _seed_source(db, a.id, "patreon", "https://p/alice-m2")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-multi", platform="patreon", cursor=None, limit=60,
    )
    assert len(page.cards) == 1
    assert page.cards[0]["name"] == "alice-multi"


# --- image_count via LEFT JOIN -------------------------------------------


@pytest.mark.asyncio
async def test_list_image_count_reflects_artist_id_links(db):
    a = await _seed_artist(db, "alice-count")
    for i in range(4):
        await _seed_image(db, a.id, f"count-{i}")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-count", platform=None, cursor=None, limit=60,
    )
    target = next(c for c in page.cards if c["name"] == "alice-count")
    assert target["image_count"] == 4


@pytest.mark.asyncio
async def test_list_zero_image_artist_preserved_by_left_join(db):
    """LEFT JOIN ImageRecord keeps the artist row even with no images."""
    await _seed_artist(db, "alice-zero")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-zero", platform=None, cursor=None, limit=60,
    )
    target = next(c for c in page.cards if c["name"] == "alice-zero")
    assert target["image_count"] == 0
    assert target["preview_thumbnails"] == []


# --- Preview thumbnails (window function) --------------------------------


@pytest.mark.asyncio
async def test_list_previews_returns_top_three_by_image_id_desc(db):
    a = await _seed_artist(db, "alice-prev")
    for i in range(5):
        await _seed_image(db, a.id, f"prev-{i}")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-prev", platform=None, cursor=None, limit=60,
    )
    target = next(c for c in page.cards if c["name"] == "alice-prev")
    assert len(target["preview_thumbnails"]) == 3
    for url in target["preview_thumbnails"]:
        assert url.startswith("/images/thumbs/")


# --- is_subscription field plumbed through -------------------------------


@pytest.mark.asyncio
async def test_list_is_subscription_field_present(db):
    await _seed_artist(db, "alice-sub", is_subscription=True)
    await _seed_artist(db, "bob-sub", is_subscription=False)
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="-sub", platform=None, cursor=None, limit=60,
    )
    alice = next(c for c in page.cards if c["name"] == "alice-sub")
    bob = next(c for c in page.cards if c["name"] == "bob-sub")
    assert alice["is_subscription"] is True
    assert bob["is_subscription"] is False


# --- Combined q + platform -----------------------------------------------


@pytest.mark.asyncio
async def test_list_combined_q_and_platform(db):
    alice = await _seed_artist(db, "alice-combo-f")
    await _seed_source(db, alice.id, "patreon", "https://p/alice-combo")
    bob = await _seed_artist(db, "bob-combo-f")
    await _seed_source(db, bob.id, "patreon", "https://p/bob-combo")
    await db.commit()

    page = await ArtistDirectoryService(db).list_artists(
        q="alice", platform="patreon", cursor=None, limit=60,
    )
    names = [c["name"] for c in page.cards]
    assert names == ["alice-combo-f"]
