"""
test_task_179_1.py - task-179.1 테스트 코드 (헤임달, 테스터)

테스트 대상:
  1. /home/jay/workspace/scripts/project-map.py
  2. /home/jay/workspace/prompts/team_prompts.py
  3. /home/jay/workspace/dispatch.py
"""

import importlib
import json
import os
import sys
import time
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# ────────────────────────────────────────────────────────────────────────────────
# sys.path 설정 - workspace 루트에서 모듈 import 가능하도록
# ────────────────────────────────────────────────────────────────────────────────
WORKSPACE_ROOT = Path("/home/jay/workspace")
if str(WORKSPACE_ROOT) not in sys.path:
    sys.path.insert(0, str(WORKSPACE_ROOT))

SCRIPTS_DIR = WORKSPACE_ROOT / "scripts"
PROJECT_MAP_SCRIPT = SCRIPTS_DIR / "project-map.py"


# ────────────────────────────────────────────────────────────────────────────────
# 헬퍼: project-map.py를 모듈로 동적 로드
# ────────────────────────────────────────────────────────────────────────────────
def load_project_map_module():
    """scripts/project-map.py를 모듈로 로드하여 반환."""
    import importlib.util

    spec = importlib.util.spec_from_file_location("project_map", str(PROJECT_MAP_SCRIPT))
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


# ════════════════════════════════════════════════════════════════════════════════
# 1. project-map.py 단위 테스트
# ════════════════════════════════════════════════════════════════════════════════


class TestProjectMapEmpty:
    """빈 디렉토리에서 generate_markdown 실행 시 '없음' 섹션 표시 확인."""

    def test_empty_dir_produces_valid_markdown(self, tmp_path):
        pm = load_project_map_module()
        output_file = tmp_path / "map.md"

        content = pm.generate_markdown(
            project_root=tmp_path,
            output_path=output_file,
            depth=3,
            include_tests=False,
        )

        # 마크다운 헤더 존재
        assert "# Project Map:" in content

        # 섹션 헤더 존재
        assert "## Directory Tree" in content
        assert "## Types & Interfaces" in content
        assert "## API Routes" in content
        assert "## Components" in content

        # 빈 디렉토리 → 각 섹션이 "없음" 으로 표시
        assert "_(타입/인터페이스 없음)_" in content
        assert "_(API 라우트 없음)_" in content
        assert "_(컴포넌트 없음)_" in content


class TestProjectMapNextjs:
    """Next.js 프로젝트 시뮬레이션 - 파일 생성 후 추출 결과 검증."""

    def _setup_project(self, tmp_path: Path) -> Path:
        """테스트용 Next.js 유사 프로젝트 파일 트리 생성."""
        # app/api/search/route.ts - GET, POST 메서드
        route_dir = tmp_path / "app" / "api" / "search"
        route_dir.mkdir(parents=True)
        (route_dir / "route.ts").write_text(
            "export async function GET(req: Request) { return new Response('ok'); }\n"
            "export async function POST(req: Request) { return new Response('ok'); }\n",
            encoding="utf-8",
        )

        # components/SearchBar.tsx - export default function
        comp_dir = tmp_path / "components"
        comp_dir.mkdir(parents=True)
        (comp_dir / "SearchBar.tsx").write_text(
            "export default function SearchBar() { return <div />; }\n",
            encoding="utf-8",
        )

        # types/model.ts - export interface
        types_dir = tmp_path / "types"
        types_dir.mkdir(parents=True)
        (types_dir / "model.ts").write_text(
            "export interface SearchResult { id: string; title: string; }\n"
            "export type SearchQuery = { q: string; };\n",
            encoding="utf-8",
        )

        return tmp_path

    def test_api_routes_extracted(self, tmp_path):
        pm = load_project_map_module()
        root = self._setup_project(tmp_path)
        output_file = tmp_path / "map.md"

        content = pm.generate_markdown(
            project_root=root,
            output_path=output_file,
            depth=3,
            include_tests=False,
        )

        # API 라우트 섹션에 GET, POST 가 포함되어야 함
        assert "GET" in content
        assert "POST" in content
        assert "/api/search" in content

    def test_components_extracted(self, tmp_path):
        pm = load_project_map_module()
        root = self._setup_project(tmp_path)
        output_file = tmp_path / "map.md"

        content = pm.generate_markdown(
            project_root=root,
            output_path=output_file,
            depth=3,
            include_tests=False,
        )

        # 컴포넌트 섹션에 SearchBar 포함
        assert "SearchBar" in content

    def test_types_extracted(self, tmp_path):
        pm = load_project_map_module()
        root = self._setup_project(tmp_path)
        output_file = tmp_path / "map.md"

        content = pm.generate_markdown(
            project_root=root,
            output_path=output_file,
            depth=3,
            include_tests=False,
        )

        # 타입/인터페이스 섹션에 SearchResult, SearchQuery 포함
        assert "SearchResult" in content
        assert "SearchQuery" in content


class TestProjectMapExcludePatterns:
    """제외 패턴 (node_modules, .git, __pycache__) 트리에서 제외 확인."""

    def test_excluded_dirs_not_in_tree(self, tmp_path):
        pm = load_project_map_module()

        # 제외되어야 할 디렉토리 생성
        (tmp_path / "node_modules").mkdir()
        (tmp_path / "node_modules" / "some_pkg").mkdir()
        (tmp_path / ".git").mkdir()
        (tmp_path / "__pycache__").mkdir()

        # 포함되어야 할 디렉토리
        (tmp_path / "src").mkdir()
        (tmp_path / "src" / "index.ts").write_text("export const x = 1;")

        tree_lines = pm.build_tree(tmp_path, depth=3, project_root=tmp_path)
        tree_text = "\n".join(tree_lines)

        # 제외 디렉토리가 트리에 없어야 함
        assert "node_modules" not in tree_text
        assert "__pycache__" not in tree_text

        # src는 포함되어야 함
        assert "src" in tree_text

    def test_is_excluded_dir_function(self):
        pm = load_project_map_module()

        assert pm.is_excluded_dir("node_modules") is True
        assert pm.is_excluded_dir(".git") is True
        assert pm.is_excluded_dir("__pycache__") is True
        assert pm.is_excluded_dir(".next") is True
        assert pm.is_excluded_dir("dist") is True
        assert pm.is_excluded_dir("src") is False
        assert pm.is_excluded_dir("components") is False


class TestProjectMapDepth:
    """--depth 옵션: depth=1 시 하위 디렉토리는 보이지만 더 깊은 곳은 미표시."""

    def test_depth_1_hides_deep_directories(self, tmp_path):
        pm = load_project_map_module()

        # 2단계 깊이 디렉토리 생성
        deep = tmp_path / "src" / "components" / "ui"
        deep.mkdir(parents=True)
        (deep / "Button.tsx").write_text("export default function Button() {}")

        # depth=1: src/ 는 보이지만 src/components/는 안 보여야 함
        tree_lines = pm.build_tree(tmp_path, depth=1, project_root=tmp_path)
        tree_text = "\n".join(tree_lines)

        assert "src" in tree_text
        assert "components" not in tree_text
        assert "ui" not in tree_text

    def test_depth_2_shows_one_level_deep(self, tmp_path):
        pm = load_project_map_module()

        deep = tmp_path / "src" / "components" / "ui"
        deep.mkdir(parents=True)
        (deep / "Button.tsx").write_text("export default function Button() {}")

        # depth=2: src/, src/components/ 는 보이지만 ui/ 는 안 보여야 함
        tree_lines = pm.build_tree(tmp_path, depth=2, project_root=tmp_path)
        tree_text = "\n".join(tree_lines)

        assert "src" in tree_text
        assert "components" in tree_text
        assert "ui" not in tree_text


class TestProjectMapIncludeTests:
    """--include-tests: 테스트 파일 포함/제외 확인."""

    def _create_test_project(self, tmp_path: Path):
        """테스트 파일과 일반 파일이 함께 있는 프로젝트 생성."""
        # 일반 타입 파일
        types_dir = tmp_path / "types"
        types_dir.mkdir()
        (types_dir / "user.ts").write_text(
            "export interface User { id: string; }\n"
        )

        # 테스트 타입 파일 (test 패턴)
        (types_dir / "user.test.ts").write_text(
            "export interface TestUser { testId: string; }\n"
        )

        # __tests__ 디렉토리 (test 패턴)
        test_dir = tmp_path / "__tests__"
        test_dir.mkdir()
        (test_dir / "spec.ts").write_text(
            "export interface SpecData { data: string; }\n"
        )

    def test_exclude_tests_by_default(self, tmp_path):
        pm = load_project_map_module()
        self._create_test_project(tmp_path)

        types_map = pm.extract_types_interfaces(tmp_path, include_tests=False)
        all_files = " ".join(types_map.keys())

        # 테스트 파일 제외
        assert "user.test.ts" not in all_files
        # 일반 파일 포함
        assert "user.ts" in all_files

    def test_include_tests_flag(self, tmp_path):
        pm = load_project_map_module()
        self._create_test_project(tmp_path)

        types_map = pm.extract_types_interfaces(tmp_path, include_tests=True)
        all_files = " ".join(types_map.keys())

        # include_tests=True이면 테스트 파일도 포함
        assert "user.ts" in all_files
        assert "user.test.ts" in all_files

    def test_is_test_path_function(self):
        pm = load_project_map_module()

        assert pm.is_test_path("src/__tests__/foo.ts") is True
        assert pm.is_test_path("foo.test.ts") is True
        assert pm.is_test_path("foo.spec.ts") is True
        assert pm.is_test_path("src/components/Button.tsx") is False
        assert pm.is_test_path("types/model.ts") is False


# ════════════════════════════════════════════════════════════════════════════════
# 2. team_prompts.py 테스트
# ════════════════════════════════════════════════════════════════════════════════


class TestBuildProjectMapSection:
    """_build_project_map_section 함수 테스트."""

    def test_project_map_exists_includes_section(self, tmp_path, monkeypatch):
        """project-map 파일이 존재하면 프롬프트에 '프로젝트 구조 맵' 섹션 포함."""
        import prompts.team_prompts as tp

        # WORKSPACE_ROOT를 tmp_path로 오버라이드
        monkeypatch.setattr(tp, "WORKSPACE_ROOT", str(tmp_path))

        # 가짜 project-map 파일 생성
        project_id = "test-project"
        map_dir = tmp_path / "memory" / "project-maps"
        map_dir.mkdir(parents=True)
        (map_dir / f"{project_id}.md").write_text("# Project Map\n테스트 맵 내용")

        result = tp._build_project_map_section(project_id)

        assert "프로젝트 구조 맵" in result
        assert project_id in result

    def test_project_map_not_exists_returns_empty(self, tmp_path, monkeypatch):
        """project-map 파일이 존재하지 않으면 빈 문자열 반환."""
        import prompts.team_prompts as tp

        monkeypatch.setattr(tp, "WORKSPACE_ROOT", str(tmp_path))

        # map 디렉토리는 만들지만 파일은 없음
        map_dir = tmp_path / "memory" / "project-maps"
        map_dir.mkdir(parents=True)

        result = tp._build_project_map_section("nonexistent-project")

        assert result == ""

    def test_project_id_none_returns_empty(self, tmp_path, monkeypatch):
        """project_id가 None이면 빈 문자열 반환."""
        import prompts.team_prompts as tp

        monkeypatch.setattr(tp, "WORKSPACE_ROOT", str(tmp_path))

        result = tp._build_project_map_section(None)

        assert result == ""

    def test_build_prompt_includes_project_map_section(self, tmp_path, monkeypatch):
        """build_prompt 호출 시 project-map이 있으면 반환된 프롬프트에 섹션 포함."""
        import prompts.team_prompts as tp

        monkeypatch.setattr(tp, "WORKSPACE_ROOT", str(tmp_path))

        # 필요한 디렉토리 구조 생성
        project_id = "myproject"
        map_dir = tmp_path / "memory" / "project-maps"
        map_dir.mkdir(parents=True)
        (map_dir / f"{project_id}.md").write_text("# Project Map")

        tasks_dir = tmp_path / "memory" / "tasks"
        tasks_dir.mkdir(parents=True)

        prompt = tp.build_prompt(
            team_id="dev2-team",
            task_id="task-179.1",
            task_desc="테스트 작업",
            project_id=project_id,
        )

        assert "프로젝트 구조 맵" in prompt

    def test_build_prompt_excludes_project_map_section_when_not_exists(
        self, tmp_path, monkeypatch
    ):
        """build_prompt 호출 시 project-map이 없으면 반환된 프롬프트에 섹션 미포함."""
        import prompts.team_prompts as tp

        monkeypatch.setattr(tp, "WORKSPACE_ROOT", str(tmp_path))

        tasks_dir = tmp_path / "memory" / "tasks"
        tasks_dir.mkdir(parents=True)

        prompt = tp.build_prompt(
            team_id="dev2-team",
            task_id="task-179.2",
            task_desc="테스트 작업 2",
            project_id="no-such-project",
        )

        assert "프로젝트 구조 맵" not in prompt


# ════════════════════════════════════════════════════════════════════════════════
# 3. dispatch.py 테스트
# ════════════════════════════════════════════════════════════════════════════════


class TestDispatchRefreshMapFlag:
    """--refresh-map 플래그 argparse 파싱 테스트."""

    def test_refresh_map_default_is_true(self):
        """--refresh-map 미지정 시 기본값 True로 파싱 (자동 갱신)."""
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument("--team", required=False, default="dev2-team")
        parser.add_argument("--task", required=False, default="test task")
        parser.add_argument("--level", default="normal")
        parser.add_argument("--session", default=None)
        parser.add_argument("--project", default=None)
        parser.add_argument("--chain", default=None)
        parser.add_argument(
            "--refresh-map",
            action=argparse.BooleanOptionalAction,
            default=True,
        )

        args = parser.parse_args([])
        assert args.refresh_map is True

    def test_no_refresh_map_disables(self):
        """--no-refresh-map 지정 시 False로 파싱."""
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument("--team", required=False, default="dev2-team")
        parser.add_argument("--task", required=False, default="test task")
        parser.add_argument("--level", default="normal")
        parser.add_argument("--session", default=None)
        parser.add_argument("--project", default=None)
        parser.add_argument("--chain", default=None)
        parser.add_argument(
            "--refresh-map",
            action=argparse.BooleanOptionalAction,
            default=True,
        )

        args = parser.parse_args(["--no-refresh-map"])
        assert args.refresh_map is False


class TestDispatchAutoRefresh:
    """24시간 기준 자동갱신 및 최신 맵 스킵 테스트."""

    def _make_dispatch_env(self, tmp_path: Path):
        """dispatch() 호출에 필요한 최소 디렉토리 구조 생성."""
        # memory 구조
        (tmp_path / "memory" / "tasks").mkdir(parents=True)
        (tmp_path / "memory" / "project-maps").mkdir(parents=True)
        (tmp_path / "memory" / "events").mkdir(parents=True)
        (tmp_path / "memory" / "reports").mkdir(parents=True)
        (tmp_path / "memory" / "logs").mkdir(parents=True)

        # projects/myproject 디렉토리
        project_dir = tmp_path / "projects" / "myproject"
        project_dir.mkdir(parents=True)

        # scripts/project-map.py 스텁 (실제 스크립트 참조)
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir(parents=True)
        # 실제 스크립트를 가리키는 심볼릭 링크 대신 빈 파일로 존재 표시
        (scripts_dir / "project-map.py").write_text("# stub")

        # task-timers.json 초기화
        timer_file = tmp_path / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps({"tasks": {}}), encoding="utf-8")

        # task-timer.py 스텁
        (tmp_path / "memory" / "task-timer.py").write_text("# stub")

        return tmp_path

    def test_old_map_triggers_refresh(self, tmp_path, monkeypatch):
        """mtime이 25시간 전인 경우 → project-map.py 호출되어야 함."""
        import importlib

        # tmp_path에 환경 구성
        env = self._make_dispatch_env(tmp_path)

        # 25시간 전 mtime으로 project-map 파일 생성
        map_path = env / "memory" / "project-maps" / "myproject.md"
        map_path.write_text("# Old Map")
        old_time = time.time() - (25 * 3600)
        os.utime(str(map_path), (old_time, old_time))

        # WORKSPACE 환경변수 오버라이드
        monkeypatch.setenv("WORKSPACE_ROOT", str(env))

        # dispatch 모듈을 새로 로드 (WORKSPACE 반영을 위해)
        import dispatch as dispatch_mod

        monkeypatch.setattr(dispatch_mod, "WORKSPACE", env)

        # 봇 키 환경변수 설정 (실제 dispatch() 호출 전에 필요)
        monkeypatch.setenv("COKACDIR_KEY_DEV2", "fake-dev2-key")
        monkeypatch.setattr(dispatch_mod, "BOT_KEYS", {
            "anu": "fake-anu-key",
            "dev1": "fake-dev1-key",
            "dev2": "fake-dev2-key",
            "dev3": "fake-dev3-key",
        })

        captured_calls = []

        def mock_subprocess_run(cmd, **kwargs):
            captured_calls.append(cmd)
            result = MagicMock()
            result.returncode = 0
            result.stdout = json.dumps({"ok": True})
            result.stderr = ""
            return result

        with patch("dispatch.subprocess.run", side_effect=mock_subprocess_run):
            dispatch_mod.dispatch(
                team_id="dev2-team",
                task_desc="refresh map test task",
                level="normal",
                project_id="myproject",
                refresh_map=True,
            )

        # subprocess.run 호출 중 project-map.py 가 포함된 호출이 있어야 함
        project_map_calls = [
            cmd for cmd in captured_calls
            if any("project-map.py" in str(arg) for arg in cmd)
        ]
        assert len(project_map_calls) >= 1, (
            f"project-map.py 호출이 없음. 실제 호출 목록: {captured_calls}"
        )

    def test_fresh_map_skips_refresh(self, tmp_path, monkeypatch):
        """mtime이 1시간 전인 경우 → project-map.py 호출 안 됨."""
        env = self._make_dispatch_env(tmp_path)

        # 1시간 전 mtime으로 project-map 파일 생성 (최신)
        map_path = env / "memory" / "project-maps" / "myproject.md"
        map_path.write_text("# Fresh Map")
        fresh_time = time.time() - 3600  # 정확히 1시간 전 (24시간 미만)
        os.utime(str(map_path), (fresh_time, fresh_time))

        monkeypatch.setenv("WORKSPACE_ROOT", str(env))

        import dispatch as dispatch_mod

        monkeypatch.setattr(dispatch_mod, "WORKSPACE", env)
        monkeypatch.setenv("COKACDIR_KEY_DEV2", "fake-dev2-key")
        monkeypatch.setattr(dispatch_mod, "BOT_KEYS", {
            "anu": "fake-anu-key",
            "dev1": "fake-dev1-key",
            "dev2": "fake-dev2-key",
            "dev3": "fake-dev3-key",
        })

        captured_calls = []

        def mock_subprocess_run(cmd, **kwargs):
            captured_calls.append(cmd)
            result = MagicMock()
            result.returncode = 0
            result.stdout = json.dumps({"ok": True})
            result.stderr = ""
            return result

        with patch("dispatch.subprocess.run", side_effect=mock_subprocess_run):
            dispatch_mod.dispatch(
                team_id="dev2-team",
                task_desc="fresh map test task",
                level="normal",
                project_id="myproject",
                refresh_map=True,
            )

        # project-map.py 가 포함된 호출이 없어야 함
        project_map_calls = [
            cmd for cmd in captured_calls
            if any("project-map.py" in str(arg) for arg in cmd)
        ]
        assert len(project_map_calls) == 0, (
            f"최신 맵인데 project-map.py가 호출됨: {project_map_calls}"
        )

    def test_map_not_exists_triggers_refresh(self, tmp_path, monkeypatch):
        """project-map 파일이 없는 경우 → project-map.py 호출되어야 함."""
        env = self._make_dispatch_env(tmp_path)

        # map 파일을 생성하지 않음
        monkeypatch.setenv("WORKSPACE_ROOT", str(env))

        import dispatch as dispatch_mod

        monkeypatch.setattr(dispatch_mod, "WORKSPACE", env)
        monkeypatch.setenv("COKACDIR_KEY_DEV2", "fake-dev2-key")
        monkeypatch.setattr(dispatch_mod, "BOT_KEYS", {
            "anu": "fake-anu-key",
            "dev1": "fake-dev1-key",
            "dev2": "fake-dev2-key",
            "dev3": "fake-dev3-key",
        })

        captured_calls = []

        def mock_subprocess_run(cmd, **kwargs):
            captured_calls.append(cmd)
            result = MagicMock()
            result.returncode = 0
            result.stdout = json.dumps({"ok": True})
            result.stderr = ""
            return result

        with patch("dispatch.subprocess.run", side_effect=mock_subprocess_run):
            dispatch_mod.dispatch(
                team_id="dev2-team",
                task_desc="no map test task",
                level="normal",
                project_id="myproject",
                refresh_map=True,
            )

        project_map_calls = [
            cmd for cmd in captured_calls
            if any("project-map.py" in str(arg) for arg in cmd)
        ]
        assert len(project_map_calls) >= 1, (
            f"맵 파일 없는데 project-map.py 호출 없음. 실제 호출: {captured_calls}"
        )

    def test_refresh_map_false_skips_always(self, tmp_path, monkeypatch):
        """refresh_map=False이면 맵이 오래되어도 project-map.py 호출 안 됨."""
        env = self._make_dispatch_env(tmp_path)

        # 25시간 전 오래된 맵
        map_path = env / "memory" / "project-maps" / "myproject.md"
        map_path.write_text("# Old Map")
        old_time = time.time() - (25 * 3600)
        os.utime(str(map_path), (old_time, old_time))

        monkeypatch.setenv("WORKSPACE_ROOT", str(env))

        import dispatch as dispatch_mod

        monkeypatch.setattr(dispatch_mod, "WORKSPACE", env)
        monkeypatch.setenv("COKACDIR_KEY_DEV2", "fake-dev2-key")
        monkeypatch.setattr(dispatch_mod, "BOT_KEYS", {
            "anu": "fake-anu-key",
            "dev1": "fake-dev1-key",
            "dev2": "fake-dev2-key",
            "dev3": "fake-dev3-key",
        })

        captured_calls = []

        def mock_subprocess_run(cmd, **kwargs):
            captured_calls.append(cmd)
            result = MagicMock()
            result.returncode = 0
            result.stdout = json.dumps({"ok": True})
            result.stderr = ""
            return result

        with patch("dispatch.subprocess.run", side_effect=mock_subprocess_run):
            dispatch_mod.dispatch(
                team_id="dev2-team",
                task_desc="no refresh flag test",
                level="normal",
                project_id="myproject",
                refresh_map=False,  # 갱신 비활성화
            )

        project_map_calls = [
            cmd for cmd in captured_calls
            if any("project-map.py" in str(arg) for arg in cmd)
        ]
        assert len(project_map_calls) == 0, (
            f"refresh_map=False인데 project-map.py 호출됨: {project_map_calls}"
        )
