"""FC-3k: admin Celery task integration tests.

task_always_eager + signal handlers from FC-3i populate task_run.
We assert the wrapper passes args through correctly and that
task_run lifecycle status flips as expected.
"""
import pytest
from sqlalchemy import func, select

import backend.app.tasks.admin  # noqa: F401 — register tasks
from backend.app.celery_app import celery
from backend.app.models import Artist, ImageRecord, TaskRun

pytestmark = pytest.mark.integration


@pytest.fixture(autouse=True)
def _eager_celery(monkeypatch):
    monkeypatch.setattr(celery.conf, "task_always_eager", True)
    monkeypatch.setattr(celery.conf, "task_eager_propagates", False)


@pytest.fixture(autouse=True)
def fake_images_root(monkeypatch, tmp_path):
    monkeypatch.setattr("backend.app.tasks.admin.IMAGES_ROOT", tmp_path)


# --- registration ----------------------------------------------------


def test_delete_artist_cascade_task_registered():
    assert (
        "backend.app.tasks.admin.delete_artist_cascade_task"
        in celery.tasks
    )


def test_bulk_delete_images_task_registered():
    assert (
        "backend.app.tasks.admin.bulk_delete_images_task"
        in celery.tasks
    )


def test_dedup_videos_task_registered():
    assert "backend.app.tasks.admin.dedup_videos_task" in celery.tasks


# --- delete_artist_cascade_task -------------------------------------


@pytest.mark.asyncio
async def test_delete_artist_cascade_task_removes_artist_and_records_ok(
    db_sync, tmp_path,
):
    from backend.app.tasks.admin import delete_artist_cascade_task

    a = Artist(name="Doomed", slug="doomed")
    db_sync.add(a)
    db_sync.flush()
    for i in range(2):
        f = tmp_path / f"d{i}.jpg"
        f.write_bytes(b"x")
        db_sync.add(ImageRecord(
            artist_id=a.id, path=str(f),
            sha256=f"{i:064x}", size_bytes=10, mime="image/jpeg",
            origin="imported_filesystem",
        ))
    db_sync.commit()
    artist_id = a.id

    result = delete_artist_cascade_task.delay(artist_id=artist_id).get()
    assert result["summary"]["images_deleted"] == 2

    surviving = db_sync.execute(
        select(func.count(Artist.id)).where(Artist.id == artist_id)
    ).scalar_one()
    assert surviving == 0

    # FC-3i task_run lifecycle: task should have an 'ok' row.
    status = db_sync.execute(
        select(TaskRun.status)
        .where(TaskRun.task_name.endswith(".delete_artist_cascade_task"))
        .order_by(TaskRun.id.desc()).limit(1)
    ).scalar_one()
    assert status == "ok"


@pytest.mark.asyncio
async def test_delete_artist_cascade_task_records_failure(
    db_sync, monkeypatch,
):
    from backend.app.tasks.admin import delete_artist_cascade_task

    def _boom(*a, **kw):
        raise RuntimeError("synthetic cascade fail")
    monkeypatch.setattr(
        "backend.app.services.cleanup_service.delete_artist_cascade", _boom,
    )
    with pytest.raises(RuntimeError):
        delete_artist_cascade_task.delay(artist_id=1).get()

    row = db_sync.execute(
        select(TaskRun.status, TaskRun.error_message)
        .where(TaskRun.task_name.endswith(".delete_artist_cascade_task"))
        .order_by(TaskRun.id.desc()).limit(1)
    ).one()
    assert row.status == "error"
    assert "synthetic" in (row.error_message or "")


# --- bulk_delete_images_task ----------------------------------------


@pytest.mark.asyncio
async def test_bulk_delete_images_task_removes_listed_images(
    db_sync, tmp_path,
):
    from backend.app.tasks.admin import bulk_delete_images_task

    a = Artist(name="B", slug="b")
    db_sync.add(a)
    db_sync.flush()
    ids = []
    for i in range(3):
        f = tmp_path / f"b{i}.jpg"
        f.write_bytes(b"x")
        img = ImageRecord(
            artist_id=a.id, path=str(f),
            sha256=f"a{i:063x}", size_bytes=10, mime="image/jpeg",
            origin="imported_filesystem",
        )
        db_sync.add(img)
        db_sync.flush()
        ids.append(img.id)
    db_sync.commit()

    result = bulk_delete_images_task.delay(image_ids=ids).get()
    assert result["images_deleted"] == 3

    surviving = db_sync.execute(
        select(func.count(ImageRecord.id))
        .where(ImageRecord.id.in_(ids))
    ).scalar_one()
    assert surviving == 0


# --- prune_missing_file_records_task (#859 orphan repair) -----------


def _img(db_sync, artist, *, path, sha):
    img = ImageRecord(
        artist_id=artist.id, path=str(path),
        sha256=sha, size_bytes=10, mime="image/jpeg",
        origin="downloaded",
    )
    db_sync.add(img)
    db_sync.flush()
    return img.id


@pytest.mark.asyncio
async def test_prune_missing_file_records_deletes_only_orphans(db_sync, tmp_path):
    """Records whose backing file is gone get deleted; records whose file is
    present survive. Below the abort sample size, fraction is irrelevant."""
    from backend.app.tasks.admin import prune_missing_file_records_task

    a = Artist(name="Orph", slug="orph")
    db_sync.add(a)
    db_sync.flush()

    present = []
    for i in range(3):
        f = tmp_path / f"present{i}.jpg"
        f.write_bytes(b"real")
        present.append(_img(db_sync, a, path=f, sha=f"{i:064x}"))
    missing = [
        _img(db_sync, a, path=tmp_path / f"gone{i}.jpg", sha=f"f{i:063x}")
        for i in range(2)
    ]
    db_sync.commit()

    result = prune_missing_file_records_task.delay().get()
    assert result["checked"] == 5
    assert result["missing"] == 2
    assert result["deleted"] == 2

    db_sync.expire_all()
    surviving = db_sync.execute(
        select(ImageRecord.id).where(ImageRecord.id.in_(present + missing))
    ).scalars().all()
    assert set(surviving) == set(present)


@pytest.mark.asyncio
async def test_prune_missing_file_records_aborts_when_mostly_missing(db_sync, tmp_path):
    """NFS-stall guard: a large sample that's mostly missing means the
    filesystem is unhealthy, not that the library evaporated — abort, delete
    nothing."""
    from backend.app.tasks.admin import prune_missing_file_records_task

    a = Artist(name="Stall", slug="stall")
    db_sync.add(a)
    db_sync.flush()
    ids = [
        _img(db_sync, a, path=tmp_path / f"phantom{i}.jpg", sha=f"{i:064x}")
        for i in range(55)  # >= _ORPHAN_MIN_SAMPLE, all missing -> frac 1.0
    ]
    db_sync.commit()

    result = prune_missing_file_records_task.delay().get()
    assert result["deleted"] == 0
    assert "aborted" in result

    db_sync.expire_all()
    surviving = db_sync.execute(
        select(func.count(ImageRecord.id)).where(ImageRecord.id.in_(ids))
    ).scalar_one()
    assert surviving == 55


@pytest.mark.asyncio
async def test_bulk_delete_images_task_idempotent_on_empty_list(db_sync):
    from backend.app.tasks.admin import bulk_delete_images_task
    result = bulk_delete_images_task.delay(image_ids=[]).get()
    assert result["images_deleted"] == 0


@pytest.mark.asyncio
async def test_bulk_delete_images_task_reports_missing_ids(db_sync):
    from backend.app.tasks.admin import bulk_delete_images_task
    result = bulk_delete_images_task.delay(
        image_ids=[9_999_998, 9_999_999],
    ).get()
    assert result["images_deleted"] == 0
    assert sorted(result["missing_ids"]) == [9_999_998, 9_999_999]
