# -*- coding: utf-8 -*-
"""AbortController 层级化中断控制器测试"""
import threading
import time
import pytest

from abyss.concurrency import (
    AbortReason,
    AbortError,
    AbortSignal,
    AbortController,
)


class TestAbortReason:
    """AbortReason 枚举测试"""

    def test_enum_values(self):
        assert AbortReason.USER_CANCEL.value == "user_cancel"
        assert AbortReason.TIMEOUT.value == "timeout"
        assert AbortReason.PARENT_ABORTED.value == "parent_aborted"
        assert AbortReason.BUDGET_EXCEEDED.value == "budget_exceeded"

    def test_all_distinct(self):
        values = [r.value for r in AbortReason]
        assert len(set(values)) == len(values)


class TestAbortError:
    """AbortError 异常测试"""

    def test_raise_with_reason(self):
        with pytest.raises(AbortError) as exc_info:
            raise AbortError(AbortReason.USER_CANCEL)
        assert exc_info.value.reason == AbortReason.USER_CANCEL

    def test_raise_with_timeout_reason(self):
        with pytest.raises(AbortError) as exc_info:
            raise AbortError(AbortReason.TIMEOUT)
        assert exc_info.value.reason == AbortReason.TIMEOUT

    def test_message_contains_reason_value(self):
        err = AbortError(AbortReason.PARENT_ABORTED)
        assert "parent_aborted" in str(err)


class TestAbortControllerBasic:
    """AbortController 基础功能测试"""

    def test_initial_state_not_aborted(self):
        ctrl = AbortController()
        assert ctrl.is_aborted() is False
        assert ctrl.reason is None

    def test_abort_sets_state(self):
        ctrl = AbortController()
        ctrl.abort(AbortReason.USER_CANCEL)
        assert ctrl.is_aborted() is True
        assert ctrl.reason == AbortReason.USER_CANCEL

    def test_abort_default_reason(self):
        ctrl = AbortController()
        ctrl.abort()
        assert ctrl.is_aborted() is True
        assert ctrl.reason == AbortReason.USER_CANCEL

    def test_abort_is_idempotent(self):
        """CAS 保证：多次 abort 只触发一次回调"""
        ctrl = AbortController()
        call_count = [0]
        ctrl.on_abort(lambda r: call_count.__setitem__(0, call_count[0] + 1))

        ctrl.abort(AbortReason.USER_CANCEL)
        ctrl.abort(AbortReason.TIMEOUT)
        ctrl.abort(AbortReason.BUDGET_EXCEEDED)

        assert ctrl.is_aborted() is True
        assert ctrl.reason == AbortReason.USER_CANCEL  # 保留首次 reason
        assert call_count[0] == 1  # 监听器只触发一次

    def test_throw_if_aborted_when_not_aborted(self):
        ctrl = AbortController()
        # 不应抛异常
        ctrl.throw_if_aborted()

    def test_throw_if_aborted_when_aborted(self):
        ctrl = AbortController()
        ctrl.abort(AbortReason.TIMEOUT)
        with pytest.raises(AbortError) as exc_info:
            ctrl.throw_if_aborted()
        assert exc_info.value.reason == AbortReason.TIMEOUT

    def test_signal_property_returns_signal(self):
        ctrl = AbortController()
        signal = ctrl.signal
        assert isinstance(signal, AbortSignal)
        assert signal.is_aborted() is False

    def test_signal_reflects_parent_state(self):
        ctrl = AbortController()
        signal = ctrl.signal
        ctrl.abort(AbortReason.USER_CANCEL)
        assert signal.is_aborted() is True

    def test_signal_throw_if_aborted(self):
        ctrl = AbortController()
        signal = ctrl.signal
        ctrl.abort(AbortReason.BUDGET_EXCEEDED)
        with pytest.raises(AbortError) as exc_info:
            signal.throw_if_aborted()
        assert exc_info.value.reason == AbortReason.BUDGET_EXCEEDED


class TestAbortControllerListeners:
    """on_abort 监听器测试"""

    def test_single_listener_called(self):
        ctrl = AbortController()
        received = []
        ctrl.on_abort(lambda r: received.append(r))
        ctrl.abort(AbortReason.USER_CANCEL)
        assert received == [AbortReason.USER_CANCEL]

    def test_multiple_listeners_called_in_order(self):
        ctrl = AbortController()
        order = []
        ctrl.on_abort(lambda r: order.append(f"first:{r.value}"))
        ctrl.on_abort(lambda r: order.append(f"second:{r.value}"))
        ctrl.abort(AbortReason.TIMEOUT)
        assert order == ["first:timeout", "second:timeout"]

    def test_listener_exception_does_not_block_others(self):
        """单个监听器异常不应阻塞其他监听器"""
        ctrl = AbortController()
        calls = []
        ctrl.on_abort(lambda r: (_ for _ in ()).throw(ValueError("boom")))
        ctrl.on_abort(lambda r: calls.append(r))
        ctrl.abort(AbortReason.USER_CANCEL)
        assert calls == [AbortReason.USER_CANCEL]

    def test_listener_registered_after_abort_not_called(self):
        """abort 之后再注册的监听器不应被调用"""
        ctrl = AbortController()
        ctrl.abort(AbortReason.USER_CANCEL)
        calls = []
        ctrl.on_abort(lambda r: calls.append(r))
        assert calls == []

    def test_on_abort_returns_handle_for_removal(self):
        """on_abort 返回可调用 handle 用于移除监听器"""
        ctrl = AbortController()
        calls = []
        handle = ctrl.on_abort(lambda r: calls.append(r))
        handle.remove()
        ctrl.abort(AbortReason.USER_CANCEL)
        assert calls == []


class TestAbortControllerHierarchy:
    """父子层级中断传播测试"""

    def test_child_inherits_parent_abort(self):
        parent = AbortController()
        child = AbortController(parent=parent)
        parent.abort(AbortReason.USER_CANCEL)
        assert child.is_aborted() is True
        assert child.reason == AbortReason.PARENT_ABORTED

    def test_child_abort_does_not_propagate_upward(self):
        """子 abort 不应影响父"""
        parent = AbortController()
        child = AbortController(parent=parent)
        child.abort(AbortReason.TIMEOUT)
        assert child.is_aborted() is True
        assert parent.is_aborted() is False

    def test_grandchild_inherits_grandparent_abort(self):
        """祖父 abort 时孙辈也应级联中断"""
        grandparent = AbortController()
        parent = AbortController(parent=grandparent)
        child = AbortController(parent=parent)
        grandparent.abort(AbortReason.BUDGET_EXCEEDED)
        assert parent.is_aborted() is True
        assert parent.reason == AbortReason.PARENT_ABORTED
        assert child.is_aborted() is True
        assert child.reason == AbortReason.PARENT_ABORTED

    def test_multiple_children_all_aborted(self):
        parent = AbortController()
        child1 = AbortController(parent=parent)
        child2 = AbortController(parent=parent)
        child3 = AbortController(parent=parent)
        parent.abort(AbortReason.USER_CANCEL)
        assert child1.is_aborted()
        assert child2.is_aborted()
        assert child3.is_aborted()

    def test_child_listener_triggered_on_parent_abort(self):
        parent = AbortController()
        child = AbortController(parent=parent)
        child_calls = []
        child.on_abort(lambda r: child_calls.append(r))
        parent.abort(AbortReason.USER_CANCEL)
        assert child_calls == [AbortReason.PARENT_ABORTED]

    def test_cascade_after_manual_child_abort(self):
        """子已手动 abort 后，父 abort 不应再次触发子的监听器"""
        parent = AbortController()
        child = AbortController(parent=parent)
        child_calls = []
        child.on_abort(lambda r: child_calls.append(r))
        child.abort(AbortReason.TIMEOUT)
        assert child_calls == [AbortReason.TIMEOUT]
        parent.abort(AbortReason.USER_CANCEL)
        # 子监听器不应被再次触发（CAS 保证幂等）
        assert child_calls == [AbortReason.TIMEOUT]
        assert child.reason == AbortReason.TIMEOUT  # 保留首次 reason

    def test_deep_hierarchy_performance(self):
        """深层级联应在合理时间内完成（无递归栈溢出）"""
        root = AbortController()
        current = root
        for _ in range(100):
            current = AbortController(parent=current)
        leaf = current

        start = time.time()
        root.abort(AbortReason.USER_CANCEL)
        elapsed = time.time() - start
        assert leaf.is_aborted() is True
        assert elapsed < 1.0  # 100 层级联应在 1s 内完成


class TestAbortControllerConcurrency:
    """多线程并发测试"""

    def test_concurrent_abort_safe(self):
        """多线程同时 abort 应安全（CAS 保证）"""
        ctrl = AbortController()
        call_count = [0]
        ctrl.on_abort(lambda r: call_count.__setitem__(0, call_count[0] + 1))

        def aborter():
            ctrl.abort(AbortReason.USER_CANCEL)

        threads = [threading.Thread(target=aborter) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert ctrl.is_aborted() is True
        assert call_count[0] == 1  # 仅一个线程成功触发回调

    def test_concurrent_abort_with_listener(self):
        ctrl = AbortController()
        received = []
        received_lock = threading.Lock()

        def listener(reason):
            with received_lock:
                received.append(reason)

        ctrl.on_abort(listener)

        threads = [
            threading.Thread(target=ctrl.abort, args=(AbortReason.USER_CANCEL,))
            for _ in range(5)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(received) == 1
        assert received[0] == AbortReason.USER_CANCEL

    def test_signal_checked_from_another_thread(self):
        """子线程能感知主线程的中断信号"""
        ctrl = AbortController()
        signal = ctrl.signal
        observed = []

        def worker():
            for _ in range(100):
                if signal.is_aborted():
                    observed.append(signal.reason_at_call_time if hasattr(signal, "reason_at_call_time") else ctrl.reason)
                    return
                time.sleep(0.01)

        t = threading.Thread(target=worker)
        t.start()
        time.sleep(0.05)
        ctrl.abort(AbortReason.USER_CANCEL)
        t.join(timeout=1.0)
        assert not t.is_alive()
        assert len(observed) == 1


class TestAbortSignalReadOnly:
    """AbortSignal 只读视图测试"""

    def test_signal_cannot_abort(self):
        """AbortSignal 不应暴露 abort 方法"""
        ctrl = AbortController()
        signal = ctrl.signal
        assert not hasattr(signal, "abort")

    def test_signal_shares_state_with_controller(self):
        ctrl = AbortController()
        signal = ctrl.signal
        ctrl.abort(AbortReason.USER_CANCEL)
        assert signal.is_aborted() is True

    def test_signal_throw_if_aborted_independent(self):
        ctrl = AbortController()
        signal1 = ctrl.signal
        signal2 = ctrl.signal
        ctrl.abort(AbortReason.TIMEOUT)
        with pytest.raises(AbortError):
            signal1.throw_if_aborted()
        with pytest.raises(AbortError):
            signal2.throw_if_aborted()


class TestAbortControllerUseCases:
    """实际使用场景测试"""

    def test_use_case_interrupt_long_tool(self):
        """场景：长任务执行前检查中断"""
        ctrl = AbortController()
        results = []

        def fake_long_task():
            for i in range(10):
                ctrl.throw_if_aborted()
                results.append(i)
                time.sleep(0.001)

        t = threading.Thread(target=fake_long_task)
        t.start()
        time.sleep(0.005)
        ctrl.abort(AbortReason.USER_CANCEL)
        t.join(timeout=1.0)

        assert not t.is_alive()
        assert len(results) < 10  # 应在中间被中断
        assert ctrl.is_aborted() is True

    def test_use_case_subagent_inherits_main_interrupt(self):
        """场景：子 Agent 继承主流程中断"""
        main_ctrl = AbortController()
        subagent_ctrl = AbortController(parent=main_ctrl)

        # 主流程未中断时，子 Agent 正常执行
        assert subagent_ctrl.is_aborted() is False

        # 用户按 Esc，主流程中断
        main_ctrl.abort(AbortReason.USER_CANCEL)

        # 子 Agent 立即感知
        assert subagent_ctrl.is_aborted() is True
        with pytest.raises(AbortError):
            subagent_ctrl.throw_if_aborted()

    def test_use_case_independent_subagent_abort(self):
        """场景：子 Agent 独立中断不影响主流程"""
        main_ctrl = AbortController()
        subagent_ctrl = AbortController(parent=main_ctrl)

        # 子 Agent 超时
        subagent_ctrl.abort(AbortReason.TIMEOUT)
        assert subagent_ctrl.is_aborted() is True
        # 主流程继续
        assert main_ctrl.is_aborted() is False

    def test_use_case_budget_exceeded_terminates_all(self):
        """场景：预算超限时级联终止所有子任务"""
        root = AbortController()
        worker1 = AbortController(parent=root)
        worker2 = AbortController(parent=root)
        worker1_child = AbortController(parent=worker1)

        root.abort(AbortReason.BUDGET_EXCEEDED)

        assert worker1.reason == AbortReason.PARENT_ABORTED
        assert worker2.reason == AbortReason.PARENT_ABORTED
        assert worker1_child.reason == AbortReason.PARENT_ABORTED
