import pytest

from backend.app.models import TagKind
from backend.app.services.tag_service import (
    TagMergeConflict,
    TagService,
    TagValidationError,
)

pytestmark = pytest.mark.integration


@pytest.mark.asyncio
async def test_find_or_create_creates(db):
    svc = TagService(db)
    t = await svc.find_or_create("Alice", TagKind.character, fandom_id=None)
    assert t.id is not None
    assert t.name == "Alice"
    assert t.kind == TagKind.character


@pytest.mark.asyncio
async def test_find_or_create_idempotent(db):
    svc = TagService(db)
    a = await svc.find_or_create("Bob", TagKind.artist)
    b = await svc.find_or_create("Bob", TagKind.artist)
    assert a.id == b.id


@pytest.mark.asyncio
async def test_same_name_different_kind_coexists(db):
    svc = TagService(db)
    artist = await svc.find_or_create("Forge", TagKind.artist)
    series = await svc.find_or_create("Forge", TagKind.series)
    assert artist.id != series.id


@pytest.mark.asyncio
async def test_fandom_id_requires_character(db):
    svc = TagService(db)
    fandom = await svc.find_or_create("Naruto", TagKind.fandom)
    with pytest.raises(TagValidationError, match="character"):
        await svc.find_or_create("Bob", TagKind.artist, fandom_id=fandom.id)


@pytest.mark.asyncio
async def test_fandom_id_must_reference_fandom(db):
    svc = TagService(db)
    artist = await svc.find_or_create("X", TagKind.artist)
    with pytest.raises(TagValidationError, match="fandom"):
        await svc.find_or_create("Y", TagKind.character, fandom_id=artist.id)


@pytest.mark.asyncio
async def test_character_with_fandom(db):
    svc = TagService(db)
    fandom = await svc.find_or_create("Naruto", TagKind.fandom)
    char = await svc.find_or_create("Sasuke", TagKind.character, fandom_id=fandom.id)
    assert char.fandom_id == fandom.id


@pytest.mark.asyncio
async def test_find_or_create_dedups_case_insensitive(db):
    """#701: find_or_create matches case-insensitively so a differently-cased
    entry finds the existing tag instead of forking. (Display Title-Casing is
    applied at the create API, not here — this path is shared with the ML
    tagger, which keeps the booru vocabulary's casing.)"""
    svc = TagService(db)
    t1 = await svc.find_or_create("hatsune miku", TagKind.character)
    assert t1.name == "hatsune miku"  # stored as given; not recased here
    t2 = await svc.find_or_create("HATSUNE MIKU", TagKind.character)
    assert t2.id == t1.id  # case variant → same tag, no fork


@pytest.mark.asyncio
async def test_normalize_tag_name():
    from backend.app.services.tag_service import normalize_tag_name
    assert normalize_tag_name("hatsune miku") == "Hatsune Miku"
    assert normalize_tag_name("  looking   at  viewer ") == "Looking At Viewer"
    # Acronym-preserving (2026-06-06): only the first letter of each word is
    # touched, so already-uppercase tails survive (DC stays DC, not Dc).
    assert normalize_tag_name("DC comics") == "DC Comics"
    assert normalize_tag_name("dc") == "Dc"
    assert normalize_tag_name("NSFW variant") == "NSFW Variant"
    assert normalize_tag_name("HATSUNE MIKU") == "HATSUNE MIKU"


@pytest.mark.asyncio
async def test_character_same_name_different_fandoms(db):
    svc = TagService(db)
    f1 = await svc.find_or_create("Series A", TagKind.fandom)
    f2 = await svc.find_or_create("Series B", TagKind.fandom)
    c1 = await svc.find_or_create("Alice", TagKind.character, fandom_id=f1.id)
    c2 = await svc.find_or_create("Alice", TagKind.character, fandom_id=f2.id)
    assert c1.id != c2.id


@pytest.mark.asyncio
async def test_empty_name_raises(db):
    svc = TagService(db)
    with pytest.raises(TagValidationError):
        await svc.find_or_create("   ", TagKind.general)


@pytest.mark.asyncio
async def test_autocomplete_ranking(db):
    svc = TagService(db)
    await svc.find_or_create("alice", TagKind.character)
    await svc.find_or_create("alicent", TagKind.character)
    await svc.find_or_create("malice", TagKind.character)
    hits = await svc.autocomplete("alice")
    # exact match first, then prefix, then substring
    names = [h.name for h in hits]
    assert names[0] == "alice"
    assert names[1] == "alicent"
    assert names[2] == "malice"


@pytest.mark.asyncio
async def test_autocomplete_kind_filter(db):
    svc = TagService(db)
    await svc.find_or_create("Foo", TagKind.artist)
    await svc.find_or_create("Foo", TagKind.character)
    hits = await svc.autocomplete("Foo", kind=TagKind.artist)
    assert len(hits) == 1
    assert hits[0].kind == "artist"


@pytest.mark.asyncio
async def test_autocomplete_empty_query_returns_nothing(db):
    svc = TagService(db)
    await svc.find_or_create("Foo", TagKind.artist)
    assert await svc.autocomplete("") == []


@pytest.mark.asyncio
async def test_set_fandom_assigns_changes_and_clears(db):
    svc = TagService(db)
    f1 = await svc.find_or_create("Bleach", TagKind.fandom)
    f2 = await svc.find_or_create("Naruto", TagKind.fandom)
    char = await svc.find_or_create("Ichigo", TagKind.character)
    assert char.fandom_id is None
    assert (await svc.set_fandom(char.id, f1.id)).fandom_id == f1.id
    assert (await svc.set_fandom(char.id, f2.id)).fandom_id == f2.id
    assert (await svc.set_fandom(char.id, None)).fandom_id is None


@pytest.mark.asyncio
async def test_set_fandom_rejects_non_character(db):
    svc = TagService(db)
    fandom = await svc.find_or_create("Naruto", TagKind.fandom)
    series = await svc.find_or_create("Arc 1", TagKind.series)
    with pytest.raises(TagValidationError, match="character"):
        await svc.set_fandom(series.id, fandom.id)


@pytest.mark.asyncio
async def test_set_fandom_rejects_non_fandom_reference(db):
    svc = TagService(db)
    general = await svc.find_or_create("misc", TagKind.general)
    char = await svc.find_or_create("Rukia", TagKind.character)
    with pytest.raises(TagValidationError, match="fandom"):
        await svc.set_fandom(char.id, general.id)


@pytest.mark.asyncio
async def test_set_fandom_collision_raises_merge_conflict(db):
    svc = TagService(db)
    f1 = await svc.find_or_create("Bleach", TagKind.fandom)
    f2 = await svc.find_or_create("Naruto", TagKind.fandom)
    c1 = await svc.find_or_create("Renji", TagKind.character, fandom_id=f1.id)
    c2 = await svc.find_or_create("Renji", TagKind.character, fandom_id=f2.id)
    with pytest.raises(TagMergeConflict) as ei:
        await svc.set_fandom(c1.id, f2.id)  # collides with c2 in f2
    assert ei.value.target_id == c2.id


@pytest.mark.asyncio
async def test_set_fandom_merge_resolves_collision(db):
    from sqlalchemy import select

    from backend.app.models import Tag

    svc = TagService(db)
    f1 = await svc.find_or_create("Bleach", TagKind.fandom)
    f2 = await svc.find_or_create("Naruto", TagKind.fandom)
    c1 = await svc.find_or_create("Renji", TagKind.character, fandom_id=f1.id)
    c2 = await svc.find_or_create("Renji", TagKind.character, fandom_id=f2.id)
    survivor = await svc.set_fandom(c1.id, f2.id, merge=True)
    assert survivor.id == c2.id  # merged INTO the existing character
    gone = (
        await db.execute(select(Tag).where(Tag.id == c1.id))
    ).scalar_one_or_none()
    assert gone is None
