#!/usr/bin/env python3
"""
dispatch.py 배치 추적 기능 테스트
테스터: 아르고스 (Argos)
대상: register_batch_task(), batch_status(), CLI --batch-status / --batch-id

테스트 항목:
    1. register_batch_task() - 새 배치 파일 생성, 기존 배치에 태스크 추가, JSON 구조 검증
    2. batch_status()        - 정상 조회, 존재하지 않는 batch_id, .done 파일 처리, 전체 완료 시 completed_at 갱신
    3. CLI (argparse)        - --batch-status 조회 모드, --team --task --batch-id 파싱
"""

import json
import os
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path
from unittest.mock import patch

import pytest

# dispatch 모듈 경로 등록
sys.path.insert(0, "/home/jay/workspace")
import dispatch as dispatch_module


# ──────────────────────────────────────────────────────────────
# 공통 픽스처
# ──────────────────────────────────────────────────────────────

@pytest.fixture
def tmp_workspace(tmp_path):
    """임시 워크스페이스 생성 (memory/batches, memory/events 포함)"""
    (tmp_path / "memory" / "batches").mkdir(parents=True)
    (tmp_path / "memory" / "events").mkdir(parents=True)
    (tmp_path / "memory" / "tasks").mkdir(parents=True)
    return tmp_path


@pytest.fixture(autouse=False)
def patch_workspace(tmp_workspace):
    """dispatch_module.WORKSPACE를 임시 디렉토리로 교체하고 테스트 후 복원"""
    original = dispatch_module.WORKSPACE
    dispatch_module.WORKSPACE = tmp_workspace
    yield tmp_workspace
    dispatch_module.WORKSPACE = original


# ──────────────────────────────────────────────────────────────
# 1. register_batch_task() 테스트
# ──────────────────────────────────────────────────────────────

class TestRegisterBatchTask:
    """register_batch_task() 단위 테스트"""

    # 1-1. 새 배치 파일 생성 확인
    def test_1_1_creates_new_batch_file(self, patch_workspace):
        """새 batch_id로 호출하면 {batch_id}.json 파일이 생성된다"""
        batch_id = "batch-test-001"
        dispatch_module.register_batch_task(batch_id, "task-1.1", "dev1-team")

        batch_file = patch_workspace / "memory" / "batches" / f"{batch_id}.json"
        assert batch_file.exists(), f"배치 파일이 생성돼야 함: {batch_file}"

    # 1-2. 기존 배치에 태스크 추가 확인
    def test_1_2_appends_task_to_existing_batch(self, patch_workspace):
        """기존 배치 파일에 태스크를 추가하면 tasks 배열 길이가 증가한다"""
        batch_id = "batch-test-002"

        dispatch_module.register_batch_task(batch_id, "task-10.1", "dev1-team")
        dispatch_module.register_batch_task(batch_id, "task-10.2", "dev2-team")

        batch_file = patch_workspace / "memory" / "batches" / f"{batch_id}.json"
        data = json.loads(batch_file.read_text(encoding="utf-8"))

        assert len(data["tasks"]) == 2, f"tasks 배열에 2개가 있어야 함: {data['tasks']}"
        task_ids = [t["task_id"] for t in data["tasks"]]
        assert "task-10.1" in task_ids
        assert "task-10.2" in task_ids

    # 1-3. JSON 구조 검증: 필수 필드 확인
    def test_1_3_json_structure_has_required_fields(self, patch_workspace):
        """생성된 JSON에 batch_id, tasks, created_at, completed_at 필드가 있어야 한다"""
        batch_id = "batch-test-003"
        dispatch_module.register_batch_task(batch_id, "task-20.1", "dev1-team")

        batch_file = patch_workspace / "memory" / "batches" / f"{batch_id}.json"
        data = json.loads(batch_file.read_text(encoding="utf-8"))

        assert "batch_id" in data, "batch_id 필드가 있어야 함"
        assert "tasks" in data, "tasks 필드가 있어야 함"
        assert "created_at" in data, "created_at 필드가 있어야 함"
        assert "completed_at" in data, "completed_at 필드가 있어야 함"
        assert data["batch_id"] == batch_id, f"batch_id 값이 일치해야 함: {data['batch_id']}"

    # 1-4. tasks 배열 내 태스크 항목 구조 검증
    def test_1_4_task_entry_structure(self, patch_workspace):
        """tasks 배열 내 각 항목에 task_id, team, status, report_path 필드가 있어야 한다"""
        batch_id = "batch-test-004"
        dispatch_module.register_batch_task(batch_id, "task-30.1", "dev2-team")

        batch_file = patch_workspace / "memory" / "batches" / f"{batch_id}.json"
        data = json.loads(batch_file.read_text(encoding="utf-8"))
        task_entry = data["tasks"][0]

        assert "task_id" in task_entry, "task_id 필드가 있어야 함"
        assert "team" in task_entry, "team 필드가 있어야 함"
        assert "status" in task_entry, "status 필드가 있어야 함"
        assert "report_path" in task_entry, "report_path 필드가 있어야 함"
        assert task_entry["task_id"] == "task-30.1"
        assert task_entry["team"] == "dev2-team"
        assert task_entry["status"] == "dispatched"

    # 1-5. 최초 생성 시 completed_at은 None
    def test_1_5_completed_at_is_none_on_creation(self, patch_workspace):
        """신규 배치 파일 생성 직후 completed_at은 None이어야 한다"""
        batch_id = "batch-test-005"
        dispatch_module.register_batch_task(batch_id, "task-40.1", "dev3-team")

        batch_file = patch_workspace / "memory" / "batches" / f"{batch_id}.json"
        data = json.loads(batch_file.read_text(encoding="utf-8"))

        assert data["completed_at"] is None, \
            f"신규 배치의 completed_at은 None이어야 함: {data['completed_at']}"

    # 1-6. report_path 형식 검증
    def test_1_6_report_path_format(self, patch_workspace):
        """report_path는 'memory/reports/{task_id}.md' 형식이어야 한다"""
        batch_id = "batch-test-006"
        task_id = "task-50.1"
        dispatch_module.register_batch_task(batch_id, task_id, "dev1-team")

        batch_file = patch_workspace / "memory" / "batches" / f"{batch_id}.json"
        data = json.loads(batch_file.read_text(encoding="utf-8"))
        report_path = data["tasks"][0]["report_path"]

        assert report_path == f"memory/reports/{task_id}.md", \
            f"report_path 형식이 올바르지 않음: {report_path}"


# ──────────────────────────────────────────────────────────────
# 2. batch_status() 테스트
# ──────────────────────────────────────────────────────────────

class TestBatchStatus:
    """batch_status() 단위 테스트"""

    def _create_batch_file(self, tmp_workspace, batch_id, tasks, completed_at=None):
        """테스트용 배치 파일 직접 생성"""
        batches_dir = tmp_workspace / "memory" / "batches"
        batches_dir.mkdir(parents=True, exist_ok=True)
        data = {
            "batch_id": batch_id,
            "tasks": tasks,
            "created_at": datetime.now().isoformat(),
            "completed_at": completed_at,
        }
        batch_file = batches_dir / f"{batch_id}.json"
        batch_file.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        return batch_file

    # 2-1. 정상 배치 상태 조회
    def test_2_1_normal_batch_status_query(self, patch_workspace):
        """정상적인 batch_id로 조회하면 배치 데이터가 반환된다"""
        batch_id = "batch-status-001"
        self._create_batch_file(
            patch_workspace, batch_id,
            [{"task_id": "task-1.1", "team": "dev1-team", "status": "dispatched",
              "report_path": "memory/reports/task-1.1.md"}]
        )

        result = dispatch_module.batch_status(batch_id)

        assert "status" not in result or result.get("status") != "error", \
            f"정상 조회인데 에러가 반환됨: {result}"
        assert result["batch_id"] == batch_id, f"batch_id가 일치해야 함: {result}"
        assert "tasks" in result, "tasks 필드가 있어야 함"
        assert len(result["tasks"]) == 1

    # 2-2. 존재하지 않는 batch_id → error 반환
    def test_2_2_nonexistent_batch_id_returns_error(self, patch_workspace):
        """존재하지 않는 batch_id로 조회하면 status=error와 message가 반환된다"""
        result = dispatch_module.batch_status("batch-nonexistent-999")

        assert result["status"] == "error", \
            f"존재하지 않는 batch_id이면 status=error여야 함: {result}"
        assert "message" in result, "error 시 message 필드가 있어야 함"

    # 2-3. .done 파일이 있는 경우 completed 상태 반영
    def test_2_3_done_file_marks_task_completed(self, patch_workspace):
        """.done 파일이 존재하는 태스크는 status=completed로 반영된다"""
        batch_id = "batch-done-001"
        task_id = "task-done-1.1"
        self._create_batch_file(
            patch_workspace, batch_id,
            [{"task_id": task_id, "team": "dev1-team", "status": "dispatched",
              "report_path": f"memory/reports/{task_id}.md"}]
        )

        # .done 파일 생성
        events_dir = patch_workspace / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)
        done_file = events_dir / f"{task_id}.done"
        done_file.write_text("done", encoding="utf-8")

        result = dispatch_module.batch_status(batch_id)
        task_result = result["tasks"][0]

        assert task_result["status"] == "completed", \
            f".done 파일이 있으면 status=completed여야 함: {task_result}"

    # 2-4. 모든 태스크 완료 시 completed_at 갱신
    def test_2_4_all_tasks_done_sets_completed_at(self, patch_workspace):
        """모든 태스크가 completed 상태이고 completed_at이 None이면 갱신된다"""
        batch_id = "batch-alldone-001"
        task_id_1 = "task-alldone-1.1"
        task_id_2 = "task-alldone-1.2"
        self._create_batch_file(
            patch_workspace, batch_id,
            [
                {"task_id": task_id_1, "team": "dev1-team", "status": "dispatched",
                 "report_path": f"memory/reports/{task_id_1}.md"},
                {"task_id": task_id_2, "team": "dev2-team", "status": "dispatched",
                 "report_path": f"memory/reports/{task_id_2}.md"},
            ],
            completed_at=None
        )

        # 두 태스크 모두 .done 파일 생성
        events_dir = patch_workspace / "memory" / "events"
        (events_dir / f"{task_id_1}.done").write_text("done")
        (events_dir / f"{task_id_2}.done").write_text("done")

        result = dispatch_module.batch_status(batch_id)

        assert result["completed_at"] is not None, \
            f"모든 태스크 완료 시 completed_at이 갱신돼야 함: {result}"
        # ISO 포맷 검증 (최소 길이)
        assert len(result["completed_at"]) >= 19, \
            f"completed_at이 ISO 형식이어야 함: {result['completed_at']}"

    # 2-5. 태스크가 일부만 완료된 경우 completed_at은 None 유지
    def test_2_5_partial_completion_keeps_completed_at_none(self, patch_workspace):
        """일부 태스크만 완료된 경우 completed_at은 None으로 유지된다"""
        batch_id = "batch-partial-001"
        task_id_done = "task-partial-1.1"
        task_id_pending = "task-partial-1.2"
        self._create_batch_file(
            patch_workspace, batch_id,
            [
                {"task_id": task_id_done, "team": "dev1-team", "status": "dispatched",
                 "report_path": f"memory/reports/{task_id_done}.md"},
                {"task_id": task_id_pending, "team": "dev2-team", "status": "dispatched",
                 "report_path": f"memory/reports/{task_id_pending}.md"},
            ],
            completed_at=None
        )

        # 첫 번째 태스크만 .done
        events_dir = patch_workspace / "memory" / "events"
        (events_dir / f"{task_id_done}.done").write_text("done")

        result = dispatch_module.batch_status(batch_id)

        assert result["completed_at"] is None, \
            f"일부 미완료 태스크가 있으면 completed_at이 None이어야 함: {result}"

    # 2-6. task-timers.json status 반영
    def test_2_6_timer_status_reflected_when_no_done_file(self, patch_workspace):
        """task-timers.json에 running 상태가 있으면 해당 태스크 status에 반영된다"""
        batch_id = "batch-timer-001"
        task_id = "task-timer-1.1"
        self._create_batch_file(
            patch_workspace, batch_id,
            [{"task_id": task_id, "team": "dev1-team", "status": "dispatched",
              "report_path": f"memory/reports/{task_id}.md"}]
        )

        # task-timers.json에 running 등록
        timer_data = {"tasks": {task_id: {"status": "running"}}}
        timer_file = patch_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        result = dispatch_module.batch_status(batch_id)
        task_result = result["tasks"][0]

        assert task_result["status"] == "running", \
            f"timer에 running이면 status=running이어야 함: {task_result}"

    # 2-7. .done 파일이 timer보다 우선 적용
    def test_2_7_done_file_takes_priority_over_timer(self, patch_workspace):
        """.done 파일이 있으면 task-timers.json 상태와 무관하게 completed가 된다"""
        batch_id = "batch-priority-001"
        task_id = "task-priority-1.1"
        self._create_batch_file(
            patch_workspace, batch_id,
            [{"task_id": task_id, "team": "dev1-team", "status": "dispatched",
              "report_path": f"memory/reports/{task_id}.md"}]
        )

        # task-timers.json에 running 등록
        timer_data = {"tasks": {task_id: {"status": "running"}}}
        timer_file = patch_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        # .done 파일도 생성
        events_dir = patch_workspace / "memory" / "events"
        (events_dir / f"{task_id}.done").write_text("done")

        result = dispatch_module.batch_status(batch_id)
        task_result = result["tasks"][0]

        assert task_result["status"] == "completed", \
            f".done 파일이 timer보다 우선해야 함: {task_result}"

    # 2-8. 빈 tasks 배열인 경우
    def test_2_8_empty_tasks_batch(self, patch_workspace):
        """tasks 배열이 비어있는 배치도 정상적으로 조회된다"""
        batch_id = "batch-empty-001"
        self._create_batch_file(patch_workspace, batch_id, [])

        result = dispatch_module.batch_status(batch_id)

        # 에러가 아닌 배치 데이터 반환
        assert result.get("status") != "error", f"빈 배치도 에러가 아니어야 함: {result}"
        assert result["batch_id"] == batch_id
        assert result["tasks"] == []

    # 2-9. cancelled 태스크도 all_completed에 포함
    def test_2_9_cancelled_counts_as_completed(self, patch_workspace):
        """cancelled 태스크는 completed와 동일하게 all_completed에 포함된다"""
        batch_id = "batch-cancelled-001"
        task_id_cancelled = "task-cancel-1.1"
        task_id_done = "task-cancel-1.2"
        self._create_batch_file(
            patch_workspace, batch_id,
            [
                {"task_id": task_id_cancelled, "team": "dev1-team", "status": "dispatched",
                 "report_path": f"memory/reports/{task_id_cancelled}.md"},
                {"task_id": task_id_done, "team": "dev2-team", "status": "dispatched",
                 "report_path": f"memory/reports/{task_id_done}.md"},
            ],
            completed_at=None
        )

        # task_cancelled → timer에서 cancelled로 설정
        timer_data = {"tasks": {task_id_cancelled: {"status": "cancelled"}}}
        timer_file = patch_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        # task_done → .done 파일
        events_dir = patch_workspace / "memory" / "events"
        (events_dir / f"{task_id_done}.done").write_text("done")

        result = dispatch_module.batch_status(batch_id)

        # cancelled + completed = 모두 완료 → completed_at 갱신
        assert result["completed_at"] is not None, \
            f"cancelled + completed 조합이면 completed_at이 갱신돼야 함: {result}"


# ──────────────────────────────────────────────────────────────
# 3. CLI (argparse) 테스트
# ──────────────────────────────────────────────────────────────

class TestCLI:
    """CLI 테스트 (subprocess integration 및 argparse 파싱)"""

    DISPATCH_PY = "/home/jay/workspace/dispatch.py"

    # 3-1. --batch-status 옵션으로 조회 모드 동작 확인 (subprocess)
    def test_3_1_batch_status_cli_returns_json(self, tmp_path):
        """--batch-status 옵션으로 실행하면 JSON이 출력된다"""
        # 임시 워크스페이스 구성
        batches_dir = tmp_path / "memory" / "batches"
        batches_dir.mkdir(parents=True)
        (tmp_path / "memory" / "events").mkdir(parents=True)

        batch_id = "batch-cli-001"
        batch_data = {
            "batch_id": batch_id,
            "tasks": [
                {"task_id": "task-cli-1.1", "team": "dev1-team",
                 "status": "dispatched", "report_path": "memory/reports/task-cli-1.1.md"}
            ],
            "created_at": datetime.now().isoformat(),
            "completed_at": None,
        }
        batch_file = batches_dir / f"{batch_id}.json"
        batch_file.write_text(json.dumps(batch_data, ensure_ascii=False, indent=2))

        env = os.environ.copy()
        env["WORKSPACE_ROOT"] = str(tmp_path)

        proc = subprocess.run(
            ["python3", self.DISPATCH_PY, "--batch-status", batch_id],
            capture_output=True, text=True, env=env
        )

        assert proc.returncode == 0, \
            f"--batch-status 실행이 성공해야 함 (returncode=0): stderr={proc.stderr}"

        output = json.loads(proc.stdout)
        assert output["batch_id"] == batch_id, \
            f"출력된 batch_id가 일치해야 함: {output}"

    # 3-2. 존재하지 않는 batch_id로 --batch-status 실행
    def test_3_2_batch_status_cli_nonexistent_returns_error(self, tmp_path):
        """존재하지 않는 batch_id로 --batch-status 실행하면 error JSON이 출력된다"""
        (tmp_path / "memory" / "batches").mkdir(parents=True)
        (tmp_path / "memory" / "events").mkdir(parents=True)

        env = os.environ.copy()
        env["WORKSPACE_ROOT"] = str(tmp_path)

        proc = subprocess.run(
            ["python3", self.DISPATCH_PY, "--batch-status", "batch-not-exist-99999"],
            capture_output=True, text=True, env=env
        )

        assert proc.returncode == 0, \
            f"returncode=0이어야 함 (error는 JSON으로 출력): stderr={proc.stderr}"

        output = json.loads(proc.stdout)
        assert output["status"] == "error", \
            f"존재하지 않는 배치이면 status=error여야 함: {output}"

    # 3-3. --team --task --batch-id 옵션 argparse 파싱 확인 (argparse 동작만 검증)
    def test_3_3_argparse_batch_id_option_parsed(self):
        """--team, --task, --batch-id 옵션이 argparse에서 올바르게 파싱된다"""
        import argparse

        # dispatch.py의 parser와 동일한 구조로 파싱 테스트
        parser = argparse.ArgumentParser()
        parser.add_argument("--team", choices=["dev1-team", "dev2-team", "dev3-team"])
        parser.add_argument("--task")
        parser.add_argument("--level", default="normal",
                            choices=["normal", "critical", "security"])
        parser.add_argument("--batch-id", default=None, dest="batch_id")
        parser.add_argument("--batch-status", default=None, dest="batch_status",
                            metavar="BATCH_ID")

        args = parser.parse_args([
            "--team", "dev1-team",
            "--task", "API 서버 구축",
            "--batch-id", "batch-20260302-001",
        ])

        assert args.team == "dev1-team", f"team 파싱 오류: {args.team}"
        assert args.task == "API 서버 구축", f"task 파싱 오류: {args.task}"
        assert args.batch_id == "batch-20260302-001", f"batch_id 파싱 오류: {args.batch_id}"
        assert args.batch_status is None, f"batch_status는 None이어야 함: {args.batch_status}"

    # 3-4. --batch-status 옵션 파싱 확인 (다른 옵션 없이 단독 사용)
    def test_3_4_argparse_batch_status_option_parsed(self):
        """--batch-status 단독 사용 시 올바르게 파싱된다"""
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument("--team", choices=["dev1-team", "dev2-team", "dev3-team"])
        parser.add_argument("--task")
        parser.add_argument("--batch-id", default=None, dest="batch_id")
        parser.add_argument("--batch-status", default=None, dest="batch_status",
                            metavar="BATCH_ID")

        args = parser.parse_args(["--batch-status", "batch-20260302-001"])

        assert args.batch_status == "batch-20260302-001", \
            f"batch_status 파싱 오류: {args.batch_status}"
        assert args.team is None, "batch-status 모드에서 team은 None이어야 함"
        assert args.task is None, "batch-status 모드에서 task는 None이어야 함"

    # 3-5. --team --task 없이 실행하면 에러 종료
    def test_3_5_cli_exits_error_without_team_and_task(self, tmp_path):
        """--team / --task / --batch-status 없이 실행하면 0이 아닌 종료코드를 반환한다"""
        (tmp_path / "memory" / "batches").mkdir(parents=True)
        (tmp_path / "memory" / "events").mkdir(parents=True)

        env = os.environ.copy()
        env["WORKSPACE_ROOT"] = str(tmp_path)

        proc = subprocess.run(
            ["python3", self.DISPATCH_PY],
            capture_output=True, text=True, env=env
        )

        assert proc.returncode != 0, \
            f"--team --task 없이 실행하면 비정상 종료해야 함: returncode={proc.returncode}"
