#!/usr/bin/env python3
"""utils/bot_status.py — BotStatusManager 단위 테스트 스위트

작성자: 하누만 (dev4-team)
대상:   BotStatusManager 클래스 전 메서드
전략:   tmp_path로 격리된 파일시스템, 실제 constants.json 데이터 재현
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

import pytest

# workspace 루트를 sys.path에 추가
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from utils.bot_status import BotStatusManager


# ---------------------------------------------------------------------------
# 공통 상수
# ---------------------------------------------------------------------------

# constants.json 에서 가져온 실제 매핑
TEAM_TO_BOT: dict[str, str] = {
    "dev1-team": "bot-b",
    "dev2-team": "bot-c",
    "dev3-team": "bot-d",
    "dev4-team": "bot-e",
    "dev5-team": "bot-f",
    "dev6-team": "bot-g",
    "dev7-team": "bot-h",
    "dev8-team": "bot-i",
}

TEAMS: dict[str, str] = {
    "dev1-team": "dev1",
    "dev2-team": "dev2",
    "dev3-team": "dev3",
    "dev4-team": "dev4",
    "dev5-team": "dev5",
    "dev6-team": "dev6",
    "dev7-team": "dev7",
    "dev8-team": "dev8",
}

LOGICAL_TEAMS: dict = {
    "design": {
        "keywords": ["배너", "이미지", "디자인", "시안", "광고 크리에이티브", "카드뉴스 디자인", "비주얼", "포스터"],
        "anti_keywords": ["HTML 수정", "CSS 버그", "코드 수정", "렌더러 수정"],
        "description": "비주얼 창작 전문. 코드 수정은 dev팀 소관.",
    },
    "marketing": {
        "keywords": ["카피", "마케팅 전략", "광고 문구", "SEO", "콘텐츠 전략", "캠페인 기획"],
        "anti_keywords": [],
        "description": "마케팅/카피 전문.",
    },
    "content": {
        "keywords": ["블로그 작성", "콘텐츠 제작", "포스팅 작성"],
        "anti_keywords": [],
        "description": "콘텐츠 제작 전문.",
    },
}


# ---------------------------------------------------------------------------
# 헬퍼 함수
# ---------------------------------------------------------------------------


def make_constants(tmp_path: Path) -> Path:
    """tmp_path/config/constants.json 생성 후 경로 반환."""
    config_dir = tmp_path / "config"
    config_dir.mkdir(parents=True, exist_ok=True)
    constants_path = config_dir / "constants.json"
    constants_path.write_text(
        json.dumps(
            {
                "team_to_bot": TEAM_TO_BOT,
                "teams": TEAMS,
                "logical_teams": LOGICAL_TEAMS,
            },
            ensure_ascii=False,
        )
    )
    return constants_path


def make_task_timers(tmp_path: Path, tasks: dict) -> Path:
    """tmp_path/memory/task-timers.json 생성 후 경로 반환."""
    memory_dir = tmp_path / "memory"
    memory_dir.mkdir(parents=True, exist_ok=True)
    timers_path = memory_dir / "task-timers.json"
    timers_path.write_text(json.dumps({"tasks": tasks}, ensure_ascii=False))
    return timers_path


def make_manager(tmp_path: Path, tasks: dict | None = None) -> BotStatusManager:
    """constants + task-timers를 tmp_path에 준비하고 BotStatusManager 반환.

    tasks=None 이면 task-timers.json을 생성하지 않는다.
    """
    make_constants(tmp_path)
    if tasks is not None:
        make_task_timers(tmp_path, tasks)
    return BotStatusManager(workspace_root=tmp_path)


# ---------------------------------------------------------------------------
# 테스트용 작업 레코드 빌더
# ---------------------------------------------------------------------------


def running_task(
    task_id: str,
    team_id: str,
    bot: str,
    description: str = "작업 설명",
) -> dict:
    return {
        "task_id": task_id,
        "team_id": team_id,
        "description": description,
        "status": "running",
        "start_time": "2026-04-04T00:00:00+00:00",
        "end_time": None,
        "bot": bot,
    }


def completed_task(
    task_id: str,
    team_id: str,
    bot: str,
) -> dict:
    return {
        "task_id": task_id,
        "team_id": team_id,
        "description": "완료된 작업",
        "status": "completed",
        "start_time": "2026-04-04T00:00:00+00:00",
        "end_time": "2026-04-04T01:00:00+00:00",
        "bot": bot,
    }


# ===========================================================================
# Class 1: TestGetBusyBots
# ===========================================================================


class TestGetBusyBots:
    """get_busy_bots() — running 봇 조회 테스트"""

    def test_single_running_task_returns_bot(self, tmp_path: Path) -> None:
        """running 작업 1개 → 해당 봇 정보 반환."""
        mgr = make_manager(
            tmp_path,
            {"task-100.1": running_task("task-100.1", "dev1-team", "bot-b")},
        )

        result = mgr.get_busy_bots()

        assert "bot-b" in result, "bot-b는 busy 봇 목록에 있어야 한다"
        assert result["bot-b"]["task_id"] == "task-100.1", "task_id가 일치해야 한다"
        assert result["bot-b"]["team_id"] == "dev1-team", "team_id가 일치해야 한다"

    def test_logical_team_design_occupies_bot_b(self, tmp_path: Path) -> None:
        """논리적 팀(design)이 bot-b 점유 → bot-b가 busy 봇에 포함."""
        mgr = make_manager(
            tmp_path,
            {"task-200.1": running_task("task-200.1", "design", "bot-b")},
        )

        result = mgr.get_busy_bots()

        assert "bot-b" in result, "design 팀이 bot-b를 점유하면 bot-b가 반환되어야 한다"
        assert result["bot-b"]["team_id"] == "design"

    def test_completed_task_excluded(self, tmp_path: Path) -> None:
        """completed 작업은 busy 봇에서 제외."""
        mgr = make_manager(
            tmp_path,
            {"task-300.1": completed_task("task-300.1", "dev1-team", "bot-b")},
        )

        result = mgr.get_busy_bots()

        assert "bot-b" not in result, "completed 작업의 봇은 busy 목록에 없어야 한다"

    def test_exclude_task_id_removes_own_task(self, tmp_path: Path) -> None:
        """exclude_task_id 지정 시 해당 작업의 봇은 제외."""
        mgr = make_manager(
            tmp_path,
            {"task-400.1": running_task("task-400.1", "dev2-team", "bot-c")},
        )

        result = mgr.get_busy_bots(exclude_task_id="task-400.1")

        assert "bot-c" not in result, "자기 자신 task_id를 제외하면 bot-c는 없어야 한다"

    def test_missing_timers_file_returns_empty_dict(self, tmp_path: Path) -> None:
        """task-timers.json 없으면 빈 dict 반환 (파일 없음 케이스)."""
        mgr = make_manager(tmp_path, tasks=None)  # 파일 생성 안 함

        result = mgr.get_busy_bots()

        assert result == {}, "task-timers.json이 없으면 빈 dict를 반환해야 한다"

    def test_multiple_running_tasks_all_returned(self, tmp_path: Path) -> None:
        """여러 running 작업 → 관련 봇 모두 반환."""
        mgr = make_manager(
            tmp_path,
            {
                "task-500.1": running_task("task-500.1", "dev1-team", "bot-b"),
                "task-500.2": running_task("task-500.2", "dev2-team", "bot-c"),
                "task-500.3": running_task("task-500.3", "dev3-team", "bot-d"),
            },
        )

        result = mgr.get_busy_bots()

        assert "bot-b" in result, "bot-b가 포함되어야 한다"
        assert "bot-c" in result, "bot-c가 포함되어야 한다"
        assert "bot-d" in result, "bot-d가 포함되어야 한다"
        assert len(result) == 3, "총 3개의 busy 봇이 반환되어야 한다"


# ===========================================================================
# Class 2: TestGetIdleBots
# ===========================================================================


class TestGetIdleBots:
    """get_idle_bots() — 유휴 봇 목록 테스트"""

    def test_all_bots_idle_returns_eight(self, tmp_path: Path) -> None:
        """모든 봇이 유휴 상태 → 8개 반환."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.get_idle_bots()

        assert len(result) == 8, f"유휴 봇은 8개여야 한다, 실제: {len(result)}"

    def test_two_busy_returns_six_idle(self, tmp_path: Path) -> None:
        """2개 봇 busy → 유휴 봇 6개."""
        mgr = make_manager(
            tmp_path,
            {
                "task-600.1": running_task("task-600.1", "dev1-team", "bot-b"),
                "task-600.2": running_task("task-600.2", "dev2-team", "bot-c"),
            },
        )

        result = mgr.get_idle_bots()

        assert len(result) == 6, f"유휴 봇은 6개여야 한다, 실제: {len(result)}"
        assert "bot-b" not in result, "bot-b는 유휴 목록에 없어야 한다"
        assert "bot-c" not in result, "bot-c는 유휴 목록에 없어야 한다"

    def test_all_bots_busy_returns_empty_list(self, tmp_path: Path) -> None:
        """8개 봇 모두 busy → 빈 리스트."""
        tasks = {
            f"task-700.{i}": running_task(
                f"task-700.{i}", f"dev{i}-team", bot
            )
            for i, bot in enumerate(
                ["bot-b", "bot-c", "bot-d", "bot-e", "bot-f", "bot-g", "bot-h", "bot-i"],
                start=1,
            )
        }
        mgr = make_manager(tmp_path, tasks)

        result = mgr.get_idle_bots()

        assert result == [], "모든 봇이 busy이면 빈 리스트를 반환해야 한다"


# ===========================================================================
# Class 3: TestIsBotAvailable
# ===========================================================================


class TestIsBotAvailable:
    """is_bot_available() — 봇 가용 여부 테스트"""

    def test_idle_bot_is_available(self, tmp_path: Path) -> None:
        """유휴 봇 → True."""
        mgr = make_manager(tmp_path, tasks={})

        assert mgr.is_bot_available("bot-b") is True, "유휴 봇은 available이어야 한다"

    def test_busy_bot_is_not_available(self, tmp_path: Path) -> None:
        """busy 봇 → False."""
        mgr = make_manager(
            tmp_path,
            {"task-800.1": running_task("task-800.1", "dev1-team", "bot-b")},
        )

        assert mgr.is_bot_available("bot-b") is False, "running 작업 중인 봇은 available이 아니어야 한다"


# ===========================================================================
# Class 4: TestGetTeamStatus
# ===========================================================================


class TestGetTeamStatus:
    """get_team_status() — 팀 상태 문자열 테스트"""

    def test_dev_team_with_running_task_returns_working(self, tmp_path: Path) -> None:
        """dev팀이 running 작업 있음 → '작업중'."""
        mgr = make_manager(
            tmp_path,
            {"task-900.1": running_task("task-900.1", "dev1-team", "bot-b")},
        )

        status = mgr.get_team_status("dev1-team")

        assert status == "작업중", f"running 작업이 있는 팀은 '작업중'이어야 한다, 실제: {status!r}"

    def test_dev_team_bot_occupied_by_logical_returns_occupied(self, tmp_path: Path) -> None:
        """dev1-team의 봇(bot-b)이 논리적 팀(design)에 점유 → '봇점유(...)' 포함."""
        mgr = make_manager(
            tmp_path,
            {"task-1000.1": running_task("task-1000.1", "design", "bot-b")},
        )

        status = mgr.get_team_status("dev1-team")

        assert "봇점유" in status, f"봇 점유 상태는 '봇점유'를 포함해야 한다, 실제: {status!r}"

    def test_team_with_no_tasks_returns_idle(self, tmp_path: Path) -> None:
        """아무 작업 없는 팀 → '유휴'."""
        mgr = make_manager(tmp_path, tasks={})

        status = mgr.get_team_status("dev3-team")

        assert status == "유휴", f"작업 없는 팀은 '유휴'여야 한다, 실제: {status!r}"


# ===========================================================================
# Class 5: TestGetBotOccupation
# ===========================================================================


class TestGetBotOccupation:
    """get_bot_occupation() — 물리 봇 점유 탐지 테스트"""

    def test_design_occupies_bot_b_returns_dev1_info(self, tmp_path: Path) -> None:
        """design 팀이 bot-b 점유 → dev1 점유 정보 반환."""
        mgr = make_manager(
            tmp_path,
            {"task-1100.1": running_task("task-1100.1", "design", "bot-b")},
        )

        result = mgr.get_bot_occupation()

        assert "dev1" in result, "bot-b를 점유하면 dev1 엔트리가 있어야 한다"
        assert result["dev1"]["team"] == "design", "점유 팀이 design이어야 한다"
        assert result["dev1"]["bot_id"] == "bot-b", "점유 봇이 bot-b이어야 한다"
        assert result["dev1"]["task_id"] == "task-1100.1", "task_id가 일치해야 한다"

    def test_dev_team_own_task_not_in_occupation(self, tmp_path: Path) -> None:
        """dev1-team 자체 작업은 봇 점유(cross-team occupation)로 잡히지 않음."""
        mgr = make_manager(
            tmp_path,
            {"task-1200.1": running_task("task-1200.1", "dev1-team", "bot-b")},
        )

        result = mgr.get_bot_occupation()

        assert "dev1" not in result, "자기 팀 작업은 타팀 점유로 잡혀선 안 된다"

    def test_task_without_bot_field_is_ignored(self, tmp_path: Path) -> None:
        """bot 필드 없는 작업은 점유 탐지에서 무시."""
        task_no_bot = {
            "task_id": "task-1300.1",
            "team_id": "design",
            "description": "봇 미지정 작업",
            "status": "running",
            "start_time": "2026-04-04T00:00:00+00:00",
            "end_time": None,
            # "bot" 키 없음
        }
        mgr = make_manager(tmp_path, {"task-1300.1": task_no_bot})

        result = mgr.get_bot_occupation()

        assert result == {}, "bot 필드 없는 작업은 점유 결과가 비어야 한다"

    def test_completed_task_not_in_occupation(self, tmp_path: Path) -> None:
        """completed 작업은 봇 점유에 포함되지 않음."""
        mgr = make_manager(
            tmp_path,
            {"task-1400.1": completed_task("task-1400.1", "design", "bot-b")},
        )

        result = mgr.get_bot_occupation()

        assert "dev1" not in result, "완료된 작업은 점유로 잡혀선 안 된다"


# ===========================================================================
# Class 6: TestSuggestTeam
# ===========================================================================


class TestSuggestTeam:
    """suggest_team() — 키워드 기반 팀 추천 테스트"""

    def test_design_keyword_returns_design(self, tmp_path: Path) -> None:
        """디자인 키워드 → 'design' 추천."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.suggest_team("신제품 배너 디자인 제작")

        assert result == "design", f"디자인 키워드는 'design'을 추천해야 한다, 실제: {result!r}"

    def test_marketing_keyword_returns_marketing(self, tmp_path: Path) -> None:
        """마케팅 키워드 → 'marketing' 추천."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.suggest_team("신규 캠페인 기획 및 카피 작성")

        assert result == "marketing", f"마케팅 키워드는 'marketing'을 추천해야 한다, 실제: {result!r}"

    def test_no_matching_keyword_returns_none(self, tmp_path: Path) -> None:
        """매칭 키워드 없음 → None."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.suggest_team("서버 배포 파이프라인 점검")

        assert result is None, f"매칭 없으면 None이어야 한다, 실제: {result!r}"

    def test_anti_keyword_excludes_team(self, tmp_path: Path) -> None:
        """anti-keyword가 포함되면 해당 팀은 추천에서 제외."""
        mgr = make_manager(tmp_path, tasks={})

        # "배너"는 design 키워드이나 "HTML 수정"은 design anti-keyword
        result = mgr.suggest_team("배너 HTML 수정 요청")

        assert result != "design", f"anti-keyword가 있으면 design은 추천되지 않아야 한다, 실제: {result!r}"

    def test_empty_string_returns_none(self, tmp_path: Path) -> None:
        """빈 문자열 → None."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.suggest_team("")

        assert result is None, f"빈 문자열은 None이어야 한다, 실제: {result!r}"


# ===========================================================================
# Class 7: TestValidateRouting
# ===========================================================================


class TestValidateRouting:
    """validate_routing() — 라우팅 검증 경고 테스트"""

    def test_dev_team_with_design_task_returns_warning(self, tmp_path: Path) -> None:
        """dev팀에 디자인 작업 배정 → 경고 문자열 반환."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.validate_routing("dev1-team", "신제품 배너 디자인 제작")

        assert result is not None, "dev팀에 디자인 작업은 경고를 반환해야 한다"
        assert isinstance(result, str), f"경고는 문자열이어야 한다, 실제: {type(result)}"

    def test_dev_team_with_coding_task_returns_none(self, tmp_path: Path) -> None:
        """dev팀에 코딩 작업 → 경고 없음 (None)."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.validate_routing("dev1-team", "API 엔드포인트 구현 및 유닛 테스트 작성")

        assert result is None, f"dev팀 코딩 작업은 경고 없이 None이어야 한다, 실제: {result!r}"

    def test_logical_team_with_own_task_returns_none(self, tmp_path: Path) -> None:
        """논리적 팀(design)에 디자인 작업 → 경고 없음."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.validate_routing("design", "배너 디자인 시안 작성")

        assert result is None, f"design 팀의 디자인 작업은 경고가 없어야 한다, 실제: {result!r}"

    def test_override_routing_suppresses_warning(self, tmp_path: Path) -> None:
        """override_routing=True → 경고 억제 (None 반환)."""
        mgr = make_manager(tmp_path, tasks={})

        result = mgr.validate_routing("dev1-team", "신제품 배너 디자인 제작", override_routing=True)

        assert result is None, f"override_routing=True 이면 경고가 억제되어야 한다, 실제: {result!r}"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
