import numpy as np
import pytest

from backend.app.models import ImageRecord, TagKind
from backend.app.models.tag import image_tag
from backend.app.services.ml.centroids import CentroidService
from backend.app.services.tag_service import TagService

pytestmark = pytest.mark.integration


def _img(sha: str, embedding: list[float] | None) -> ImageRecord:
    return ImageRecord(
        path=f"/images/{sha}.jpg",
        sha256=sha,
        size_bytes=1,
        mime="image/jpeg",
        width=1,
        height=1,
        origin="imported_filesystem",
        integrity_status="unknown",
        siglip_embedding=embedding,
    )


async def _attach(db, image_id: int, tag_id: int):
    await db.execute(
        image_tag.insert().values(
            image_record_id=image_id, tag_id=tag_id, source="manual"
        )
    )


@pytest.mark.asyncio
async def test_recompute_skips_too_few_members(db):
    tags = TagService(db)
    tag = await tags.find_or_create("Lonely", TagKind.character)
    img = _img("a" * 64, [0.1] * 1152)
    db.add(img)
    await db.flush()
    await _attach(db, img.id, tag.id)

    svc = CentroidService(db)
    assert await svc.recompute_for_tag(tag.id) is False


@pytest.mark.asyncio
async def test_recompute_writes_centroid(db):
    tags = TagService(db)
    tag = await tags.find_or_create("Popular", TagKind.character)
    for i in range(5):
        img = _img(f"{i:064d}", [float(i)] * 1152)
        db.add(img)
        await db.flush()
        await _attach(db, img.id, tag.id)

    svc = CentroidService(db)
    assert await svc.recompute_for_tag(tag.id) is True

    from backend.app.models import TagReferenceEmbedding
    cen = await db.get(TagReferenceEmbedding, tag.id)
    assert cen is not None
    assert cen.reference_count == 5
    assert abs(np.array(cen.embedding)[0] - 2.0) < 1e-4


@pytest.mark.asyncio
async def test_recompute_skips_ineligible_kind(db):
    tags = TagService(db)
    tag = await tags.find_or_create("somearchive", TagKind.archive)
    for i in range(5):
        img = _img(f"arch{i:060d}", [1.0] * 1152)
        db.add(img)
        await db.flush()
        await _attach(db, img.id, tag.id)
    svc = CentroidService(db)
    assert await svc.recompute_for_tag(tag.id) is False


@pytest.mark.asyncio
async def test_list_drifted_includes_uncomputed(db):
    tags = TagService(db)
    tag = await tags.find_or_create("Drifty", TagKind.character)
    for i in range(5):
        img = _img(f"d{i:063d}", [0.5] * 1152)
        db.add(img)
        await db.flush()
        await _attach(db, img.id, tag.id)
    svc = CentroidService(db)
    drifted = await svc.list_drifted()
    assert tag.id in drifted


@pytest.mark.asyncio
async def test_find_similar_tags(db):
    tags = TagService(db)
    tag = await tags.find_or_create("SimTag", TagKind.character)
    for i in range(5):
        img = _img(f"s{i:063d}", [1.0] * 1152)
        db.add(img)
        await db.flush()
        await _attach(db, img.id, tag.id)
    svc = CentroidService(db)
    await svc.recompute_for_tag(tag.id)

    query_img = _img("q" * 64, [1.0] * 1152)
    db.add(query_img)
    await db.flush()
    hits = await svc.find_similar_tags(query_img.id, limit=10)
    assert any(h.tag_id == tag.id for h in hits)
    sim = next(h.similarity for h in hits if h.tag_id == tag.id)
    assert sim > 0.99
