"""
test_memory_enforcement.py

task-2419 통합 회귀 테스트:
- Fix 1: scripts/hooks/pre-commit-memory-check.py (★ 마커 의무 검증)
- Fix 2: scripts/memory_violation_detector.py (위반 자동 감지 CLI)
- Fix 3: utils/memory_check.py 스키마 확장 (pending/acknowledged/structured)

격리: pytest tmp_path + 환경변수 override (WORKSPACE_ROOT, MEMORY_CHECK_FEEDBACK_DIR)
회귀 0: 기존 issue_mc 정상 발급 + memory_items_read 평면 배열 유지
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

import pytest

WORKSPACE_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(WORKSPACE_ROOT))

from utils.memory_check import (  # type: ignore[import-not-found]  # noqa: E402
    ack_mc,
    get_mc_by_task,
    get_pending_mcs,
    issue_mc,
)


# ===========================================================================
# Fix 1 — pre-commit hook
# ===========================================================================


def _init_repo(tmp_path: Path) -> Path:
    """임시 git repo 초기화. workspace 구조 모방."""
    repo = tmp_path / "repo"
    repo.mkdir()
    subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
    subprocess.run(["git", "config", "user.email", "t@t"], cwd=repo, check=True)
    subprocess.run(["git", "config", "user.name", "t"], cwd=repo, check=True)
    (repo / "memory").mkdir()
    (repo / "memory" / "MEMORY.md").write_text(
        "# WS Memory\n- 일반 항목\n", encoding="utf-8"
    )
    return repo


def _run_precommit(repo: Path, env_extra: dict[str, str] | None = None) -> tuple[int, str]:
    """pre-commit-memory-check.py를 임시 repo에서 실행. (exit_code, stderr)."""
    hook_path = WORKSPACE_ROOT / "scripts" / "hooks" / "pre-commit-memory-check.py"
    env = os.environ.copy()
    env["WORKSPACE_ROOT"] = str(repo)
    env["MEMORY_CHECK_FEEDBACK_DIR"] = str(repo / "anu_memory_nonexist")
    if env_extra:
        env.update(env_extra)
    result = subprocess.run(
        ["python3", str(hook_path)],
        cwd=repo,
        capture_output=True,
        text=True,
        env=env,
    )
    return result.returncode, result.stderr


class TestFix1PreCommitMemoryCheck:
    def test_block_unregistered_feedback(self, tmp_path):
        """신규 feedback_*.md 파일 + ★ 미등록 → exit 1."""
        repo = _init_repo(tmp_path)
        fb = repo / "feedback_new_rule.md"
        fb.write_text("새 피드백\n", encoding="utf-8")
        subprocess.run(["git", "add", "feedback_new_rule.md"], cwd=repo, check=True)

        code, err = _run_precommit(repo)

        assert code == 1
        assert "feedback_new_rule.md" in err

    def test_pass_when_registered(self, tmp_path):
        """★ 마커로 MEMORY.md에 등록된 feedback → exit 0."""
        repo = _init_repo(tmp_path)
        (repo / "memory" / "MEMORY.md").write_text(
            "# WS Memory\n★ feedback_new_rule.md — 신규 룰\n", encoding="utf-8"
        )
        fb = repo / "feedback_new_rule.md"
        fb.write_text("새 피드백\n", encoding="utf-8")
        subprocess.run(["git", "add", "memory/MEMORY.md", "feedback_new_rule.md"], cwd=repo, check=True)

        code, _ = _run_precommit(repo)

        assert code == 0

    def test_skip_env_bypass(self, tmp_path):
        """SKIP_MEMORY_CHECK=1 → 미등록이어도 exit 0."""
        repo = _init_repo(tmp_path)
        fb = repo / "feedback_emergency.md"
        fb.write_text("긴급\n", encoding="utf-8")
        subprocess.run(["git", "add", "feedback_emergency.md"], cwd=repo, check=True)

        code, _ = _run_precommit(repo, env_extra={"SKIP_MEMORY_CHECK": "1"})

        assert code == 0

    def test_test_files_excluded(self, tmp_path):
        """feedback_test_*.md 패턴 → 검사 제외, exit 0."""
        repo = _init_repo(tmp_path)
        fb = repo / "feedback_test_fixture.md"
        fb.write_text("test fixture\n", encoding="utf-8")
        subprocess.run(["git", "add", "feedback_test_fixture.md"], cwd=repo, check=True)

        code, _ = _run_precommit(repo)

        assert code == 0


# ===========================================================================
# Fix 2 — memory_violation_detector CLI
# ===========================================================================


def _run_detector(repo: Path, args: list[str]) -> tuple[int, str, str]:
    """detector CLI를 임시 repo에서 실행. (exit_code, stdout, stderr)."""
    cli = WORKSPACE_ROOT / "scripts" / "memory_violation_detector.py"
    env = os.environ.copy()
    env["WORKSPACE_ROOT"] = str(repo)
    result = subprocess.run(
        ["python3", str(cli), *args],
        cwd=repo,
        capture_output=True,
        text=True,
        env=env,
    )
    return result.returncode, result.stdout, result.stderr


def _seed_rules(repo: Path, rules_yaml: str) -> None:
    """memory/specs/memory-violation-rules.yaml 작성."""
    spec_dir = repo / "memory" / "specs"
    spec_dir.mkdir(parents=True, exist_ok=True)
    (spec_dir / "memory-violation-rules.yaml").write_text(rules_yaml, encoding="utf-8")


def _commit(repo: Path, msg: str, files: list[tuple[str, str]]) -> str:
    """파일 작성 + 커밋. commit SHA 반환."""
    for path, content in files:
        full = repo / path
        full.parent.mkdir(parents=True, exist_ok=True)
        full.write_text(content, encoding="utf-8")
        subprocess.run(["git", "add", path], cwd=repo, check=True)
    subprocess.run(
        ["git", "commit", "-q", "--no-verify", "-m", msg],
        cwd=repo,
        check=True,
    )
    sha = subprocess.run(
        ["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True
    ).stdout.strip()
    return sha


class TestFix2ViolationDetector:
    RULES_YAML = """version: "1.0"
rules:
  - id: rule-A
    name: "Forbidden import"
    target: "changed_files"
    file_pattern: "*.py"
    pattern_regex: "^import forbidden_module"
    severity: high
    description: "Forbidden module import"
  - id: rule-B
    name: "Bad commit phrase"
    target: "commit_messages"
    pattern_regex: "BADWORD"
    severity: high
    description: "Bad commit phrase"
  - id: rule-C
    name: "MC pending"
    target: "log_entries"
    log_field: "pending"
    expected_value: false
    severity: medium
    description: "MC pending"
"""

    def test_clean_commit_passes(self, tmp_path):
        """위반 없는 commit → exit 0."""
        repo = _init_repo(tmp_path)
        _seed_rules(repo, self.RULES_YAML)
        sha = _commit(repo, "clean: add module", [("ok.py", "import os\n")])

        code, stdout, _ = _run_detector(repo, ["--commit", sha])

        assert code == 0
        assert "PASS" in stdout

    def test_changed_files_violation(self, tmp_path):
        """changed_files 룰 위반 → exit 1, rule-A 보고."""
        repo = _init_repo(tmp_path)
        _seed_rules(repo, self.RULES_YAML)
        sha = _commit(
            repo, "feat: add", [("bad.py", "import forbidden_module\n")]
        )

        code, _, stderr = _run_detector(repo, ["--commit", sha])

        assert code == 1
        assert "rule-A" in stderr

    def test_commit_message_violation(self, tmp_path):
        """commit_messages 룰 위반 → exit 1, rule-B 보고."""
        repo = _init_repo(tmp_path)
        _seed_rules(repo, self.RULES_YAML)
        sha = _commit(repo, "fix: BADWORD merged", [("ok.py", "x = 1\n")])

        code, _, stderr = _run_detector(repo, ["--commit", sha])

        assert code == 1
        assert "rule-B" in stderr

    def test_log_entries_violation(self, tmp_path):
        """log_entries 룰: pending=True인 entry → exit 1."""
        repo = _init_repo(tmp_path)
        _seed_rules(repo, self.RULES_YAML)
        log_file = repo / "memory" / "memory-check-log.json"
        log_file.write_text(
            json.dumps(
                {
                    "checks": [
                        {
                            "mc_id": "MC-0001",
                            "task_id": "task-X",
                            "pending": True,
                        }
                    ]
                }
            ),
            encoding="utf-8",
        )

        code, _, stderr = _run_detector(repo, ["--task-id", "task-X"])

        assert code == 1
        assert "rule-C" in stderr

    def test_staged_mode(self, tmp_path):
        """--staged 모드 동작."""
        repo = _init_repo(tmp_path)
        _seed_rules(repo, self.RULES_YAML)
        # 초기 commit 1개 필요
        _commit(repo, "init", [("seed.txt", "x")])
        bad = repo / "evil.py"
        bad.write_text("import forbidden_module\n", encoding="utf-8")
        subprocess.run(["git", "add", "evil.py"], cwd=repo, check=True)

        code, _, stderr = _run_detector(repo, ["--staged"])

        assert code == 1
        assert "rule-A" in stderr


# ===========================================================================
# Fix 3 — MC schema (pending/acknowledged/structured)
# ===========================================================================


@pytest.fixture
def isolated_log(tmp_path):
    """memory_check 모듈의 기본 경로를 tmp_path로 격리.

    feedback_dir는 비어있는 디렉토리로 두어 매칭 0건 보장.
    """
    log_path = tmp_path / "memory-check-log.json"
    memory_md = tmp_path / "MEMORY.md"
    memory_md.write_text(
        "# WS\n★ 항목 1: 절대 X 금지\n★ 항목 2: 항상 Y 수행\n",
        encoding="utf-8",
    )
    feedback_dir = tmp_path / "anu_feedback"
    feedback_dir.mkdir()

    return {
        "log_path": log_path,
        "memory_path": memory_md,
        "feedback_dir": feedback_dir,
    }


class TestFix3MCSchema:
    def test_issue_mc_pending_default_true(self, isolated_log):
        """issue_mc 직후 pending=True, acknowledged=False, acked_at=None."""
        result = issue_mc(
            "task-X",
            "test desc",
            log_path=isolated_log["log_path"],
            memory_path=isolated_log["memory_path"],
            feedback_dir=isolated_log["feedback_dir"],
            _skip_anu_memory=True,
        )

        assert result["pending"] is True
        assert result["acknowledged"] is False

        with open(isolated_log["log_path"], encoding="utf-8") as f:
            data = json.load(f)
        last = data["checks"][-1]
        assert last["pending"] is True
        assert last["acknowledged"] is False
        assert last["acked_at"] is None

    def test_issue_mc_memory_items_read_preserved(self, isolated_log):
        """회귀 0: 평면 memory_items_read 배열 유지."""
        result = issue_mc(
            "task-Y",
            "test",
            log_path=isolated_log["log_path"],
            memory_path=isolated_log["memory_path"],
            feedback_dir=isolated_log["feedback_dir"],
            _skip_anu_memory=True,
        )

        assert isinstance(result["memory_items_read"], list)
        assert len(result["memory_items_read"]) >= 2  # ★ 항목 2개

    def test_issue_mc_structured_items(self, isolated_log):
        """memory_items_structured: memory_item_id/source_path/item_type/ack_required 포함."""
        result = issue_mc(
            "task-Z",
            "test",
            log_path=isolated_log["log_path"],
            memory_path=isolated_log["memory_path"],
            feedback_dir=isolated_log["feedback_dir"],
            _skip_anu_memory=True,
        )

        items = result["memory_items_structured"]
        assert len(items) >= 2
        for it in items:
            assert set(["memory_item_id", "source_path", "item_type", "ack_required"]).issubset(
                it.keys()
            )
            assert it["ack_required"] is True

    def test_ack_mc_success(self, isolated_log):
        """ack_mc 호출 후 pending=False, acknowledged=True, acked_at 기록."""
        result = issue_mc(
            "task-A",
            "test",
            log_path=isolated_log["log_path"],
            memory_path=isolated_log["memory_path"],
            feedback_dir=isolated_log["feedback_dir"],
            _skip_anu_memory=True,
        )
        mc_id = result["mc_id"]

        ok = ack_mc(mc_id, log_path=isolated_log["log_path"])

        assert ok is True
        with open(isolated_log["log_path"], encoding="utf-8") as f:
            data = json.load(f)
        entry = next(e for e in data["checks"] if e["mc_id"] == mc_id)
        assert entry["pending"] is False
        assert entry["acknowledged"] is True
        assert entry["acked_at"] is not None

    def test_ack_mc_unknown_id(self, isolated_log):
        """존재하지 않는 mc_id → False."""
        # 빈 로그 파일 작성 (lock 경로 부모 보장)
        isolated_log["log_path"].parent.mkdir(parents=True, exist_ok=True)
        isolated_log["log_path"].write_text('{"checks": []}', encoding="utf-8")

        ok = ack_mc("MC-9999", log_path=isolated_log["log_path"])

        assert ok is False

    def test_get_pending_mcs(self, isolated_log):
        """get_pending_mcs: ack 안 된 entries만 반환."""
        r1 = issue_mc(
            "task-P1",
            "t",
            log_path=isolated_log["log_path"],
            memory_path=isolated_log["memory_path"],
            feedback_dir=isolated_log["feedback_dir"],
            _skip_anu_memory=True,
        )
        r2 = issue_mc(
            "task-P2",
            "t",
            log_path=isolated_log["log_path"],
            memory_path=isolated_log["memory_path"],
            feedback_dir=isolated_log["feedback_dir"],
            _skip_anu_memory=True,
        )
        ack_mc(r1["mc_id"], log_path=isolated_log["log_path"])

        pending = get_pending_mcs(log_path=isolated_log["log_path"])

        ids = {e["mc_id"] for e in pending}
        assert r2["mc_id"] in ids
        assert r1["mc_id"] not in ids

    def test_get_mc_by_task(self, isolated_log):
        """get_mc_by_task: task_id로 가장 최근 entry 조회."""
        r = issue_mc(
            "task-Q",
            "t",
            log_path=isolated_log["log_path"],
            memory_path=isolated_log["memory_path"],
            feedback_dir=isolated_log["feedback_dir"],
            _skip_anu_memory=True,
        )

        entry = get_mc_by_task("task-Q", log_path=isolated_log["log_path"])

        assert entry is not None
        assert entry["mc_id"] == r["mc_id"]
        assert entry["pending"] is True
