"""Tests for glreview.state_machine module."""

from __future__ import annotations

import pytest

from glreview.errors import AlreadyInStateError, InvalidStateTransitionError
from glreview.registry import ModuleStatus
from glreview.state_machine import (
    Status,
    VALID_TRANSITIONS,
    cancel_review,
    restart_review,
    signoff,
    start_review,
    validate_transition,
)


class TestStatus:
    """Tests for Status enum."""

    def test_enum_values(self):
        """Status enum has expected values."""
        assert Status.NEEDS_REVIEW.value == "needs_review"
        assert Status.IN_PROGRESS.value == "in_progress"
        assert Status.REVIEWED.value == "reviewed"

    def test_from_string_valid(self):
        """from_string converts valid strings."""
        assert Status.from_string("needs_review") == Status.NEEDS_REVIEW
        assert Status.from_string("in_progress") == Status.IN_PROGRESS
        assert Status.from_string("reviewed") == Status.REVIEWED

    def test_from_string_invalid(self):
        """from_string raises for invalid strings."""
        with pytest.raises(ValueError, match="Invalid status"):
            Status.from_string("invalid")


class TestValidTransitions:
    """Tests for VALID_TRANSITIONS mapping."""

    def test_needs_review_transitions(self):
        """needs_review can transition to in_progress."""
        assert Status.IN_PROGRESS in VALID_TRANSITIONS[Status.NEEDS_REVIEW]

    def test_in_progress_transitions(self):
        """in_progress can transition to reviewed or needs_review."""
        valid = VALID_TRANSITIONS[Status.IN_PROGRESS]
        assert Status.REVIEWED in valid
        assert Status.NEEDS_REVIEW in valid

    def test_reviewed_transitions(self):
        """reviewed can transition to in_progress or needs_review."""
        valid = VALID_TRANSITIONS[Status.REVIEWED]
        assert Status.IN_PROGRESS in valid
        assert Status.NEEDS_REVIEW in valid


class TestValidateTransition:
    """Tests for validate_transition function."""

    def test_valid_transition_allowed(self):
        """Valid transition does not raise."""
        validate_transition(Status.NEEDS_REVIEW, Status.IN_PROGRESS)
        validate_transition(Status.IN_PROGRESS, Status.REVIEWED)
        validate_transition(Status.IN_PROGRESS, Status.NEEDS_REVIEW)

    def test_same_state_raises(self):
        """Transition to same state raises AlreadyInStateError."""
        with pytest.raises(AlreadyInStateError):
            validate_transition(Status.NEEDS_REVIEW, Status.NEEDS_REVIEW)

    def test_invalid_transition_raises(self):
        """Invalid transition raises InvalidStateTransitionError."""
        with pytest.raises(InvalidStateTransitionError) as exc_info:
            validate_transition(Status.NEEDS_REVIEW, Status.REVIEWED)
        assert exc_info.value.current == "needs_review"
        assert exc_info.value.target == "reviewed"
        assert "in_progress" in exc_info.value.valid_targets

    def test_force_skips_validation(self):
        """force=True skips all validation."""
        validate_transition(Status.NEEDS_REVIEW, Status.NEEDS_REVIEW, force=True)
        validate_transition(Status.NEEDS_REVIEW, Status.REVIEWED, force=True)


class TestStartReview:
    """Tests for start_review function."""

    def test_transitions_to_in_progress(self):
        """Transitions module to in_progress."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        start_review(module, "abc123")
        assert module.status == "in_progress"

    def test_sets_commit(self):
        """Sets review_started_commit."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        start_review(module, "abc123")
        assert module.review_started_commit == "abc123"

    def test_sets_issue_url(self):
        """Sets review_issue when provided."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        start_review(module, "abc123", issue_url="#42")
        assert module.review_issue == "#42"

    def test_sets_assignee(self):
        """Sets assigned_reviewer when provided."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        start_review(module, "abc123", assignee="@alice")
        assert module.assigned_reviewer == "@alice"

    def test_from_reviewed_allowed(self):
        """Can start review from reviewed state."""
        module = ModuleStatus(path="src/main.py", status="reviewed")
        start_review(module, "abc123")
        assert module.status == "in_progress"

    def test_from_in_progress_raises(self):
        """Cannot start review when already in progress."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        with pytest.raises(AlreadyInStateError):
            start_review(module, "abc123")

    def test_force_allows_restart(self):
        """force=True allows restarting in_progress review."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
            review_started_commit="old123",
        )
        start_review(module, "new456", force=True)
        assert module.review_started_commit == "new456"


class TestSignoff:
    """Tests for signoff function."""

    def test_transitions_to_reviewed(self):
        """Transitions module to reviewed."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        signoff(module, "abc123", "@alice")
        assert module.status == "reviewed"

    def test_sets_commit_and_date(self):
        """Sets last_reviewed_commit and last_reviewed_date."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        signoff(module, "abc123", "@alice")
        assert module.last_reviewed_commit == "abc123"
        assert module.last_reviewed_date is not None
        # Date format is YYYY-MM-DD
        assert len(module.last_reviewed_date) == 10

    def test_adds_reviewer(self):
        """Adds reviewer to reviewers list."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        signoff(module, "abc123", "@alice")
        assert "@alice" in module.reviewers

    def test_adds_at_prefix(self):
        """Adds @ prefix to reviewer without one."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        signoff(module, "abc123", "bob")
        assert "@bob" in module.reviewers

    def test_multiple_reviewers(self):
        """Handles comma-separated reviewers."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        signoff(module, "abc123", "@alice, bob, @charlie")
        assert "@alice" in module.reviewers
        assert "@bob" in module.reviewers
        assert "@charlie" in module.reviewers

    def test_merges_with_existing_reviewers(self):
        """Merges new reviewers with existing list."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
            reviewers=["@alice"],
        )
        signoff(module, "abc123", "@bob")
        assert "@alice" in module.reviewers
        assert "@bob" in module.reviewers

    def test_clears_in_progress_fields(self):
        """Clears assigned_reviewer and review_started_commit."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
            assigned_reviewer="@alice",
            review_started_commit="old123",
        )
        signoff(module, "abc123", "@alice")
        assert module.assigned_reviewer is None
        assert module.review_started_commit is None

    def test_from_needs_review_raises(self):
        """Cannot signoff without being in_progress."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        with pytest.raises(InvalidStateTransitionError):
            signoff(module, "abc123", "@alice")

    def test_force_allows_direct_signoff(self):
        """force=True allows signoff from any state."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        signoff(module, "abc123", "@alice", force=True)
        assert module.status == "reviewed"


class TestCancelReview:
    """Tests for cancel_review function."""

    def test_in_progress_to_needs_review(self):
        """Canceling never-reviewed module goes to needs_review."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        result = cancel_review(module)
        assert result == "needs_review"
        assert module.status == "needs_review"

    def test_in_progress_to_reviewed(self):
        """Canceling previously-reviewed module goes to reviewed."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
            last_reviewed_commit="old123",
        )
        result = cancel_review(module)
        assert result == "reviewed"
        assert module.status == "reviewed"

    def test_clears_in_progress_fields(self):
        """Clears review_issue, assigned_reviewer, review_started_commit."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
            review_issue="#42",
            assigned_reviewer="@alice",
            review_started_commit="abc123",
        )
        cancel_review(module)
        assert module.review_issue is None
        assert module.assigned_reviewer is None
        assert module.review_started_commit is None

    def test_from_needs_review_raises(self):
        """Cannot cancel when not in progress or reviewed."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        # Raises AlreadyInStateError because it would transition to needs_review
        with pytest.raises(AlreadyInStateError):
            cancel_review(module)

    def test_force_allows_from_any_state(self):
        """force=True allows cancel from any state."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        result = cancel_review(module, force=True)
        assert module.status == result


class TestRestartReview:
    """Tests for restart_review function."""

    def test_updates_commit(self):
        """Updates review_started_commit."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
            review_started_commit="old123",
        )
        restart_review(module, "new456")
        assert module.review_started_commit == "new456"

    def test_preserves_status(self):
        """Keeps status as in_progress."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
        )
        restart_review(module, "abc123")
        assert module.status == "in_progress"

    def test_preserves_other_fields(self):
        """Preserves issue and assignee."""
        module = ModuleStatus(
            path="src/main.py",
            status="in_progress",
            review_issue="#42",
            assigned_reviewer="@alice",
        )
        restart_review(module, "abc123")
        assert module.review_issue == "#42"
        assert module.assigned_reviewer == "@alice"

    def test_from_needs_review_raises(self):
        """Cannot restart from needs_review state."""
        module = ModuleStatus(path="src/main.py", status="needs_review")
        with pytest.raises(InvalidStateTransitionError) as exc_info:
            restart_review(module, "abc123")
        assert "Only in_progress" in str(exc_info.value.valid_targets)

    def test_from_reviewed_raises(self):
        """Cannot restart from reviewed state."""
        module = ModuleStatus(path="src/main.py", status="reviewed")
        with pytest.raises(InvalidStateTransitionError):
            restart_review(module, "abc123")
