#!/usr/bin/env python3
"""
server.py 3가지 버그 수정 단위 테스트
테스터: 아르고스 (Argos)
대상: DataLoader 클래스
    - Bug 1: get_member_status() 30분 TTL (working 상태 since 타임스탬프 검사)
    - Bug 2: get_running_tasks_by_team() 2시간 TTL (running 태스크 start_time stale 검사)
    - Bug 3: load_member_status() JSON 자동 복구 (깨진 JSON → {"members":{}} 덮어쓰기)

실행:
    pytest /home/jay/workspace/teams/dev1/test_task_112_1.py -v
"""

import sys
import json
import os
import tempfile
from pathlib import Path
from datetime import datetime, timedelta
import unittest

# server.py 임포트를 위한 path 설정
sys.path.insert(0, '/home/jay/workspace/dashboard')
from server import DataLoader


# ──────────────────────────────────────────────────────────────
# 공통 헬퍼 함수
# ──────────────────────────────────────────────────────────────

def make_workspace(tmp_dir: Path) -> Path:
    """임시 workspace 디렉토리 구조 생성"""
    memory_dir = tmp_dir / "memory"
    memory_dir.mkdir(parents=True, exist_ok=True)
    (memory_dir / "events").mkdir(parents=True, exist_ok=True)
    (memory_dir / "logs").mkdir(parents=True, exist_ok=True)
    return tmp_dir


def make_loader(tmp_dir: Path) -> DataLoader:
    """임시 workspace를 가리키는 DataLoader 인스턴스 반환"""
    return DataLoader(tmp_dir)


def iso(dt: datetime) -> str:
    """datetime → ISO 8601 문자열 변환"""
    return dt.isoformat()


# ──────────────────────────────────────────────────────────────
# Bug 1: get_member_status() 30분 TTL 테스트
# ──────────────────────────────────────────────────────────────

class TestBug1MemberStatusTTL(unittest.TestCase):
    """
    Bug 1: member-status.json의 working 상태에서 since 타임스탬프가
    30분(1800초) 초과이면 working을 무시해야 한다.

    테스트 대상: DataLoader.get_member_status()
    """

    def setUp(self):
        """각 테스트마다 새 임시 workspace + DataLoader 준비"""
        self.tmp = tempfile.TemporaryDirectory()
        self.workspace = make_workspace(Path(self.tmp.name))
        self.loader = make_loader(self.workspace)

    def tearDown(self):
        self.tmp.cleanup()

    def _write_member_status(self, member_id: str, status: str, since: str = None):
        """memory/events/member-status.json 작성 헬퍼"""
        entry = {"status": status}
        if since is not None:
            entry["since"] = since
        data = {"members": {member_id: entry}}
        path = self.workspace / "memory" / "events" / "member-status.json"
        path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        self.loader.load_member_status()

    def _make_member(self, member_id: str) -> dict:
        """테스트용 멤버 dict 반환"""
        return {"id": member_id, "name": member_id}

    # ── 테스트 1: since가 10분 전 → working 반환 ──────────────

    def test_member_working_within_30min(self):
        """since가 10분 전이면 working이 유효하므로 'working'을 반환해야 한다"""
        member_id = "alice"
        since_dt = datetime.now() - timedelta(minutes=10)
        self._write_member_status(member_id, "working", iso(since_dt))

        member = self._make_member(member_id)
        result = self.loader.get_member_status(member, "dev1-team", {}, is_lead=False)

        self.assertEqual(
            result, "working",
            f"10분 전 since면 working이어야 함, 실제: {result}"
        )

    # ── 테스트 2: since가 40분 전 → working 아닌 상태 반환 ────

    def test_member_working_over_30min(self):
        """since가 40분 전이면 stale이므로 'working'이 아닌 상태를 반환해야 한다"""
        member_id = "bob"
        since_dt = datetime.now() - timedelta(minutes=40)
        self._write_member_status(member_id, "working", iso(since_dt))

        member = self._make_member(member_id)
        result = self.loader.get_member_status(member, "dev1-team", {}, is_lead=False)

        self.assertNotEqual(
            result, "working",
            f"40분 전 since면 working이 아니어야 함 (stale), 실제: {result}"
        )

    # ── 테스트 3: since가 정확히 30분 → working 반환 (경계값) ─

    def test_member_working_exactly_30min(self):
        """since가 30분 직전(1795초 전)이면 경계값이므로 'working'을 반환해야 한다"""
        member_id = "carol"
        # 1795초 전: 실행 시간 마진 포함, TTL 조건이 "초과"(>1800)이므로 working 유지
        since_dt = datetime.now() - timedelta(seconds=1795)
        self._write_member_status(member_id, "working", iso(since_dt))

        member = self._make_member(member_id)
        result = self.loader.get_member_status(member, "dev1-team", {}, is_lead=False)

        self.assertEqual(
            result, "working",
            f"정확히 30분(1800초) 전은 경계값으로 working이어야 함, 실제: {result}"
        )

    # ── 테스트 4: since 없음 → working 반환 (방어적) ──────────

    def test_member_working_no_since_field(self):
        """since 필드가 없으면 TTL 검사를 건너뛰고 'working'을 반환해야 한다"""
        member_id = "dave"
        # since 없이 status만 기록
        self._write_member_status(member_id, "working", since=None)

        member = self._make_member(member_id)
        result = self.loader.get_member_status(member, "dev1-team", {}, is_lead=False)

        self.assertEqual(
            result, "working",
            f"since 없으면 방어적으로 working을 반환해야 함, 실제: {result}"
        )

    # ── 테스트 5: since가 잘못된 형식 → working 반환 (방어적) ─

    def test_member_working_invalid_since(self):
        """since가 파싱 불가능한 형식이면 TTL 검사를 건너뛰고 'working'을 반환해야 한다"""
        member_id = "eve"
        # 파싱 불가능한 since 값
        self._write_member_status(member_id, "working", since="not-a-valid-timestamp")

        member = self._make_member(member_id)
        result = self.loader.get_member_status(member, "dev1-team", {}, is_lead=False)

        self.assertEqual(
            result, "working",
            f"잘못된 since 형식이면 방어적으로 working을 반환해야 함, 실제: {result}"
        )


# ──────────────────────────────────────────────────────────────
# Bug 2: get_running_tasks_by_team() 2시간 TTL 테스트
# ──────────────────────────────────────────────────────────────

class TestBug2RunningTasksTTL(unittest.TestCase):
    """
    Bug 2: task-timers.json의 running 태스크에서 start_time이
    2시간(7200초) 초과이면 stale로 판단하여 결과에서 제외해야 한다.

    테스트 대상: DataLoader.get_running_tasks_by_team()
    """

    def setUp(self):
        """각 테스트마다 새 임시 workspace + DataLoader 준비"""
        self.tmp = tempfile.TemporaryDirectory()
        self.workspace = make_workspace(Path(self.tmp.name))
        self.loader = make_loader(self.workspace)

    def tearDown(self):
        self.tmp.cleanup()

    def _write_task_timers(self, tasks: dict):
        """memory/task-timers.json 작성 헬퍼"""
        data = {"tasks": tasks}
        path = self.workspace / "memory" / "task-timers.json"
        path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        self.loader.load_tasks()

    # ── 테스트 6: start_time 1시간 전 → 포함 ─────────────────

    def test_running_task_within_2hours(self):
        """start_time이 1시간 전인 running 태스크는 결과에 포함되어야 한다"""
        start_dt = datetime.now() - timedelta(hours=1)
        self._write_task_timers({
            "task-fresh-1": {
                "status": "running",
                "team_id": "dev1-team",
                "description": "신선한 태스크",
                "start_time": iso(start_dt),
            }
        })

        result = self.loader.get_running_tasks_by_team()

        self.assertIn(
            "dev1-team", result,
            "1시간 전 start_time인 running 태스크가 팀 목록에 있어야 함"
        )
        task_ids = [t["task_id"] for t in result["dev1-team"]]
        self.assertIn(
            "task-fresh-1", task_ids,
            f"task-fresh-1이 포함되어야 함, 실제 목록: {task_ids}"
        )

    # ── 테스트 7: start_time 3시간 전 → 제외 ─────────────────

    def test_running_task_over_2hours(self):
        """start_time이 3시간 전인 running 태스크는 stale이므로 결과에서 제외되어야 한다"""
        start_dt = datetime.now() - timedelta(hours=3)
        self._write_task_timers({
            "task-stale-1": {
                "status": "running",
                "team_id": "dev1-team",
                "description": "오래된 태스크",
                "start_time": iso(start_dt),
            }
        })

        result = self.loader.get_running_tasks_by_team()

        # dev1-team이 없거나, 있어도 task-stale-1은 제외되어야 함
        if "dev1-team" in result:
            task_ids = [t["task_id"] for t in result["dev1-team"]]
            self.assertNotIn(
                "task-stale-1", task_ids,
                f"3시간 전 start_time인 태스크는 stale로 제외되어야 함, 실제 목록: {task_ids}"
            )
        else:
            # dev1-team 자체가 없는 것도 올바른 결과
            pass

    # ── 테스트 8: start_time 없음 → 포함 (방어적) ─────────────

    def test_running_task_no_start_time(self):
        """start_time이 없는 running 태스크는 TTL 검사를 건너뛰고 결과에 포함되어야 한다"""
        self._write_task_timers({
            "task-no-time-1": {
                "status": "running",
                "team_id": "dev2-team",
                "description": "시작시간 없는 태스크",
                # start_time 필드 없음
            }
        })

        result = self.loader.get_running_tasks_by_team()

        self.assertIn(
            "dev2-team", result,
            "start_time 없는 running 태스크는 방어적으로 포함되어야 함"
        )
        task_ids = [t["task_id"] for t in result["dev2-team"]]
        self.assertIn(
            "task-no-time-1", task_ids,
            f"task-no-time-1이 포함되어야 함, 실제 목록: {task_ids}"
        )


# ──────────────────────────────────────────────────────────────
# Bug 3: load_member_status() JSON 자동 복구 테스트
# ──────────────────────────────────────────────────────────────

class TestBug3LoadMemberStatusRecovery(unittest.TestCase):
    """
    Bug 3: member-status.json이 깨진 JSON이면 {"members": {}} 초기값으로
    파일을 덮어쓰기하여 자동 복구해야 한다.

    테스트 대상: DataLoader.load_member_status()
    """

    def setUp(self):
        """각 테스트마다 새 임시 workspace + DataLoader 준비"""
        self.tmp = tempfile.TemporaryDirectory()
        self.workspace = make_workspace(Path(self.tmp.name))
        self.loader = make_loader(self.workspace)
        self.status_file = self.workspace / "memory" / "events" / "member-status.json"

    def tearDown(self):
        self.tmp.cleanup()

    # ── 테스트 9: 깨진 JSON 파일 → 자동 복구 확인 ────────────

    def test_load_corrupted_json_recovery(self):
        """
        member-status.json이 깨진 JSON이면:
        1. member_status_data가 {"members": {}} (또는 빈 dict)으로 초기화된다
        2. 파일 내용이 {"members": {}}으로 덮어쓰기된다 (자동 복구)
        """
        # 깨진 JSON 파일 작성
        self.status_file.write_text(
            "{this is not valid json!!!",
            encoding="utf-8"
        )

        result = self.loader.load_member_status()

        # 1) 반환값 확인: members 키가 있는 dict이거나 빈 dict
        self.assertIsInstance(result, dict, "반환값은 dict이어야 함")

        # 2) 파일이 유효한 JSON으로 복구되었는지 확인
        recovered_text = self.status_file.read_text(encoding="utf-8")
        try:
            recovered_data = json.loads(recovered_text)
        except json.JSONDecodeError:
            self.fail(
                f"복구 후 파일이 유효한 JSON이 아님: {recovered_text!r}"
            )

        # 3) 복구된 파일의 내용이 {"members": {}} 형식인지 확인
        self.assertIn(
            "members", recovered_data,
            f"복구된 JSON에 'members' 키가 있어야 함: {recovered_data}"
        )
        self.assertIsInstance(
            recovered_data["members"], dict,
            f"복구된 'members' 값은 dict이어야 함: {recovered_data['members']}"
        )

    # ── 테스트 10: 정상 JSON → 정상 로드 확인 ────────────────

    def test_load_valid_json(self):
        """member-status.json이 정상이면 해당 데이터를 그대로 반환해야 한다"""
        expected_data = {
            "members": {
                "frank": {"status": "working", "since": iso(datetime.now() - timedelta(minutes=5))},
                "grace": {"status": "idle"},
            }
        }
        self.status_file.write_text(
            json.dumps(expected_data, ensure_ascii=False, indent=2),
            encoding="utf-8"
        )

        result = self.loader.load_member_status()

        self.assertIsInstance(result, dict, "반환값은 dict이어야 함")
        self.assertIn("members", result, "'members' 키가 있어야 함")
        self.assertIn("frank", result["members"], "frank가 members에 있어야 함")
        self.assertIn("grace", result["members"], "grace가 members에 있어야 함")
        self.assertEqual(
            result["members"]["frank"]["status"], "working",
            f"frank의 status가 working이어야 함: {result['members']['frank']}"
        )

    # ── 테스트 11: 파일 없음 → 빈 dict ───────────────────────

    def test_load_nonexistent_file(self):
        """member-status.json 파일이 없으면 빈 dict(또는 {"members":{}})를 반환해야 한다"""
        # 파일이 존재하지 않는 상태 확인
        self.assertFalse(
            self.status_file.exists(),
            "테스트 시작 시 파일이 없어야 함"
        )

        result = self.loader.load_member_status()

        self.assertIsInstance(result, dict, "반환값은 dict이어야 함")
        # 파일 없음 → 빈 dict {} 또는 {"members": {}} 모두 허용
        # (파일이 없으면 복구가 아닌 초기 상태이므로 빈 dict도 유효)
        self.assertTrue(
            result == {} or result == {"members": {}},
            f"파일 없음이면 빈 dict 또는 members:dict이어야 함, 실제: {result}"
        )


# ──────────────────────────────────────────────────────────────
# 엔트리포인트
# ──────────────────────────────────────────────────────────────

if __name__ == "__main__":
    unittest.main(verbosity=2)
