"""test_capture.py - capture 모듈 TDD 테스트 (RED → GREEN)

capture_input() 함수:
- 스킬 실행 시 사용된 user_input을 skills/<skill_name>/evals/test-inputs.yaml 에 저장
- 중복 방지 (text 동일하면 스킵)
- FIFO 최대 20개 유지
- id는 "input-YYYYMMDD-HHMMSS" 형식
- evals 디렉토리 없으면 자동 생성
- 예외 발생 시 raise하지 않고 None 반환 (경고만 출력)
"""

from __future__ import annotations

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

import yaml

# 모듈 경로 추가
_WORKSPACE_ROOT = str(Path(__file__).resolve().parent.parent.parent.parent)
if _WORKSPACE_ROOT not in sys.path:
    sys.path.insert(0, _WORKSPACE_ROOT)

from scripts.autoresearch.capture import capture_input


class TestCaptureInputCreate(unittest.TestCase):
    """테스트 1: 빈 상태에서 입력 캡처 → test-inputs.yaml 생성됨"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()
        self.skill_name = "ad-creative"
        # 스킬 디렉토리만 만들고 evals는 만들지 않음
        skill_dir = Path(self.tmpdir) / self.skill_name
        skill_dir.mkdir(parents=True)

    def test_creates_test_inputs_yaml(self) -> None:
        """빈 상태에서 capture_input 호출 시 test-inputs.yaml 파일이 생성된다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="보험 FA 모집 광고, 타겟: 30대 보험설계사",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        self.assertTrue(yaml_path.exists(), f"test-inputs.yaml 파일이 생성되어야 함: {yaml_path}")

    def test_yaml_has_correct_structure(self) -> None:
        """생성된 YAML이 올바른 구조를 가진다 (inputs 키 + id/text 필드)."""
        capture_input(
            skill_name=self.skill_name,
            user_input="보험 FA 모집 광고, 타겟: 30대 보험설계사",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        self.assertIn("inputs", data, "YAML 최상위에 'inputs' 키가 있어야 함")
        self.assertIsInstance(data["inputs"], list)
        self.assertEqual(len(data["inputs"]), 1)

        entry = data["inputs"][0]
        self.assertIn("id", entry)
        self.assertIn("text", entry)
        self.assertEqual(entry["text"], "보험 FA 모집 광고, 타겟: 30대 보험설계사")


class TestCaptureInputAppend(unittest.TestCase):
    """테스트 2: 기존 입력에 추가 → 리스트 끝에 추가됨"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()
        self.skill_name = "ad-creative"
        evals_dir = Path(self.tmpdir) / self.skill_name / "evals"
        evals_dir.mkdir(parents=True)

        # 기존 test-inputs.yaml 생성
        existing_data = {
            "inputs": [
                {"id": "insurance-fa-recruit", "text": "보험 FA 모집 광고, 타겟: 30대 보험설계사"},
            ]
        }
        yaml_path = evals_dir / "test-inputs.yaml"
        yaml_path.write_text(yaml.dump(existing_data, allow_unicode=True), encoding="utf-8")

    def test_appends_to_existing_list(self) -> None:
        """기존 YAML에 새 항목이 리스트 끝에 추가된다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="삼성생명 종신보험 신상품 출시 광고",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        self.assertEqual(len(data["inputs"]), 2, "기존 1개 + 신규 1개 = 2개이어야 함")
        self.assertEqual(data["inputs"][0]["text"], "보험 FA 모집 광고, 타겟: 30대 보험설계사")
        self.assertEqual(data["inputs"][1]["text"], "삼성생명 종신보험 신상품 출시 광고")

    def test_new_entry_is_at_end(self) -> None:
        """새 항목이 리스트의 마지막에 추가된다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="새로운 광고 카피",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        self.assertEqual(data["inputs"][-1]["text"], "새로운 광고 카피", "마지막 항목이 새로 추가된 항목이어야 함")


class TestCaptureInputDuplicate(unittest.TestCase):
    """테스트 3: 동일 텍스트 중복 → 스킵"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()
        self.skill_name = "ad-creative"
        evals_dir = Path(self.tmpdir) / self.skill_name / "evals"
        evals_dir.mkdir(parents=True)

        existing_data = {
            "inputs": [
                {"id": "insurance-fa-recruit", "text": "보험 FA 모집 광고, 타겟: 30대 보험설계사"},
            ]
        }
        yaml_path = evals_dir / "test-inputs.yaml"
        yaml_path.write_text(yaml.dump(existing_data, allow_unicode=True), encoding="utf-8")

    def test_duplicate_text_is_skipped(self) -> None:
        """동일 텍스트가 이미 있으면 추가하지 않는다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="보험 FA 모집 광고, 타겟: 30대 보험설계사",  # 동일 텍스트
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        self.assertEqual(len(data["inputs"]), 1, "중복 텍스트는 추가하지 않아야 함")

    def test_duplicate_returns_none(self) -> None:
        """중복 시에도 None을 반환한다."""
        result = capture_input(
            skill_name=self.skill_name,
            user_input="보험 FA 모집 광고, 타겟: 30대 보험설계사",
            skills_dir=self.tmpdir,
        )
        self.assertIsNone(result)


class TestCaptureInputFIFO(unittest.TestCase):
    """테스트 4: 21번째 입력 → 1번째가 삭제되고 20개 유지 (FIFO)"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()
        self.skill_name = "ad-creative"
        evals_dir = Path(self.tmpdir) / self.skill_name / "evals"
        evals_dir.mkdir(parents=True)

        # 20개의 기존 입력 생성
        inputs = [{"id": f"input-old-{i:02d}", "text": f"기존 입력 {i:02d}"} for i in range(1, 21)]
        existing_data = {"inputs": inputs}
        yaml_path = evals_dir / "test-inputs.yaml"
        yaml_path.write_text(yaml.dump(existing_data, allow_unicode=True), encoding="utf-8")

    def test_max_20_entries_after_21st(self) -> None:
        """20개가 꽉 찬 상태에서 21번째 추가 시 최대 20개를 유지한다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="21번째 새 입력",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        self.assertEqual(len(data["inputs"]), 20, "최대 20개를 유지해야 함")

    def test_oldest_entry_removed_fifo(self) -> None:
        """FIFO: 가장 오래된 (첫 번째) 항목이 삭제된다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="21번째 새 입력",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        # 첫 번째 항목("기존 입력 01")이 삭제되어야 함
        texts = [entry["text"] for entry in data["inputs"]]
        self.assertNotIn("기존 입력 01", texts, "가장 오래된 항목이 삭제되어야 함 (FIFO)")

    def test_newest_entry_is_last(self) -> None:
        """새로 추가된 항목이 리스트 마지막에 있다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="21번째 새 입력",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        self.assertEqual(data["inputs"][-1]["text"], "21번째 새 입력", "새 항목이 마지막이어야 함")


class TestCaptureInputIdFormat(unittest.TestCase):
    """테스트 5: id 형식이 "input-YYYYMMDD-HHMMSS" 패턴 준수"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()
        self.skill_name = "ad-creative"
        skill_dir = Path(self.tmpdir) / self.skill_name
        skill_dir.mkdir(parents=True)

    def test_id_format_matches_pattern(self) -> None:
        """생성된 id가 'input-YYYYMMDD-HHMMSS' 형식을 따른다."""
        capture_input(
            skill_name=self.skill_name,
            user_input="테스트 입력",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        entry_id = data["inputs"][0]["id"]
        pattern = r"^input-\d{8}-\d{6}$"
        self.assertRegex(entry_id, pattern, f"id '{entry_id}'가 'input-YYYYMMDD-HHMMSS' 형식이어야 함")

    def test_id_uses_timestamp(self) -> None:
        """id에 포함된 날짜가 현재 시각과 일치한다."""
        from datetime import datetime

        fixed_time = datetime(2026, 3, 26, 14, 30, 22)

        with patch("scripts.autoresearch.capture.datetime") as mock_dt:
            mock_dt.now.return_value = fixed_time

            capture_input(
                skill_name=self.skill_name,
                user_input="타임스탬프 테스트",
                skills_dir=self.tmpdir,
            )

        yaml_path = Path(self.tmpdir) / self.skill_name / "evals" / "test-inputs.yaml"
        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))

        entry_id = data["inputs"][0]["id"]
        self.assertEqual(entry_id, "input-20260326-143022")


class TestCaptureInputEvalsDir(unittest.TestCase):
    """테스트 6: evals 디렉토리 없으면 자동 생성"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()
        self.skill_name = "new-skill"
        # 스킬 디렉토리는 있지만 evals는 없음
        skill_dir = Path(self.tmpdir) / self.skill_name
        skill_dir.mkdir(parents=True)

    def test_creates_evals_dir_if_missing(self) -> None:
        """evals 디렉토리가 없으면 자동으로 생성된다."""
        evals_dir = Path(self.tmpdir) / self.skill_name / "evals"
        self.assertFalse(evals_dir.exists(), "사전 조건: evals 디렉토리가 없어야 함")

        capture_input(
            skill_name=self.skill_name,
            user_input="테스트 입력",
            skills_dir=self.tmpdir,
        )

        self.assertTrue(evals_dir.exists(), "evals 디렉토리가 자동 생성되어야 함")

    def test_creates_skill_dir_if_missing(self) -> None:
        """스킬 디렉토리 자체가 없어도 자동으로 생성된다."""
        skill_name = "brand-new-skill"
        skill_dir = Path(self.tmpdir) / skill_name
        self.assertFalse(skill_dir.exists(), "사전 조건: 스킬 디렉토리가 없어야 함")

        capture_input(
            skill_name=skill_name,
            user_input="완전 새 스킬의 입력",
            skills_dir=self.tmpdir,
        )

        yaml_path = Path(self.tmpdir) / skill_name / "evals" / "test-inputs.yaml"
        self.assertTrue(yaml_path.exists(), "스킬/evals/test-inputs.yaml이 생성되어야 함")


class TestCaptureInputExceptionHandling(unittest.TestCase):
    """테스트 7: 예외 발생 시 raise하지 않고 None 반환"""

    def test_returns_none_on_success(self) -> None:
        """정상 동작 시 None을 반환한다."""
        tmpdir = tempfile.mkdtemp()
        result = capture_input(
            skill_name="some-skill",
            user_input="테스트",
            skills_dir=tmpdir,
        )
        self.assertIsNone(result)

    def test_does_not_raise_on_io_error(self) -> None:
        """파일 I/O 오류가 발생해도 예외를 raise하지 않는다."""
        with patch("builtins.open", side_effect=OSError("디스크 꽉 참")):
            # 예외가 발생하지 않아야 함
            try:
                result = capture_input(
                    skill_name="some-skill",
                    user_input="테스트",
                    skills_dir="/nonexistent/path",
                )
                self.assertIsNone(result)
            except OSError:
                self.fail("OSError가 raise되지 않아야 함")

    def test_does_not_raise_on_permission_error(self) -> None:
        """권한 오류가 발생해도 예외를 raise하지 않는다."""
        with patch("pathlib.Path.mkdir", side_effect=PermissionError("권한 없음")):
            try:
                result = capture_input(
                    skill_name="some-skill",
                    user_input="테스트",
                    skills_dir="/read-only-path",
                )
                self.assertIsNone(result)
            except PermissionError:
                self.fail("PermissionError가 raise되지 않아야 함")

    def test_does_not_raise_on_yaml_parse_error(self) -> None:
        """기존 YAML 파싱 오류가 발생해도 예외를 raise하지 않는다."""
        tmpdir = tempfile.mkdtemp()
        skill_name = "broken-skill"
        evals_dir = Path(tmpdir) / skill_name / "evals"
        evals_dir.mkdir(parents=True)

        # 잘못된 YAML 작성
        yaml_path = evals_dir / "test-inputs.yaml"
        yaml_path.write_text("invalid: yaml: content: [unclosed", encoding="utf-8")

        try:
            result = capture_input(
                skill_name=skill_name,
                user_input="테스트",
                skills_dir=tmpdir,
            )
            self.assertIsNone(result)
        except Exception:
            self.fail("YAML 파싱 오류 시 예외가 raise되지 않아야 함")


if __name__ == "__main__":
    unittest.main()
