# -*- coding: utf-8 -*-
"""task-2729+12 회귀 테스트 — B안 CODE/DATA 분리 + legacy NOOP 검증.

아르고스(QA) 작성.

검증 범위:
  A. CODE/DATA 분리: entrypoint.sh / anu-pickup.service / anu-pickup.path 파일 파싱.
  B. legacy NOOP 회귀: isolated temp DATA root 를 사용하여 driver scan_once 동작 검증.
"""
from __future__ import annotations

import importlib.util as _ilu
import os
import sys
import time
from pathlib import Path

# ── repo 루트 도출 (tests/regression/__file__ 기준 두 단계 상위) ──────────────
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# ── sys.path 보정 및 dispatch 패키지 worktree 본으로 고정 ────────────────────
# regression/conftest.py 가 worktree root 를 sys.path[0] 에 보장하지만,
# tests/conftest.py 가 먼저 canonical dispatch 를 sys.modules 에 캐싱하는 경우
# submodule(anu_pickup_driver 등)이 canonical 에 없으면 ModuleNotFoundError.
# 아래 패턴(test_anu_pickup_driver_2721.py 와 동일)으로 worktree 본을 강제 고정한다.
if _REPO_ROOT not in sys.path:
    sys.path.insert(0, _REPO_ROOT)

_real_dispatch_init = Path(_REPO_ROOT) / "dispatch" / "__init__.py"
_cached_dispatch = sys.modules.get("dispatch")
if _cached_dispatch is None or (
    getattr(_cached_dispatch, "__file__", "") or ""
) != str(_real_dispatch_init):
    for _k in [k for k in list(sys.modules) if k == "dispatch" or k.startswith("dispatch.")]:
        del sys.modules[_k]
    _spec = _ilu.spec_from_file_location(
        "dispatch",
        _real_dispatch_init,
        submodule_search_locations=[str(Path(_REPO_ROOT) / "dispatch")],
    )
    assert _spec is not None and _spec.loader is not None
    _pkg = _ilu.module_from_spec(_spec)
    sys.modules["dispatch"] = _pkg
    _spec.loader.exec_module(_pkg)

_ENTRYPOINT_SH = os.path.join(_REPO_ROOT, "scripts", "anu_pickup_entrypoint.sh")
_SERVICE_FILE = os.path.join(_REPO_ROOT, "deploy", "systemd", "anu-pickup.service")
_PATH_FILE = os.path.join(_REPO_ROOT, "deploy", "systemd", "anu-pickup.path")


# ═══════════════════════════════════════════════════════════════════════════════
# A. CODE/DATA 분리 (파일 파싱 검증)
# ═══════════════════════════════════════════════════════════════════════════════

def _read(path: str) -> str:
    with open(path, "r", encoding="utf-8") as fh:
        return fh.read()


def test_entrypoint_cd_pythonpath_uses_code_root():
    """entrypoint.sh 가 cd 와 PYTHONPATH 를 CODE_ROOT 기반으로 사용한다."""
    content = _read(_ENTRYPOINT_SH)

    # 반드시 존재해야 할 라인들
    assert 'cd "${CODE_ROOT}"' in content, (
        "entrypoint.sh 에 `cd \"${CODE_ROOT}\"` 가 없습니다"
    )
    assert 'PYTHONPATH="${CODE_ROOT}"' in content, (
        "entrypoint.sh 에 `PYTHONPATH=\"${CODE_ROOT}\"` 가 없습니다"
    )

    # WORKSPACE 기반 cd / PYTHONPATH 는 없어야 함
    assert 'cd "${WORKSPACE}"' not in content, (
        "entrypoint.sh 에 `cd \"${WORKSPACE}\"` 가 남아 있습니다 — CODE/DATA 분리 위반"
    )
    assert 'PYTHONPATH="${WORKSPACE}"' not in content, (
        "entrypoint.sh 에 `PYTHONPATH=\"${WORKSPACE}\"` 가 남아 있습니다 — CODE/DATA 분리 위반"
    )


def test_entrypoint_flag_lock_stay_canonical():
    """entrypoint.sh 의 FLAG_FILE 과 LOCK_DIR 은 ${WORKSPACE}(canonical DATA) 기반이어야 한다."""
    content = _read(_ENTRYPOINT_SH)

    # FLAG_FILE 은 WORKSPACE 기반
    assert 'FLAG_FILE="${WORKSPACE}/' in content, (
        "FLAG_FILE 이 ${WORKSPACE} 기반이 아닙니다"
    )

    # LOCK_DIR fallback 도 WORKSPACE 기반 (XDG_RUNTIME_DIR 없을 때 경로)
    assert 'LOCK_DIR="${WORKSPACE}/' in content, (
        "LOCK_DIR fallback 이 ${WORKSPACE} 기반이 아닙니다"
    )

    # FLAG_FILE / LOCK_DIR 이 CODE_ROOT 기반으로 설정되어서는 안 됨
    assert 'FLAG_FILE="${CODE_ROOT}' not in content, (
        "FLAG_FILE 이 CODE_ROOT 기반으로 변경되었습니다 — DATA canonical 위반"
    )
    assert 'LOCK_DIR="${CODE_ROOT}' not in content, (
        "LOCK_DIR 이 CODE_ROOT 기반으로 변경되었습니다 — DATA canonical 위반"
    )


def test_entrypoint_code_root_default():
    """CODE_ROOT 기본값이 ${HOME}/p0b-pickup-main 을 참조한다."""
    content = _read(_ENTRYPOINT_SH)

    assert 'CODE_ROOT="${PICKUP_CODE_ROOT:-${HOME}/p0b-pickup-main}"' in content, (
        "CODE_ROOT 기본값 라인이 없거나 형식이 다릅니다"
    )


def test_entrypoint_code_root_fail_closed():
    """CODE_ROOT 부재 또는 driver 파일 누락 시 fail-closed 블록이 존재한다 (git 의존 제거)."""
    content = _read(_ENTRYPOINT_SH)

    # CODE_ROOT 디렉토리 부재 조건 ([[ ! -d "${CODE_ROOT}" ]])
    assert '! -d "${CODE_ROOT}"' in content, (
        "entrypoint.sh 에 CODE_ROOT 디렉토리 부재 조건 `! -d \"${CODE_ROOT}\"` 이 없습니다"
    )

    # driver 파일 존재 체크 (fail-closed 핵심)
    assert "${CODE_ROOT}/dispatch/anu_pickup_driver.py" in content, (
        "entrypoint.sh 에 driver 파일 체크 `${CODE_ROOT}/dispatch/anu_pickup_driver.py` 가 없습니다"
    )

    # fail-closed: exit 0 (no-op) 이 CODE_ROOT 관련 블록에 있어야 함
    assert "exit 0" in content, (
        "entrypoint.sh 에 fail-closed exit 0 (no-op) 이 없습니다"
    )

    # git 의존 제거 회귀 보호: git -C 가 더 이상 없어야 함
    assert "git -C" not in content, (
        "entrypoint.sh 에 `git -C` 가 남아 있습니다 — git 의존이 제거되지 않았습니다"
    )


def test_service_points_to_code_root():
    """anu-pickup.service 가 CODE_ROOT(%h/p0b-pickup-main) 를 참조한다."""
    content = _read(_SERVICE_FILE)

    assert "WorkingDirectory=%h/p0b-pickup-main" in content, (
        "anu-pickup.service WorkingDirectory 가 %h/p0b-pickup-main 이 아닙니다"
    )
    assert "Environment=PYTHONPATH=%h/p0b-pickup-main" in content, (
        "anu-pickup.service Environment PYTHONPATH 가 %h/p0b-pickup-main 이 아닙니다"
    )
    assert "%h/p0b-pickup-main/scripts/" in content, (
        "anu-pickup.service ExecStart 이 %h/p0b-pickup-main/scripts/ 를 참조하지 않습니다"
    )


def test_path_unit_watches_canonical_events():
    """anu-pickup.path 는 canonical events 경로(%h/workspace/memory/events/task-*.result.json)를 watch 한다."""
    content = _read(_PATH_FILE)

    assert "PathExistsGlob=%h/workspace/memory/events/task-*.result.json" in content, (
        "anu-pickup.path 가 canonical events 경로를 watch 하지 않습니다"
    )

    # CODE_ROOT 경로(%h/p0b-pickup-main)를 watch 해서는 안 됨
    assert "PathExistsGlob=%h/p0b-pickup-main" not in content, (
        "anu-pickup.path 가 CODE_ROOT 경로를 watch 하도록 잘못 변경되었습니다"
    )


# ═══════════════════════════════════════════════════════════════════════════════
# B. legacy NOOP 회귀 (isolated temp DATA root)
# ═══════════════════════════════════════════════════════════════════════════════

def _make_events_dir(tmp_path):
    """tmp_path 에 memory/events 와 memory/state 구조를 생성하고 events 디렉토리를 반환."""
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    state_dir = tmp_path / "memory" / "state"
    state_dir.mkdir(parents=True, exist_ok=True)
    return events_dir


def _enable_driver(tmp_path):
    """memory/state/p0b_driver_enabled 에 'enabled' 기록."""
    flag = tmp_path / "memory" / "state" / "p0b_driver_enabled"
    flag.parent.mkdir(parents=True, exist_ok=True)
    flag.write_text("enabled\n", encoding="utf-8")


def _create_task_files(events_dir, names=None):
    """task-*.result.json 파일들을 과거 mtime 으로 생성하고 경로 목록 반환."""
    if names is None:
        names = [
            "task-A.result.json",
            "task-B.result.json",
            "task-C.result.json",
            "task-D.result.json",
        ]
    paths = []
    past_time = time.time() - 86400  # 하루 전
    for name in names:
        p = events_dir / name
        p.write_text("{}", encoding="utf-8")
        os.utime(str(p), (past_time, past_time))
        paths.append(str(p))
    return paths


def test_legacy_cutoff_noop_skip_zero_move(tmp_path):
    """legacy_cutoff=True + 미래 activation_epoch → 모든 task-* 파일이 NOOP_LEGACY_SKIP.
    파일 이동/삭제 0, quarantine 디렉토리 미생성 또는 비어있음."""
    from dispatch.anu_pickup_driver import (
        scan_once,
        VERDICT_NOOP_LEGACY_SKIP,
        QUARANTINE_DIR_REL,
    )

    events_dir = _make_events_dir(tmp_path)
    _enable_driver(tmp_path)
    task_files = _create_task_files(events_dir)

    # scan 전 파일 목록 스냅샷
    before = set(os.listdir(str(events_dir)))
    assert len(before) == len(task_files), "사전 조건: 파일 생성 수 불일치"

    future_epoch = time.time() + 1000.0  # 현재보다 1000초 미래

    records = scan_once(
        str(tmp_path),
        legacy_cutoff=True,
        activation_epoch=future_epoch,
        launcher_fn=None,
        write_evidence=False,
        max_files=1000,
        flag_reader=lambda: "enabled",
    )

    # scan 후 파일 목록 스냅샷
    after = set(os.listdir(str(events_dir)))

    # 검증 1: 모든 record 가 NOOP_LEGACY_SKIP
    task_records = [r for r in records if r.result_path]
    assert len(task_records) >= len(task_files), (
        f"task-* 파일 {len(task_files)}개에 대한 record 부족: {len(task_records)}건"
    )
    for rec in task_records:
        assert rec.verdict == VERDICT_NOOP_LEGACY_SKIP, (
            f"{rec.result_path}: verdict={rec.verdict!r} (NOOP_LEGACY_SKIP 기대)"
        )
        assert rec.quarantined is False, (
            f"{rec.result_path}: quarantined=True — legacy skip 시 quarantine 금지"
        )

    # 검증 2: 파일 목록 동일 (이동/삭제 0)
    assert before == after, (
        f"scan 전후 events 디렉토리 파일 목록이 달라졌습니다.\n"
        f"  before: {sorted(before)}\n"
        f"  after:  {sorted(after)}"
    )

    # 검증 3: quarantine 디렉토리 미생성 또는 비어있음
    quarantine_dir = tmp_path / QUARANTINE_DIR_REL.replace("/", os.sep)
    if quarantine_dir.exists():
        quarantine_files = list(quarantine_dir.iterdir())
        assert len(quarantine_files) == 0, (
            f"quarantine 디렉토리에 파일이 생성되었습니다: {quarantine_files}"
        )


def test_non_target_noop(tmp_path):
    """task- 로 시작하지 않는 파일은 NOOP_NOT_TARGET 을 반환한다."""
    from dispatch.anu_pickup_driver import (
        scan_once,
        VERDICT_NOOP_NOT_TARGET,
    )

    events_dir = _make_events_dir(tmp_path)
    _enable_driver(tmp_path)

    # 비대상 파일들
    non_target_names = [
        "foo.result.json",       # task- 로 시작하지 않음
        "task-x.tmp",            # .result.json 으로 끝나지 않음
        "bar-task-1.result.json",  # task- 로 시작하지 않음 (중간에 task 포함)
    ]
    for name in non_target_names:
        p = events_dir / name
        p.write_text("{}", encoding="utf-8")
        past_time = time.time() - 86400
        os.utime(str(p), (past_time, past_time))

    # 정상 대상 파일 1개도 포함
    target_file = events_dir / "task-target-001.result.json"
    target_file.write_text("{}", encoding="utf-8")
    past_time = time.time() - 86400
    os.utime(str(target_file), (past_time, past_time))

    non_target_paths = [str(events_dir / n) for n in non_target_names]

    # paths= 인자로 비대상 파일만 직접 전달
    records = scan_once(
        str(tmp_path),
        paths=non_target_paths,
        legacy_cutoff=False,
        launcher_fn=None,
        write_evidence=False,
        flag_reader=lambda: "enabled",
    )

    assert len(records) == len(non_target_paths), (
        f"비대상 파일 {len(non_target_paths)}개에 대해 record {len(records)}건 반환"
    )
    for rec in records:
        assert rec.verdict == VERDICT_NOOP_NOT_TARGET, (
            f"{rec.result_path}: verdict={rec.verdict!r} (NOOP_NOT_TARGET 기대)"
        )


def test_no_real_spawn_launcher_none(tmp_path):
    """launcher_fn=None 기본 → 어떤 record 도 real spawn/wake 를 하지 않는다.
    NOOP_LEGACY_SKIP 으로 surface-only 처리됨을 확인한다."""
    from dispatch.anu_pickup_driver import (
        scan_once,
        VERDICT_NOOP_LEGACY_SKIP,
        VERDICT_WAKE_BUILT,
    )

    events_dir = _make_events_dir(tmp_path)
    _enable_driver(tmp_path)
    task_files = _create_task_files(events_dir, names=[
        "task-spawn-test-001.result.json",
        "task-spawn-test-002.result.json",
    ])

    future_epoch = time.time() + 1000.0

    records = scan_once(
        str(tmp_path),
        legacy_cutoff=True,
        activation_epoch=future_epoch,
        launcher_fn=None,   # ← 명시적 None: real spawn 0
        write_evidence=False,
        max_files=1000,
        flag_reader=lambda: "enabled",
    )

    task_records = [r for r in records if r.result_path]
    assert len(task_records) >= len(task_files)

    # launcher_fn=None → WAKE_BUILT 없음 (real spawn 0)
    wake_records = [r for r in task_records if r.verdict == VERDICT_WAKE_BUILT]
    assert len(wake_records) == 0, (
        f"launcher_fn=None 인데 WAKE_BUILT record 가 {len(wake_records)}건 발생했습니다"
    )

    # legacy cutoff + 미래 epoch → 모두 NOOP_LEGACY_SKIP (surface only)
    for rec in task_records:
        assert rec.verdict == VERDICT_NOOP_LEGACY_SKIP, (
            f"{rec.result_path}: verdict={rec.verdict!r} — launcher_fn=None + legacy_cutoff 시 NOOP_LEGACY_SKIP 기대"
        )

    # fire_cron_id 가 설정되지 않음 (real launch 0)
    for rec in task_records:
        assert rec.fire_cron_id is None, (
            f"{rec.result_path}: fire_cron_id={rec.fire_cron_id!r} — launcher_fn=None 인데 설정됨"
        )
