"""#1714 队列隔离 + 作废消息清除 单元测试。

覆盖：
1. task_routes：generate_video → generation，ingest_asset/classify/duplication → transcode
2. purge_stale_messages_from_queues：Redis 队列中作废任务消息被物理移除，未命中保留
3. revoke_and_purge：revoke 广播 + 队列清理同时生效
4. ensure_task_claimable：终态任务抛 StaleTaskDiscarded，pending 放行
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest
from celery import Celery

from packages.shared.celery_orphan_guard import (
    StaleTaskDiscarded,
    _extract_business_ids,
    ensure_task_claimable,
    purge_stale_messages_from_queues,
    revoke_and_purge,
)
from packages.shared.celery_queues import (
    QUEUE_GENERATION,
    QUEUE_TRANSCODE,
    apply_queue_settings,
    task_routes,
)

BROKER_URL = "redis://localhost:6379/15"
TEST_QUEUES = ("_test_gen_q", "_test_transcode_q")


# ── 1. 路由表 ──────────────────────────────────────────────────────────


def test_routes_send_generation_to_generation_queue():
    assert task_routes["worker.generate_video"]["queue"] == QUEUE_GENERATION


def test_routes_send_ingest_to_transcode_queue():
    assert task_routes["worker.ingest_asset"]["queue"] == QUEUE_TRANSCODE
    assert task_routes["worker.classify_asset"]["queue"] == QUEUE_TRANSCODE
    assert task_routes["worker.process_duplication_check"]["queue"] == QUEUE_TRANSCODE
    assert task_routes["worker.check_duplicate"]["queue"] == QUEUE_TRANSCODE


def test_apply_queue_settings_configures_celery_app():
    app = Celery("test-routes")
    apply_queue_settings(app)
    queue_names = {q.name for q in app.conf.task_queues}
    assert queue_names == {"generation", "transcode", "celery"}
    assert app.conf.task_default_queue == "celery"


# ── Redis 队列消息清理（需要本地 redis；不可用时 skip） ─────────────────


def _redis_available() -> bool:
    try:
        import redis

        return bool(redis.Redis.from_url(BROKER_URL).ping())
    except Exception:
        return False


@pytest.fixture()
def redis_client():
    import redis

    client = redis.Redis.from_url(BROKER_URL)
    for q in TEST_QUEUES:
        client.delete(q)
    yield client
    for q in TEST_QUEUES:
        client.delete(q)


def _publish(app: Celery, queue: str, celery_id: str, business_id: str) -> None:
    from kombu import Queue
    from kombu.pools import producers

    with app.connection_for_write() as conn:
        with producers[conn].acquire(block=True) as prod:
            prod.publish(
                (business_id,),
                exchange="",
                routing_key=queue,
                serializer="json",
                headers={"id": celery_id, "task": "worker.generate_video"},
                retry=False,
                delivery_mode=1,
                declare=[Queue(queue, routing_key=queue, durable=False)],
            )


@pytest.mark.skipif(not _redis_available(), reason="本地 redis 不可用")
def test_purge_removes_stale_business_message_and_keeps_others(redis_client):
    app = Celery("test-purge")
    app.conf.broker_url = BROKER_URL
    _publish(app, TEST_QUEUES[0], "celery-1", "task-KEEP-A")
    _publish(app, TEST_QUEUES[0], "celery-2", "task-STALE-B")
    _publish(app, TEST_QUEUES[0], "celery-3", "task-KEEP-C")
    _publish(app, TEST_QUEUES[1], "celery-4", "task-STALE-B")  # 同一业务任务在转码队列？不应出现但验证全队列扫描

    removed = purge_stale_messages_from_queues(BROKER_URL, TEST_QUEUES, business_task_ids={"task-STALE-B"})
    assert removed == 2

    remaining = []
    for raw in redis_client.lrange(TEST_QUEUES[0], 0, -1):
        _celery_id, biz_id = _extract_business_ids(raw)
        remaining.append(biz_id)
    assert set(remaining) == {"task-KEEP-A", "task-KEEP-C"}
    assert redis_client.llen(TEST_QUEUES[1]) == 0


@pytest.mark.skipif(not _redis_available(), reason="本地 redis 不可用")
def test_purge_matches_by_celery_message_id(redis_client):
    app = Celery("test-purge-msg-id")
    app.conf.broker_url = BROKER_URL
    _publish(app, TEST_QUEUES[0], "celery-stale-id", "task-X")
    _publish(app, TEST_QUEUES[0], "celery-good-id", "task-Y")

    removed = purge_stale_messages_from_queues(BROKER_URL, TEST_QUEUES, celery_task_ids={"celery-stale-id"})
    assert removed == 1
    assert redis_client.llen(TEST_QUEUES[0]) == 1


@pytest.mark.skipif(not _redis_available(), reason="本地 redis 不可用")
def test_revoke_and_purge_calls_control_revoke(redis_client):
    app = Celery("test-revoke")
    app.conf.broker_url = BROKER_URL
    app.control = MagicMock()
    _publish(app, TEST_QUEUES[0], "celery-revoke-1", "task-R")

    removed = revoke_and_purge(
        app,
        BROKER_URL,
        business_task_ids={"task-R"},
        celery_task_ids={"celery-revoke-1"},
        queue_names=TEST_QUEUES,
    )
    assert removed == 1
    app.control.revoke.assert_called_once_with("celery-revoke-1")


def test_purge_empty_ids_is_noop():
    assert purge_stale_messages_from_queues(BROKER_URL, TEST_QUEUES) == 0


# ── 2. 执行前状态守卫 ──────────────────────────────────────────────────


def test_guard_allows_pending():
    status = ensure_task_claimable("t1", lambda _id: "pending", task_label="generation")
    assert status == "pending"


def test_guard_rejects_failed():
    with pytest.raises(StaleTaskDiscarded) as exc:
        ensure_task_claimable("t2", lambda _id: "failed", task_label="generation")
    assert exc.value.task_id == "t2"
    assert exc.value.status == "failed"


def test_guard_rejects_cancelled_and_completed():
    with pytest.raises(StaleTaskDiscarded):
        ensure_task_claimable("t3", lambda _id: "cancelled")
    with pytest.raises(StaleTaskDiscarded):
        ensure_task_claimable("t4", lambda _id: "completed")


def test_guard_missing_task_returns_empty():
    assert ensure_task_claimable("t5", lambda _id: None) == ""
