"""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.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", create_issue=False)

        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, create_issue=False)

        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_fails_on_changes(self, mock_commit, mock_has_changes, service):
        """Returns failure when file changed during review."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = True

        result = service.signoff("src/in_progress.py", reviewer="@bob")
        assert result.success is False
        assert "changed during review" in result.message

    @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()

    @patch("glreview.review_service.close_issue")
    @patch("glreview.review_service.get_gitlab_host")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.state_machine")
    @patch("glreview.review_service.get_current_commit")
    def test_start_review_cleans_up_orphaned_issue(
        self, mock_commit, mock_sm, mock_project, mock_host, mock_close
    ):
        """Cleans up orphaned GitLab issue when state transition fails."""
        config = Config(root=Path("/tmp/test"))
        reg = Registry()
        reg.set(ModuleStatus(path="src/module.py", status="needs_review"))
        service = ReviewService(reg, config)

        mock_commit.return_value = "abc123"
        mock_sm.start_review.side_effect = RuntimeError("state error")
        mock_project.return_value = "org/project"
        mock_host.return_value = "gitlab.com"
        mock_close.return_value = True

        result = service.start_review(
            "src/module.py", create_issue=True, issue_url="https://gitlab.com/org/project/-/issues/99"
        )

        assert result.success is False
        mock_close.assert_called_once()

    @patch("glreview.review_service.close_issue")
    @patch("glreview.review_service.get_gitlab_host")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.state_machine")
    @patch("glreview.review_service.get_current_commit")
    def test_start_review_no_cleanup_without_create_issue(
        self, mock_commit, mock_sm, mock_project, mock_host, mock_close
    ):
        """Does not clean up when create_issue is False."""
        config = Config(root=Path("/tmp/test"))
        reg = Registry()
        reg.set(ModuleStatus(path="src/module.py", status="needs_review"))
        service = ReviewService(reg, config)

        mock_commit.return_value = "abc123"
        mock_sm.start_review.side_effect = RuntimeError("state error")

        result = service.start_review(
            "src/module.py", create_issue=False, issue_url="https://gitlab.com/org/project/-/issues/99"
        )

        assert result.success is False
        mock_close.assert_not_called()


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_returns_failure(
        self, mock_host, mock_project, mock_gitlab, mock_get_issue, service, registry
    ):
        """Returns failure tuple 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_issue.assignees = ["alice"]
        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 False
        assert "not closed" in error.lower()
        assert assignees == ["alice"]

    @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"]


class TestPostReviewToIssue:
    """Tests for _post_review_to_issue label update warning."""

    @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="https://gitlab.com/org/project/-/issues/42",
            )
        )
        return reg

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

    @patch("glreview.review_service.update_issue_review_label")
    @patch("glreview.review_service.add_issue_comment")
    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_label_update_failure_still_returns_true(
        self, mock_host, mock_project, mock_gitlab, mock_comment, mock_label,
        service, registry, caplog
    ):
        """Returns True when comment posts but label update fails."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_gitlab.return_value = True
        mock_comment.return_value = True
        mock_label.return_value = False

        module = registry.get("src/main.py")
        parsed = MagicMock()
        parsed.required_items = []

        import logging
        with caplog.at_level(logging.WARNING, logger="glreview.review_service"):
            result = service._post_review_to_issue(module, "review text", parsed)

        assert result is True
        assert "label update failed" in caplog.text


# ---------------------------------------------------------------------------
# Additional coverage tests
# ---------------------------------------------------------------------------

from glreview.review_service import (
    BatchStartResult,
    ClaudeReviewResult,
    GitLabInfo,
    get_gitlab_info,
)


class TestGetGitlabInfo:
    """Tests for the get_gitlab_info() module-level function."""

    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_returns_none_when_host_missing(
        self, mock_host, mock_project, mock_available, temp_dir
    ):
        """Returns None when host is not available."""
        mock_host.return_value = None
        mock_project.return_value = "org/project"

        config = Config(root=temp_dir)
        result = get_gitlab_info(config)
        assert result is None
        mock_available.assert_not_called()

    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_returns_none_when_project_missing(
        self, mock_host, mock_project, mock_available, temp_dir
    ):
        """Returns None when project is not available."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = None

        config = Config(root=temp_dir)
        result = get_gitlab_info(config)
        assert result is None

    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_returns_gitlab_info_when_available(
        self, mock_host, mock_project, mock_available, temp_dir
    ):
        """Returns GitLabInfo with availability when host and project are set."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = True

        config = Config(root=temp_dir)
        result = get_gitlab_info(config)
        assert isinstance(result, GitLabInfo)
        assert result.host == "gitlab.com"
        assert result.project == "org/project"
        assert result.available is True

    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_returns_gitlab_info_unavailable(
        self, mock_host, mock_project, mock_available, temp_dir
    ):
        """Returns GitLabInfo with available=False when gitlab is unreachable."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = False

        config = Config(root=temp_dir)
        result = get_gitlab_info(config)
        assert isinstance(result, GitLabInfo)
        assert result.available is False


class TestCreateIssue:
    """Tests for _create_review_issue hitting the create_issue call path."""

    @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", lines=100))
        return reg

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

    @patch("glreview.review_service.create_issue")
    @patch("glreview.review_service._render_issue_description")
    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_create_issue_success(
        self, mock_host, mock_project, mock_available, mock_render, mock_create,
        service
    ):
        """Successful create_issue returns issue URL."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = True
        mock_render.return_value = "description"
        mock_issue = MagicMock()
        mock_issue.url = "https://gitlab.com/org/project/-/issues/99"
        mock_issue.number = "99"
        mock_create.return_value = mock_issue

        module = service.registry.get("src/main.py")
        result = service._create_review_issue("src/main.py", module, "abc123")

        assert result == "https://gitlab.com/org/project/-/issues/99"
        mock_create.assert_called_once()

    @patch("glreview.review_service.create_issue")
    @patch("glreview.review_service._render_issue_description")
    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_create_issue_returns_hash_number_when_no_url(
        self, mock_host, mock_project, mock_available, mock_render, mock_create,
        service
    ):
        """Returns #number when created issue has no url attribute."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = True
        mock_render.return_value = "description"
        mock_issue = MagicMock()
        mock_issue.url = None
        mock_issue.number = "99"
        mock_create.return_value = mock_issue

        module = service.registry.get("src/main.py")
        result = service._create_review_issue("src/main.py", module, "abc123")

        assert result == "#99"


class TestCleanupOrphanedIssue:
    """Tests for _cleanup_orphaned_issue close_issue call and failure logging."""

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

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

    @patch("glreview.review_service.close_issue")
    @patch("glreview.review_service.get_gitlab_host")
    @patch("glreview.review_service.get_gitlab_project")
    def test_cleanup_calls_close_issue(
        self, mock_project, mock_host, mock_close, service
    ):
        """Successfully closes the orphaned issue."""
        mock_project.return_value = "org/project"
        mock_host.return_value = "gitlab.com"
        mock_close.return_value = True

        service._cleanup_orphaned_issue(
            "https://gitlab.com/org/project/-/issues/99"
        )

        mock_close.assert_called_once()

    @patch("glreview.review_service.close_issue")
    @patch("glreview.review_service.get_gitlab_host")
    @patch("glreview.review_service.get_gitlab_project")
    def test_cleanup_logs_warning_on_failure(
        self, mock_project, mock_host, mock_close, service, caplog
    ):
        """Logs a warning when close_issue returns False."""
        mock_project.return_value = "org/project"
        mock_host.return_value = "gitlab.com"
        mock_close.return_value = False

        import logging

        with caplog.at_level(logging.WARNING, logger="glreview.review_service"):
            service._cleanup_orphaned_issue(
                "https://gitlab.com/org/project/-/issues/42"
            )

        assert "Failed to clean up orphaned issue" in caplog.text

    def test_cleanup_returns_early_for_invalid_url(self, service):
        """Returns early when issue URL has no extractable issue number."""
        # Should not raise or call any external functions
        service._cleanup_orphaned_issue("not-a-valid-issue-url")


class TestLoadReviewContext:
    """Tests for _load_review_context loading context with module_ctx having test_files."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(ModuleStatus(path="src/main.py", status="in_progress"))
        return ReviewService(reg, config)

    @patch("glreview.claude_context.load_context")
    @patch("glreview.claude_context.get_default_context_path")
    def test_context_loaded_with_test_files(
        self, mock_path, mock_load, service, config, temp_dir
    ):
        """Context is loaded and on_progress called with test_files count."""
        from glreview.claude_context import ModuleContext, ProjectContext

        context_file = temp_dir / ".glreview-context.json"
        context_file.touch()
        mock_path.return_value = context_file

        module_ctx = ModuleContext(
            path="src/main.py",
            test_files=["tests/test_main.py", "tests/test_main2.py"],
        )
        project_ctx = ProjectContext()
        project_ctx.modules["src/main.py"] = module_ctx
        mock_load.return_value = project_ctx

        progress_events = []

        def on_progress(event, data):
            progress_events.append((event, data))

        p_ctx, m_ctx, has_ctx = service._load_review_context(
            "src/main.py", no_context=False, on_progress=on_progress
        )

        assert has_ctx is True
        assert m_ctx is not None
        assert len(progress_events) == 1
        assert progress_events[0][0] == "context_loaded"
        assert progress_events[0][1]["test_files"] == 2

    def test_no_context_when_disabled(self, service):
        """Returns no context when no_context=True."""
        p_ctx, m_ctx, has_ctx = service._load_review_context(
            "src/main.py", no_context=True, on_progress=None
        )
        assert has_ctx is False
        assert p_ctx is None
        assert m_ctx is None


class TestPrepareReviewSource:
    """Tests for _prepare_review_source file-not-found and diff paths."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(
            ModuleStatus(path="src/main.py", status="in_progress", lines=100)
        )
        reg.set(
            ModuleStatus(
                path="src/reviewed.py",
                status="in_progress",
                last_reviewed_commit="abc123",
                lines=50,
            )
        )
        return ReviewService(reg, config)

    def test_file_not_found_returns_error(self, service):
        """Returns ClaudeReviewResult when file not found."""
        module = service.registry.get("src/main.py")
        result = service._prepare_review_source("src/main.py", module)
        assert isinstance(result, ClaudeReviewResult)
        assert result.success is False
        assert "not found" in result.message.lower()

    @patch("glreview.claude.chunk_source_code")
    @patch("glreview.claude.get_file_diff")
    def test_diff_fetched_for_reviewed_module(
        self, mock_diff, mock_chunk, service, config
    ):
        """Fetches diff when module has last_reviewed_commit."""
        src_dir = config.root / "src"
        src_dir.mkdir(parents=True, exist_ok=True)
        (src_dir / "reviewed.py").write_text("print('hello')\n")

        mock_diff.return_value = "--- a/src/reviewed.py\n+++ b/src/reviewed.py\n"
        chunk = MagicMock()
        chunk.content = "print('hello')\n"
        chunk.chunk_num = 1
        chunk.total_chunks = 1
        mock_chunk.return_value = [chunk]

        module = service.registry.get("src/reviewed.py")
        result = service._prepare_review_source("src/reviewed.py", module)

        assert not isinstance(result, ClaudeReviewResult)
        source_code, diff, chunks = result
        assert diff is not None
        mock_diff.assert_called_once_with("src/reviewed.py", "abc123", cwd=config.root)


class TestExecuteChunkReviews:
    """Tests for _execute_chunk_reviews on_progress and diff_only mode."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(ModuleStatus(path="src/main.py", status="in_progress", lines=200))
        return ReviewService(reg, config)

    @patch("glreview.claude.run_claude_review")
    @patch("glreview.claude.build_review_prompt")
    def test_on_progress_chunk_start_and_done(
        self, mock_prompt, mock_review, service
    ):
        """Calls on_progress with chunk_start and chunk_done for multi-chunk."""
        mock_prompt.return_value = "prompt"
        mock_review.return_value = ("review text", True)

        chunk1 = MagicMock()
        chunk1.content = "code part 1"
        chunk1.chunk_num = 1
        chunk1.total_chunks = 2
        chunk1.start_line = 1
        chunk1.end_line = 50

        chunk2 = MagicMock()
        chunk2.content = "code part 2"
        chunk2.chunk_num = 2
        chunk2.total_chunks = 2
        chunk2.start_line = 51
        chunk2.end_line = 100

        progress_events = []

        def on_progress(event, data):
            progress_events.append((event, data))

        module = service.registry.get("src/main.py")
        result = service._execute_chunk_reviews(
            path="src/main.py",
            module=module,
            chunks=[chunk1, chunk2],
            diff=None,
            diff_only=False,
            model="opus",
            project_ctx=None,
            module_ctx=None,
            on_progress=on_progress,
        )

        assert isinstance(result, list)
        assert len(result) == 2
        event_names = [e[0] for e in progress_events]
        assert "chunk_start" in event_names
        assert "chunk_done" in event_names

    @patch("glreview.claude.run_claude_review")
    @patch("glreview.claude.build_review_prompt")
    def test_diff_only_mode(self, mock_prompt, mock_review, service):
        """In diff_only mode, code_to_review is replaced with diff content."""
        mock_prompt.return_value = "prompt"
        mock_review.return_value = ("review text", True)

        chunk = MagicMock()
        chunk.content = "full source code"
        chunk.chunk_num = 1
        chunk.total_chunks = 1

        diff_text = "--- a/src/main.py\n+++ b/src/main.py\n@@ -1 +1 @@\n-old\n+new"
        module = service.registry.get("src/main.py")

        result = service._execute_chunk_reviews(
            path="src/main.py",
            module=module,
            chunks=[chunk],
            diff=diff_text,
            diff_only=True,
            model="opus",
            project_ctx=None,
            module_ctx=None,
            on_progress=None,
        )

        assert isinstance(result, list)
        # The prompt should have been built with diff-only content
        call_kwargs = mock_prompt.call_args
        source_arg = call_kwargs.kwargs.get("source_code", "")
        assert "Diff only mode" in source_arg


class TestFinalizeReview:
    """Tests for _finalize_review combining multiple chunk results."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/main.py",
                status="in_progress",
                review_issue="https://gitlab.com/org/project/-/issues/42",
            )
        )
        return ReviewService(reg, config)

    @patch("glreview.claude.parse_review_output")
    def test_combine_multiple_chunks(self, mock_parse, service):
        """Multiple chunk results are combined into a single review."""
        mock_parsed = MagicMock()
        mock_parsed.overall_assessment = "OK"
        mock_parsed.required_items = []
        mock_parsed.suggested_items = []
        mock_parse.return_value = mock_parsed

        chunk1 = MagicMock()
        chunk2 = MagicMock()

        module = service.registry.get("src/main.py")
        result = service._finalize_review(
            module,
            all_results=["chunk 1 review", "chunk 2 review"],
            chunks=[chunk1, chunk2],
            post=False,
            has_context=True,
        )

        assert result.success is True
        assert "Combined Review" in result.review_text
        assert "Chunk 1" in result.review_text
        assert "Chunk 2" in result.review_text
        assert result.chunk_count == 2
        assert result.has_context is True


class TestRunClaudeReview:
    """Tests for the top-level run_claude_review method on ReviewService."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(ModuleStatus(path="src/main.py", status="in_progress", lines=500))
        return ReviewService(reg, config)

    @patch("glreview.claude.parse_review_output")
    @patch("glreview.claude.run_claude_review")
    @patch("glreview.claude.build_review_prompt")
    @patch("glreview.claude.chunk_source_code")
    @patch("glreview.claude.get_file_diff")
    @patch("glreview.claude_context.get_default_context_path")
    def test_large_file_progress(
        self, mock_ctx_path, mock_diff, mock_chunk, mock_prompt,
        mock_review, mock_parse, service, config
    ):
        """Calls on_progress with 'large_file' when multiple chunks."""
        # Setup: context path does not exist
        mock_ctx_path.return_value = config.root / "nonexistent.json"

        # Create source file
        src_dir = config.root / "src"
        src_dir.mkdir(parents=True, exist_ok=True)
        (src_dir / "main.py").write_text("x = 1\n" * 500)

        mock_diff.return_value = None

        chunk1 = MagicMock()
        chunk1.content = "part1"
        chunk1.chunk_num = 1
        chunk1.total_chunks = 2
        chunk1.start_line = 1
        chunk1.end_line = 250
        chunk2 = MagicMock()
        chunk2.content = "part2"
        chunk2.chunk_num = 2
        chunk2.total_chunks = 2
        chunk2.start_line = 251
        chunk2.end_line = 500
        mock_chunk.return_value = [chunk1, chunk2]

        mock_prompt.return_value = "prompt"
        mock_review.return_value = ("review text", True)

        mock_parsed = MagicMock()
        mock_parsed.overall_assessment = "OK"
        mock_parsed.required_items = []
        mock_parsed.suggested_items = []
        mock_parse.return_value = mock_parsed

        progress_events = []

        def on_progress(event, data):
            progress_events.append((event, data))

        result = service.run_claude_review(
            "src/main.py", on_progress=on_progress
        )

        assert result.success is True
        event_names = [e[0] for e in progress_events]
        assert "large_file" in event_names

    @patch("glreview.claude.build_review_prompt")
    @patch("glreview.claude.chunk_source_code")
    @patch("glreview.claude.get_file_diff")
    @patch("glreview.claude_context.get_default_context_path")
    def test_dry_run_returns_prompt(
        self, mock_ctx_path, mock_diff, mock_chunk, mock_prompt,
        service, config
    ):
        """dry_run=True returns prompt without calling Claude."""
        mock_ctx_path.return_value = config.root / "nonexistent.json"

        src_dir = config.root / "src"
        src_dir.mkdir(parents=True, exist_ok=True)
        (src_dir / "main.py").write_text("x = 1\n")

        mock_diff.return_value = None

        chunk = MagicMock()
        chunk.content = "x = 1\n"
        chunk.chunk_num = 1
        chunk.total_chunks = 1
        chunk.start_line = 1
        chunk.end_line = 1
        mock_chunk.return_value = [chunk]

        mock_prompt.return_value = "the review prompt text"

        result = service.run_claude_review(
            "src/main.py", dry_run=True
        )

        assert result.success is True
        assert result.prompt == "the review prompt text"
        assert "Dry run" in result.message

    @patch("glreview.claude.build_review_prompt")
    @patch("glreview.claude.chunk_source_code")
    @patch("glreview.claude.get_file_diff")
    @patch("glreview.claude_context.get_default_context_path")
    def test_dry_run_diff_only_mode(
        self, mock_ctx_path, mock_diff, mock_chunk, mock_prompt,
        service, config
    ):
        """dry_run with diff_only uses diff content in the prompt."""
        mock_ctx_path.return_value = config.root / "nonexistent.json"

        src_dir = config.root / "src"
        src_dir.mkdir(parents=True, exist_ok=True)
        (src_dir / "main.py").write_text("x = 1\n")

        mock_diff.return_value = "some diff"

        chunk = MagicMock()
        chunk.content = "x = 1\n"
        chunk.chunk_num = 1
        chunk.total_chunks = 1
        mock_chunk.return_value = [chunk]

        mock_prompt.return_value = "diff prompt"

        # Need to set last_reviewed_commit on the module for diff to be fetched
        module = service.registry.get("src/main.py")
        module.last_reviewed_commit = "abc123"

        result = service.run_claude_review(
            "src/main.py", dry_run=True, diff_only=True
        )

        assert result.success is True
        # Verify build_review_prompt was called with diff-only source
        call_kwargs = mock_prompt.call_args
        source_arg = call_kwargs.kwargs.get("source_code") or call_kwargs[1].get("source_code")
        assert "Diff only mode" in source_arg


class TestPostReviewToIssueFull:
    """Tests for _post_review_to_issue full success path and exception handling."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/main.py",
                status="in_progress",
                review_issue="https://gitlab.com/org/project/-/issues/42",
            )
        )
        return ReviewService(reg, config)

    @patch("glreview.review_service.update_issue_review_label")
    @patch("glreview.review_service.add_issue_comment")
    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_full_success_path(
        self, mock_host, mock_project, mock_available, mock_comment, mock_label,
        service
    ):
        """Full success path: comment posted and label updated."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = True
        mock_comment.return_value = True
        mock_label.return_value = True

        module = service.registry.get("src/main.py")
        parsed = MagicMock()
        parsed.required_items = []

        result = service._post_review_to_issue(module, "review text", parsed)
        assert result is True
        mock_comment.assert_called_once()
        mock_label.assert_called_once()
        # Label should be "ok" since no required items
        label_args = mock_label.call_args
        assert label_args[0][2] == "ok"

    @patch("glreview.review_service.update_issue_review_label")
    @patch("glreview.review_service.add_issue_comment")
    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_label_required_when_required_items(
        self, mock_host, mock_project, mock_available, mock_comment, mock_label,
        service
    ):
        """Label is 'required' when there are required items."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = True
        mock_comment.return_value = True
        mock_label.return_value = True

        module = service.registry.get("src/main.py")
        parsed = MagicMock()
        parsed.required_items = [MagicMock()]  # one required item

        result = service._post_review_to_issue(module, "review text", parsed)
        assert result is True
        label_args = mock_label.call_args
        assert label_args[0][2] == "required"

    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_exception_handling(
        self, mock_host, mock_project, mock_available, service, caplog
    ):
        """Exceptions are caught and logged, returns False."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.side_effect = ConnectionError("connection failed")

        module = service.registry.get("src/main.py")
        parsed = MagicMock()
        parsed.required_items = []

        import logging

        with caplog.at_level(logging.WARNING, logger="glreview.review_service"):
            result = service._post_review_to_issue(module, "review text", parsed)

        assert result is False
        assert "Failed to post review to issue" in caplog.text


class TestIssueHasClaudeReview:
    """Tests for _issue_has_claude_review checking for existing review comment."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(
            ModuleStatus(
                path="src/main.py",
                status="in_progress",
                review_issue="https://gitlab.com/org/project/-/issues/42",
            )
        )
        reg.set(
            ModuleStatus(
                path="src/no_issue.py",
                status="in_progress",
            )
        )
        return ReviewService(reg, config)

    def test_no_issue_number_returns_false(self, service):
        """Returns False when module has no review issue."""
        module = service.registry.get("src/no_issue.py")
        assert service._issue_has_claude_review(module) is False

    @patch("glreview.review_service.find_claude_review_comment")
    @patch("glreview.review_service.get_gitlab_host")
    @patch("glreview.review_service.get_gitlab_project")
    def test_finds_existing_review(
        self, mock_project, mock_host, mock_find, service
    ):
        """Returns True when find_claude_review_comment finds a comment."""
        mock_project.return_value = "org/project"
        mock_host.return_value = "gitlab.com"
        mock_find.return_value = "## Claude Code Review\n\nSome review"

        module = service.registry.get("src/main.py")
        assert service._issue_has_claude_review(module) is True

    @patch("glreview.review_service.find_claude_review_comment")
    @patch("glreview.review_service.get_gitlab_host")
    @patch("glreview.review_service.get_gitlab_project")
    def test_no_existing_review(
        self, mock_project, mock_host, mock_find, service
    ):
        """Returns False when find_claude_review_comment returns None."""
        mock_project.return_value = "org/project"
        mock_host.return_value = "gitlab.com"
        mock_find.return_value = None

        module = service.registry.get("src/main.py")
        assert service._issue_has_claude_review(module) is False


class TestBatchStartReviews:
    """Tests for batch_start_reviews full batch flow."""

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

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

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

    @patch("glreview.review_service.ReviewService._create_review_issue")
    @patch("glreview.review_service.get_current_commit")
    def test_batch_basic_start(self, mock_commit, mock_create_issue, service):
        """Basic batch start without claude_review."""
        mock_commit.return_value = "abc123"
        mock_create_issue.return_value = "#1"

        result = service.batch_start_reviews(
            claude_review=False, on_save=None
        )

        assert isinstance(result, BatchStartResult)
        assert result.started == 2
        assert result.errors == 0
        assert len(result.results) == 2

    @patch("glreview.review_service.ReviewService._create_review_issue")
    @patch("glreview.review_service.get_current_commit")
    def test_batch_with_on_progress_and_on_save(
        self, mock_commit, mock_create_issue, service
    ):
        """Callbacks on_progress and on_save are invoked."""
        mock_commit.return_value = "abc123"
        mock_create_issue.return_value = "#1"

        progress_calls = []
        save_calls = []

        def on_progress(path, step, res):
            progress_calls.append((path, step, res))

        def on_save():
            save_calls.append(True)

        result = service.batch_start_reviews(
            claude_review=False,
            on_progress=on_progress,
            on_save=on_save,
        )

        assert result.started == 2
        assert len(progress_calls) == 2
        assert all(c[1] == "start" for c in progress_calls)
        assert len(save_calls) == 2

    @patch("glreview.review_service.ReviewService.run_claude_review")
    @patch("glreview.review_service.ReviewService._create_review_issue")
    @patch("glreview.review_service.get_current_commit")
    def test_batch_with_claude_review(
        self, mock_commit, mock_create_issue, mock_claude, service
    ):
        """Runs Claude review when claude_review=True."""
        mock_commit.return_value = "abc123"
        mock_create_issue.return_value = "#1"
        mock_claude.return_value = ClaudeReviewResult(
            success=True, message="Review completed", posted=True
        )

        progress_calls = []

        def on_progress(path, step, res):
            progress_calls.append((path, step, res))

        result = service.batch_start_reviews(
            claude_review=True,
            model="opus",
            on_progress=on_progress,
        )

        assert result.started == 2
        assert result.reviewed == 2
        assert mock_claude.call_count == 2
        # Should have start + review for each module
        steps = [c[1] for c in progress_calls]
        assert steps.count("start") == 2
        assert steps.count("review") == 2

    @patch("glreview.review_service.ReviewService._issue_has_claude_review")
    @patch("glreview.review_service.ReviewService.run_claude_review")
    @patch("glreview.review_service.ReviewService._create_review_issue")
    @patch("glreview.review_service.get_current_commit")
    def test_batch_skip_reviewed(
        self, mock_commit, mock_create_issue, mock_claude, mock_has_review, service
    ):
        """Skips Claude review when skip_reviewed=True and issue already has review."""
        mock_commit.return_value = "abc123"
        mock_create_issue.return_value = "#1"
        mock_has_review.return_value = True
        mock_claude.return_value = ClaudeReviewResult(
            success=True, message="Review completed", posted=True
        )

        progress_calls = []

        def on_progress(path, step, res):
            progress_calls.append((path, step, res))

        result = service.batch_start_reviews(
            claude_review=True,
            skip_reviewed=True,
            on_progress=on_progress,
        )

        assert result.started == 2
        assert result.skipped == 2
        assert result.reviewed == 0
        mock_claude.assert_not_called()
        skip_steps = [c for c in progress_calls if c[1] == "skip"]
        assert len(skip_steps) == 2

    @patch("glreview.review_service.ReviewService._create_review_issue")
    @patch("glreview.review_service.get_current_commit")
    def test_batch_errors_counted(self, mock_commit, mock_create_issue, service):
        """Errors are counted when start_review fails."""
        mock_commit.return_value = "abc123"
        # Issue creation fails, causing start_review to return failure
        mock_create_issue.return_value = None

        result = service.batch_start_reviews()

        assert result.errors == 2
        assert result.started == 0

    @patch("glreview.review_service.ReviewService.start_review")
    def test_batch_keyboard_interrupt(self, mock_start, service):
        """KeyboardInterrupt sets interrupted flag and re-raises."""
        mock_start.side_effect = KeyboardInterrupt()

        with pytest.raises(KeyboardInterrupt):
            service.batch_start_reviews()

    @patch("glreview.review_service.ReviewService._issue_has_claude_review")
    @patch("glreview.review_service.ReviewService.run_claude_review")
    @patch("glreview.review_service.ReviewService._create_review_issue")
    @patch("glreview.review_service.get_current_commit")
    def test_batch_claude_review_not_posted(
        self, mock_commit, mock_create_issue, mock_claude, mock_has_review, service
    ):
        """Claude review that is not posted does not increment reviewed count."""
        mock_commit.return_value = "abc123"
        mock_create_issue.return_value = "#1"
        mock_has_review.return_value = False
        mock_claude.return_value = ClaudeReviewResult(
            success=True, message="Review completed", posted=False
        )

        result = service.batch_start_reviews(
            claude_review=True,
            skip_reviewed=False,
        )

        assert result.started == 2
        assert result.reviewed == 0


# ---------------------------------------------------------------------------
# Additional tests for remaining uncovered lines
# ---------------------------------------------------------------------------


class TestCreateReviewIssueEdgeCases:
    """Cover lines 334 and 339 of _create_review_issue."""

    @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", lines=100))
        return reg

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

    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_returns_none_when_gitlab_unavailable(
        self, mock_host, mock_project, mock_available, service
    ):
        """Returns None when GitLab is not available."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = False

        module = service.registry.get("src/main.py")
        result = service._create_review_issue("src/main.py", module, "abc123")
        assert result is None

    @patch("glreview.review_service.create_issue")
    @patch("glreview.review_service._render_issue_description")
    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_builds_diff_url_for_re_review(
        self, mock_host, mock_project, mock_available, mock_render, mock_create,
        service
    ):
        """Builds diff_url when module was previously reviewed."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = True
        mock_render.return_value = "description"
        mock_issue = MagicMock()
        mock_issue.url = "https://gitlab.com/org/project/-/issues/99"
        mock_issue.number = "99"
        mock_create.return_value = mock_issue

        # Module with previous review
        module = ModuleStatus(
            path="src/main.py",
            status="reviewed",
            last_reviewed_commit="prev123",
            lines=100,
        )
        result = service._create_review_issue("src/main.py", module, "new456")
        assert result is not None
        # Verify _render_issue_description was called with a diff_url
        render_kwargs = mock_render.call_args[1]
        assert render_kwargs["diff_url"] is not None
        assert "prev123" in render_kwargs["diff_url"]


class TestPostReviewToIssueEdgeCases:
    """Cover lines 847 and 859 of _post_review_to_issue."""

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

    def test_returns_false_when_no_issue_number(self, config):
        """Returns False when module has no review_issue."""
        reg = Registry()
        reg.set(ModuleStatus(
            path="src/main.py", status="in_progress", review_issue=None
        ))
        service = ReviewService(reg, config)
        module = reg.get("src/main.py")
        parsed = MagicMock()
        parsed.required_items = []

        result = service._post_review_to_issue(module, "review text", parsed)
        assert result is False

    @patch("glreview.review_service.add_issue_comment")
    @patch("glreview.review_service.gitlab_available")
    @patch("glreview.review_service.get_gitlab_project")
    @patch("glreview.review_service.get_gitlab_host")
    def test_returns_false_when_comment_fails(
        self, mock_host, mock_project, mock_available, mock_comment, config
    ):
        """Returns False when add_issue_comment returns False."""
        mock_host.return_value = "gitlab.com"
        mock_project.return_value = "org/project"
        mock_available.return_value = True
        mock_comment.return_value = False

        reg = Registry()
        reg.set(ModuleStatus(
            path="src/main.py", status="in_progress",
            review_issue="https://gitlab.com/org/project/-/issues/42",
        ))
        service = ReviewService(reg, config)
        module = reg.get("src/main.py")
        parsed = MagicMock()
        parsed.required_items = []

        result = service._post_review_to_issue(module, "review text", parsed)
        assert result is False


class TestRunClaudeReviewFileNotFound:
    """Cover line 788 of run_claude_review (file not found)."""

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

    @pytest.fixture
    def service(self, config):
        reg = Registry()
        reg.set(ModuleStatus(
            path="src/nonexistent.py", status="in_progress",
            review_started_commit="abc123", lines=50,
        ))
        return ReviewService(reg, config)

    def test_returns_error_when_file_not_found(self, service):
        """Returns error result when source file doesn't exist."""
        result = service.run_claude_review(path="src/nonexistent.py")
        assert result.success is False
        assert "not found" in result.message.lower() or "File not found" in result.message
