"""
Tests for the unified state tracking module.

Tests cover:
- PersuadeState loading and saving
- SyncState tracking and queries
- SessionState validity and updates
- State file persistence
"""

import json
from datetime import UTC, datetime, timedelta
from pathlib import Path

from persuade.state import (
    SESSION_VALIDITY_HOURS,
    STATE_FILE_PATH,
    STATE_SCHEMA_VERSION,
    PersuadeState,
    SessionState,
    SyncEntry,
    SyncState,
)


class TestSyncEntry:
    """Tests for SyncEntry dataclass."""

    def test_to_dict(self) -> None:
        """Test converting SyncEntry to dict."""
        entry = SyncEntry(
            source=".github/workflows/ci.yml",
            destination=".github/workflows/ci.yml",
            synced_at="2026-01-24T12:00:00+00:00",
            checksum="abc123",
        )
        result = entry.to_dict()

        assert result["source"] == ".github/workflows/ci.yml"
        assert result["destination"] == ".github/workflows/ci.yml"
        assert result["synced_at"] == "2026-01-24T12:00:00+00:00"
        assert result["checksum"] == "abc123"

    def test_from_dict(self) -> None:
        """Test creating SyncEntry from dict."""
        data = {
            "source": ".github/workflows/ci.yml",
            "destination": ".github/workflows/ci.yml",
            "synced_at": "2026-01-24T12:00:00+00:00",
            "checksum": "abc123",
        }
        entry = SyncEntry.from_dict(data)

        assert entry.source == ".github/workflows/ci.yml"
        assert entry.destination == ".github/workflows/ci.yml"
        assert entry.synced_at == "2026-01-24T12:00:00+00:00"
        assert entry.checksum == "abc123"

    def test_from_dict_missing_fields(self) -> None:
        """Test creating SyncEntry from incomplete dict."""
        entry = SyncEntry.from_dict({})

        assert entry.source == ""
        assert entry.destination == ""
        assert entry.synced_at == ""
        assert entry.checksum == ""


class TestSyncState:
    """Tests for SyncState tracking."""

    def test_add_entry(self) -> None:
        """Test adding a sync entry."""
        sync = SyncState()
        sync.add_entry(
            source=".github/workflows/ci.yml",
            destination=".github/workflows/ci.yml",
            checksum="abc123",
        )

        assert ".github/workflows/ci.yml" in sync.synced_files
        entry = sync.synced_files[".github/workflows/ci.yml"]
        assert entry.source == ".github/workflows/ci.yml"
        assert entry.checksum == "abc123"
        assert sync.last_sync != ""

    def test_remove_entry(self) -> None:
        """Test removing a sync entry."""
        sync = SyncState()
        sync.add_entry("source.txt", "dest.txt")

        assert sync.remove_entry("dest.txt") is True
        assert "dest.txt" not in sync.synced_files
        assert sync.remove_entry("nonexistent.txt") is False

    def test_get_destinations(self) -> None:
        """Test getting all destinations."""
        sync = SyncState()
        sync.add_entry("a.txt", "dest/a.txt")
        sync.add_entry("b.txt", "dest/b.txt")

        destinations = sync.get_destinations()
        assert len(destinations) == 2
        assert "dest/a.txt" in destinations
        assert "dest/b.txt" in destinations

    def test_get_external_destinations(self) -> None:
        """Test getting destinations outside .claude/."""
        sync = SyncState()
        sync.add_entry("a.txt", ".claude/a.txt")
        sync.add_entry("b.yml", ".github/workflows/b.yml")
        sync.add_entry("c.yml", "lefthook.yml")

        external = sync.get_external_destinations()
        assert len(external) == 2
        assert ".github/workflows/b.yml" in external
        assert "lefthook.yml" in external
        assert ".claude/a.txt" not in external


class TestSessionState:
    """Tests for SessionState tracking."""

    def test_is_valid_fresh(self) -> None:
        """Test that fresh session is valid."""
        session = SessionState()
        session.update(current_task="Testing")

        assert session.is_valid() is True

    def test_is_valid_empty(self) -> None:
        """Test that empty session is invalid."""
        session = SessionState()

        assert session.is_valid() is False

    def test_is_valid_expired(self) -> None:
        """Test that old session is invalid."""
        old_time = datetime.now(UTC) - timedelta(hours=SESSION_VALIDITY_HOURS + 1)
        session = SessionState(last_updated=old_time.isoformat())

        assert session.is_valid() is False

    def test_update(self) -> None:
        """Test updating session fields."""
        session = SessionState()
        session.update(
            working_directory="/path/to/project",
            git_branch="feature/test",
            current_task="Testing",
        )

        assert session.working_directory == "/path/to/project"
        assert session.git_branch == "feature/test"
        assert session.current_task == "Testing"
        assert session.last_updated != ""

    def test_update_partial(self) -> None:
        """Test partial update preserves other fields."""
        session = SessionState(
            working_directory="/old/path",
            git_branch="main",
            current_task="Old task",
        )
        session.update(current_task="New task")

        assert session.working_directory == "/old/path"
        assert session.git_branch == "main"
        assert session.current_task == "New task"

    def test_add_pattern(self) -> None:
        """Test adding learned patterns."""
        session = SessionState()
        session.add_pattern("prefers pytest")
        session.add_pattern("uses ruff")
        session.add_pattern("prefers pytest")  # Duplicate

        assert len(session.learned_patterns) == 2
        assert "prefers pytest" in session.learned_patterns
        assert "uses ruff" in session.learned_patterns

    def test_add_pattern_empty(self) -> None:
        """Test that empty pattern is not added."""
        session = SessionState()
        session.add_pattern("")

        assert len(session.learned_patterns) == 0

    def test_clear(self) -> None:
        """Test clearing session state."""
        session = SessionState(
            last_updated=datetime.now(UTC).isoformat(),
            working_directory="/path",
            git_branch="main",
            current_task="Task",
            learned_patterns=["pattern1"],
        )
        session.clear()

        assert session.last_updated == ""
        assert session.working_directory == ""
        assert session.git_branch == ""
        assert session.current_task == ""
        assert session.learned_patterns == []


class TestPersuadeState:
    """Tests for unified PersuadeState."""

    def test_load_nonexistent(self, tmp_path: Path) -> None:
        """Test loading from nonexistent file creates new state."""
        state = PersuadeState.load(tmp_path)

        assert state.version == STATE_SCHEMA_VERSION
        assert len(state.sync.synced_files) == 0
        assert state.session.last_updated == ""
        assert state.meta.created_at != ""

    def test_save_and_load(self, tmp_path: Path) -> None:
        """Test saving and loading state."""
        # Create and modify state
        state = PersuadeState.load(tmp_path)
        state.sync.add_entry("source.yml", "dest.yml")
        state.session.update(current_task="Testing")
        state.save(tmp_path)

        # Load and verify
        loaded = PersuadeState.load(tmp_path)
        assert "dest.yml" in loaded.sync.synced_files
        assert loaded.session.current_task == "Testing"

    def test_save_creates_directory(self, tmp_path: Path) -> None:
        """Test that save creates state directory."""
        state = PersuadeState.load(tmp_path)
        state.save(tmp_path)

        state_file = tmp_path / STATE_FILE_PATH
        assert state_file.exists()
        assert state_file.parent.exists()

    def test_load_invalid_json(self, tmp_path: Path) -> None:
        """Test loading invalid JSON creates new state."""
        state_file = tmp_path / STATE_FILE_PATH
        state_file.parent.mkdir(parents=True, exist_ok=True)
        state_file.write_text("not valid json")

        state = PersuadeState.load(tmp_path)
        assert state.version == STATE_SCHEMA_VERSION

    def test_clear_session(self, tmp_path: Path) -> None:
        """Test clearing session preserves sync."""
        state = PersuadeState.load(tmp_path)
        state.sync.add_entry("source.yml", "dest.yml")
        state.session.update(current_task="Task")
        state.clear_session()

        assert "dest.yml" in state.sync.synced_files
        assert state.session.current_task == ""

    def test_clear_all(self, tmp_path: Path) -> None:
        """Test clearing all state."""
        state = PersuadeState.load(tmp_path)
        state.sync.add_entry("source.yml", "dest.yml")
        state.session.update(current_task="Task")
        state.clear_all()

        assert len(state.sync.synced_files) == 0
        assert state.session.current_task == ""

    def test_to_dict_and_from_dict(self) -> None:
        """Test round-trip serialization."""
        original = PersuadeState()
        original.sync.add_entry("source.yml", "dest.yml")
        original.session.update(current_task="Task")

        data = original.to_dict()
        loaded = PersuadeState.from_dict(data)

        assert "dest.yml" in loaded.sync.synced_files
        assert loaded.session.current_task == "Task"


class TestPersuadeStateIntegration:
    """Integration tests for state with sync workflow."""

    def test_track_multiple_files(self, tmp_path: Path) -> None:
        """Test tracking multiple synced files."""
        state = PersuadeState.load(tmp_path)

        # Simulate sync of multiple files
        files = [
            (".github/workflows/ci.yml", ".github/workflows/ci.yml"),
            (".github/workflows/release.yml", ".github/workflows/release.yml"),
            ("lefthook.yml", "lefthook.yml"),
            (".claude/settings.json", ".claude/settings.json"),
        ]

        for source, dest in files:
            state.sync.add_entry(source, dest)

        state.save(tmp_path)

        # Verify external files
        external = state.sync.get_external_destinations()
        assert len(external) == 3
        assert ".github/workflows/ci.yml" in external
        assert "lefthook.yml" in external
        assert ".claude/settings.json" not in external

    def test_state_file_format(self, tmp_path: Path) -> None:
        """Test that state file is valid JSON with expected structure."""
        state = PersuadeState.load(tmp_path)
        state.sync.add_entry("source.yml", "dest.yml")
        state.session.update(current_task="Testing")
        state.save(tmp_path)

        state_file = tmp_path / STATE_FILE_PATH
        with open(state_file) as f:
            data = json.load(f)

        assert "version" in data
        assert "sync" in data
        assert "session" in data
        assert "meta" in data
        assert "synced_files" in data["sync"]
        assert "current_task" in data["session"]
