#!/usr/bin/env python3
"""bot-status-watchdog.py 테스트

테스트 항목:
1. parse_since_time() - ISO 8601 파싱
2. find_bot_process() - 프로세스 검색
3. should_transition_to_idle() - idle 전환 조건
4. check_and_recover_stuck_bots() - stuck 봇 복구
5. save_bot_activity() - 원자적 쓰기
"""

import importlib.util
import json
import os
import sys
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# 스크립트를 모듈로 로드
_WORKSPACE = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
SCRIPT_PATH = _WORKSPACE / "scripts" / "bot-status-watchdog.py"
spec = importlib.util.spec_from_file_location("bot_status_watchdog", SCRIPT_PATH)
bot_status_watchdog = importlib.util.module_from_spec(spec)
sys.modules["bot_status_watchdog"] = bot_status_watchdog
spec.loader.exec_module(bot_status_watchdog)


class TestParseSinceTime:
    """parse_since_time() 테스트"""

    def test_parse_utc_format(self):
        """UTC 형식 파싱 (2026-03-17T06:54:35Z)"""
        result = bot_status_watchdog.parse_since_time("2026-03-17T06:54:35Z")
        assert result is not None
        assert result.year == 2026
        assert result.month == 3
        assert result.day == 17
        assert result.hour == 6
        assert result.minute == 54
        assert result.second == 35
        assert result.tzinfo == timezone.utc

    def test_parse_kst_format(self):
        """KST 형식 파싱 (2026-03-17T15:54:35+09:00)"""
        result = bot_status_watchdog.parse_since_time("2026-03-17T15:54:35+09:00")
        assert result is not None
        assert result.year == 2026
        assert result.month == 3
        assert result.day == 17

    def test_parse_invalid_format(self):
        """잘못된 형식 파싱 → None 반환"""
        assert bot_status_watchdog.parse_since_time("invalid") is None
        assert bot_status_watchdog.parse_since_time("") is None
        assert bot_status_watchdog.parse_since_time(None) is None


class TestFindBotProcess:
    """find_bot_process() 테스트"""

    def test_find_process_pattern_mapping(self):
        """봇 이름 → workspace 패턴 매핑 확인"""
        # 패턴이 올바르게 매핑되어 있는지 확인
        patterns = bot_status_watchdog.BOT_WORKSPACE_PATTERNS
        assert "anu" in patterns
        assert "dev1" in patterns
        assert "dev2" in patterns
        assert "dev3" in patterns

    @patch("subprocess.run")
    def test_find_process_returns_pids(self, mock_run):
        """프로세스 발견 시 PID 목록 반환"""
        mock_run.return_value = MagicMock(returncode=0, stdout="12345\n67890\n", stderr="")

        pids = bot_status_watchdog.find_bot_process("dev1")
        assert 12345 in pids
        assert 67890 in pids

    @patch("subprocess.run")
    def test_find_process_no_process(self, mock_run):
        """프로세스 없으면 빈 리스트 반환"""
        mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="")

        pids = bot_status_watchdog.find_bot_process("dev1")
        assert pids == []

    def test_find_process_unknown_bot(self):
        """알 수 없는 봇 → 빈 리스트"""
        pids = bot_status_watchdog.find_bot_process("unknown")
        assert pids == []


class TestShouldTransitionToIdle:
    """should_transition_to_idle() 테스트"""

    def test_not_timeout_yet(self):
        """30분 미만 → 전환 안 함"""
        since_time = datetime.now(timezone.utc) - timedelta(minutes=20)

        with patch.object(bot_status_watchdog, "find_bot_process", return_value=[]):
            should_idle, reason = bot_status_watchdog.should_transition_to_idle("dev1", since_time, 20.0)

        assert should_idle is False
        assert reason == "not_timeout"

    @patch.object(bot_status_watchdog, "find_bot_process", return_value=[12345, 67890])
    def test_process_still_running(self, mock_find):
        """프로세스 살아있음 → 전환 안 함"""
        since_time = datetime.now(timezone.utc) - timedelta(minutes=35)

        should_idle, reason = bot_status_watchdog.should_transition_to_idle("dev1", since_time, 35.0)

        assert should_idle is False
        assert "still_running" in reason
        assert "12345" in reason

    @patch.object(bot_status_watchdog, "find_bot_process", return_value=[])
    @patch.object(bot_status_watchdog, "find_recent_done_file")
    def test_done_file_exists(self, mock_done, mock_find):
        """.done 파일 있음 → 전환"""
        since_time = datetime.now(timezone.utc) - timedelta(minutes=35)
        mock_done.return_value = _WORKSPACE / "memory" / "events" / "task-123.1.dev1.done"

        should_idle, reason = bot_status_watchdog.should_transition_to_idle("dev1", since_time, 35.0)

        assert should_idle is True
        assert "completed" in reason
        assert ".done" in reason

    @patch.object(bot_status_watchdog, "find_bot_process", return_value=[])
    @patch.object(bot_status_watchdog, "find_recent_done_file", return_value=None)
    @patch.object(bot_status_watchdog, "find_recent_report")
    def test_report_exists(self, mock_report, mock_done, mock_find):
        """보고서 파일 있음 → 전환"""
        since_time = datetime.now(timezone.utc) - timedelta(minutes=35)
        mock_report.return_value = _WORKSPACE / "memory" / "reports" / "task-123.1.md"

        should_idle, reason = bot_status_watchdog.should_transition_to_idle("dev1", since_time, 35.0)

        assert should_idle is True
        assert "completed" in reason
        assert "보고서" in reason

    @patch.object(bot_status_watchdog, "find_bot_process", return_value=[])
    @patch.object(bot_status_watchdog, "find_recent_done_file", return_value=None)
    @patch.object(bot_status_watchdog, "find_recent_report", return_value=None)
    def test_timeout_no_process(self, mock_report, mock_done, mock_find):
        """프로세스 없음 + 30분 초과 → 전환"""
        since_time = datetime.now(timezone.utc) - timedelta(minutes=35)

        should_idle, reason = bot_status_watchdog.should_transition_to_idle("dev1", since_time, 35.0)

        assert should_idle is True
        assert "timeout" in reason


class TestCheckAndRecoverStuckBots:
    """check_and_recover_stuck_bots() 테스트"""

    @pytest.fixture
    def temp_bot_activity(self, tmp_path):
        """임시 bot-activity.json 생성"""
        # 40분 전 시간 생성
        old_time = datetime.now(timezone.utc) - timedelta(minutes=40)
        old_time_str = old_time.strftime("%Y-%m-%dT%H:%M:%SZ")

        # 10분 전 시간 생성 (타임아웃 미만)
        recent_time = datetime.now(timezone.utc) - timedelta(minutes=10)
        recent_time_str = recent_time.strftime("%Y-%m-%dT%H:%M:%SZ")

        data = {
            "bots": {
                "dev1": {"status": "processing", "since": old_time_str},  # 40분 전 → stuck
                "dev2": {"status": "processing", "since": recent_time_str},  # 10분 전 → OK
                "dev3": {"status": "idle", "since": recent_time_str},  # idle → 무시
            }
        }

        bot_activity_file = tmp_path / "bot-activity.json"
        bot_activity_file.write_text(json.dumps(data, indent=2), encoding="utf-8")

        return tmp_path, data

    def test_recover_stuck_bots(self, temp_bot_activity, monkeypatch):
        """stuck 봇 복구 테스트"""
        tmp_path, data = temp_bot_activity

        # 경로 패치
        monkeypatch.setattr(bot_status_watchdog, "BOT_ACTIVITY_FILE", tmp_path / "bot-activity.json")
        monkeypatch.setattr(bot_status_watchdog, "EVENTS_DIR", tmp_path / "events")
        monkeypatch.setattr(bot_status_watchdog, "REPORTS_DIR", tmp_path / "reports")
        monkeypatch.setattr(bot_status_watchdog, "WATCHDOG_LOG", tmp_path / "bot-watchdog.log")

        # 프로세스 없음 + 파일 없음 → idle 전환
        with patch.object(bot_status_watchdog, "find_bot_process", return_value=[]):
            with patch.object(bot_status_watchdog, "find_recent_done_file", return_value=None):
                with patch.object(bot_status_watchdog, "find_recent_report", return_value=None):
                    recovered = bot_status_watchdog.check_and_recover_stuck_bots()

        # dev1만 복구되어야 함 (40분 전, 프로세스 없음)
        assert recovered == 1

        # 봇 상태 다시 로드해서 확인
        updated_data = json.loads((tmp_path / "bot-activity.json").read_text(encoding="utf-8"))
        assert updated_data["bots"]["dev1"]["status"] == "idle"
        assert updated_data["bots"]["dev2"]["status"] == "processing"

    def test_no_stuck_bots(self, tmp_path, monkeypatch):
        """stuck 봇이 없는 경우"""
        monkeypatch.setattr(bot_status_watchdog, "BOT_ACTIVITY_FILE", tmp_path / "bot-activity.json")
        monkeypatch.setattr(bot_status_watchdog, "EVENTS_DIR", tmp_path / "events")
        monkeypatch.setattr(bot_status_watchdog, "REPORTS_DIR", tmp_path / "reports")
        monkeypatch.setattr(bot_status_watchdog, "WATCHDOG_LOG", tmp_path / "bot-watchdog.log")

        recent_time = datetime.now(timezone.utc) - timedelta(minutes=10)
        recent_time_str = recent_time.strftime("%Y-%m-%dT%H:%M:%SZ")

        data = {
            "bots": {
                "dev1": {"status": "processing", "since": recent_time_str},
                "dev2": {"status": "idle", "since": recent_time_str},
            }
        }

        (tmp_path / "bot-activity.json").write_text(json.dumps(data, indent=2), encoding="utf-8")

        with patch.object(bot_status_watchdog, "find_bot_process", return_value=[]):
            recovered = bot_status_watchdog.check_and_recover_stuck_bots()

        assert recovered == 0

    def test_skip_if_process_running(self, tmp_path, monkeypatch):
        """프로세스 살아있으면 idle 전환 안 함"""
        monkeypatch.setattr(bot_status_watchdog, "BOT_ACTIVITY_FILE", tmp_path / "bot-activity.json")
        monkeypatch.setattr(bot_status_watchdog, "EVENTS_DIR", tmp_path / "events")
        monkeypatch.setattr(bot_status_watchdog, "REPORTS_DIR", tmp_path / "reports")
        monkeypatch.setattr(bot_status_watchdog, "WATCHDOG_LOG", tmp_path / "bot-watchdog.log")

        # 40분 전 시간
        old_time = datetime.now(timezone.utc) - timedelta(minutes=40)
        old_time_str = old_time.strftime("%Y-%m-%dT%H:%M:%SZ")

        data = {"bots": {"dev1": {"status": "processing", "since": old_time_str}}}

        (tmp_path / "bot-activity.json").write_text(json.dumps(data, indent=2), encoding="utf-8")

        # 프로세스 살아있음
        with patch.object(bot_status_watchdog, "find_bot_process", return_value=[12345]):
            recovered = bot_status_watchdog.check_and_recover_stuck_bots()

        # idle 전환 안 됨
        assert recovered == 0

        updated_data = json.loads((tmp_path / "bot-activity.json").read_text(encoding="utf-8"))
        assert updated_data["bots"]["dev1"]["status"] == "processing"


class TestSaveBotActivity:
    """save_bot_activity() 테스트"""

    def test_atomic_write(self, tmp_path, monkeypatch):
        """원자적 쓰기 테스트"""
        monkeypatch.setattr(bot_status_watchdog, "BOT_ACTIVITY_FILE", tmp_path / "bot-activity.json")

        data = {"bots": {"dev1": {"status": "idle", "since": "2026-03-17T07:00:00Z"}}}

        result = bot_status_watchdog.save_bot_activity(data)
        assert result is True

        # 파일이 생성되었는지 확인
        bot_activity_file = tmp_path / "bot-activity.json"
        assert bot_activity_file.exists()

        # 내용 확인
        loaded = json.loads(bot_activity_file.read_text(encoding="utf-8"))
        assert loaded == data


class TestWatchdogOnce:
    """워치독 1회 실행 테스트"""

    def test_run_once(self, tmp_path, monkeypatch):
        """1회 실행 테스트"""
        # 경로 패치
        monkeypatch.setattr(bot_status_watchdog, "BOT_ACTIVITY_FILE", tmp_path / "bot-activity.json")
        monkeypatch.setattr(bot_status_watchdog, "EVENTS_DIR", tmp_path / "events")
        monkeypatch.setattr(bot_status_watchdog, "REPORTS_DIR", tmp_path / "reports")
        monkeypatch.setattr(bot_status_watchdog, "WATCHDOG_LOG", tmp_path / "bot-watchdog.log")

        # 40분 전 stuck 봇 생성
        old_time = datetime.now(timezone.utc) - timedelta(minutes=40)
        old_time_str = old_time.strftime("%Y-%m-%dT%H:%M:%SZ")

        data = {"bots": {"dev1": {"status": "processing", "since": old_time_str}}}

        (tmp_path / "bot-activity.json").write_text(json.dumps(data, indent=2), encoding="utf-8")

        # 1회 실행
        with patch.object(bot_status_watchdog, "find_bot_process", return_value=[]):
            with patch.object(bot_status_watchdog, "find_recent_done_file", return_value=None):
                with patch.object(bot_status_watchdog, "find_recent_report", return_value=None):
                    bot_status_watchdog.run_once()

        # 로그 파일 생성 확인
        log_file = tmp_path / "bot-watchdog.log"
        assert log_file.exists()

        # 봇 상태가 idle로 전환되었는지 확인
        updated_data = json.loads((tmp_path / "bot-activity.json").read_text(encoding="utf-8"))
        assert updated_data["bots"]["dev1"]["status"] == "idle"


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