"""task-2524+1 회귀 테스트 — automation observability v0

검증 7건:
1. mutation 정적 검사 PASS (gh pr merge / cokacdir --cron / --apply / git push 부재)
2. chat=6937032012 격리 (다른 chat record 자동 필터)
3. token raw value 0 노출 (ghs_/ghp_/github_pat_ prefix 부재)
4. 7 source 정합 (observability 7 키 모두 존재)
5. 기존 자동화 탭 회귀 (load_automation_status 응답에 observability 키 존재)
6. dead code 제거 검증 (routes_post.py 토글 2 함수 부재 + server.py router 등록 부재)
7. task-2526 영역 무수정 (parallel_safe 정합)
"""

from __future__ import annotations

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

# project import
PROJECT_ROOT = Path(__file__).resolve().parents[2]
# 워크트리 우선 (conftest의 /home/jay/workspace 인서트보다 앞에)
sys.path.insert(0, str(PROJECT_ROOT))
# 캐시된 dashboard 모듈 invalidate (다른 테스트가 /home/jay/workspace에서 먼저 로드했을 수 있음)
for _mod_name in [m for m in list(sys.modules) if m == "dashboard" or m.startswith("dashboard.")]:
    sys.modules.pop(_mod_name, None)

from dashboard.data_loader import DataLoader  # type: ignore[import-not-found]  # noqa: E402

# 검증: 본 테스트는 worktree 버전의 모듈을 검증해야 함
import dashboard.data_loader as _dashboard_data_loader_mod  # noqa: E402

_loaded_path = str(_dashboard_data_loader_mod.__file__ or "")
assert str(PROJECT_ROOT) in _loaded_path, (
    f"wrong dashboard.data_loader loaded: {_loaded_path}"
)

DASHBOARD = PROJECT_ROOT / "dashboard"
TESTS_2524 = Path(__file__)


# ────────────────────────────────────────────────
# 검증 1: mutation 정적 검사 PASS
# ────────────────────────────────────────────────
def test_observability_mutation_zero():
    """변경 4 파일에서 mutation 명령어 부재 (read-only 강제)."""
    targets = [
        DASHBOARD / "components" / "AutomationView.js",
        DASHBOARD / "components" / "AutomationObservabilitySection.js",
    ]
    forbidden_patterns = [
        r"gh\s+pr\s+merge",
        r"cokacdir\s+--cron",
        r"--apply\b",
        r"subprocess.*git\s+push",
        r"subprocess.*commit",
        r"fetch\(['\"][^'\"]*['\"]\s*,\s*\{[^}]*method:\s*['\"]POST",
        r"fetch\(['\"][^'\"]*['\"]\s*,\s*\{[^}]*method:\s*['\"]PUT",
        r"fetch\(['\"][^'\"]*['\"]\s*,\s*\{[^}]*method:\s*['\"]DELETE",
    ]
    for target in targets:
        assert target.exists(), f"missing: {target}"
        content = target.read_text(encoding="utf-8")
        for pat in forbidden_patterns:
            matches = re.findall(pat, content, re.MULTILINE | re.DOTALL)
            assert not matches, f"mutation pattern '{pat}' found in {target.name}: {matches}"

    # data_loader.py의 새 collector helper들도 mutation 부재
    dl_content = (DASHBOARD / "data_loader.py").read_text(encoding="utf-8")
    # "_collect_" prefix 영역만 추출
    collector_section = re.search(
        r"# ── observability v0.*?def toggle_pipeline_enabled",
        dl_content,
        re.DOTALL,
    )
    assert collector_section, "collector section not found"
    section_text = collector_section.group(0)
    for pat in [r"gh\s+pr\s+merge", r"cokacdir\s+--cron", r"--apply\b", r"git\s+push", r"git.*commit"]:
        assert not re.search(pat, section_text), f"mutation pattern '{pat}' found in collectors"


# ────────────────────────────────────────────────
# 검증 2: chat=6937032012 격리
# ────────────────────────────────────────────────
def test_chat_isolation_filters_other_chats(tmp_path):
    """다른 chat record는 schedule_history 응답에서 자동 필터됨."""
    history_dir = tmp_path / "schedule_history"
    history_dir.mkdir()
    log_path = history_dir / "TEST0001.log"
    records = [
        {"ts": "2026-05-10T09:00:00+09:00", "schedule_id": "TEST0001", "chat_id": 6937032012, "status": "ok", "prompt": "owner", "duration_ms": 100},
        {"ts": "2026-05-10T09:01:00+09:00", "schedule_id": "TEST0001", "chat_id": 9999999999, "status": "ok", "prompt": "OTHER_CHAT_LEAK_PROBE", "duration_ms": 200},
        {"ts": "2026-05-10T09:02:00+09:00", "schedule_id": "TEST0001", "chat_id": 1111111111, "status": "error", "prompt": "OTHER_CHAT_ERROR_PROBE", "duration_ms": 300},
    ]
    log_path.write_text("\n".join(json.dumps(r) for r in records) + "\n", encoding="utf-8")

    workspace = tmp_path / "workspace"
    (workspace / "memory").mkdir(parents=True)
    dl = DataLoader(workspace)

    with patch("dashboard.data_loader.Path") as mock_path:
        # only override the schedule_history path lookup
        original = Path

        def _path_side(arg):
            if arg == "/home/jay/.cokacdir/schedule_history":
                return history_dir
            return original(arg)

        mock_path.side_effect = _path_side
        result = dl._collect_schedule_history_chat_filtered(chat_id=6937032012)

    assert result["available"] is True
    assert result["chat_id"] == 6937032012
    # leak probes must NOT appear
    blob = json.dumps(result, ensure_ascii=False)
    assert "OTHER_CHAT_LEAK_PROBE" not in blob
    assert "OTHER_CHAT_ERROR_PROBE" not in blob
    # only chat=6937032012 record
    assert len(result["records"]) == 1
    assert result["records"][0]["schedule_id"] == "TEST0001"


# ────────────────────────────────────────────────
# 검증 3: token raw value 0 노출
# ────────────────────────────────────────────────
def test_token_raw_value_zero_exposure(tmp_path):
    """load_automation_status 응답 JSON에 token prefix 부재."""
    workspace = tmp_path / "workspace"
    (workspace / "memory").mkdir(parents=True)
    dl = DataLoader(workspace)
    # gh CLI mocked to fail (no output) so we don't actually call gh
    with patch("dashboard.data_loader.subprocess.run") as mock_run:
        mock_run.return_value = type("R", (), {"returncode": 1, "stdout": "", "stderr": ""})()
        result = dl.load_automation_status()

    blob = json.dumps(result, ensure_ascii=False)
    assert "ghs_" not in blob, "real token prefix ghs_ leaked"
    assert "ghp_" not in blob, "real token prefix ghp_ leaked"
    assert "github_pat_" not in blob, "real token prefix github_pat_ leaked"


# ────────────────────────────────────────────────
# 검증 4: 7 source 정합 (observability 7 키)
# ────────────────────────────────────────────────
def test_observability_7_keys_present(tmp_path):
    workspace = tmp_path / "workspace"
    (workspace / "memory").mkdir(parents=True)
    dl = DataLoader(workspace)
    with patch("dashboard.data_loader.subprocess.run") as mock_run:
        mock_run.return_value = type("R", (), {"returncode": 1, "stdout": "", "stderr": ""})()
        result = dl.load_automation_status()

    assert "observability" in result
    obs = result["observability"]
    expected_keys = {
        "task_timer",
        "schedule_history",
        "pr_state",
        "ci_gemini",
        "lifecycle",
        "token_source",
        "critical_7",
    }
    assert set(obs.keys()) == expected_keys, f"7 source mismatch: {set(obs.keys())} != {expected_keys}"


# ────────────────────────────────────────────────
# 검증 5: 기존 자동화 탭 회귀
# ────────────────────────────────────────────────
def test_load_automation_status_regression(tmp_path):
    """load_automation_status() 응답 200 등가 + observability 키 + active flag."""
    workspace = tmp_path / "workspace"
    (workspace / "memory").mkdir(parents=True)
    dl = DataLoader(workspace)
    with patch("dashboard.data_loader.subprocess.run") as mock_run:
        mock_run.return_value = type("R", (), {"returncode": 1, "stdout": "", "stderr": ""})()
        result = dl.load_automation_status()

    assert isinstance(result, dict)
    assert "observability" in result
    assert "last_updated" in result
    assert "active" in result


# ────────────────────────────────────────────────
# 검증 6: dead code 제거 검증
# ────────────────────────────────────────────────
def test_dead_code_removed():
    """routes_post.py 토글 2 함수 부재 + server.py router 등록 부재."""
    routes_post = (DASHBOARD / "routes_post.py").read_text(encoding="utf-8")
    assert "def handle_post_automation_toggle" not in routes_post, \
        "handle_post_automation_toggle still present (dead code)"
    assert "def handle_post_automation_system_toggle" not in routes_post, \
        "handle_post_automation_system_toggle still present (dead code)"
    assert "handle_post_automation_toggle" not in routes_post, \
        "handle_post_automation_toggle still referenced in __all__"
    assert "handle_post_automation_system_toggle" not in routes_post, \
        "handle_post_automation_system_toggle still referenced in __all__"

    server_py = (DASHBOARD / "server.py").read_text(encoding="utf-8")
    assert "/api/automation/toggle" not in server_py, "/api/automation/toggle router still registered"
    assert "/api/automation/system-toggle" not in server_py, "/api/automation/system-toggle router still registered"

    # AutomationView.js: COMPLETED_FEATURES + 토글 핸들러 부재
    av = (DASHBOARD / "components" / "AutomationView.js").read_text(encoding="utf-8")
    assert "COMPLETED_FEATURES" not in av, "COMPLETED_FEATURES dead code present"
    assert "handleSystemToggle" not in av, "handleSystemToggle dead code present"
    assert "handlePipelineToggle" not in av, "handlePipelineToggle dead code present"
    assert "/api/automation/toggle" not in av
    assert "/api/automation/system-toggle" not in av


# ────────────────────────────────────────────────
# 검증 7: task-2526 영역 무수정 (parallel_safe 정합)
# ────────────────────────────────────────────────
def test_task_2526_area_untouched():
    """parallel_safe — task-2526 / task-2523 영역의 본 task 변경분 부재."""
    forbidden_paths = [
        PROJECT_ROOT / "scripts" / "safe_cron_dispatch.py",
        PROJECT_ROOT / "utils" / "cron_targeting_audit.py",
        PROJECT_ROOT / "tests" / "regression" / "test_cron_session_safety_guard_2526.py",
        PROJECT_ROOT / "memory" / "specs" / "cron-targeting-spec.md",
    ]
    # 본 task의 expected_files (정확히 6 파일)
    expected_files = {
        DASHBOARD / "components" / "AutomationView.js",
        DASHBOARD / "components" / "AutomationObservabilitySection.js",
        DASHBOARD / "data_loader.py",
        DASHBOARD / "routes_post.py",
        DASHBOARD / "server.py",
        TESTS_2524,
    }
    # 모든 expected file이 존재
    for f in expected_files:
        assert f.exists(), f"expected file missing: {f}"
    # forbidden path는 expected와 무겹침 (parallel_safe 정합)
    for fp in forbidden_paths:
        assert fp not in expected_files, f"task-2526 영역 침범: {fp}"
