#!/usr/bin/env python3
"""utils/delegate_controller.py + delegate_runner.py 테스트 스위트 (TDD — RED → GREEN)"""

from __future__ import annotations

import sys
import threading
import time
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from utils.delegate_controller import (
    BLOCKED_TOOLS,
    MAX_CONCURRENT,
    MAX_DEPTH,
    DelegateController,
    SubAgentResult,
    SubAgentTask,
)
from utils.delegate_runner import run_subagent

# ---------------------------------------------------------------------------
# 픽스처
# ---------------------------------------------------------------------------


@pytest.fixture
def ctrl():
    """기본 깊이(0) DelegateController."""
    return DelegateController(parent_depth=0)


# ---------------------------------------------------------------------------
# 1. can_delegate — 깊이 제한
# ---------------------------------------------------------------------------


class TestCanDelegateDepth:
    """깊이 제한 검증"""

    def test_depth_zero_allowed(self, ctrl):
        """깊이 0은 위임 가능."""
        ok, reason = ctrl.can_delegate()
        assert ok is True
        assert reason == "OK"

    def test_depth_one_allowed(self):
        """깊이 1도 위임 가능 (MAX_DEPTH=2)."""
        c = DelegateController(parent_depth=1)
        ok, _ = c.can_delegate()
        assert ok is True

    def test_depth_at_max_denied(self):
        """깊이가 MAX_DEPTH 이상이면 거부."""
        c = DelegateController(parent_depth=MAX_DEPTH)
        ok, reason = c.can_delegate()
        assert ok is False
        assert str(MAX_DEPTH) in reason

    def test_depth_exceeds_max_denied(self):
        """MAX_DEPTH+1 깊이는 거부."""
        c = DelegateController(parent_depth=MAX_DEPTH + 1)
        ok, _ = c.can_delegate()
        assert ok is False

    def test_reason_contains_current_depth(self):
        """거부 이유에 현재 깊이가 포함된다."""
        depth = MAX_DEPTH + 1
        c = DelegateController(parent_depth=depth)
        _, reason = c.can_delegate()
        assert str(depth) in reason


# ---------------------------------------------------------------------------
# 2. can_delegate — 동시 실행 제한
# ---------------------------------------------------------------------------


class TestCanDelegateConcurrency:
    """동시 자식 수 제한 검증"""

    def test_concurrent_limit_respected(self, ctrl):
        """_active_children이 MAX_CONCURRENT이면 거부."""
        with ctrl._children_lock:
            ctrl._active_children.extend(["dummy"] * MAX_CONCURRENT)
        ok, reason = ctrl.can_delegate()
        assert ok is False
        assert str(MAX_CONCURRENT) in reason

    def test_concurrent_below_limit_allowed(self, ctrl):
        """_active_children이 MAX_CONCURRENT 미만이면 허용."""
        with ctrl._children_lock:
            ctrl._active_children.extend(["dummy"] * (MAX_CONCURRENT - 1))
        ok, _ = ctrl.can_delegate()
        assert ok is True


# ---------------------------------------------------------------------------
# 3. filter_tools
# ---------------------------------------------------------------------------


class TestFilterTools:
    """BLOCKED_TOOLS 필터링"""

    def test_blocked_tools_removed(self, ctrl):
        """BLOCKED_TOOLS 항목이 제거된다."""
        tools = list(BLOCKED_TOOLS) + ["read_file", "bash"]
        result = ctrl.filter_tools(tools)
        for blocked in BLOCKED_TOOLS:
            assert blocked not in result

    def test_allowed_tools_kept(self, ctrl):
        """허용 도구는 보존된다."""
        tools = ["read_file", "bash", "write_file"]
        result = ctrl.filter_tools(tools)
        assert result == tools

    def test_none_toolsets_returns_empty(self, ctrl):
        """toolsets=None이면 빈 리스트 반환."""
        assert ctrl.filter_tools(None) == []

    def test_empty_toolsets_returns_empty(self, ctrl):
        """빈 리스트 입력 시 빈 리스트 반환."""
        assert ctrl.filter_tools([]) == []

    def test_only_blocked_returns_empty(self, ctrl):
        """모두 차단 도구면 빈 리스트."""
        result = ctrl.filter_tools(list(BLOCKED_TOOLS))
        assert result == []


# ---------------------------------------------------------------------------
# 4. run — 배치 실행
# ---------------------------------------------------------------------------


class TestRunBatch:
    """배치 서브에이전트 실행"""

    def test_empty_tasks_returns_empty(self, ctrl):
        """빈 태스크 목록은 빈 결과 반환."""
        results = ctrl.run([])
        assert results == []

    def test_single_task_returns_one_result(self, ctrl):
        """단일 태스크는 결과 1개 반환."""
        tasks = [SubAgentTask(goal="test goal")]
        results = ctrl.run(tasks)
        assert len(results) == 1

    def test_results_count_matches_tasks(self, ctrl):
        """결과 수가 태스크 수와 일치한다."""
        tasks = [SubAgentTask(goal=f"task {i}") for i in range(3)]
        results = ctrl.run(tasks)
        assert len(results) == 3

    def test_task_index_assigned_correctly(self, ctrl):
        """각 결과의 task_index가 올바르게 할당된다."""
        tasks = [SubAgentTask(goal=f"task {i}") for i in range(3)]
        results = ctrl.run(tasks)
        indices = sorted(r.task_index for r in results)
        assert indices == [0, 1, 2]

    def test_result_status_is_valid(self, ctrl):
        """결과의 status가 유효한 값이다."""
        valid_statuses = {"completed", "failed", "error", "interrupted"}
        tasks = [SubAgentTask(goal="hello")]
        results = ctrl.run(tasks)
        for r in results:
            assert r.status in valid_statuses

    def test_result_is_subagent_result_type(self, ctrl):
        """반환 타입이 SubAgentResult이다."""
        tasks = [SubAgentTask(goal="test")]
        results = ctrl.run(tasks)
        assert isinstance(results[0], SubAgentResult)

    def test_duration_is_non_negative(self, ctrl):
        """duration_seconds가 0 이상이다."""
        tasks = [SubAgentTask(goal="test")]
        results = ctrl.run(tasks)
        assert results[0].duration_seconds >= 0.0


# ---------------------------------------------------------------------------
# 5. interrupt_children
# ---------------------------------------------------------------------------


class TestInterruptChildren:
    """인터럽트 전파"""

    def test_interrupt_sets_event(self, ctrl):
        """interrupt_children() 호출 후 _interrupted가 set 된다."""
        ctrl.interrupt_children()
        assert ctrl._interrupted.is_set()

    def test_interrupted_run_status(self, ctrl):
        """인터럽트 후 run()이 'interrupted' 상태를 포함할 수 있다."""
        ctrl.interrupt_children()
        tasks = [SubAgentTask(goal="test")]
        results = ctrl.run(tasks)
        # 인터럽트 상태이면 interrupted 또는 error 허용
        assert results[0].status in {"interrupted", "error", "completed", "failed"}


# ---------------------------------------------------------------------------
# 6. run_subagent (delegate_runner)
# ---------------------------------------------------------------------------


class TestRunSubagent:
    """run_subagent 래퍼 동작"""

    def test_returns_subagent_result(self):
        """SubAgentResult 타입을 반환한다."""
        task = SubAgentTask(goal="simple task")
        event = threading.Event()
        result = run_subagent(task, depth=0, interrupted=event, task_index=0)
        assert isinstance(result, SubAgentResult)

    def test_task_index_preserved(self):
        """task_index가 결과에 보존된다."""
        task = SubAgentTask(goal="task")
        event = threading.Event()
        result = run_subagent(task, depth=0, interrupted=event, task_index=7)
        assert result.task_index == 7

    def test_interrupted_event_returns_interrupted(self):
        """인터럽트 이벤트가 set된 경우 status='interrupted'."""
        task = SubAgentTask(goal="task")
        event = threading.Event()
        event.set()
        result = run_subagent(task, depth=0, interrupted=event, task_index=0)
        assert result.status == "interrupted"

    def test_duration_positive_on_normal_run(self):
        """정상 실행 시 duration_seconds >= 0."""
        task = SubAgentTask(goal="task")
        event = threading.Event()
        result = run_subagent(task, depth=0, interrupted=event, task_index=0)
        assert result.duration_seconds >= 0.0
