import pytest
from sqlalchemy import select

from backend.app.models import ImageRecord, ImageRegion, MLSettings, TagHead, TagKind
from backend.app.services.tag_service import TagService

pytestmark = pytest.mark.integration


@pytest.mark.asyncio
async def test_autocomplete_empty(client):
    resp = await client.get("/api/tags/autocomplete?q=")
    assert resp.status_code == 200
    assert await resp.get_json() == []


@pytest.mark.asyncio
async def test_create_tag(client):
    resp = await client.post("/api/tags", json={"name": "Bob", "kind": "artist"})
    assert resp.status_code == 201
    body = await resp.get_json()
    assert body["name"] == "Bob"
    assert body["kind"] == "artist"


@pytest.mark.asyncio
async def test_create_tag_rejects_invalid_kind(client):
    resp = await client.post("/api/tags", json={"name": "Bob", "kind": "notakind"})
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_create_character_without_fandom_ok(client):
    resp = await client.post("/api/tags", json={"name": "Alice", "kind": "character"})
    assert resp.status_code == 201


@pytest.mark.asyncio
async def test_create_character_with_bad_fandom_id(client):
    artist_resp = await client.post("/api/tags", json={"name": "X", "kind": "artist"})
    artist_id = (await artist_resp.get_json())["id"]
    resp = await client.post(
        "/api/tags", json={"name": "Y", "kind": "character", "fandom_id": artist_id}
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_create_tag_missing_name_400(client):
    """name is still required; only `kind` became optional."""
    resp = await client.post("/api/tags", json={})
    assert resp.status_code == 400
    resp = await client.post("/api/tags", json={"kind": "artist"})
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_create_tag_name_only_defaults_to_general(client):
    """IR-style: name without kind and without `kind:` prefix → general.
    #701: operator-entered names are Title-Cased at the create endpoint."""
    resp = await client.post("/api/tags", json={"name": "sunset"})
    assert resp.status_code == 201
    body = await resp.get_json()
    assert body["name"] == "Sunset"
    assert body["kind"] == "general"


@pytest.mark.asyncio
async def test_create_tag_with_kind_prefix(client):
    """IR-style: `name="character:Saber"` without explicit kind → parsed as character."""
    resp = await client.post("/api/tags", json={"name": "character:Saber"})
    assert resp.status_code == 201
    body = await resp.get_json()
    assert body["name"] == "Saber"
    assert body["kind"] == "character"


@pytest.mark.asyncio
async def test_explicit_kind_overrides_prefix_parsing(client):
    """If caller passes explicit kind, don't re-parse the name —
    colon and prefix stay literal. #701: first letter capitalized, tail
    preserved (acronym-safe), so the 'S' in Saber survives."""
    resp = await client.post(
        "/api/tags", json={"name": "character:Saber", "kind": "general"}
    )
    assert resp.status_code == 201
    body = await resp.get_json()
    assert body["name"] == "Character:Saber"
    assert body["kind"] == "general"


@pytest.mark.asyncio
async def test_unknown_prefix_kept_literal(client):
    """`http:example` — `http` not in KNOWN_KINDS → kind=general, literal name.
    #701: Title-Cased as one word (no internal whitespace to split on)."""
    resp = await client.post("/api/tags", json={"name": "http://example.com"})
    assert resp.status_code == 201
    body = await resp.get_json()
    assert body["name"] == "Http://example.com"
    assert body["kind"] == "general"


# --- #1206 Step 4: applied-tag grounding endpoint (hover on applied chips) ----


@pytest.mark.asyncio
async def test_applied_tag_grounding_returns_winning_region(client, db):
    # Hovering an applied chip fetches the crop that best explains the tag. Here
    # the whole-image vector is orthogonal to the head but a concept crop aligns,
    # so the crop wins the max-over-bag → grounding is that region's box.
    ver = (await db.execute(
        select(MLSettings).where(MLSettings.id == 1)
    )).scalar_one().embedder_model_version
    img = ImageRecord(
        path="/images/grchip.jpg", sha256="gc" * 32, size_bytes=1,
        mime="image/jpeg", width=1, height=1, origin="imported_filesystem",
        integrity_status="unknown",
        siglip_embedding=[0.0] * 5 + [3.0] + [0.0] * 1146,   # whole-image ⟂ head
    )
    db.add(img)
    await db.flush()
    tag = await TagService(db).find_or_create("glasses", TagKind.general)
    db.add(TagHead(
        tag_id=tag.id, embedding_version=ver,
        weights=[1.0] + [0.0] * 1151, bias=0.0, suggest_threshold=0.5,
        auto_apply_threshold=None, n_pos=10, n_neg=30,
        ap=0.8, precision_cv=0.9, recall=0.6,
    ))
    db.add(ImageRegion(
        image_record_id=img.id, kind="concept",
        rx=0.4, ry=0.4, rw=0.3, rh=0.3,
        siglip_embedding=[3.0] + [0.0] * 1151, embedding_version=ver,
    ))
    await db.commit()

    resp = await client.get(f"/api/images/{img.id}/tags/{tag.id}/grounding")
    assert resp.status_code == 200
    body = await resp.get_json()
    assert body["has_head"] is True
    assert body["grounding"]["bbox"] == pytest.approx([0.4, 0.4, 0.3, 0.3])
    assert body["grounding"]["kind"] == "concept"


@pytest.mark.asyncio
async def test_applied_tags_expose_source_and_confirmed(client, db):
    # The chip UI needs each applied tag's source (auto vs manual) + confirmed
    # state to badge auto-tags and offer Keep/confirm (milestone 139).
    from backend.app.models import TagPositiveConfirmation
    from backend.app.models.tag import image_tag

    img = ImageRecord(
        path="/images/srcflag.jpg", sha256="sf" * 32, size_bytes=1,
        mime="image/jpeg", width=1, height=1, origin="imported_filesystem",
        integrity_status="unknown",
    )
    db.add(img)
    await db.flush()
    auto = await TagService(db).find_or_create("autotag", TagKind.general)
    manual = await TagService(db).find_or_create("manualtag", TagKind.general)
    await db.execute(image_tag.insert().values(
        image_record_id=img.id, tag_id=auto.id, source="head_auto"))
    await db.execute(image_tag.insert().values(
        image_record_id=img.id, tag_id=manual.id, source="manual"))
    db.add(TagPositiveConfirmation(image_record_id=img.id, tag_id=manual.id))
    await db.commit()

    resp = await client.get(f"/api/images/{img.id}/tags")
    assert resp.status_code == 200
    by_id = {t["id"]: t for t in await resp.get_json()}
    assert by_id[auto.id]["source"] == "head_auto"
    assert by_id[auto.id]["confirmed"] is False
    assert by_id[manual.id]["source"] == "manual"
    assert by_id[manual.id]["confirmed"] is True     # has a confirmation row


@pytest.mark.asyncio
async def test_applied_tag_grounding_no_head(client, db):
    # A tag with no head can't be localized → has_head False, grounding null; the
    # chip shows no overlay. Validates the response contract the frontend reads.
    img = ImageRecord(
        path="/images/grchip2.jpg", sha256="gd" * 32, size_bytes=1,
        mime="image/jpeg", width=1, height=1, origin="imported_filesystem",
        integrity_status="unknown",
    )
    db.add(img)
    await db.flush()
    tag = await TagService(db).find_or_create("NoHeadHere", TagKind.general)
    await db.commit()

    resp = await client.get(f"/api/images/{img.id}/tags/{tag.id}/grounding")
    assert resp.status_code == 200
    assert await resp.get_json() == {"grounding": None, "has_head": False}
