"""TDD RED Phase 4 테스트 모음.

대상 함수 (아직 미구현):
  - orchestrator.event_bus.scan_done_events : memory/events/*.done 스캔 → incoming/ 복사

기능 요약:
  - events_dir/*.done 파일을 스캔하여 incoming_dir/로 복사
  - 원본 .done 파일은 보존 (Layer 2 독립성)
  - 이미 incoming/ 또는 processed/에 있으면 건너뜀 (중복 방지)
  - symlink이면 거부 (보안)

작성자 : 헤임달 (dev2-team tester)
날짜   : 2026-03-24
"""

import os
import sys
from pathlib import Path

import pytest

# ---------------------------------------------------------------------------
# sys.path 세팅 — 미구현 함수 import 시도가 가능해야 함
# ---------------------------------------------------------------------------
WORKSPACE_ROOT = "/home/jay/workspace"
if WORKSPACE_ROOT not in sys.path:
    sys.path.insert(0, WORKSPACE_ROOT)

# ---------------------------------------------------------------------------
# 미구현 함수 import  (RED 단계: ImportError 발생이 정상)
# ---------------------------------------------------------------------------
from orchestrator.event_bus import consume_event, scan_done_events  # noqa: E402

# ===========================================================================
# T1. 단일 팀 완료 → Layer 2 알림 + Layer 3 이벤트 소비 독립 동작
# ===========================================================================


class TestSingleTeamDone:
    """T1: 단일 .done 파일 스캔 → Layer 2·3 독립 동작 확인."""

    def test_scan_copies_done_file_to_incoming(self, tmp_path):
        """events/에 .done 파일이 있으면 scan_done_events가 incoming/에 복사하고 파일명을 반환해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-001.done"
        (events_dir / event_file).write_text("done")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert event_file in copied, f"복사된 파일 목록에 {event_file}이 없음: {copied}"
        assert (incoming_dir / event_file).exists(), "incoming/에 .done 파일이 복사되지 않음"

    def test_scan_preserves_original_done_file(self, tmp_path):
        """scan_done_events 호출 후 원본 .done 파일이 events/에 그대로 남아 있어야 한다 (Layer 2 독립성)."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-001.done"
        (events_dir / event_file).write_text("done")

        scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert (events_dir / event_file).exists(), "scan 후 원본 .done 파일이 삭제됨 — Layer 2 독립성 위반"

    def test_consume_event_after_scan_moves_to_processed(self, tmp_path):
        """scan → incoming 복사 후 consume_event가 incoming → processed로 이동하고 True를 반환해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-001.done"
        (events_dir / event_file).write_text("done")

        scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))
        result = consume_event(str(incoming_dir), str(processed_dir), event_file)

        assert result is True, "scan 후 consume_event가 True를 반환하지 않음"
        assert (processed_dir / event_file).exists(), "consume 후 processed/에 파일이 없음"
        assert not (incoming_dir / event_file).exists(), "consume 후 incoming/에 파일이 남아있음"

    def test_original_done_file_survives_consume_event(self, tmp_path):
        """consume_event 완료 후에도 events/의 원본 .done 파일이 보존되어야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-001.done"
        (events_dir / event_file).write_text("done")

        scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))
        consume_event(str(incoming_dir), str(processed_dir), event_file)

        assert (
            events_dir / event_file
        ).exists(), "consume_event 완료 후 원본 .done 파일이 삭제됨 — Layer 2 독립성 위반"


# ===========================================================================
# T2. 중복 알림 방지 — scan 2회 연속 호출 시 복사 0건
# ===========================================================================


class TestDuplicatePrevention:
    """T2: 중복 방지 — 이미 incoming/ 또는 processed/에 있으면 재복사하지 않음."""

    def test_second_scan_returns_empty_when_already_in_incoming(self, tmp_path):
        """scan_done_events 2회 연속 호출 시 두 번째 호출에서는 복사 0건이어야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-002.done"
        (events_dir / event_file).write_text("done")

        first_copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))
        second_copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert event_file in first_copied, f"첫 번째 scan에서 {event_file}이 복사되지 않음"
        assert (
            event_file not in second_copied
        ), f"두 번째 scan에서 이미 incoming/에 있는 {event_file}을 다시 복사함 — 중복 방지 실패"
        assert len(second_copied) == 0, f"두 번째 scan 복사 건수가 0이어야 하는데 {len(second_copied)}건임"

    def test_scan_skips_file_already_in_processed(self, tmp_path):
        """processed/에 이미 있는 파일은 scan_done_events가 복사하지 않아야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-already-done.done"
        (events_dir / event_file).write_text("done")
        # processed/에 미리 배치 (이미 처리된 상태 시뮬레이션)
        (processed_dir / event_file).write_text("done")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert (
            event_file not in copied
        ), f"processed/에 이미 있는 {event_file}을 scan이 복사함 — 중복 파이프라인 시작 위험"

    def test_scan_copies_only_new_files_when_mixed(self, tmp_path):
        """신규 .done만 복사하고, 이미 incoming/ 또는 processed/에 있는 것은 건너뛰어야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        new_file = "pipeline-new.done"
        in_incoming = "pipeline-in-incoming.done"
        in_processed = "pipeline-in-processed.done"

        (events_dir / new_file).write_text("done")
        (events_dir / in_incoming).write_text("done")
        (events_dir / in_processed).write_text("done")
        (incoming_dir / in_incoming).write_text("done")
        (processed_dir / in_processed).write_text("done")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert new_file in copied, f"신규 파일 {new_file}이 복사 목록에 없음"
        assert in_incoming not in copied, f"이미 incoming/에 있는 {in_incoming}이 다시 복사됨"
        assert in_processed not in copied, f"이미 processed/에 있는 {in_processed}이 다시 복사됨"


# ===========================================================================
# T3. 동시 완료 시나리오 — 여러 .done 파일 동시 생성
# ===========================================================================


class TestConcurrentDoneFiles:
    """T3: 여러 .done 파일 동시 생성 → 모두 독립적으로 처리 가능."""

    def test_scan_copies_all_done_files_at_once(self, tmp_path):
        """events/에 여러 .done 파일이 있을 때 scan_done_events가 모두 incoming/에 복사해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_files = [
            "team-alpha.done",
            "team-beta.done",
            "team-gamma.done",
            "team-delta.done",
            "team-epsilon.done",
        ]
        for ef in event_files:
            (events_dir / ef).write_text("done")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert len(copied) == len(event_files), f"복사 건수가 {len(event_files)}이어야 하는데 {len(copied)}건임"
        for ef in event_files:
            assert ef in copied, f"{ef}이 복사 목록에 없음"
            assert (incoming_dir / ef).exists(), f"incoming/에 {ef}이 없음"

    def test_each_copied_file_can_be_consumed_independently(self, tmp_path):
        """incoming/에 복사된 각 파일을 독립적으로 consume_event 할 수 있어야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_files = ["team-A.done", "team-B.done", "team-C.done"]
        for ef in event_files:
            (events_dir / ef).write_text("done")

        scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        for ef in event_files:
            result = consume_event(str(incoming_dir), str(processed_dir), ef)
            assert result is True, f"{ef} 소비 시 True를 반환하지 않음"
            assert (processed_dir / ef).exists(), f"처리 후 processed/에 {ef}이 없음"

    def test_scan_preserves_all_original_done_files(self, tmp_path):
        """여러 .done 동시 스캔 후에도 모든 원본 파일이 events/에 보존되어야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_files = ["team-X.done", "team-Y.done", "team-Z.done"]
        for ef in event_files:
            (events_dir / ef).write_text("done")

        scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        for ef in event_files:
            assert (events_dir / ef).exists(), f"원본 {ef}이 scan 후 삭제됨 — Layer 2 독립성 위반"


# ===========================================================================
# T4. 수동 체이닝(chain_manager.py)과 auto_orch 자동 체이닝 공존 확인
# ===========================================================================


class TestManualAndAutoChainCoexistence:
    """T4: .done 원본 보존 + .done.notified 생성 — 양쪽 경로가 충돌 없이 독립 동작."""

    def test_scan_copies_to_incoming_while_notified_marker_coexists(self, tmp_path):
        """scan_done_events가 incoming/에 복사하는 동안 .done.notified 파일이 공존해도 충돌 없어야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-chain.done"
        notified_marker = "pipeline-chain.done.notified"

        (events_dir / event_file).write_text("done")
        # notify-completion.py 시뮬레이션: .done.notified 마커 생성
        (events_dir / notified_marker).write_text("notified")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert event_file in copied, "scan이 .done 파일을 복사하지 않음"
        assert (incoming_dir / event_file).exists(), "incoming/에 .done 파일이 없음"
        # .done.notified는 *.done 패턴이 아니므로 복사되지 않아야 함
        assert notified_marker not in copied, ".done.notified가 scan 결과에 포함됨"
        assert not (incoming_dir / notified_marker).exists(), ".done.notified가 incoming/에 복사됨"

    def test_original_done_available_for_both_paths_after_scan(self, tmp_path):
        """scan 후 원본 .done이 보존되어 chain_manager와 auto_orch 양쪽 모두 사용 가능해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-multi-consumer.done"
        original_content = "pipeline_id=multi-consumer"
        (events_dir / event_file).write_text(original_content)

        scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        # Layer 2 경로: 원본 파일 여전히 읽기 가능
        assert (events_dir / event_file).exists(), "원본 .done이 삭제되어 Layer 2 경로 차단됨"
        assert (events_dir / event_file).read_text() == original_content, "원본 .done 내용이 변경됨"

        # Layer 3 경로: incoming/에 복사본 존재
        assert (incoming_dir / event_file).exists(), "Layer 3 경로용 incoming/ 복사본이 없음"

    def test_notified_marker_does_not_affect_scan_result(self, tmp_path):
        """.done.notified 마커가 있어도 scan_done_events 결과가 .done 파일만 포함해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        # .done.notified만 있고 .done은 없는 경우
        (events_dir / "pipeline-notified-only.done.notified").write_text("notified")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert len(copied) == 0, ".done.notified만 있는데 scan이 파일을 복사함"


# ===========================================================================
# T5. .done → .done.clear rename 후 Layer 3가 재처리하지 않음
# ===========================================================================


class TestDoneClearRename:
    """T5: .done → .done.clear rename 후 scan이 무시하는지 확인."""

    def test_done_clear_is_ignored_by_scan(self, tmp_path):
        """.done.clear 파일은 *.done 패턴이 아니므로 scan_done_events가 무시해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        cleared_file = "pipeline-003.done.clear"
        (events_dir / cleared_file).write_text("cleared")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert cleared_file not in copied, ".done.clear 파일이 scan 결과에 포함됨"
        assert not (incoming_dir / cleared_file).exists(), ".done.clear 파일이 incoming/에 복사됨"

    def test_scan_then_rename_to_clear_then_no_reprocess(self, tmp_path):
        """scan → .done.clear rename → 재scan 시 재처리 없음을 확인한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-anu.done"
        clear_file = "pipeline-anu.done.clear"
        (events_dir / event_file).write_text("done")

        # 첫 번째 scan → incoming/에 복사
        first_copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))
        assert event_file in first_copied, "첫 번째 scan에서 복사 실패"

        # 아누 처리 시뮬레이션: .done → .done.clear rename
        (events_dir / event_file).rename(events_dir / clear_file)
        assert not (events_dir / event_file).exists(), ".done → .done.clear rename 실패"

        # 두 번째 scan → .done.clear는 무시, 이미 incoming/에 있는 것도 중복 없음
        second_copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert len(second_copied) == 0, f"rename 후 재scan에서 불필요한 복사가 발생함: {second_copied}"

    def test_invalid_pattern_task_done_clear_is_ignored(self, tmp_path):
        """task-123.done.clear 형식은 scan_done_events가 무시해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        (events_dir / "task-123.done.clear").write_text("cleared")
        (events_dir / "pipeline-valid.done").write_text("done")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert "task-123.done.clear" not in copied, "task-123.done.clear가 scan 결과에 포함됨"
        assert "pipeline-valid.done" in copied, "유효한 .done 파일이 scan 결과에 없음"


# ===========================================================================
# 추가: symlink 거부 테스트
# ===========================================================================


class TestSymlinkRejection:
    """scan_done_events의 symlink 보안 거부 테스트."""

    def test_scan_rejects_symlink_done_file(self, tmp_path):
        """events/의 .done 파일이 symlink이면 scan_done_events가 무시하고 복사하지 않아야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        real_file = tmp_path / "real.done"
        real_file.write_text("done")
        symlink_file = events_dir / "pipeline-symlink.done"
        symlink_file.symlink_to(real_file)

        assert symlink_file.is_symlink(), "symlink 생성 실패"

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert "pipeline-symlink.done" not in copied, "symlink .done 파일이 scan 결과에 포함됨 — 보안 거부 실패"
        assert not (
            incoming_dir / "pipeline-symlink.done"
        ).exists(), "symlink .done이 incoming/에 복사됨 — 보안 거부 실패"

    def test_scan_copies_real_file_but_skips_symlink(self, tmp_path):
        """실제 .done 파일은 복사하고 symlink .done은 건너뛰어야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        # 실제 파일
        (events_dir / "pipeline-real.done").write_text("done")

        # symlink 파일
        real_file = tmp_path / "real_target.done"
        real_file.write_text("done")
        (events_dir / "pipeline-symlink.done").symlink_to(real_file)

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert "pipeline-real.done" in copied, "실제 .done 파일이 복사 목록에 없음"
        assert "pipeline-symlink.done" not in copied, "symlink .done 파일이 복사 목록에 포함됨"


# ===========================================================================
# 추가: 엣지 케이스 테스트
# ===========================================================================


class TestEdgeCases:
    """빈 디렉토리, 미존재 디렉토리, 잘못된 패턴 등 엣지 케이스."""

    def test_empty_events_dir_returns_empty_list(self, tmp_path):
        """events_dir에 .done 파일이 없을 때 scan_done_events는 빈 리스트를 반환해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert copied == [], f"빈 디렉토리인데 빈 리스트가 아닌 {copied}를 반환함"

    def test_nonexistent_events_dir_returns_empty_list(self, tmp_path):
        """events_dir가 존재하지 않을 때 scan_done_events는 예외 없이 빈 리스트를 반환해야 한다."""
        events_dir = tmp_path / "nonexistent_events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        incoming_dir.mkdir()
        processed_dir.mkdir()

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert copied == [], f"미존재 디렉토리인데 빈 리스트가 아닌 {copied}를 반환함"

    def test_scan_returns_list_type(self, tmp_path):
        """scan_done_events는 항상 list를 반환해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        result = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert isinstance(result, list), f"scan_done_events 반환 타입이 list가 아님: {type(result)}"

    def test_non_done_extension_files_are_ignored(self, tmp_path):
        """.txt, .yaml, .json 등 .done이 아닌 파일은 scan_done_events가 무시해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        (events_dir / "pipeline-001.txt").write_text("not done")
        (events_dir / "pipeline-001.yaml").write_text("not done")
        (events_dir / "pipeline-001.json").write_text("not done")
        (events_dir / "README").write_text("readme")
        (events_dir / "pipeline-001.done").write_text("done")  # 유효한 것만 하나

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert len(copied) == 1, f"유효한 .done 파일이 1개인데 복사 건수가 {len(copied)}건임"
        assert "pipeline-001.done" in copied, ".done 파일이 복사 목록에 없음"

    def test_done_clear_pattern_not_matched(self, tmp_path):
        """*.done.clear, *.done.notified 등 확장자 패턴이 *.done과 다른 파일은 무시해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        invalid_patterns = [
            "pipeline-001.done.clear",
            "pipeline-002.done.notified",
            "pipeline-003.done.bak",
            "task-123.done.clear",
        ]
        for fname in invalid_patterns:
            (events_dir / fname).write_text("invalid")

        copied = scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        assert len(copied) == 0, f"잘못된 패턴 파일들인데 {len(copied)}건이 복사됨: {copied}"


# ===========================================================================
# 추가: consume_event 기존 동작과의 독립성 확인
# ===========================================================================


class TestConsumeEventIndependence:
    """consume_event의 기존 동작이 scan_done_events 도입 후에도 그대로 유지되는지 확인."""

    def test_consume_event_still_rejects_symlink_in_incoming(self, tmp_path):
        """scan 도입 후에도 consume_event는 incoming/의 symlink를 거부해야 한다."""
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        incoming_dir.mkdir()
        processed_dir.mkdir()

        real_file = tmp_path / "real_event.done"
        real_file.write_text("done")
        symlink_in_incoming = incoming_dir / "pipeline-symlink.done"
        symlink_in_incoming.symlink_to(real_file)

        result = consume_event(str(incoming_dir), str(processed_dir), "pipeline-symlink.done")

        assert result is False, "consume_event가 symlink를 거부하지 않음 — 보안 거부 실패"

    def test_consume_event_atomic_rename_still_works(self, tmp_path):
        """scan으로 복사된 파일에 대해 consume_event의 원자적 rename이 정상 동작해야 한다."""
        events_dir = tmp_path / "events"
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        events_dir.mkdir()
        incoming_dir.mkdir()
        processed_dir.mkdir()

        event_file = "pipeline-atomic.done"
        (events_dir / event_file).write_text("done")

        scan_done_events(str(events_dir), str(incoming_dir), str(processed_dir))

        result = consume_event(str(incoming_dir), str(processed_dir), event_file)
        assert result is True, "원자적 rename이 실패함"
        assert not (incoming_dir / event_file).exists(), "rename 후 incoming/에 파일이 남아있음"
        assert (processed_dir / event_file).exists(), "rename 후 processed/에 파일이 없음"

    def test_consume_event_returns_false_when_file_not_in_incoming(self, tmp_path):
        """scan 없이 incoming/에 파일이 없는 상태에서 consume_event는 False를 반환해야 한다."""
        incoming_dir = tmp_path / "incoming"
        processed_dir = tmp_path / "processed"
        incoming_dir.mkdir()
        processed_dir.mkdir()

        result = consume_event(str(incoming_dir), str(processed_dir), "pipeline-not-there.done")

        assert result is False, "incoming/에 없는 파일인데 consume_event가 True를 반환함"
