"""Tests for learnings-archiver.py - TDD approach (RED phase first).

테스트 케이스:
1. add_learning → 파일에 올바른 형식으로 추가되는지
2. get_learnings → 해당 스킬만 반환, 다른 스킬 learning 미포함
3. archive_expired → 만료 항목 이동 + 원본에서 제거 + archive에 존재
4. jay-feedback → expires_at null → TTL 아카이브에서 제외
5. 잘못된 source → 에러 발생
6. 빈 파일에서 get_learnings → 빈 리스트 반환
"""

import json
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pytest

# Add the scripts directory to path so we can import the module
sys.path.insert(0, str(Path(__file__).parent.parent))

import learnings_archiver as la


class TestAddLearning:
    """테스트 1: add_learning → 파일에 올바른 형식으로 추가되는지."""

    def test_add_learning_creates_valid_jsonl_entry(self, tmp_path: Path) -> None:
        """add_learning 호출 시 learnings.jsonl에 올바른 형식으로 추가된다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"

        la.add_learning(
            skill_name="satori-cardnews",
            source="champion-battle",
            learning="CTA를 마지막 슬라이드가 아닌 3번째에 배치하면 시선 흐름이 자연스럽다",
            learnings_path=learnings_file,
            archive_path=archive_file,
        )

        assert learnings_file.exists()
        lines = learnings_file.read_text(encoding="utf-8").strip().splitlines()
        assert len(lines) == 1

        entry = json.loads(lines[0])
        assert "id" in entry
        assert entry["skill_name"] == "satori-cardnews"
        assert entry["source"] == "champion-battle"
        assert entry["learning"] == "CTA를 마지막 슬라이드가 아닌 3번째에 배치하면 시선 흐름이 자연스럽다"
        assert "created_at" in entry
        assert "expires_at" in entry
        # expires_at은 created_at + 60일이어야 한다
        created = datetime.fromisoformat(entry["created_at"])
        expires = datetime.fromisoformat(entry["expires_at"])
        delta = expires - created
        assert 59 <= delta.days <= 61

    def test_add_learning_appends_multiple_entries(self, tmp_path: Path) -> None:
        """add_learning을 여러 번 호출하면 append-only로 추가된다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"

        la.add_learning(
            skill_name="skill-a",
            source="self-review",
            learning="첫 번째 학습",
            learnings_path=learnings_file,
            archive_path=archive_file,
        )
        la.add_learning(
            skill_name="skill-b",
            source="cross-model",
            learning="두 번째 학습",
            learnings_path=learnings_file,
            archive_path=archive_file,
        )

        lines = learnings_file.read_text(encoding="utf-8").strip().splitlines()
        assert len(lines) == 2

    def test_add_learning_jay_feedback_has_null_expires_at(self, tmp_path: Path) -> None:
        """source가 jay-feedback이면 expires_at이 null이어야 한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"

        la.add_learning(
            skill_name="satori-cardnews",
            source="jay-feedback",
            learning="jay의 중요 피드백",
            learnings_path=learnings_file,
            archive_path=archive_file,
        )

        lines = learnings_file.read_text(encoding="utf-8").strip().splitlines()
        entry = json.loads(lines[0])
        assert entry["expires_at"] is None

    def test_add_learning_id_format(self, tmp_path: Path) -> None:
        """id는 learn-NNN 또는 UUID 형식이어야 한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"

        la.add_learning(
            skill_name="test-skill",
            source="github-learn",
            learning="테스트 학습",
            learnings_path=learnings_file,
            archive_path=archive_file,
        )

        lines = learnings_file.read_text(encoding="utf-8").strip().splitlines()
        entry = json.loads(lines[0])
        entry_id = entry["id"]
        # learn-NNN 형식이거나 UUID 형식이어야 한다
        is_learn_format = entry_id.startswith("learn-")
        is_uuid_format = len(entry_id) == 36 and entry_id.count("-") == 4
        assert is_learn_format or is_uuid_format


class TestGetLearnings:
    """테스트 2: get_learnings → 해당 스킬만 반환, 다른 스킬 learning 미포함."""

    def test_get_learnings_returns_only_target_skill(self, tmp_path: Path) -> None:
        """get_learnings는 지정한 스킬의 활성 항목만 반환한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        now = datetime.now()
        future = (now + timedelta(days=30)).isoformat()
        past = (now - timedelta(days=1)).isoformat()

        entries = [
            {
                "id": "learn-001",
                "skill_name": "skill-a",
                "source": "self-review",
                "learning": "skill-a의 학습",
                "created_at": now.isoformat(),
                "expires_at": future,
            },
            {
                "id": "learn-002",
                "skill_name": "skill-b",
                "source": "self-review",
                "learning": "skill-b의 학습 (다른 스킬)",
                "created_at": now.isoformat(),
                "expires_at": future,
            },
            {
                "id": "learn-003",
                "skill_name": "skill-a",
                "source": "cross-model",
                "learning": "skill-a의 만료된 학습",
                "created_at": now.isoformat(),
                "expires_at": past,
            },
        ]
        learnings_file.write_text("\n".join(json.dumps(e) for e in entries), encoding="utf-8")

        result = la.get_learnings("skill-a", learnings_path=learnings_file)

        # skill-a의 활성 항목만 반환 (만료된 것, 다른 스킬 제외)
        assert len(result) == 1
        assert result[0]["id"] == "learn-001"
        assert result[0]["skill_name"] == "skill-a"

    def test_get_learnings_excludes_other_skills(self, tmp_path: Path) -> None:
        """get_learnings는 다른 스킬의 learning을 절대 포함하지 않는다."""
        learnings_file = tmp_path / "learnings.jsonl"
        now = datetime.now()
        future = (now + timedelta(days=30)).isoformat()

        entries = [
            {
                "id": "learn-001",
                "skill_name": "skill-b",
                "source": "self-review",
                "learning": "다른 스킬의 학습",
                "created_at": now.isoformat(),
                "expires_at": future,
            },
        ]
        learnings_file.write_text("\n".join(json.dumps(e) for e in entries), encoding="utf-8")

        result = la.get_learnings("skill-a", learnings_path=learnings_file)

        assert len(result) == 0

    def test_get_learnings_includes_null_expires_at(self, tmp_path: Path) -> None:
        """expires_at이 null인 항목은 항상 활성으로 반환한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        now = datetime.now()

        entries = [
            {
                "id": "learn-001",
                "skill_name": "skill-a",
                "source": "jay-feedback",
                "learning": "영구 유지 학습",
                "created_at": now.isoformat(),
                "expires_at": None,
            },
        ]
        learnings_file.write_text("\n".join(json.dumps(e) for e in entries), encoding="utf-8")

        result = la.get_learnings("skill-a", learnings_path=learnings_file)

        assert len(result) == 1
        assert result[0]["expires_at"] is None


class TestArchiveExpired:
    """테스트 3: archive_expired → 만료 항목 이동 + 원본에서 제거 + archive에 존재."""

    def test_archive_expired_moves_expired_entries(self, tmp_path: Path) -> None:
        """archive_expired는 만료된 항목을 archive로 이동하고 원본에서 제거한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"
        now = datetime.now()
        future = (now + timedelta(days=30)).isoformat()
        past = (now - timedelta(days=1)).isoformat()

        entries = [
            {
                "id": "learn-001",
                "skill_name": "skill-a",
                "source": "self-review",
                "learning": "활성 학습",
                "created_at": now.isoformat(),
                "expires_at": future,
            },
            {
                "id": "learn-002",
                "skill_name": "skill-a",
                "source": "cross-model",
                "learning": "만료된 학습",
                "created_at": now.isoformat(),
                "expires_at": past,
            },
        ]
        learnings_file.write_text("\n".join(json.dumps(e) for e in entries), encoding="utf-8")

        count = la.archive_expired(learnings_path=learnings_file, archive_path=archive_file)

        assert count == 1

        # 원본에서 만료 항목 제거 확인
        remaining = [json.loads(line) for line in learnings_file.read_text().strip().splitlines()]
        assert len(remaining) == 1
        assert remaining[0]["id"] == "learn-001"

        # archive에 만료 항목 존재 확인
        archived = [json.loads(line) for line in archive_file.read_text().strip().splitlines()]
        assert len(archived) == 1
        assert archived[0]["id"] == "learn-002"

    def test_archive_expired_appends_to_existing_archive(self, tmp_path: Path) -> None:
        """archive_expired는 기존 archive에 append한다 (덮어쓰지 않는다)."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"
        now = datetime.now()
        past = (now - timedelta(days=1)).isoformat()

        # 기존 archive에 항목이 있는 상태
        existing_archived = {
            "id": "learn-old",
            "skill_name": "skill-b",
            "source": "self-review",
            "learning": "기존 아카이브 항목",
            "created_at": now.isoformat(),
            "expires_at": (now - timedelta(days=10)).isoformat(),
        }
        archive_file.write_text(json.dumps(existing_archived) + "\n", encoding="utf-8")

        # 새 만료 항목 추가
        new_expired = {
            "id": "learn-new",
            "skill_name": "skill-a",
            "source": "cross-model",
            "learning": "새 만료 항목",
            "created_at": now.isoformat(),
            "expires_at": past,
        }
        learnings_file.write_text(json.dumps(new_expired) + "\n", encoding="utf-8")

        la.archive_expired(learnings_path=learnings_file, archive_path=archive_file)

        archived = [json.loads(line) for line in archive_file.read_text().strip().splitlines()]
        assert len(archived) == 2
        ids = [a["id"] for a in archived]
        assert "learn-old" in ids
        assert "learn-new" in ids

    def test_archive_expired_returns_count(self, tmp_path: Path) -> None:
        """archive_expired는 아카이브된 항목 수를 반환한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"
        now = datetime.now()
        past = (now - timedelta(days=1)).isoformat()

        entries = [
            {
                "id": f"learn-{i:03d}",
                "skill_name": "skill-a",
                "source": "self-review",
                "learning": f"만료 학습 {i}",
                "created_at": now.isoformat(),
                "expires_at": past,
            }
            for i in range(3)
        ]
        learnings_file.write_text("\n".join(json.dumps(e) for e in entries), encoding="utf-8")

        count = la.archive_expired(learnings_path=learnings_file, archive_path=archive_file)

        assert count == 3


class TestJayFeedbackTTLExclusion:
    """테스트 4: jay-feedback → expires_at null → TTL 아카이브에서 제외."""

    def test_jay_feedback_with_null_expires_at_not_archived(self, tmp_path: Path) -> None:
        """source가 jay-feedback이고 expires_at이 null이면 아카이브하지 않는다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"
        now = datetime.now()
        past = (now - timedelta(days=1)).isoformat()

        entries = [
            {
                "id": "learn-001",
                "skill_name": "skill-a",
                "source": "jay-feedback",
                "learning": "jay의 영구 피드백",
                "created_at": now.isoformat(),
                "expires_at": None,  # null → 아카이브 제외
            },
            {
                "id": "learn-002",
                "skill_name": "skill-a",
                "source": "self-review",
                "learning": "만료된 일반 학습",
                "created_at": now.isoformat(),
                "expires_at": past,
            },
        ]
        learnings_file.write_text("\n".join(json.dumps(e) for e in entries), encoding="utf-8")

        count = la.archive_expired(learnings_path=learnings_file, archive_path=archive_file)

        # jay-feedback null은 아카이브 안 됨, 만료된 self-review만 아카이브
        assert count == 1

        remaining = [json.loads(line) for line in learnings_file.read_text().strip().splitlines()]
        remaining_ids = [r["id"] for r in remaining]
        assert "learn-001" in remaining_ids
        assert "learn-002" not in remaining_ids

    def test_jay_feedback_with_expired_date_NOT_archived(self, tmp_path: Path) -> None:
        """source가 jay-feedback이면 expires_at이 명시적으로 설정되어 만료되어도 아카이브하지 않는다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"
        now = datetime.now()
        past = (now - timedelta(days=1)).isoformat()

        entries = [
            {
                "id": "learn-001",
                "skill_name": "skill-a",
                "source": "jay-feedback",
                "learning": "만료된 jay 피드백",
                "created_at": now.isoformat(),
                "expires_at": past,  # 명시적으로 만료 날짜가 있어도 jay-feedback은 immutable
            },
        ]
        learnings_file.write_text("\n".join(json.dumps(e) for e in entries), encoding="utf-8")

        count = la.archive_expired(learnings_path=learnings_file, archive_path=archive_file)

        # jay-feedback은 immutable: expires_at이 있어도 아카이브되지 않음
        assert count == 0


class TestInvalidSource:
    """테스트 5: 잘못된 source → 에러 발생."""

    def test_add_learning_invalid_source_raises_error(self, tmp_path: Path) -> None:
        """유효하지 않은 source로 add_learning 호출 시 ValueError가 발생한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"

        with pytest.raises(ValueError, match="Invalid source"):
            la.add_learning(
                skill_name="test-skill",
                source="invalid-source",
                learning="테스트 학습",
                learnings_path=learnings_file,
                archive_path=archive_file,
            )

    def test_add_learning_valid_sources_do_not_raise(self, tmp_path: Path) -> None:
        """유효한 source 목록은 모두 에러 없이 추가된다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"

        valid_sources = [
            "self-review",
            "cross-model",
            "jay-feedback",
            "online-expert",
            "github-learn",
            "champion-battle",
        ]

        for source in valid_sources:
            la.add_learning(
                skill_name="test-skill",
                source=source,
                learning=f"{source} 학습",
                learnings_path=learnings_file,
                archive_path=archive_file,
            )

        lines = learnings_file.read_text(encoding="utf-8").strip().splitlines()
        assert len(lines) == len(valid_sources)


class TestEmptyFileGetLearnings:
    """테스트 6: 빈 파일에서 get_learnings → 빈 리스트 반환."""

    def test_get_learnings_from_empty_file_returns_empty_list(self, tmp_path: Path) -> None:
        """learnings.jsonl이 비어있으면 get_learnings는 빈 리스트를 반환한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        learnings_file.write_text("", encoding="utf-8")

        result = la.get_learnings("skill-a", learnings_path=learnings_file)

        assert result == []

    def test_get_learnings_from_nonexistent_file_returns_empty_list(self, tmp_path: Path) -> None:
        """learnings.jsonl이 존재하지 않으면 get_learnings는 빈 리스트를 반환한다."""
        learnings_file = tmp_path / "learnings.jsonl"

        result = la.get_learnings("skill-a", learnings_path=learnings_file)

        assert result == []

    def test_archive_expired_from_empty_file_returns_zero(self, tmp_path: Path) -> None:
        """learnings.jsonl이 비어있으면 archive_expired는 0을 반환한다."""
        learnings_file = tmp_path / "learnings.jsonl"
        archive_file = tmp_path / "learnings-archive.jsonl"
        learnings_file.write_text("", encoding="utf-8")

        count = la.archive_expired(learnings_path=learnings_file, archive_path=archive_file)

        assert count == 0
