"""Tests for memory-janitor.py - TDD approach (RED phase first)."""

import json
import os
import sys
import time
from datetime import datetime, timedelta
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 memory_janitor as mj


class TestMemoryMdCheck:
    """Tests for MEMORY.md line count checking."""

    def test_check_memory_md_under_limit(self, tmp_path: Path) -> None:
        """180줄짜리 MEMORY.md는 OK 상태를 반환해야 한다."""
        memory_md = tmp_path / "MEMORY.md"
        memory_md.write_text("\n".join([f"line {i}" for i in range(180)]))

        result = mj.check_memory_md(memory_md)

        assert result["line_count"] == 180
        assert result["limit"] == 200
        assert result["status"] == "OK"
        assert result["file"] == str(memory_md)

    def test_check_memory_md_over_limit(self, tmp_path: Path) -> None:
        """201줄짜리 MEMORY.md는 WARNING 상태를 반환해야 한다."""
        memory_md = tmp_path / "MEMORY.md"
        memory_md.write_text("\n".join([f"line {i}" for i in range(201)]))

        result = mj.check_memory_md(memory_md)

        assert result["line_count"] == 201
        assert result["limit"] == 200
        assert result["status"] == "WARNING"

    def test_check_memory_md_exactly_at_limit(self, tmp_path: Path) -> None:
        """정확히 200줄인 MEMORY.md는 OK 상태를 반환해야 한다."""
        memory_md = tmp_path / "MEMORY.md"
        memory_md.write_text("\n".join([f"line {i}" for i in range(200)]))

        result = mj.check_memory_md(memory_md)

        assert result["line_count"] == 200
        assert result["status"] == "OK"

    def test_check_memory_md_not_found(self, tmp_path: Path) -> None:
        """MEMORY.md가 없으면 NOT_FOUND 상태를 반환해야 한다."""
        missing = tmp_path / "MEMORY.md"

        result = mj.check_memory_md(missing)

        assert result["status"] == "NOT_FOUND"
        assert result["line_count"] == 0


class TestMemoryFilesReport:
    """Tests for memory/ directory file size reporting."""

    def test_aggregate_empty_directory(self, tmp_path: Path) -> None:
        """빈 memory 디렉토리를 graceful하게 처리해야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()

        result = mj.aggregate_memory_files(memory_dir)

        assert result["total_files"] == 0
        assert result["total_size_bytes"] == 0
        assert result["by_directory"] == {}
        assert result["largest_files"] == []

    def test_aggregate_single_file(self, tmp_path: Path) -> None:
        """단일 파일의 크기를 정확히 집계해야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()
        f = memory_dir / "test.md"
        f.write_text("hello world")  # 11 bytes

        result = mj.aggregate_memory_files(memory_dir)

        assert result["total_files"] == 1
        assert result["total_size_bytes"] == 11
        assert len(result["largest_files"]) == 1

    def test_aggregate_multiple_directories(self, tmp_path: Path) -> None:
        """여러 하위 디렉토리의 크기를 디렉토리별로 집계해야 한다."""
        memory_dir = tmp_path / "memory"
        reports_dir = memory_dir / "reports"
        whisper_dir = memory_dir / "whisper"
        reports_dir.mkdir(parents=True)
        whisper_dir.mkdir(parents=True)

        (reports_dir / "report1.md").write_text("A" * 100)
        (reports_dir / "report2.md").write_text("B" * 200)
        (whisper_dir / "note.md").write_text("C" * 50)

        result = mj.aggregate_memory_files(memory_dir)

        assert result["total_files"] == 3
        assert result["total_size_bytes"] == 350

        # by_directory 키는 memory_dir 기준 상대 경로 포함
        dirs = result["by_directory"]
        assert any("reports" in k for k in dirs)
        assert any("whisper" in k for k in dirs)

        reports_key = next(k for k in dirs if "reports" in k)
        assert dirs[reports_key]["files"] == 2
        assert dirs[reports_key]["size_bytes"] == 300

    def test_largest_files_top_10(self, tmp_path: Path) -> None:
        """상위 10개 파일만 반환해야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()

        for i in range(15):
            (memory_dir / f"file{i:02d}.md").write_text("X" * (i + 1) * 10)

        result = mj.aggregate_memory_files(memory_dir)

        assert len(result["largest_files"]) == 10
        # 크기 내림차순 정렬 확인
        sizes = [f["size_bytes"] for f in result["largest_files"]]
        assert sizes == sorted(sizes, reverse=True)

    def test_aggregate_nonexistent_directory(self, tmp_path: Path) -> None:
        """존재하지 않는 디렉토리도 graceful하게 처리해야 한다."""
        missing = tmp_path / "memory"

        result = mj.aggregate_memory_files(missing)

        assert result["total_files"] == 0
        assert result["total_size_bytes"] == 0


class TestStaleFilesDetection:
    """Tests for detecting files not accessed in 30+ days."""

    def test_no_stale_files_recent(self, tmp_path: Path) -> None:
        """최근에 수정된 파일은 stale 목록에 없어야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()
        f = memory_dir / "recent.md"
        f.write_text("recent content")
        # mtime은 현재 시각 (기본값)

        result = mj.detect_stale_files(memory_dir, days=30)

        assert result == []

    def test_detects_old_files(self, tmp_path: Path) -> None:
        """31일 이상 수정되지 않은 파일은 stale로 감지해야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()
        old_file = memory_dir / "old-note.md"
        old_file.write_text("old content")

        # 31일 전 mtime 설정
        old_time = time.time() - (31 * 24 * 3600)
        os.utime(old_file, (old_time, old_time))

        result = mj.detect_stale_files(memory_dir, days=30)

        assert len(result) == 1
        assert "old-note.md" in result[0]["path"]
        assert result[0]["days_stale"] >= 31

    def test_exactly_30_days_not_stale(self, tmp_path: Path) -> None:
        """정확히 30일 된 파일은 stale이 아니다 (30일 초과만 stale)."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()
        border_file = memory_dir / "border.md"
        border_file.write_text("border content")

        # 정확히 30일 전 mtime 설정
        border_time = time.time() - (30 * 24 * 3600)
        os.utime(border_file, (border_time, border_time))

        result = mj.detect_stale_files(memory_dir, days=30)

        assert result == []

    def test_mixed_files(self, tmp_path: Path) -> None:
        """최근 파일과 오래된 파일이 섞여있을 때 오래된 것만 반환해야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()

        # 최근 파일
        recent = memory_dir / "recent.md"
        recent.write_text("recent")

        # 오래된 파일
        old = memory_dir / "stale.md"
        old.write_text("stale")
        old_time = time.time() - (60 * 24 * 3600)
        os.utime(old, (old_time, old_time))

        result = mj.detect_stale_files(memory_dir, days=30)

        assert len(result) == 1
        assert "stale.md" in result[0]["path"]

    def test_last_modified_format(self, tmp_path: Path) -> None:
        """last_modified 필드가 YYYY-MM-DD 형식이어야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()
        old = memory_dir / "old.md"
        old.write_text("x")
        old_time = time.time() - (40 * 24 * 3600)
        os.utime(old, (old_time, old_time))

        result = mj.detect_stale_files(memory_dir, days=30)

        assert len(result) == 1
        # YYYY-MM-DD 형식 검증
        datetime.strptime(result[0]["last_modified"], "%Y-%m-%d")

    def test_stale_empty_directory(self, tmp_path: Path) -> None:
        """빈 디렉토리에서 stale 파일 감지는 빈 목록을 반환해야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()

        result = mj.detect_stale_files(memory_dir, days=30)

        assert result == []


class TestDuplicateDetection:
    """Tests for detecting duplicate or similar files."""

    def test_hyphen_underscore_similarity(self, tmp_path: Path) -> None:
        """하이픈/언더스코어 차이로 유사한 파일명을 감지해야 한다."""
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        (scripts_dir / "health_score.py").write_text("version1")
        (scripts_dir / "health-score.py").write_text("version2")

        result = mj.detect_duplicates([scripts_dir])

        similar = [d for d in result if d["reason"] == "similar_name"]
        assert len(similar) >= 1
        files = similar[0]["files"]
        assert any("health_score.py" in f for f in files)
        assert any("health-score.py" in f for f in files)

    def test_same_size_same_directory(self, tmp_path: Path) -> None:
        """같은 디렉토리 내 동일한 크기의 파일을 감지해야 한다."""
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        (scripts_dir / "file_a.py").write_text("exactly ten")  # 11 bytes
        (scripts_dir / "file_b.py").write_text("exactly ten")  # 11 bytes

        result = mj.detect_duplicates([scripts_dir])

        size_dups = [d for d in result if d["reason"] == "same_size"]
        assert len(size_dups) >= 1

    def test_no_duplicates(self, tmp_path: Path) -> None:
        """중복이 없을 때 빈 목록을 반환해야 한다."""
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        (scripts_dir / "unique_a.py").write_text("content a")
        (scripts_dir / "unique_b.py").write_text("different content here")

        result = mj.detect_duplicates([scripts_dir])

        # 크기가 다르고 이름도 다르면 중복 없음
        assert result == []

    def test_similar_name_different_dirs(self, tmp_path: Path) -> None:
        """다른 디렉토리에 있는 유사한 이름 파일도 감지해야 한다."""
        dir_a = tmp_path / "dir_a"
        dir_b = tmp_path / "dir_b"
        dir_a.mkdir()
        dir_b.mkdir()
        (dir_a / "report_gen.py").write_text("a")
        (dir_b / "report-gen.py").write_text("b")

        result = mj.detect_duplicates([dir_a, dir_b])

        similar = [d for d in result if d["reason"] == "similar_name"]
        assert len(similar) >= 1

    def test_empty_directories(self, tmp_path: Path) -> None:
        """빈 디렉토리 목록도 graceful하게 처리해야 한다."""
        result = mj.detect_duplicates([])
        assert result == []


class TestRecommendations:
    """Tests for recommendation generation."""

    def test_recommendation_memory_md_ok(self) -> None:
        """MEMORY.md가 제한 이내이면 OK 권고사항을 생성해야 한다."""
        memory_md_check = {
            "file": "MEMORY.md",
            "line_count": 180,
            "limit": 200,
            "status": "OK",
        }
        stale_files: list = []
        duplicates: list = []

        recs = mj.generate_recommendations(memory_md_check, stale_files, duplicates)

        assert any("180" in r and "200" in r for r in recs)

    def test_recommendation_memory_md_warning(self) -> None:
        """MEMORY.md가 제한 초과이면 WARNING 권고사항을 생성해야 한다."""
        memory_md_check = {
            "file": "MEMORY.md",
            "line_count": 250,
            "limit": 200,
            "status": "WARNING",
        }
        stale_files: list = []
        duplicates: list = []

        recs = mj.generate_recommendations(memory_md_check, stale_files, duplicates)

        assert any("초과" in r or "WARNING" in r or "정리" in r for r in recs)

    def test_recommendation_stale_files(self) -> None:
        """stale 파일이 있으면 아카이브 권고사항을 생성해야 한다."""
        memory_md_check = {"status": "OK", "line_count": 50, "limit": 200, "file": ""}
        stale_files = [
            {"path": "old1.md", "last_modified": "2026-01-01", "days_stale": 73},
            {"path": "old2.md", "last_modified": "2026-01-01", "days_stale": 45},
        ]
        duplicates: list = []

        recs = mj.generate_recommendations(memory_md_check, stale_files, duplicates)

        assert any("2" in r and "아카이브" in r for r in recs)

    def test_recommendation_duplicates(self) -> None:
        """중복 파일이 있으면 해당 파일명 권고사항을 생성해야 한다."""
        memory_md_check = {"status": "OK", "line_count": 50, "limit": 200, "file": ""}
        stale_files: list = []
        duplicates = [
            {
                "files": ["scripts/health_score.py", "scripts/health-score.py"],
                "reason": "similar_name",
            }
        ]

        recs = mj.generate_recommendations(memory_md_check, stale_files, duplicates)

        assert any("health_score" in r or "health-score" in r for r in recs)

    def test_recommendation_no_issues(self) -> None:
        """문제가 없을 때도 최소한 하나의 권고사항이 있어야 한다."""
        memory_md_check = {"status": "OK", "line_count": 50, "limit": 200, "file": ""}
        stale_files: list = []
        duplicates: list = []

        recs = mj.generate_recommendations(memory_md_check, stale_files, duplicates)

        assert len(recs) >= 1


class TestFullReport:
    """Integration tests for the full report generation."""

    def test_full_report_structure(self, tmp_path: Path) -> None:
        """전체 리포트가 올바른 JSON 스키마를 갖춰야 한다."""
        # Setup
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()
        (memory_dir / "MEMORY.md").write_text("\n".join([f"line {i}" for i in range(50)]))
        (memory_dir / "test.md").write_text("test content")

        result = mj.generate_report(
            memory_dir=memory_dir,
            memory_md_path=memory_dir / "MEMORY.md",
            scan_dirs=[memory_dir],
        )

        # 필수 키 존재 확인
        assert "timestamp" in result
        assert "memory_files" in result
        assert "memory_md_check" in result
        assert "stale_files" in result
        assert "duplicates" in result
        assert "recommendations" in result

        # timestamp 형식 확인
        datetime.fromisoformat(result["timestamp"])

    def test_full_report_json_serializable(self, tmp_path: Path) -> None:
        """전체 리포트가 JSON 직렬화 가능해야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()

        result = mj.generate_report(
            memory_dir=memory_dir,
            memory_md_path=tmp_path / "MEMORY.md",
            scan_dirs=[memory_dir],
        )

        # JSON 직렬화 오류 없어야 함
        json_str = json.dumps(result)
        assert json_str is not None

    def test_full_report_memory_md_check_fields(self, tmp_path: Path) -> None:
        """memory_md_check 섹션에 필수 필드가 있어야 한다."""
        memory_dir = tmp_path / "memory"
        memory_dir.mkdir()
        memory_md = tmp_path / "MEMORY.md"
        memory_md.write_text("\n".join([f"line {i}" for i in range(100)]))

        result = mj.generate_report(
            memory_dir=memory_dir,
            memory_md_path=memory_md,
            scan_dirs=[],
        )

        check = result["memory_md_check"]
        assert "file" in check
        assert "line_count" in check
        assert "limit" in check
        assert "status" in check
