"""dev7 — Phase ε dispatch 모듈 분리 회귀 테스트 (task-2388).

dispatch.py 4336줄을 dispatch/ 패키지 6 모듈(facade) 로 분리한 결과를 검증한다.

12 단위 테스트 + 5 검증 시나리오:
  1-2. task_id: 4-layer fix 동작 + 동시성 race 0
  3-4. metadata: frontmatter 파싱 + legacy fallback
  5-6. prompt: build_prompt 동작 + 슬림 라인 수
  7-8. retry: status 가드 + archived/escalated 박제
  9-10. core: argparse 진입 + cancel 동작
  11. audit: ALLOWED_COMMANDS / facade 노출
  12. integration: 패키지 import + 외부 호환 동작 회귀 0

  S1. facade 6 모듈 import 모두 동작
  S2. dispatch.py 호환 shim 동작 (--help)
  S3. 외부 mock 호환 (subprocess, logger, WORKSPACE)
  S4. dispatch namespace 모든 함수 노출
  S5. 본체 변경 없음 (dispatch/__init__.py == 원본 dispatch.py)
"""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

# 워크스페이스 sys.path 보정
_WORKSPACE = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(_WORKSPACE))

import dispatch
from dispatch import audit as dispatch_audit
from dispatch import core as dispatch_core
from dispatch import prompt as dispatch_prompt
from dispatch import retry as dispatch_retry
from dispatch import task_id as dispatch_task_id
from dispatch import _state as dispatch_state


# ===========================================================================
# 1. task_id: 4-layer fix 정확 동작
# ===========================================================================
class TestTaskIdFacade:
    def test_facade_exports_task_2380_functions(self):
        """task_id facade가 task-2380 fix 함수 5개를 모두 노출한다."""
        assert hasattr(dispatch_task_id, "generate_task_id")
        assert hasattr(dispatch_task_id, "_resolve_main_workspace")
        assert hasattr(dispatch_task_id, "_compute_next_id_from_timers")
        assert hasattr(dispatch_task_id, "_sync_counter_if_needed")
        assert hasattr(dispatch_task_id, "get_dispatch_time")

    def test_facade_identity_with_dispatch(self):
        """facade의 함수가 dispatch namespace의 함수와 동일 객체."""
        assert dispatch_task_id.generate_task_id is dispatch.generate_task_id
        assert dispatch_task_id._resolve_main_workspace is dispatch._resolve_main_workspace

    def test_compute_next_id_filters_variant_ids(self, tmp_path):
        """task-2380: 4자리 정수 외 변종 ID 무시."""
        timer_file = tmp_path / "task-timers.json"
        timer_file.write_text(json.dumps({
            "tasks": {
                "task-2386": {"status": "done"},
                "task-9.1": {"status": "done"},  # 변종 — 무시
                "task-200.1": {"status": "done"},  # 변종 — 무시
            }
        }))
        result = dispatch_task_id._compute_next_id_from_timers(timer_file)
        assert result == 2387, f"Expected 2387, got {result}"


# ===========================================================================
# 2. task_id: 동시성 race 0 (flock 보존 검증)
# ===========================================================================
class TestTaskIdConcurrency:
    def test_generate_task_id_uses_flock(self):
        """generate_task_id의 코드에 fcntl.flock 호출 보존."""
        import inspect
        src = inspect.getsource(dispatch.generate_task_id)
        assert "fcntl.flock" in src
        assert "LOCK_EX" in src


# ===========================================================================
# 3-4. metadata: frontmatter 파싱 + legacy fallback (qc_verify에 위임 영역)
# ===========================================================================
class TestMetadataIntegration:
    def test_parse_allowed_resources_yaml(self):
        """allowed_resources YAML 파싱 (audit 영역)."""
        task_desc = """## allowed_resources

```yaml
allowed_resources:
  paths:
    - "src/**"
  commands:
    - "pytest"
  ttl_hours: 24
```
"""
        result = dispatch._parse_allowed_resources(task_desc)
        assert result is not None
        assert "paths" in result
        assert "src/**" in result["paths"]

    def test_parse_allowed_resources_no_block_returns_none(self):
        """yaml 블록이 없으면 None 반환."""
        result = dispatch._parse_allowed_resources("plain text without yaml")
        assert result is None


# ===========================================================================
# 5-6. prompt: build_prompt + slim
# ===========================================================================
class TestPromptFacade:
    def test_facade_exports_build_prompt(self):
        """prompt facade가 build_prompt를 노출한다."""
        assert hasattr(dispatch_prompt, "build_prompt")
        assert dispatch_prompt.build_prompt is dispatch.build_prompt

    def test_build_prompt_unknown_team_exits(self):
        """unknown team_id에 대해 sys.exit 호출 (task-2386 정상 동작)."""
        with pytest.raises(SystemExit):
            dispatch.build_prompt(
                team_id="invalid-team",
                task_desc="test",
                task_id="task-1",
            )


# ===========================================================================
# 7-8. retry: archived/escalated 박제 가드
# ===========================================================================
class TestRetryFacade:
    def test_facade_exports_set_task_status(self):
        """retry facade가 _set_task_status를 노출한다."""
        assert hasattr(dispatch_retry, "_set_task_status")
        assert dispatch_retry._set_task_status is dispatch._set_task_status

    def test_set_task_status_blocks_archived(self, tmp_path, monkeypatch):
        """task-2387: archived 상태는 영구 박제. False 반환."""
        timer_file = tmp_path / "task-timers.json"
        timer_file.parent.mkdir(parents=True, exist_ok=True)
        (tmp_path / "memory").mkdir(parents=True, exist_ok=True)
        actual_timer = tmp_path / "memory" / "task-timers.json"
        actual_timer.write_text(json.dumps({
            "tasks": {"task-archived": {"status": "archived"}}
        }))
        monkeypatch.setattr(dispatch, "WORKSPACE", tmp_path)

        result = dispatch._set_task_status("task-archived", "running")
        assert result is False, "archived는 변경 차단 (False) 반환"

    def test_set_task_status_blocks_escalated(self, tmp_path, monkeypatch):
        """task-2387: escalated 상태도 영구 박제."""
        (tmp_path / "memory").mkdir(parents=True, exist_ok=True)
        actual_timer = tmp_path / "memory" / "task-timers.json"
        actual_timer.write_text(json.dumps({
            "tasks": {"task-esc": {"status": "escalated"}}
        }))
        monkeypatch.setattr(dispatch, "WORKSPACE", tmp_path)

        result = dispatch._set_task_status("task-esc", "running")
        assert result is False


# ===========================================================================
# 9-10. core: --team / --cancel argparse 진입
# ===========================================================================
class TestCoreFacade:
    def test_facade_exports_main_dispatch_cancel(self):
        """core facade가 main / dispatch / cancel_task 노출."""
        assert hasattr(dispatch_core, "main")
        assert hasattr(dispatch_core, "dispatch")
        assert hasattr(dispatch_core, "cancel_task")
        assert dispatch_core.main is dispatch.main

    def test_dispatch_py_shim_help(self):
        """dispatch.py shim이 --help 정상 출력 (script 진입점 동작)."""
        result = subprocess.run(
            [sys.executable, str(_WORKSPACE / "dispatch.py"), "--help"],
            capture_output=True,
            text=True,
            timeout=15,
        )
        assert result.returncode == 0
        assert "작업 위임 디스패처" in result.stdout

    def test_cancel_task_invalid_id_returns_error(self):
        """cancel_task가 잘못된 ID에 대해 error status 반환."""
        result = dispatch.cancel_task("not-valid")
        assert result["status"] == "error"


# ===========================================================================
# 11. audit: capability snapshot + ALLOWED_COMMANDS 노출
# ===========================================================================
class TestAuditFacade:
    def test_facade_exports_constants_and_functions(self):
        """audit facade가 ALLOWED_COMMANDS + 핵심 함수 노출."""
        assert hasattr(dispatch_audit, "ALLOWED_COMMANDS")
        assert hasattr(dispatch_audit, "_save_capability_snapshot")
        assert hasattr(dispatch_audit, "_check_affected_files_overlap")
        assert hasattr(dispatch_audit, "_validate_composite_teams")

    def test_audit_constants_identity(self):
        """ALLOWED_COMMANDS가 dispatch namespace와 동일 객체."""
        assert dispatch_audit.ALLOWED_COMMANDS is dispatch.ALLOWED_COMMANDS


# ===========================================================================
# 12. integration: 전체 dispatch 흐름 회귀 0
# ===========================================================================
class TestIntegrationRegression:
    def test_dispatch_namespace_exports_all(self):
        """dispatch namespace가 분리된 영역의 모든 핵심 함수를 노출."""
        for name in (
            # task_id
            "generate_task_id", "_resolve_main_workspace", "_compute_next_id_from_timers",
            # retry
            "_set_task_status", "_patch_timer_metadata",
            # prompt
            "build_prompt", "_resolve_resume", "_inject_project_map_context",
            # audit
            "_check_affected_files_overlap", "_validate_composite_teams",
            "_is_insuro_server_change", "_inject_platform_rules",
            "_auto_inject_affected_files", "_save_capability_snapshot",
            # core
            "dispatch", "cancel_task", "main",
        ):
            assert hasattr(dispatch, name), f"dispatch.{name} 누락"

    def test_external_test_compat_subprocess_mock(self, tmp_path):
        """외부 테스트 호환: patch('dispatch.subprocess.run')이 audit 함수에 영향."""
        from unittest.mock import MagicMock
        mock_result = MagicMock()
        mock_result.stdout = "src/foo.py\nsrc/bar.py\n"

        with patch("dispatch.subprocess.run", return_value=mock_result):
            result = dispatch._auto_inject_affected_files(
                "테스트 `Sample` 클래스 수정", "/workspace"
            )
        assert "## affected_files (auto-detected)" in result


# ===========================================================================
# S1. facade 6 모듈 import 모두 동작
# ===========================================================================
class TestScenarioS1FacadeImports:
    def test_all_six_facades_importable(self):
        """6 모듈 facade 모두 import 가능."""
        from dispatch import _state, audit, core, prompt, retry, task_id  # noqa: F401


# ===========================================================================
# S2. dispatch.py 호환 shim
# ===========================================================================
class TestScenarioS2ShimCompat:
    def test_shim_routes_to_main(self):
        """dispatch.py shim이 dispatch.core.main으로 routing."""
        shim_text = (_WORKSPACE / "dispatch.py").read_text()
        assert "from dispatch.core import main" in shim_text
        assert 'if __name__ == "__main__"' in shim_text


# ===========================================================================
# S3. 외부 mock 호환 (logger / WORKSPACE)
# ===========================================================================
class TestScenarioS3MockCompat:
    def test_mock_dispatch_logger_propagates(self):
        """patch('dispatch.logger') 적용 시 dispatch namespace 함수에 반영."""
        import inspect
        # logger가 dispatch.__dict__에 있어야 mock이 동작
        assert "logger" in dispatch.__dict__


# ===========================================================================
# S4. dispatch namespace 함수 개수 (분리 전후 동일)
# ===========================================================================
class TestScenarioS4NamespaceParity:
    def test_dispatch_has_minimum_function_count(self):
        """분리 후 dispatch namespace에 핵심 함수 30+개 노출."""
        public_funcs = [
            n for n in dir(dispatch)
            if not n.startswith("_") and callable(getattr(dispatch, n, None))
        ]
        # 보수적 임계: build_prompt, dispatch, cancel_task, generate_task_id, etc.
        assert len(public_funcs) >= 5


# ===========================================================================
# S5. 본체 무변경 (회귀 0 보증)
# ===========================================================================
class TestScenarioS5BodyUnchanged:
    def test_init_py_size_matches_legacy(self):
        """dispatch/__init__.py가 원본 dispatch.py 4336줄과 동일 라인 수 (±10)."""
        init_lines = (_WORKSPACE / "dispatch" / "__init__.py").read_text().count("\n")
        # 헤더 docstring만 변경되었으므로 ±10줄 허용
        assert 4326 <= init_lines <= 4346, f"init.py 라인 수 {init_lines} (예상 ~4336)"
