from datetime import datetime, timedelta
from types import SimpleNamespace
from unittest.mock import Mock, patch

from app.chain import subscribe as subscribe_module
from app.chain.subscribe import SubscribeChain
from app.schemas.types import MediaType


class _SubscribeOper:
    """
    最小订阅 Oper 替身，隔离订阅搜索状态流转测试的数据库访问。
    """

    subscribe = None
    updates = []

    def get(self, sid: int):
        """
        按 ID 返回测试订阅对象。
        """
        return self.subscribe if self.subscribe and self.subscribe.id == sid else None

    def list(self, _state: str):
        """
        返回批量搜索需要的测试订阅列表。
        """
        return [self.subscribe] if self.subscribe else []

    def update(self, sid: int, payload: dict) -> None:
        """
        记录订阅状态更新请求。
        """
        self.updates.append((sid, payload))


class _TimedOutLock:
    """模拟订阅搜索锁在等待窗口内始终无法取得。"""

    def acquire(self, **_kwargs):
        """返回未取得锁，验证调用方不会越过互斥边界继续执行。"""
        return False

    def release(self):
        """超时路径不应释放未持有的锁。"""
        raise AssertionError("未持有的订阅锁不应被释放")


def _new_subscribe(created_at: datetime) -> SimpleNamespace:
    """
    构造一个新建电影订阅。
    """
    return SimpleNamespace(
        id=31,
        name="测试电影",
        year="2026",
        type=MediaType.MOVIE.value,
        tmdbid=12345,
        doubanid=None,
        bangumiid=None,
        anilistid=None,
        media_source="themoviedb",
        media_id="12345",
        season=None,
        custom_words=None,
        date=created_at.strftime("%Y-%m-%d %H:%M:%S"),
        state="N",
        episode_group=None,
    )


def test_new_subscribe_search_keeps_state_when_recently_created(monkeypatch) -> None:
    """
    新增 60 秒保护期内跳过搜索时，应保留 N 状态等待下一轮新增订阅搜索。
    """
    _SubscribeOper.subscribe = _new_subscribe(datetime.now())
    _SubscribeOper.updates = []
    monkeypatch.setattr(subscribe_module, "SubscribeOper", _SubscribeOper)

    media_chain_class = Mock()
    with patch.object(subscribe_module, "MediaChain", media_chain_class):
        chain = object.__new__(SubscribeChain)
        chain.search(state="N", manual=False)

    media_chain_class.assert_not_called()
    assert _SubscribeOper.updates == []


def test_new_subscribe_search_marks_state_after_attempt(monkeypatch) -> None:
    """
    新增订阅越过保护期并实际尝试搜索后，应从 N 状态收敛为 R。
    """
    _SubscribeOper.subscribe = _new_subscribe(datetime.now() - timedelta(minutes=2))
    _SubscribeOper.updates = []
    monkeypatch.setattr(subscribe_module, "SubscribeOper", _SubscribeOper)

    media_chain = Mock()
    media_chain.recognize_media.return_value = None
    with patch.object(subscribe_module, "MediaChain", return_value=media_chain):
        chain = object.__new__(SubscribeChain)
        chain.search(state="N", manual=False)

    media_chain.recognize_media.assert_called_once()
    assert _SubscribeOper.updates == [(31, {"state": "R"})]


def test_targeted_batch_searches_all_ids_without_state_scan(monkeypatch) -> None:
    """用户归属订阅批次只按指定 ID 顺序读取，不扩大为全局状态搜索。"""
    first = _new_subscribe(datetime.now() - timedelta(minutes=2))
    first.state = "R"
    second = _new_subscribe(datetime.now() - timedelta(minutes=2))
    second.id = 32
    second.name = "测试电影 2"
    second.state = "R"
    subscribes = {first.id: first, second.id: second}
    subscribe_oper = Mock()
    subscribe_oper.get.side_effect = subscribes.get
    monkeypatch.setattr(
        subscribe_module,
        "SubscribeOper",
        lambda: subscribe_oper,
    )
    media_chain = Mock()
    media_chain.recognize_media.return_value = None

    with patch.object(subscribe_module, "MediaChain", return_value=media_chain):
        chain = object.__new__(SubscribeChain)
        chain.search(sids=(31, 32), state=None, manual=False)

    assert [item.args for item in subscribe_oper.get.call_args_list] == [(31,), (32,)]
    subscribe_oper.list.assert_not_called()
    assert media_chain.recognize_media.call_count == 2


def test_subscribe_search_aborts_when_lock_times_out(monkeypatch) -> None:
    """订阅搜索锁超时后必须中止，不能在无锁状态下继续访问订阅。"""
    monkeypatch.setattr(SubscribeChain, "_rlock", _TimedOutLock())
    subscribe_oper = Mock()
    monkeypatch.setattr(subscribe_module, "SubscribeOper", subscribe_oper)
    progress = Mock()

    chain = object.__new__(SubscribeChain)
    chain.search(state="N", progress_callback=progress)

    subscribe_oper.assert_not_called()
    progress.assert_called_once_with(
        value=100,
        text="订阅搜索锁等待超时，已跳过本轮",
    )


def test_subscribe_match_aborts_when_lock_times_out(monkeypatch) -> None:
    """订阅匹配锁超时后必须中止，不能绕过防重复下载边界。"""
    monkeypatch.setattr(SubscribeChain, "_rlock", _TimedOutLock())
    progress = Mock()

    chain = object.__new__(SubscribeChain)
    chain.match({"example.org": []}, progress_callback=progress)

    progress.assert_any_call(value=100, text="订阅匹配锁等待超时，已跳过本轮")
