"""System tags (milestone #128): seeded rows, protection guards, serialization.

The three hygiene tags (wip / banner / editor screenshot) ship via migration
0075 and drive the training-hygiene exclusions. They must exist after
migration, serialize with is_system, and refuse rename/merge-away — the
mechanism keys on these exact rows. Merge-INTO stays allowed: adopting an
operator's old hygiene tag into the system row is the intended move.
"""
import pytest
from sqlalchemy import insert, select

from backend.app.models import ImageRecord, Tag, TagKind
from backend.app.models.tag import image_tag
from backend.app.services.cleanup_service import (
    prune_unused_tags,
    reset_content_tagging,
)

pytestmark = pytest.mark.integration

SYSTEM_NAMES = {"wip", "banner", "editor screenshot"}


async def _system_tag(db, name):
    return (await db.execute(
        select(Tag).where(Tag.is_system.is_(True), Tag.name == name)
    )).scalar_one()


@pytest.mark.asyncio
async def test_seeded_system_tags_exist(db):
    rows = (await db.execute(
        select(Tag.name, Tag.kind).where(Tag.is_system.is_(True))
    )).all()
    assert {r.name for r in rows} == SYSTEM_NAMES
    assert all(r.kind == TagKind.general for r in rows)


@pytest.mark.asyncio
async def test_get_tag_serializes_is_system(client, db):
    wip = await _system_tag(db, "wip")
    resp = await client.get(f"/api/tags/{wip.id}")
    assert resp.status_code == 200
    assert (await resp.get_json())["is_system"] is True


@pytest.mark.asyncio
async def test_rename_system_tag_refused(client, db):
    wip = await _system_tag(db, "wip")
    resp = await client.patch(
        f"/api/tags/{wip.id}", json={"name": "work in progress"}
    )
    assert resp.status_code == 400
    body = await resp.get_json()
    assert "system tag" in body["error"]


@pytest.mark.asyncio
async def test_merge_away_system_tag_refused(client, db):
    banner = await _system_tag(db, "banner")
    created = await client.post(
        "/api/tags", json={"name": "ad banner", "kind": "general"}
    )
    target_id = (await created.get_json())["id"]
    resp = await client.post(
        f"/api/tags/{banner.id}/merge", json={"target_id": target_id}
    )
    assert resp.status_code == 400
    body = await resp.get_json()
    assert "system tag" in body["error"]


@pytest.mark.asyncio
async def test_admin_delete_system_tag_refused(client, db):
    wip = await _system_tag(db, "wip")
    resp = await client.delete(f"/api/admin/tags/{wip.id}")
    assert resp.status_code == 400
    body = await resp.get_json()
    assert body["error"] == "system_tag"
    still = (await db.execute(
        select(Tag.id).where(Tag.id == wip.id)
    )).scalar_one_or_none()
    assert still == wip.id


@pytest.mark.asyncio
async def test_prune_unused_skips_system_tags(db):
    """The seeded system tags have zero applications and would match every
    'unused' condition — the shared predicate must exempt them (preview AND
    live delete, same conditions)."""
    preview = await db.run_sync(
        lambda s: prune_unused_tags(s, dry_run=True)
    )
    assert preview["count"] == 0
    assert preview["sample_names"] == []
    await db.run_sync(lambda s: prune_unused_tags(s, dry_run=False))
    remaining = (await db.execute(
        select(Tag.name).where(Tag.is_system.is_(True))
    )).scalars().all()
    assert set(remaining) == SYSTEM_NAMES


@pytest.mark.asyncio
async def test_reset_content_preserves_system_tags_and_applications(db):
    """Reset re-tags CONTENT concepts; hygiene flags describe the file itself
    and survive with their applications."""
    wip = await _system_tag(db, "wip")
    plain = Tag(name="doomed concept", kind=TagKind.general)
    db.add(plain)
    img = ImageRecord(
        path="/images/r.jpg", sha256="f" * 64, size_bytes=1, mime="image/jpeg",
        width=1, height=1, origin="imported_filesystem",
        integrity_status="unknown",
    )
    db.add(img)
    await db.flush()
    for tid in (wip.id, plain.id):
        await db.execute(insert(image_tag).values(
            image_record_id=img.id, tag_id=tid, source="manual",
        ))
    await db.commit()

    result = await db.run_sync(
        lambda s: reset_content_tagging(s, dry_run=False)
    )
    assert result["deleted"] == 1  # only the plain general tag
    names_left = (await db.execute(
        select(Tag.name).where(Tag.kind == TagKind.general)
    )).scalars().all()
    assert "doomed concept" not in names_left
    assert "wip" in names_left
    wip_apps = (await db.execute(
        select(image_tag.c.image_record_id)
        .where(image_tag.c.tag_id == wip.id)
    )).scalars().all()
    assert wip_apps == [img.id]


@pytest.mark.asyncio
async def test_merge_into_system_tag_allowed(client, db):
    wip = await _system_tag(db, "wip")
    created = await client.post(
        "/api/tags", json={"name": "old wip", "kind": "general"}
    )
    source_id = (await created.get_json())["id"]
    resp = await client.post(
        f"/api/tags/{source_id}/merge", json={"target_id": wip.id}
    )
    assert resp.status_code == 200
    body = await resp.get_json()
    assert body["target"]["id"] == wip.id
    assert body["source_deleted"] is True
