"""Soft auto-apply (milestone 139): the retraction sweeps drop standing
head_auto/ccip_auto tags now below their threshold, keep the ones still above,
and never touch manual or operator-confirmed tags. Sync + sklearn-free (they
score with STORED weights/vectors), so tested directly via db_sync."""
import pytest
from sqlalchemy import select

from backend.app.models import (
    CharacterPrototype,
    ImageRecord,
    ImageRegion,
    MLSettings,
    Tag,
    TagHead,
    TagKind,
    TagPositiveConfirmation,
)
from backend.app.models.tag import image_tag
from backend.app.services.ml.character_prototypes import retract_auto_applied_ccip
from backend.app.services.ml.heads import retract_auto_applied_heads

pytestmark = pytest.mark.integration


def _emb(slot: int) -> list[float]:
    v = [0.0] * 1152
    v[slot] = 3.0
    return v


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


def _img(db, sha: str, emb=None) -> 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", siglip_embedding=emb,
    )
    db.add(img)
    db.flush()
    return img


def _figure(db, image_id: int, ccip) -> 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, embedding_version="ccip-test",
    ))
    db.flush()


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


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


def _version(db) -> str:
    return db.execute(
        select(MLSettings.embedder_model_version).where(MLSettings.id == 1)
    ).scalar_one()


def _head(db, tag_id: int, slot: int, threshold: float, version: str) -> None:
    w = [0.0] * 1152
    w[slot] = 1.0
    db.add(TagHead(
        tag_id=tag_id, embedding_version=version, weights=w, bias=0.0,
        suggest_threshold=0.5, auto_apply_threshold=threshold,
        n_pos=60, n_neg=180, ap=0.9, precision_cv=0.98, recall=0.7,
    ))
    db.flush()


def _has_tag(db, image_id: int, tag_id: int) -> bool:
    return db.execute(
        select(image_tag.c.tag_id)
        .where(image_tag.c.image_record_id == image_id)
        .where(image_tag.c.tag_id == tag_id)
    ).first() is not None


def test_retract_head_auto(db_sync):
    ver = _version(db_sync)
    tag = _tag(db_sync, "glasses", TagKind.general)
    _head(db_sync, tag.id, slot=0, threshold=0.7, version=ver)
    hi = _img(db_sync, "a" * 64, _emb(0))     # aligned → ~0.73 ≥ 0.7 → keep
    lo = _img(db_sync, "b" * 64, _emb(5))     # orthogonal → 0.5 < 0.7 → retract
    man = _img(db_sync, "c" * 64, _emb(5))    # low score but manual → keep
    conf = _img(db_sync, "d" * 64, _emb(5))   # low score, head_auto, CONFIRMED → keep
    _apply(db_sync, hi.id, tag.id, "head_auto")
    _apply(db_sync, lo.id, tag.id, "head_auto")
    _apply(db_sync, man.id, tag.id, "manual")
    _apply(db_sync, conf.id, tag.id, "head_auto")
    db_sync.add(TagPositiveConfirmation(image_record_id=conf.id, tag_id=tag.id))
    db_sync.commit()

    assert retract_auto_applied_heads(db_sync) == 1
    assert not _has_tag(db_sync, lo.id, tag.id)     # retracted (below threshold)
    assert _has_tag(db_sync, hi.id, tag.id)         # kept (still above)
    assert _has_tag(db_sync, man.id, tag.id)        # kept (manual, not auto)
    assert _has_tag(db_sync, conf.id, tag.id)       # kept (operator-confirmed)


def test_retract_head_auto_noop_when_disabled(db_sync):
    s = db_sync.execute(select(MLSettings).where(MLSettings.id == 1)).scalar_one()
    s.head_auto_apply_enabled = False
    ver = _version(db_sync)
    tag = _tag(db_sync, "glasses", TagKind.general)
    _head(db_sync, tag.id, slot=0, threshold=0.7, version=ver)
    lo = _img(db_sync, "e" * 64, _emb(5))     # would be below threshold
    _apply(db_sync, lo.id, tag.id, "head_auto")
    db_sync.commit()

    assert retract_auto_applied_heads(db_sync) == 0
    assert _has_tag(db_sync, lo.id, tag.id)         # switch off → nothing retracted


def test_retract_ccip_auto(db_sync):
    char = _tag(db_sync, "Raven", TagKind.character)
    db_sync.add(CharacterPrototype(tag_id=char.id, ccip_embedding=_ccip(0)))
    hi = _img(db_sync, "f" * 64)              # figure matches prototype → keep
    lo = _img(db_sync, "g" * 64)             # figure orthogonal → retract
    conf = _img(db_sync, "h" * 64)           # orthogonal, CONFIRMED → keep
    man = _img(db_sync, "i" * 64)            # orthogonal, manual → keep
    _figure(db_sync, hi.id, _ccip(0))
    _figure(db_sync, lo.id, _ccip(5))
    _figure(db_sync, conf.id, _ccip(5))
    _figure(db_sync, man.id, _ccip(5))
    _apply(db_sync, hi.id, char.id, "ccip_auto")
    _apply(db_sync, lo.id, char.id, "ccip_auto")
    _apply(db_sync, conf.id, char.id, "ccip_auto")
    _apply(db_sync, man.id, char.id, "manual")
    db_sync.add(TagPositiveConfirmation(image_record_id=conf.id, tag_id=char.id))
    db_sync.commit()

    assert retract_auto_applied_ccip(db_sync) == 1
    assert not _has_tag(db_sync, lo.id, char.id)    # retracted (below threshold)
    assert _has_tag(db_sync, hi.id, char.id)        # kept (match ≥ threshold)
    assert _has_tag(db_sync, conf.id, char.id)      # kept (operator-confirmed)
    assert _has_tag(db_sync, man.id, char.id)       # kept (manual, not auto)
