"""todo-manager.py 테스트 (task-864.1)

모든 서브커맨드 정상 동작, 에러 케이스, JSON 무결성 검증 테스트.
"""

import json
import os
import shutil
import sys
import tempfile
from pathlib import Path

import pytest

# 테스트 대상 모듈 경로
MEMORY_DIR = Path(__file__).parent.parent / "memory"
sys.path.insert(0, str(MEMORY_DIR))

# 모듈 임포트 (하이픈이 있어서 직접 로드)
import importlib.util

spec = importlib.util.spec_from_file_location("todo_manager", MEMORY_DIR / "todo-manager.py")
tm = importlib.util.module_from_spec(spec)
spec.loader.exec_module(tm)

# todo_utils 모듈 — 파일 경로 상수(TODO_FILE 등)가 여기 있음
import todo_utils as tu


@pytest.fixture
def temp_todo_dir(tmp_path: Path) -> Path:
    """임시 디렉토리에서 테스트 (todo_utils의 파일 경로를 임시 경로로 패치)."""
    orig_todo = tu.TODO_FILE
    orig_backup = tu.BACKUP_FILE
    orig_removed = tu.REMOVED_FILE

    tu.TODO_FILE = tmp_path / "todo.json"
    tu.BACKUP_FILE = tmp_path / "todo.json.bak"
    tu.REMOVED_FILE = tmp_path / "todo-removed.json"

    yield tmp_path

    tu.TODO_FILE = orig_todo
    tu.BACKUP_FILE = orig_backup
    tu.REMOVED_FILE = orig_removed


@pytest.fixture
def sample_todo(temp_todo_dir: Path) -> dict:
    """샘플 todo.json 생성."""
    data = {
        "version": "1.0",
        "issues": [
            {
                "id": "issue-001",
                "project": "TestProj",
                "title": "테스트 이슈 1",
                "description": "설명",
                "priority": "high",
                "status": "pending",
                "created_at": "2026-01-01T00:00:00",
                "completed_at": None,
                "linked_tasks": [],
                "sub_items": [
                    {"title": "서브1", "done": False, "task_id": None},
                    {"title": "서브2", "done": True, "task_id": "task-001"},
                ],
            },
            {
                "id": "issue-002",
                "project": "OtherProj",
                "title": "테스트 이슈 2",
                "description": "",
                "priority": "medium",
                "status": "in_progress",
                "created_at": "2026-01-02T00:00:00",
                "completed_at": None,
                "linked_tasks": ["task-002"],
                "sub_items": [],
            },
        ],
        "last_synced": None,
    }
    tm.save_todo(data)
    return data


class TestLoadSave:
    """로드/저장 테스트."""

    def test_load_empty(self, temp_todo_dir: Path) -> None:
        """빈 파일 로드."""
        data = tm.load_todo()
        assert data["version"] == "1.0"
        assert data["issues"] == []

    def test_load_existing(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """기존 파일 로드."""
        data = tm.load_todo()
        assert len(data["issues"]) == 2
        assert data["issues"][0]["id"] == "issue-001"

    def test_save_creates_file(self, temp_todo_dir: Path) -> None:
        """저장 시 파일 생성."""
        data = {"version": "1.0", "issues": []}
        tm.save_todo(data)
        assert tu.TODO_FILE.exists()

    def test_save_creates_backup(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """저장 시 백업 생성."""
        data = tm.load_todo()
        data["issues"][0]["title"] = "수정됨"
        tm.save_todo(data)
        assert tu.BACKUP_FILE.exists()

    def test_atomic_write(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """atomic write 검증."""
        data = tm.load_todo()
        data["issues"].append({"id": "issue-003", "project": "X", "title": "Y"})

        # 저장 중간에 tmp 파일이 있어도 최종적으로는 정상
        tm.save_todo(data)

        # 재로드로 검증
        reloaded = tm.load_todo()
        assert len(reloaded["issues"]) == 3

    def test_json_validation(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """JSON 검증 (깨진 JSON 저장 방지)."""
        data = tm.load_todo()
        # 정상 저장
        tm.save_todo(data)

        # 저장된 파일이 유효한 JSON인지 확인
        with open(tu.TODO_FILE, encoding="utf-8") as f:
            reloaded = json.load(f)
        assert reloaded == data


class TestList:
    """list 서브커맨드 테스트."""

    def test_list_all(self, temp_todo_dir: Path, sample_todo: dict, capsys) -> None:
        """전체 목록."""
        import argparse

        args = argparse.Namespace(project=None, status=None, json=False)
        tm.cmd_list(args)
        captured = capsys.readouterr()
        assert "issue-001" in captured.out
        assert "issue-002" in captured.out

    def test_list_filter_project(
        self, temp_todo_dir: Path, sample_todo: dict, capsys
    ) -> None:
        """프로젝트 필터."""
        import argparse

        args = argparse.Namespace(project="TestProj", status=None, json=False)
        tm.cmd_list(args)
        captured = capsys.readouterr()
        assert "issue-001" in captured.out
        assert "issue-002" not in captured.out

    def test_list_filter_status(
        self, temp_todo_dir: Path, sample_todo: dict, capsys
    ) -> None:
        """상태 필터."""
        import argparse

        args = argparse.Namespace(project=None, status="in_progress", json=False)
        tm.cmd_list(args)
        captured = capsys.readouterr()
        assert "issue-001" not in captured.out
        assert "issue-002" in captured.out


class TestShow:
    """show 서브커맨드 테스트."""

    def test_show_existing(
        self, temp_todo_dir: Path, sample_todo: dict, capsys
    ) -> None:
        """존재하는 이슈 조회."""
        import argparse

        args = argparse.Namespace(issue_id="issue-001", json=False)
        tm.cmd_show(args)
        captured = capsys.readouterr()
        assert "테스트 이슈 1" in captured.out

    def test_show_not_found(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """존재하지 않는 이슈."""
        import argparse

        args = argparse.Namespace(issue_id="issue-999", json=False)
        with pytest.raises(SystemExit) as exc:
            tm.cmd_show(args)
        assert exc.value.code == 1


class TestAdd:
    """add 서브커맨드 테스트."""

    def test_add_minimal(self, temp_todo_dir: Path, capsys) -> None:
        """최소 필드로 추가."""
        import argparse

        args = argparse.Namespace(
            project="NewProj",
            title="새 이슈",
            priority=None,
            status=None,
            description=None,
            json=False,
        )
        tm.cmd_add(args)
        captured = capsys.readouterr()
        assert "success" in captured.out.lower() or '"id"' in captured.out

        data = tm.load_todo()
        assert len(data["issues"]) == 1
        assert data["issues"][0]["project"] == "NewProj"

    def test_add_full(self, temp_todo_dir: Path) -> None:
        """모든 필드로 추가."""
        import argparse

        args = argparse.Namespace(
            project="FullProj",
            title="전체 이슈",
            priority="high",
            status="in_progress",
            description="상세 설명",
            json=False,
        )
        tm.cmd_add(args)

        data = tm.load_todo()
        issue = data["issues"][0]
        assert issue["priority"] == "high"
        assert issue["status"] == "in_progress"
        assert issue["description"] == "상세 설명"

    def test_auto_id_generation(self, temp_todo_dir: Path) -> None:
        """ID 자동 생성."""
        import argparse

        # 첫 번째 추가
        args1 = argparse.Namespace(
            project="P", title="T1", priority=None, status=None, description=None, json=False
        )
        tm.cmd_add(args1)

        # 두 번째 추가
        args2 = argparse.Namespace(
            project="P", title="T2", priority=None, status=None, description=None, json=False
        )
        tm.cmd_add(args2)

        data = tm.load_todo()
        ids = [i["id"] for i in data["issues"]]
        assert len(set(ids)) == 2  # 중복 없음


class TestUpdate:
    """update 서브커맨드 테스트."""

    def test_update_status(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """상태 수정."""
        import argparse

        args = argparse.Namespace(
            issue_id="issue-001", status="done", priority=None, title=None, json=False
        )
        tm.cmd_update(args)

        data = tm.load_todo()
        issue = tm.find_issue(data, "issue-001")
        assert issue["status"] == "done"
        assert issue["completed_at"] is not None

    def test_update_priority(
        self, temp_todo_dir: Path, sample_todo: dict, capsys
    ) -> None:
        """우선순위 수정."""
        import argparse

        args = argparse.Namespace(
            issue_id="issue-001", status=None, priority="low", title=None, json=False
        )
        tm.cmd_update(args)

        data = tm.load_todo()
        issue = tm.find_issue(data, "issue-001")
        assert issue["priority"] == "low"

    def test_update_not_found(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """존재하지 않는 이슈."""
        import argparse

        args = argparse.Namespace(
            issue_id="issue-999", status="done", priority=None, title=None, json=False
        )
        with pytest.raises(SystemExit):
            tm.cmd_update(args)


class TestRemove:
    """remove 서브커맨드 테스트."""

    def test_remove_with_confirm(
        self, temp_todo_dir: Path, sample_todo: dict, monkeypatch
    ) -> None:
        """확인 후 삭제."""
        import argparse

        monkeypatch.setattr("builtins.input", lambda _: "y")
        args = argparse.Namespace(issue_id="issue-001", force=False, json=False)
        tm.cmd_remove(args)

        data = tm.load_todo()
        assert len(data["issues"]) == 1
        assert data["issues"][0]["id"] == "issue-002"

    def test_remove_force(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """강제 삭제."""
        import argparse

        args = argparse.Namespace(issue_id="issue-001", force=True, json=False)
        tm.cmd_remove(args)

        data = tm.load_todo()
        assert len(data["issues"]) == 1

    def test_remove_creates_backup(
        self, temp_todo_dir: Path, sample_todo: dict
    ) -> None:
        """삭제된 이슈 백업."""
        import argparse

        args = argparse.Namespace(issue_id="issue-001", force=True, json=False)
        tm.cmd_remove(args)

        assert tu.REMOVED_FILE.exists()
        with open(tu.REMOVED_FILE, encoding="utf-8") as f:
            removed = json.load(f)
        assert len(removed["removed"]) == 1
        assert removed["removed"][0]["id"] == "issue-001"


class TestSubAdd:
    """sub-add 서브커맨드 테스트."""

    def test_sub_add(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """sub_item 추가."""
        import argparse

        args = argparse.Namespace(issue_id="issue-001", title="새 서브", json=False)
        tm.cmd_sub_add(args)

        data = tm.load_todo()
        issue = tm.find_issue(data, "issue-001")
        assert len(issue["sub_items"]) == 3
        assert issue["sub_items"][2]["title"] == "새 서브"
        assert issue["sub_items"][2]["done"] is False


class TestSubDone:
    """sub-done 서브커맨드 테스트."""

    def test_sub_done_by_index(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """인덱스로 완료."""
        import argparse

        args = argparse.Namespace(
            issue_id="issue-001", index=0, match=None, task_id="task-123", json=False
        )
        tm.cmd_sub_done(args)

        data = tm.load_todo()
        issue = tm.find_issue(data, "issue-001")
        assert issue["sub_items"][0]["done"] is True
        assert issue["sub_items"][0]["task_id"] == "task-123"

    def test_sub_done_by_match(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """매칭으로 완료."""
        import argparse

        args = argparse.Namespace(
            issue_id="issue-001", index=None, match="서브1", task_id="task-456", json=False
        )
        tm.cmd_sub_done(args)

        data = tm.load_todo()
        issue = tm.find_issue(data, "issue-001")
        assert issue["sub_items"][0]["done"] is True

    def test_sub_done_invalid_index(
        self, temp_todo_dir: Path, sample_todo: dict
    ) -> None:
        """잘못된 인덱스."""
        import argparse

        args = argparse.Namespace(
            issue_id="issue-001", index=99, match=None, task_id="task-789", json=False
        )
        with pytest.raises(SystemExit):
            tm.cmd_sub_done(args)

    def test_sub_done_no_match(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """매칭 없음."""
        import argparse

        args = argparse.Namespace(
            issue_id="issue-001",
            index=None,
            match="없는거",
            task_id="task-000",
            json=False,
        )
        with pytest.raises(SystemExit):
            tm.cmd_sub_done(args)


class TestLink:
    """link 서브커맨드 테스트."""

    def test_link_new(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """새 링크 추가."""
        import argparse

        args = argparse.Namespace(issue_id="issue-001", task_id="task-789", json=False)
        tm.cmd_link(args)

        data = tm.load_todo()
        issue = tm.find_issue(data, "issue-001")
        assert "task-789" in issue["linked_tasks"]

    def test_link_duplicate(self, temp_todo_dir: Path, sample_todo: dict) -> None:
        """중복 링크 방지."""
        import argparse

        args = argparse.Namespace(issue_id="issue-002", task_id="task-002", json=False)
        tm.cmd_link(args)

        data = tm.load_todo()
        issue = tm.find_issue(data, "issue-002")
        assert issue["linked_tasks"].count("task-002") == 1


class TestJsonIntegrity:
    """JSON 무결성 테스트."""

    def test_save_validates_json(
        self, temp_todo_dir: Path, sample_todo: dict, monkeypatch
    ) -> None:
        """저장 전 JSON 검증."""

        # json.load가 실패하면 예외 발생
        original_load = json.load

        call_count = [0]

        def tracked_load(fp):
            call_count[0] += 1
            if call_count[0] == 2:  # 두 번째 호출 (검증)
                raise json.JSONDecodeError("test", "", 0)
            return original_load(fp)

        monkeypatch.setattr(json, "load", tracked_load)

        data = tm.load_todo()
        data["issues"][0]["title"] = "수정"

        with pytest.raises(json.JSONDecodeError):
            tm.save_todo(data)

        # 원본 파일은 변경되지 않음
        with open(tu.TODO_FILE, encoding="utf-8") as f:
            original = json.load(f)
        assert original["issues"][0]["title"] == "테스트 이슈 1"
