"""Phase-3 visual "more like this" — pgvector cosine ranking over the
precomputed SigLIP image embeddings. No query-time ML inference."""

from datetime import UTC, datetime, timedelta

import pytest
from sqlalchemy import select

from backend.app.models import ImageRecord, Tag, TagKind
from backend.app.models.tag import image_tag
from backend.app.services.gallery_service import GalleryService

pytestmark = pytest.mark.integration


def _vec(*head):
    """A 1152-dim embedding with the given leading values, rest zero. Cosine
    distance to _vec(1,0) grows as the 2nd component grows, so callers can
    order fixtures deterministically by direction."""
    v = [0.0] * 1152
    for i, x in enumerate(head):
        v[i] = float(x)
    return v


async def _img(db, n, emb):
    rec = ImageRecord(
        path=f"/images/sim/{n}.jpg", sha256=f"e{n:063d}",
        size_bytes=1, mime="image/jpeg", width=1, height=1,
        origin="imported_filesystem", integrity_status="unknown",
        siglip_embedding=emb,
    )
    base = datetime(2026, 1, 1, 12, 0, tzinfo=UTC)
    rec.created_at = base - timedelta(minutes=n)
    rec.effective_date = rec.created_at
    db.add(rec)
    await db.flush()
    return rec


@pytest.mark.asyncio
async def test_similar_ranks_nearest_first(db):
    src = await _img(db, 1, _vec(1, 0))
    near = await _img(db, 2, _vec(1, 0.05))   # almost same direction
    mid = await _img(db, 3, _vec(1, 1))       # 45°
    far = await _img(db, 4, _vec(0, 1))       # orthogonal
    svc = GalleryService(db)
    res = await svc.similar(src.id, limit=10)
    assert [i.id for i in res] == [near.id, mid.id, far.id]  # self excluded


@pytest.mark.asyncio
async def test_similar_excludes_null_embeddings(db):
    src = await _img(db, 1, _vec(1, 0))
    have = await _img(db, 2, _vec(1, 0.1))
    await _img(db, 3, None)  # un-embedded (e.g. a video) → excluded
    svc = GalleryService(db)
    res = await svc.similar(src.id, limit=10)
    assert [i.id for i in res] == [have.id]


@pytest.mark.asyncio
async def test_similar_source_without_embedding_returns_empty(db):
    src = await _img(db, 1, None)
    await _img(db, 2, _vec(1, 0))
    svc = GalleryService(db)
    assert await svc.similar(src.id, limit=10) == []


@pytest.mark.asyncio
async def test_similar_missing_source_returns_none(db):
    svc = GalleryService(db)
    assert await svc.similar(99999, limit=10) is None


@pytest.mark.asyncio
async def test_similar_excludes_presentation_tagged_images(db):
    """banner / editor-screenshot system tags (#128) hide from similar
    RESULTS — they cluster on UI chrome, not content. A banner anchor still
    gets results; `wip`-tagged images stay in (real art)."""
    src = await _img(db, 1, _vec(1, 0))
    bannered = await _img(db, 2, _vec(1, 0.02))   # nearest, but a banner
    wipped = await _img(db, 3, _vec(1, 0.3))
    plain = await _img(db, 4, _vec(1, 0.6))
    banner_tag = (await db.execute(select(Tag).where(
        Tag.is_system.is_(True), Tag.name == "banner"))).scalar_one()
    wip_tag = (await db.execute(select(Tag).where(
        Tag.is_system.is_(True), Tag.name == "wip"))).scalar_one()
    await db.execute(image_tag.insert().values(
        image_record_id=bannered.id, tag_id=banner_tag.id, source="manual"))
    await db.execute(image_tag.insert().values(
        image_record_id=wipped.id, tag_id=wip_tag.id, source="manual"))
    svc = GalleryService(db)
    res = await svc.similar(src.id, limit=10)
    assert [i.id for i in res] == [wipped.id, plain.id]
    # A presentation-tagged ANCHOR still answers — only candidates hide.
    res_from_banner = await svc.similar(bannered.id, limit=10)
    assert {i.id for i in res_from_banner} == {src.id, wipped.id, plain.id}


@pytest.mark.asyncio
async def test_similar_exclude_wip_drops_wip_neighbors(db):
    """Explore passes exclude_wip=True to also hide work-in-progress from the
    rabbit-hole (banner is always hidden; wip only when asked)."""
    src = await _img(db, 1, _vec(1, 0))
    bannered = await _img(db, 2, _vec(1, 0.02))   # always hidden
    wipped = await _img(db, 3, _vec(1, 0.3))      # hidden only with exclude_wip
    plain = await _img(db, 4, _vec(1, 0.6))
    banner_tag = (await db.execute(select(Tag).where(
        Tag.is_system.is_(True), Tag.name == "banner"))).scalar_one()
    wip_tag = (await db.execute(select(Tag).where(
        Tag.is_system.is_(True), Tag.name == "wip"))).scalar_one()
    await db.execute(image_tag.insert().values(
        image_record_id=bannered.id, tag_id=banner_tag.id, source="manual"))
    await db.execute(image_tag.insert().values(
        image_record_id=wipped.id, tag_id=wip_tag.id, source="manual"))
    svc = GalleryService(db)
    res = await svc.similar(src.id, limit=10, exclude_wip=True)
    assert [i.id for i in res] == [plain.id]        # wip + banner both gone
    # Default (gallery "similar" button) still keeps wip (#1274).
    res_default = await svc.similar(src.id, limit=10)
    assert wipped.id in {i.id for i in res_default}


@pytest.mark.asyncio
async def test_similar_composes_with_tag_filter(db):
    src = await _img(db, 1, _vec(1, 0))
    await _img(db, 2, _vec(1, 0.02))     # nearest, but untagged
    tagged = await _img(db, 3, _vec(1, 0.6))  # farther, but carries the tag
    tag = Tag(name="t", kind=TagKind.general)
    db.add(tag)
    await db.flush()
    await db.execute(image_tag.insert().values(
        image_record_id=tagged.id, tag_id=tag.id, source="manual"))
    svc = GalleryService(db)
    res = await svc.similar(src.id, limit=10, tag_ids=[tag.id])
    assert [i.id for i in res] == [tagged.id]  # scope AND-narrows the ranked set


@pytest.mark.asyncio
async def test_similar_collapses_near_duplicate_phashes(db):
    # The reported failure: a reposted image fills the whole neighbour grid.
    # A wall of same-pHash reposts must collapse to at most one, and the
    # genuinely distinct images must still come through.
    src = await _img(db, 1, _vec(1, 0))
    dupes = []
    for n in range(2, 7):                       # 5 near-identical reposts
        r = await _img(db, n, _vec(1, 0.01 * n))
        r.phash = "ffffffffffffffff"            # identical perceptual hash
        dupes.append(r)
    distinct_a = await _img(db, 7, _vec(1, 1))
    distinct_a.phash = "0000000000000000"
    distinct_b = await _img(db, 8, _vec(0, 1))
    distinct_b.phash = "0f0f0f0f0f0f0f0f"
    await db.flush()

    res = await GalleryService(db).similar(src.id, limit=10)
    ids = [i.id for i in res]
    assert sum(1 for d in dupes if d.id in ids) <= 1   # repost wall collapsed
    assert distinct_a.id in ids and distinct_b.id in ids


@pytest.mark.asyncio
async def test_similar_respects_limit(db):
    src = await _img(db, 1, _vec(1, 0))
    for n in range(2, 7):
        await _img(db, n, _vec(1, 0.1 * n))
    svc = GalleryService(db)
    res = await svc.similar(src.id, limit=2)
    assert len(res) == 2


@pytest.mark.asyncio
async def test_similar_exclude_ids_drops_walked(db):
    """Explore passes its breadcrumb as exclude_ids so already-walked images
    aren't re-served as neighbours (#1476). reach>0 runs cleanly too (small pool
    → the sampler passes through)."""
    src = await _img(db, 1, _vec(1, 0))
    walked = await _img(db, 2, _vec(1, 0.05))
    fresh = await _img(db, 3, _vec(1, 0.3))
    svc = GalleryService(db)
    res = await svc.similar(src.id, limit=10, exclude_ids=[walked.id])
    ids = {i.id for i in res}
    assert walked.id not in ids
    assert fresh.id in ids
    res_reach = await svc.similar(src.id, limit=10, reach=1.0)
    assert fresh.id in {i.id for i in res_reach}


# --- API ---

@pytest.mark.asyncio
async def test_api_similar_endpoint(client, db):
    src = await _img(db, 1, _vec(1, 0))
    near = await _img(db, 2, _vec(1, 0.05))
    await db.commit()
    resp = await client.get(f"/api/gallery/similar?similar_to={src.id}&limit=10")
    assert resp.status_code == 200
    body = await resp.get_json()
    assert [i["id"] for i in body["images"]] == [near.id]
    assert body["next_cursor"] is None


@pytest.mark.asyncio
async def test_api_similar_404_when_source_missing(client):
    resp = await client.get("/api/gallery/similar?similar_to=99999")
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_api_similar_requires_param(client):
    resp = await client.get("/api/gallery/similar")
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_image_detail_reports_has_embedding(client, db):
    embedded = await _img(db, 1, _vec(1, 0))
    plain = await _img(db, 2, None)
    await db.commit()
    e = await (await client.get(f"/api/gallery/image/{embedded.id}")).get_json()
    p = await (await client.get(f"/api/gallery/image/{plain.id}")).get_json()
    assert e["has_embedding"] is True
    assert p["has_embedding"] is False
