"""Tests for glreview.review_service module."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from glreview.config import Config
from glreview.errors import FileChangedDuringReviewError, IssueNotClosedError
from glreview.registry import ModuleStatus, Registry
from glreview.review_service import ReviewResult, ReviewService


class TestReviewResult:
    """Tests for ReviewResult dataclass."""

    def test_success_result(self):
        """Creates success result."""
        result = ReviewResult(success=True, message="Done")
        assert result.success is True
        assert result.message == "Done"

    def test_failure_result(self):
        """Creates failure result."""
        module = ModuleStatus(path="src/main.py")
        result = ReviewResult(success=False, message="Error", module=module)
        assert result.success is False
        assert result.module == module


class TestReviewService:
    """Tests for ReviewService class."""

    @pytest.fixture
    def config(self, temp_dir):
        """Create a config with temp_dir as root."""
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        """Create a registry with test modules."""
        reg = Registry()
        reg.set(ModuleStatus(path="src/main.py", status="needs_review", lines=100))
        reg.set(
            ModuleStatus(
                path="src/other.py",
                status="in_progress",
                review_started_commit="abc123",
                assigned_reviewer="@alice",
                lines=50,
            )
        )
        reg.set(
            ModuleStatus(
                path="src/done.py",
                status="reviewed",
                last_reviewed_commit="def456",
                lines=75,
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        """Create a ReviewService."""
        return ReviewService(registry, config)

    def test_get_module_exists(self, service):
        """get_module returns existing module."""
        module = service.get_module("src/main.py")
        assert module.path == "src/main.py"

    def test_get_module_not_found(self, service):
        """get_module raises for missing module."""
        from glreview.errors import ModuleNotFoundError

        with pytest.raises(ModuleNotFoundError):
            service.get_module("nonexistent.py")

    @patch("glreview.review_state.has_changes_since")
    def test_get_registry_state(self, mock_has_changes, service):
        """get_registry_state categorizes modules."""
        mock_has_changes.return_value = False

        state = service.get_registry_state()

        assert state.total == 3
        assert len(state.needs_review) == 1
        assert len(state.in_progress) == 1
        assert len(state.reviewed_current) == 1

    @patch("glreview.review_service.get_current_commit")
    def test_get_current_commit(self, mock_commit, service):
        """get_current_commit returns commit."""
        mock_commit.return_value = "abc123"
        commit = service.get_current_commit()
        assert commit == "abc123"

    @patch("glreview.review_service.has_changes_since")
    def test_check_file_changed_no_started_commit(self, mock_has_changes, service):
        """check_file_changed returns False when no started commit."""
        module = ModuleStatus(path="src/main.py")
        changed, start, current = service.check_file_changed_during_review(module)
        assert changed is False
        assert start is None
        mock_has_changes.assert_not_called()

    @patch("glreview.review_service.has_changes_since")
    @patch("glreview.review_service.get_current_commit")
    def test_check_file_changed_has_changes(
        self, mock_commit, mock_has_changes, service
    ):
        """check_file_changed detects changes."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = True

        module = ModuleStatus(
            path="src/main.py",
            review_started_commit="start123",
        )
        changed, start, current = service.check_file_changed_during_review(module)

        assert changed is True
        assert start == "start123"
        assert current == "current123"


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

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(ModuleStatus(path="src/main.py", status="needs_review"))
        reg.set(
            ModuleStatus(
                path="src/reviewed.py",
                status="reviewed",
                last_reviewed_commit="abc123",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    @patch("glreview.review_service.get_current_commit")
    def test_start_review_success(self, mock_commit, service):
        """Successfully starts a review."""
        mock_commit.return_value = "commit123"

        result = service.start_review("src/main.py", assignee="@alice")

        assert result.success is True
        assert result.module.status == "in_progress"
        assert result.module.assigned_reviewer == "@alice"
        assert result.commit == "commit123"

    def test_start_review_not_found(self, service):
        """Returns failure for missing module."""
        result = service.start_review("nonexistent.py")

        assert result.success is False
        assert "not found" in result.message

    @patch("glreview.review_state.has_changes_since")
    @patch("glreview.review_service.get_current_commit")
    def test_start_review_already_reviewed_current(
        self, mock_commit, mock_has_changes, service
    ):
        """Fails for reviewed module that hasn't changed."""
        mock_commit.return_value = "commit123"
        mock_has_changes.return_value = False  # Not stale

        result = service.start_review("src/reviewed.py")

        assert result.success is False
        assert "already reviewed" in result.message

    @patch("glreview.review_service.has_changes_since")
    @patch("glreview.review_service.get_current_commit")
    def test_start_review_force(self, mock_commit, mock_has_changes, service):
        """Force starts review on reviewed module."""
        mock_commit.return_value = "commit123"
        mock_has_changes.return_value = False

        result = service.start_review("src/reviewed.py", force=True)

        assert result.success is True


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

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/in_progress.py",
                status="in_progress",
                review_started_commit="start123",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    @patch("glreview.review_service.has_changes_since")
    @patch("glreview.review_service.get_current_commit")
    def test_signoff_success(self, mock_commit, mock_has_changes, service):
        """Successfully signs off a review."""
        mock_commit.return_value = "commit123"
        mock_has_changes.return_value = False

        result = service.signoff("src/in_progress.py", reviewer="@bob")

        assert result.success is True
        assert result.module.status == "reviewed"
        assert "@bob" in result.module.reviewers

    def test_signoff_not_found(self, service):
        """Returns failure for missing module."""
        result = service.signoff("nonexistent.py", reviewer="@bob")
        assert result.success is False

    @patch("glreview.review_service.has_changes_since")
    @patch("glreview.review_service.get_current_commit")
    def test_signoff_raises_on_changes(self, mock_commit, mock_has_changes, service):
        """Raises error when file changed during review."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = True

        with pytest.raises(FileChangedDuringReviewError):
            service.signoff("src/in_progress.py", reviewer="@bob")

    @patch("glreview.review_service.has_changes_since")
    @patch("glreview.review_service.get_current_commit")
    def test_signoff_acknowledge_changes(self, mock_commit, mock_has_changes, service):
        """Succeeds when acknowledging changes."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = True

        result = service.signoff(
            "src/in_progress.py",
            reviewer="@bob",
            acknowledge_changes=True,
        )

        assert result.success is True


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

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/in_progress.py",
                status="in_progress",
                review_issue="#42",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    def test_cancel_review_success(self, service):
        """Successfully cancels a review."""
        result = service.cancel_review("src/in_progress.py")

        assert result.success is True
        assert result.module.status == "needs_review"
        assert result.module.review_issue is None

    def test_cancel_review_not_found(self, service):
        """Returns failure for missing module."""
        result = service.cancel_review("nonexistent.py")
        assert result.success is False


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

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/in_progress.py",
                status="in_progress",
                review_started_commit="old123",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    @patch("glreview.review_service.get_current_commit")
    def test_restart_review_success(self, mock_commit, service):
        """Successfully restarts a review."""
        mock_commit.return_value = "new456"

        result = service.restart_review("src/in_progress.py")

        assert result.success is True
        assert result.module.review_started_commit == "new456"

    def test_restart_review_not_found(self, service):
        """Returns failure for missing module."""
        result = service.restart_review("nonexistent.py")
        assert result.success is False


class TestGetSummary:
    """Tests for get_summary method."""

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="a.py",
                status="reviewed",
                lines=100,
                last_reviewed_commit="abc123",
            )
        )
        reg.set(ModuleStatus(path="b.py", status="needs_review", lines=50))
        reg.set(ModuleStatus(path="c.py", status="in_progress", lines=75))
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    @patch("glreview.review_state.has_changes_since")
    def test_get_summary(self, mock_has_changes, service):
        """get_summary returns correct stats."""
        mock_has_changes.return_value = False

        summary = service.get_summary()

        assert summary["total"] == 3
        assert summary["reviewed"] == 1
        assert summary["in_progress"] == 1
        assert summary["needs_review"] == 1
        assert summary["total_lines"] == 225
        assert summary["reviewed_lines"] == 100


class TestStartReviewExceptions:
    """Tests for start_review exception handling."""

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/in_progress.py",
                status="in_progress",
                review_started_commit="abc123",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    @patch("glreview.review_service.get_current_commit")
    def test_start_review_state_machine_exception(self, mock_commit, service):
        """Returns failure when state machine raises."""
        mock_commit.return_value = "abc123"

        # Trying to start a review that's already in progress without force
        result = service.start_review("src/in_progress.py")

        assert result.success is False
        assert "already" in result.message.lower() or "in_progress" in result.message.lower()


class TestSignoffExceptions:
    """Tests for signoff exception handling."""

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/needs_review.py",
                status="needs_review",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    @patch("glreview.review_service.get_current_commit")
    def test_signoff_state_machine_exception(self, mock_commit, service):
        """Returns failure when state machine raises."""
        mock_commit.return_value = "abc123"

        # Trying to signoff on needs_review should fail
        result = service.signoff("src/needs_review.py", reviewer="@bob")

        assert result.success is False
        assert result.module is not None


class TestCancelReviewExceptions:
    """Tests for cancel_review exception handling."""

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/needs_review.py",
                status="needs_review",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    def test_cancel_review_state_machine_exception(self, service):
        """Returns failure when state machine raises."""
        # Trying to cancel needs_review without force should fail
        result = service.cancel_review("src/needs_review.py")

        assert result.success is False
        assert result.module is not None


class TestRestartReviewExceptions:
    """Tests for restart_review exception handling."""

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/needs_review.py",
                status="needs_review",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    @patch("glreview.review_service.get_current_commit")
    def test_restart_review_state_machine_exception(self, mock_commit, service):
        """Returns failure when state machine raises."""
        mock_commit.return_value = "abc123"

        # Trying to restart needs_review should fail
        result = service.restart_review("src/needs_review.py")

        assert result.success is False
        assert result.module is not None


class TestVerifyIssueClosed:
    """Tests for verify_issue_closed method."""

    @pytest.fixture
    def config(self, temp_dir):
        return Config(root=temp_dir)

    @pytest.fixture
    def registry(self):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/main.py",
                status="in_progress",
                review_issue="#42",
            )
        )
        reg.set(
            ModuleStatus(
                path="src/no_issue.py",
                status="in_progress",
            )
        )
        return reg

    @pytest.fixture
    def service(self, registry, config):
        return ReviewService(registry, config)

    def test_skip_verify(self, service, registry):
        """Returns True when skip_verify is True."""
        module = registry.get("src/main.py")
        is_closed, error, assignees = service.verify_issue_closed(module, skip_verify=True)

        assert is_closed is True
        assert error is None
        assert assignees == []

    def test_no_issue_number(self, service, registry):
        """Returns True when no issue to verify."""
        module = registry.get("src/no_issue.py")
        is_closed, error, assignees = service.verify_issue_closed(module)

        assert is_closed is True
        assert error is None

    @patch("glreview.gitlab.gitlab_available")
    @patch("glreview.git.get_gitlab_project")
    @patch("glreview.git.get_gitlab_host")
    def test_gitlab_not_available(
        self, mock_host, mock_project, mock_gitlab, service, registry
    ):
        """Returns True with warning when GitLab unavailable."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_gitlab.return_value = False

        module = registry.get("src/main.py")
        is_closed, error, assignees = service.verify_issue_closed(module)

        assert is_closed is True
        assert "not available" in error.lower()

    @patch("glreview.gitlab.get_issue")
    @patch("glreview.gitlab.gitlab_available")
    @patch("glreview.git.get_gitlab_project")
    @patch("glreview.git.get_gitlab_host")
    def test_issue_fetch_fails(
        self, mock_host, mock_project, mock_gitlab, mock_get_issue, service, registry
    ):
        """Returns True with warning when issue fetch fails."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_gitlab.return_value = True
        mock_get_issue.return_value = None

        module = registry.get("src/main.py")
        is_closed, error, assignees = service.verify_issue_closed(module)

        assert is_closed is True
        assert "could not fetch" in error.lower()

    @patch("glreview.gitlab.get_issue")
    @patch("glreview.gitlab.gitlab_available")
    @patch("glreview.git.get_gitlab_project")
    @patch("glreview.git.get_gitlab_host")
    def test_issue_not_closed_raises(
        self, mock_host, mock_project, mock_gitlab, mock_get_issue, service, registry
    ):
        """Raises IssueNotClosedError when issue is open."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_gitlab.return_value = True
        mock_issue = MagicMock()
        mock_issue.state = "opened"
        mock_get_issue.return_value = mock_issue

        module = registry.get("src/main.py")
        with pytest.raises(IssueNotClosedError):
            service.verify_issue_closed(module)

    @patch("glreview.gitlab.get_issue")
    @patch("glreview.gitlab.gitlab_available")
    @patch("glreview.git.get_gitlab_project")
    @patch("glreview.git.get_gitlab_host")
    def test_issue_closed_returns_assignees(
        self, mock_host, mock_project, mock_gitlab, mock_get_issue, service, registry
    ):
        """Returns assignees when issue is closed."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_gitlab.return_value = True
        mock_issue = MagicMock()
        mock_issue.state = "closed"
        mock_issue.assignees = ["alice", "bob"]
        mock_get_issue.return_value = mock_issue

        module = registry.get("src/main.py")
        is_closed, error, assignees = service.verify_issue_closed(module)

        assert is_closed is True
        assert error is None
        assert assignees == ["alice", "bob"]
