"""Incremental CCIP prototype builder (#1317, m138). Sync (celery ml worker),
so tested directly via db_sync — the global gate, per-character fingerprint diff,
capping, single-character exclusion, and lost-reference cleanup."""
import pytest
from sqlalchemy import func, select

from backend.app.models import (
    CcipPrototypeState,
    CharacterPrototype,
    ImageRecord,
    ImageRegion,
    MLSettings,
    Tag,
    TagKind,
    TagPositiveConfirmation,
)
from backend.app.models.tag import image_tag
from backend.app.services.ml.character_prototypes import (
    refresh_character_prototypes,
)

pytestmark = pytest.mark.integration


def _ccip(slot: int = 0) -> list[float]:
    v = [0.0] * 768
    v[slot] = 1.0
    return v


def _img(db, sha: str) -> ImageRecord:
    img = ImageRecord(
        path=f"/images/{sha}.jpg", sha256=sha, size_bytes=1, mime="image/jpeg",
        width=1, height=1, origin="imported_filesystem",
        integrity_status="unknown",
    )
    db.add(img)
    db.flush()
    return img


def _figure(db, image_id: int, ccip=None) -> None:
    db.add(ImageRegion(
        image_record_id=image_id, kind="figure",
        rx=0.0, ry=0.0, rw=1.0, rh=1.0,
        ccip_embedding=ccip if ccip is not None else _ccip(),
        embedding_version="ccip-test",
    ))
    db.flush()


def _char(db, name: str) -> Tag:
    t = Tag(name=name, kind=TagKind.character)
    db.add(t)
    db.flush()
    return t


def _tag(db, image_id: int, tag_id: int) -> None:
    db.execute(image_tag.insert().values(
        image_record_id=image_id, tag_id=tag_id, source="manual",
    ))


def _proto_count(db, tag_id: int) -> int:
    return db.execute(
        select(func.count()).select_from(CharacterPrototype)
        .where(CharacterPrototype.tag_id == tag_id)
    ).scalar_one()


def _set_cap(db, cap: int) -> None:
    s = db.execute(select(MLSettings).where(MLSettings.id == 1)).scalar_one()
    s.ccip_prototype_cap = cap
    db.flush()


def test_refresh_builds_then_gate_no_ops(db_sync):
    raven = _char(db_sync, "Raven")
    img = _img(db_sync, "a" * 64)
    _figure(db_sync, img.id)
    _tag(db_sync, img.id, raven.id)
    db_sync.commit()

    r1 = refresh_character_prototypes(db_sync)
    assert r1["skipped"] is False and r1["rebuilt"] == 1
    assert _proto_count(db_sync, raven.id) == 1
    assert db_sync.get(CcipPrototypeState, raven.id) is not None

    # Nothing changed → the global gate short-circuits (no per-character work).
    r2 = refresh_character_prototypes(db_sync)
    assert r2["skipped"] is True and r2["rebuilt"] == 0


def test_only_changed_character_rebuilt(db_sync):
    raven = _char(db_sync, "Raven")
    daphne = _char(db_sync, "Daphne")
    ri = _img(db_sync, "b" * 64)
    _figure(db_sync, ri.id)
    _tag(db_sync, ri.id, raven.id)
    di = _img(db_sync, "c" * 64)
    _figure(db_sync, di.id)
    _tag(db_sync, di.id, daphne.id)
    db_sync.commit()
    assert refresh_character_prototypes(db_sync)["rebuilt"] == 2

    # A new figure on Raven's image changes only Raven's fingerprint.
    _figure(db_sync, ri.id, _ccip(1))
    db_sync.commit()
    r = refresh_character_prototypes(db_sync)
    assert r["rebuilt"] == 1                       # Daphne untouched
    assert _proto_count(db_sync, raven.id) == 2
    assert _proto_count(db_sync, daphne.id) == 1


def test_cap_bounds_prototypes(db_sync):
    _set_cap(db_sync, 2)
    raven = _char(db_sync, "Raven")
    for i in range(3):                             # 3 single-character images
        img = _img(db_sync, f"{i}" * 64)
        _figure(db_sync, img.id)
        _tag(db_sync, img.id, raven.id)
    db_sync.commit()

    refresh_character_prototypes(db_sync)
    assert _proto_count(db_sync, raven.id) == 2    # capped from 3 → 2


def test_multi_character_image_not_referenced(db_sync):
    # A figure on a 2-character image is ambiguous (tag is image-level), so it
    # must NOT become a reference for either — parity with the legacy matcher.
    raven = _char(db_sync, "Raven")
    daphne = _char(db_sync, "Daphne")
    img = _img(db_sync, "d" * 64)
    _figure(db_sync, img.id)
    _tag(db_sync, img.id, raven.id)
    _tag(db_sync, img.id, daphne.id)
    db_sync.commit()

    refresh_character_prototypes(db_sync)
    assert _proto_count(db_sync, raven.id) == 0
    assert _proto_count(db_sync, daphne.id) == 0


def test_unconfirmed_auto_char_tag_not_referenced(db_sync):
    # A single-character image whose ONLY character tag was AUTO-applied
    # (unconfirmed) must NOT seed a prototype — else CCIP self-trains on its own
    # guess (milestone 139). Confirming it (which trips the global gate) makes it
    # a reference.
    raven = _char(db_sync, "Raven")
    img = _img(db_sync, "z" * 64)
    _figure(db_sync, img.id)
    db_sync.execute(image_tag.insert().values(
        image_record_id=img.id, tag_id=raven.id, source="ccip_auto",
    ))
    db_sync.commit()
    refresh_character_prototypes(db_sync)
    assert _proto_count(db_sync, raven.id) == 0

    db_sync.add(TagPositiveConfirmation(image_record_id=img.id, tag_id=raven.id))
    db_sync.commit()
    refresh_character_prototypes(db_sync)
    assert _proto_count(db_sync, raven.id) == 1


def test_lost_references_are_removed(db_sync):
    raven = _char(db_sync, "Raven")
    img = _img(db_sync, "e" * 64)
    _figure(db_sync, img.id)
    _tag(db_sync, img.id, raven.id)
    db_sync.commit()
    refresh_character_prototypes(db_sync)
    assert _proto_count(db_sync, raven.id) == 1

    # Untag Raven → the image is no longer a reference → prototypes + state drop.
    db_sync.execute(
        image_tag.delete()
        .where(image_tag.c.image_record_id == img.id)
        .where(image_tag.c.tag_id == raven.id)
    )
    db_sync.commit()
    r = refresh_character_prototypes(db_sync)
    assert r["removed"] == 1
    assert _proto_count(db_sync, raven.id) == 0
    assert db_sync.get(CcipPrototypeState, raven.id) is None
