"""Tests for glreview.cli module."""

from __future__ import annotations

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

import pytest
from click.testing import CliRunner

from glreview.cli import main
from glreview.registry import ModuleStatus, Registry, save_registry


@pytest.fixture
def runner():
    """Create a CLI runner."""
    return CliRunner()


@pytest.fixture
def project_dir(temp_dir):
    """Create a project directory with source files."""
    # Create source files
    src = temp_dir / "src"
    src.mkdir()
    (src / "main.py").write_text("def hello():\n    return 'Hello'\n")
    (src / "utils.py").write_text("def add(a, b):\n    return a + b\n")

    # Create pyproject.toml
    pyproject = temp_dir / "pyproject.toml"
    pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]
""")

    return temp_dir


class TestInit:
    """Tests for init command."""

    @patch("glreview.cli.get_current_commit")
    def test_init_creates_registry(self, mock_commit, runner, project_dir, monkeypatch):
        """Init creates a new registry."""
        mock_commit.return_value = "abc123"
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["init"])

        assert result.exit_code == 0
        assert "Initialized" in result.output
        assert (project_dir / "review_registry.json").exists()

    @patch("glreview.cli.get_current_commit")
    def test_init_is_idempotent(self, mock_commit, runner, project_dir, monkeypatch):
        """Init on existing registry syncs instead of failing."""
        mock_commit.return_value = "abc123"
        monkeypatch.chdir(project_dir)

        # First init
        runner.invoke(main, ["init"])
        # Second init should sync
        result = runner.invoke(main, ["init"])

        assert result.exit_code == 0
        assert "syncing" in result.output.lower() or "sync" in result.output.lower()


class TestStatus:
    """Tests for status command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry file."""
        registry = Registry()
        registry.set(ModuleStatus(path="src/main.py", status="reviewed", lines=50))
        registry.set(
            ModuleStatus(
                path="src/utils.py",
                status="needs_review",
                priority="high",
                lines=30,
            )
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    @patch("glreview.cli.has_changes_since")
    def test_status_summary(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status shows summary."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status"])

        assert result.exit_code == 0
        assert "Review Progress" in result.output or "reviewed" in result.output.lower()

    @patch("glreview.cli.has_changes_since")
    def test_status_single_module(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status shows single module details."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "src/main.py"])

        assert result.exit_code == 0
        assert "src/main.py" in result.output
        assert "reviewed" in result.output.lower()

    @patch("glreview.cli.has_changes_since")
    def test_status_json(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status --json outputs JSON."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "--json"])

        assert result.exit_code == 0
        data = json.loads(result.output)
        assert "total" in data

    def test_status_unknown_module(
        self, runner, project_dir, registry_file, monkeypatch
    ):
        """Status exits with error for unknown module."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "nonexistent.py"])

        assert result.exit_code == 1
        assert "not in registry" in result.output.lower()


class TestList:
    """Tests for list command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry file."""
        registry = Registry()
        registry.set(ModuleStatus(path="src/main.py", status="reviewed", priority="high"))
        registry.set(
            ModuleStatus(path="src/utils.py", status="needs_review", priority="low")
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    def test_list_all(self, runner, project_dir, registry_file, monkeypatch):
        """List shows all modules."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["list"])

        assert result.exit_code == 0
        assert "src/main.py" in result.output
        assert "src/utils.py" in result.output

    def test_list_filter_status(self, runner, project_dir, registry_file, monkeypatch):
        """List filters by status."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["list", "--status", "reviewed"])

        assert result.exit_code == 0
        assert "src/main.py" in result.output
        assert "src/utils.py" not in result.output

    def test_list_filter_priority(self, runner, project_dir, registry_file, monkeypatch):
        """List filters by priority."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["list", "--priority", "high"])

        assert result.exit_code == 0
        assert "src/main.py" in result.output
        assert "src/utils.py" not in result.output

    def test_list_no_matches(self, runner, project_dir, registry_file, monkeypatch):
        """List shows message when no modules match filters."""
        monkeypatch.chdir(project_dir)

        # Filter for status that doesn't exist in registry
        result = runner.invoke(main, ["list", "--status", "in_progress"])

        assert result.exit_code == 0
        assert "no modules" in result.output.lower()


class TestSync:
    """Tests for sync command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry file."""
        registry = Registry()
        registry.set(ModuleStatus(path="src/main.py", status="reviewed"))
        registry.set(ModuleStatus(path="src/deleted.py", status="needs_review"))
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    @patch("glreview.cli.get_current_commit")
    def test_sync_adds_new(
        self, mock_commit, runner, project_dir, registry_file, monkeypatch
    ):
        """Sync adds new modules."""
        mock_commit.return_value = "abc123"
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["sync"])

        assert result.exit_code == 0
        # src/utils.py should be added
        assert "utils.py" in result.output or "synced" in result.output.lower()

    @patch("glreview.cli.get_current_commit")
    def test_sync_removes_deleted(
        self, mock_commit, runner, project_dir, registry_file, monkeypatch
    ):
        """Sync removes deleted modules."""
        mock_commit.return_value = "abc123"
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["sync"])

        assert result.exit_code == 0
        # src/deleted.py should be removed
        assert "deleted.py" in result.output or "synced" in result.output.lower()


class TestReport:
    """Tests for report command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry file."""
        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/main.py",
                status="reviewed",
                lines=50,
                last_reviewed_commit="abc123",
            )
        )
        registry.set(
            ModuleStatus(path="src/utils.py", status="needs_review", lines=30)
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    @patch("glreview.cli.has_changes_since")
    def test_report_markdown(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Report generates markdown."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report"])

        assert result.exit_code == 0
        assert "Coverage" in result.output
        assert "|" in result.output  # Table formatting

    @patch("glreview.cli.has_changes_since")
    def test_report_json(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Report --format json outputs JSON."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "json"])

        assert result.exit_code == 0
        data = json.loads(result.output)
        assert "total" in data
        assert "coverage_pct" in data

    @patch("glreview.cli.has_changes_since")
    def test_report_badge(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Report --format badge outputs badge URL."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "badge"])

        assert result.exit_code == 0
        assert "shields.io" in result.output

    @patch("glreview.cli.has_changes_since")
    def test_report_output_file(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Report --output writes to file."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        output_path = project_dir / "report.md"
        result = runner.invoke(main, ["report", "--output", str(output_path)])

        assert result.exit_code == 0
        assert output_path.exists()
        content = output_path.read_text()
        assert "Coverage" in content


class TestCiConfig:
    """Tests for ci-config command."""

    def test_ci_config_outputs_yaml(self, runner):
        """ci-config outputs YAML configuration."""
        result = runner.invoke(main, ["ci-config"])

        assert result.exit_code == 0
        assert "gitlab-ci.yml" in result.output
        assert "stages:" in result.output
        assert "glreview" in result.output


class TestCheck:
    """Tests for check command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry with reviewed module."""
        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/main.py",
                status="reviewed",
                lines=50,
                last_reviewed_commit="abc123",
            )
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    @patch("glreview.review_state.has_changes_since")
    def test_check_passes_no_changes(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Check passes when no reviewed files changed."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["check"])

        assert result.exit_code == 0
        assert "No reviewed files have changed" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_check_fails_with_changes(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Check fails when reviewed files changed."""
        mock_has_changes.return_value = True
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["check"])

        assert result.exit_code == 1
        assert "changed" in result.output.lower()


class TestVersion:
    """Tests for version option."""

    def test_version(self, runner):
        """--version shows version."""
        result = runner.invoke(main, ["--version"])

        assert result.exit_code == 0
        # Should contain version number
        assert "glreview" in result.output.lower() or "version" in result.output.lower()


class TestStart:
    """Tests for start command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry file with modules."""
        registry = Registry()
        registry.set(ModuleStatus(path="src/main.py", status="needs_review", lines=50))
        registry.set(
            ModuleStatus(
                path="src/utils.py",
                status="in_progress",
                review_issue="#42",
                lines=30,
            )
        )
        registry.set(
            ModuleStatus(
                path="src/done.py",
                status="reviewed",
                last_reviewed_commit="abc123",
                lines=20,
            )
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    def test_start_module_not_found(self, runner, project_dir, registry_file, monkeypatch):
        """Start fails for unknown module."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["start", "nonexistent.py"])

        assert result.exit_code == 1
        assert "not in registry" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_start_already_in_progress(
        self, mock_commit, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Start fails for module already in progress."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["start", "src/utils.py"])

        assert result.exit_code == 1
        assert "already in progress" in result.output.lower()

    @patch("glreview.cli.has_changes_since")
    @patch("glreview.cli.get_current_commit")
    def test_start_already_reviewed_no_changes(
        self, mock_commit, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Start exits when reviewed module has no changes."""
        mock_commit.return_value = "abc123"
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["start", "src/done.py"])

        assert result.exit_code == 0
        assert "already reviewed" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_start_no_gitlab_no_issue(
        self, mock_commit, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Start without GitLab and no issue number fails."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["start", "src/main.py"])

        assert result.exit_code == 1
        assert "gitlab" in result.output.lower() or "issue" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_start_with_manual_issue(
        self, mock_commit, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Start with manual issue number succeeds."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["start", "src/main.py", "--issue", "#99"])

        assert result.exit_code == 0
        assert "registry updated" in result.output.lower()

    @patch("glreview.cli.create_issue")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_start_creates_issue(
        self, mock_commit, mock_gitlab, mock_create, runner, project_dir, registry_file, monkeypatch
    ):
        """Start creates GitLab issue when available."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = True
        mock_issue = MagicMock()
        mock_issue.number = "100"
        mock_issue.url = "https://gitlab.com/org/project/-/issues/100"
        mock_create.return_value = mock_issue
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["start", "src/main.py", "--assignee", "@alice"])

        assert result.exit_code == 0
        assert "review started" in result.output.lower()
        mock_create.assert_called_once()

    @patch("glreview.cli.create_issue")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_start_issue_creation_fails(
        self, mock_commit, mock_gitlab, mock_create, runner, project_dir, registry_file, monkeypatch
    ):
        """Start fails when issue creation fails."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = True
        mock_create.return_value = None
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["start", "src/main.py"])

        assert result.exit_code == 1
        assert "failed" in result.output.lower()


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

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry with in-progress module."""
        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/main.py",
                status="in_progress",
                review_issue="#42",
                review_started_commit="start123",
                assigned_reviewer="@alice",
                lines=50,
            )
        )
        registry.set(
            ModuleStatus(
                path="src/done.py",
                status="reviewed",
                last_reviewed_commit="abc123",
                reviewers=["@bob"],
                lines=20,
            )
        )
        registry.set(ModuleStatus(path="src/utils.py", status="needs_review", lines=30))
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    def test_signoff_module_not_found(self, runner, project_dir, registry_file, monkeypatch):
        """Signoff fails for unknown module."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["signoff", "nonexistent.py"])

        assert result.exit_code == 1
        assert "not in registry" in result.output.lower()

    @patch("glreview.cli.get_current_commit")
    def test_signoff_already_reviewed(
        self, mock_commit, runner, project_dir, registry_file, monkeypatch
    ):
        """Signoff exits for already reviewed module."""
        mock_commit.return_value = "abc123"
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["signoff", "src/done.py"])

        assert result.exit_code == 0
        assert "already reviewed" in result.output.lower()

    def test_signoff_not_in_progress(self, runner, project_dir, registry_file, monkeypatch):
        """Signoff fails for module not in progress."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["signoff", "src/utils.py"])

        assert result.exit_code == 1
        assert "not in review" in result.output.lower()

    @patch("glreview.cli.has_changes_since")
    @patch("glreview.cli.get_current_commit")
    def test_signoff_file_changed_during_review(
        self, mock_commit, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Signoff fails when file changed during review."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = True
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["signoff", "src/main.py", "--reviewer", "@bob"])

        assert result.exit_code == 1
        assert "changed during review" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.has_changes_since")
    @patch("glreview.cli.get_current_commit")
    def test_signoff_with_acknowledge(
        self, mock_commit, mock_has_changes, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Signoff with --acknowledge succeeds even with changes."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = True
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(
            main, ["signoff", "src/main.py", "--reviewer", "@bob", "--acknowledge"]
        )

        assert result.exit_code == 0
        assert "reviewed" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.has_changes_since")
    @patch("glreview.cli.get_current_commit")
    def test_signoff_success(
        self, mock_commit, mock_has_changes, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Signoff succeeds with reviewer."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = False
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(
            main, ["signoff", "src/main.py", "--reviewer", "@bob", "--skip-verify"]
        )

        assert result.exit_code == 0
        assert "reviewed" in result.output.lower()

    @patch("glreview.cli.get_issue")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.has_changes_since")
    @patch("glreview.cli.get_current_commit")
    def test_signoff_verifies_issue_closed(
        self, mock_commit, mock_has_changes, mock_gitlab, mock_get_issue,
        runner, project_dir, registry_file, monkeypatch
    ):
        """Signoff verifies issue is closed."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = False
        mock_gitlab.return_value = True
        mock_issue = MagicMock()
        mock_issue.state = "opened"
        mock_get_issue.return_value = mock_issue
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["signoff", "src/main.py", "--reviewer", "@bob"])

        assert result.exit_code == 1
        assert "not closed" in result.output.lower()

    @patch("glreview.cli.get_issue")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.has_changes_since")
    @patch("glreview.cli.get_current_commit")
    def test_signoff_issue_closed_extracts_reviewer(
        self, mock_commit, mock_has_changes, mock_gitlab, mock_get_issue,
        runner, project_dir, registry_file, monkeypatch
    ):
        """Signoff extracts reviewer from closed issue."""
        mock_commit.return_value = "current123"
        mock_has_changes.return_value = False
        mock_gitlab.return_value = True
        mock_issue = MagicMock()
        mock_issue.state = "closed"
        mock_issue.assignees = ["charlie"]
        mock_get_issue.return_value = mock_issue
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["signoff", "src/main.py"])

        assert result.exit_code == 0
        assert "@charlie" in result.output


class TestCancel:
    """Tests for cancel command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry with in-progress module."""
        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/main.py",
                status="in_progress",
                review_issue="#42",
                review_started_commit="start123",
                lines=50,
            )
        )
        registry.set(
            ModuleStatus(
                path="src/prev.py",
                status="in_progress",
                review_issue="#99",
                last_reviewed_commit="prev123",
                reviewers=["@old"],
                lines=30,
            )
        )
        registry.set(ModuleStatus(path="src/utils.py", status="needs_review", lines=20))
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    def test_cancel_module_not_found(self, runner, project_dir, registry_file, monkeypatch):
        """Cancel fails for unknown module."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["cancel", "nonexistent.py"])

        assert result.exit_code == 1
        assert "not in registry" in result.output.lower()

    def test_cancel_not_in_progress(self, runner, project_dir, registry_file, monkeypatch):
        """Cancel fails for module not in progress."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["cancel", "src/utils.py"])

        assert result.exit_code == 1
        assert "not in progress" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_cancel_no_gitlab_no_leave_open(
        self, mock_commit, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Cancel without GitLab fails when trying to close issue."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["cancel", "src/main.py"])

        assert result.exit_code == 1
        assert "cannot close issue" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_cancel_leave_open(
        self, mock_commit, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Cancel with --leave-open succeeds without GitLab."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["cancel", "src/main.py", "--leave-open"])

        assert result.exit_code == 0
        assert "review canceled" in result.output.lower()

    @patch("glreview.cli.close_issue")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_cancel_closes_issue(
        self, mock_commit, mock_gitlab, mock_close, runner, project_dir, registry_file, monkeypatch
    ):
        """Cancel closes GitLab issue."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = True
        mock_close.return_value = True
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["cancel", "src/main.py"])

        assert result.exit_code == 0
        assert "closed issue" in result.output.lower()
        mock_close.assert_called_once()

    @patch("glreview.cli.add_issue_comment")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_cancel_restart(
        self, mock_commit, mock_gitlab, mock_comment, runner, project_dir, registry_file, monkeypatch
    ):
        """Cancel --restart updates start commit."""
        mock_commit.return_value = "new456"
        mock_gitlab.return_value = True
        mock_comment.return_value = True
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["cancel", "src/main.py", "--restart"])

        assert result.exit_code == 0
        assert "review restarted" in result.output.lower()
        assert "new456" in result.output

    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_cancel_restores_previous_review(
        self, mock_commit, mock_gitlab, runner, project_dir, registry_file, monkeypatch
    ):
        """Cancel restores previous review status."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["cancel", "src/prev.py", "--leave-open"])

        assert result.exit_code == 0
        assert "restored to reviewed" in result.output.lower()


class TestClaudeReview:
    """Tests for claude-review command."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry with in-progress module."""
        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/main.py",
                status="in_progress",
                review_issue="#42",
                priority="high",
                lines=50,
            )
        )
        registry.set(ModuleStatus(path="src/utils.py", status="needs_review", lines=30))
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    def test_claude_review_not_found(self, runner, project_dir, registry_file, monkeypatch):
        """Claude review fails for unknown module."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-review", "nonexistent.py"])

        assert result.exit_code == 1
        assert "not in registry" in result.output.lower()

    def test_claude_review_not_in_progress(self, runner, project_dir, registry_file, monkeypatch):
        """Claude review fails for module not in progress."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-review", "src/utils.py"])

        assert result.exit_code == 1
        assert "not in progress" in result.output.lower()

    @patch("glreview.cli.claude_available")
    def test_claude_review_cli_not_found(
        self, mock_available, runner, project_dir, registry_file, monkeypatch
    ):
        """Claude review fails when CLI not available."""
        mock_available.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-review", "src/main.py"])

        assert result.exit_code == 1
        assert "not found" in result.output.lower()

    def test_claude_review_dry_run(self, runner, project_dir, registry_file, monkeypatch):
        """Claude review dry run shows prompt."""
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-review", "src/main.py", "--dry-run"])

        assert result.exit_code == 0
        assert "prompt" in result.output.lower()
        assert "src/main.py" in result.output

    @patch("glreview.cli.run_claude_review")
    @patch("glreview.cli.claude_available")
    def test_claude_review_success(
        self, mock_available, mock_run, runner, project_dir, registry_file, monkeypatch
    ):
        """Claude review succeeds."""
        mock_available.return_value = True
        mock_run.return_value = ("LGTM - code looks good", True)
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-review", "src/main.py"])

        assert result.exit_code == 0
        assert "LGTM" in result.output

    @patch("glreview.cli.run_claude_review")
    @patch("glreview.cli.claude_available")
    def test_claude_review_failure(
        self, mock_available, mock_run, runner, project_dir, registry_file, monkeypatch
    ):
        """Claude review handles failure."""
        mock_available.return_value = True
        mock_run.return_value = ("API Error", False)
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-review", "src/main.py"])

        assert result.exit_code == 1
        assert "error" in result.output.lower()


class TestReviewers:
    """Tests for reviewers command."""

    @patch("glreview.cli.gitlab_available")
    def test_reviewers_no_gitlab(self, mock_gitlab, runner, project_dir, monkeypatch):
        """Reviewers shows warning when GitLab unavailable."""
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["reviewers"])

        assert result.exit_code == 0
        assert "gitlab" in result.output.lower() and "not" in result.output.lower()

    @patch("glreview.cli.get_project_members")
    @patch("glreview.cli.gitlab_available")
    def test_reviewers_lists_members(
        self, mock_gitlab, mock_members, runner, project_dir, monkeypatch
    ):
        """Reviewers lists project members."""
        mock_gitlab.return_value = True

        from glreview.gitlab import GitLabMember
        mock_members.return_value = [
            GitLabMember("alice", "Alice Smith", 40),
            GitLabMember("bob", "Bob Jones", 30),
        ]
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["reviewers"])

        assert result.exit_code == 0
        assert "alice" in result.output.lower()
        assert "bob" in result.output.lower()

    @patch("glreview.cli.get_project_members")
    @patch("glreview.cli.gitlab_available")
    def test_reviewers_empty_list(
        self, mock_gitlab, mock_members, runner, project_dir, monkeypatch
    ):
        """Reviewers handles empty member list."""
        mock_gitlab.return_value = True
        mock_members.return_value = []
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["reviewers"])

        assert result.exit_code == 0
        assert "could not fetch" in result.output.lower()


class TestInitEdgeCases:
    """Additional tests for init command edge cases."""

    @patch("glreview.cli.get_current_commit")
    def test_init_no_modules_found(self, mock_commit, runner, temp_dir, monkeypatch):
        """Init with no matching modules."""
        mock_commit.return_value = "abc123"
        # Create pyproject but no source files
        pyproject = temp_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["nonexistent/**/*.py"]
""")
        monkeypatch.chdir(temp_dir)

        result = runner.invoke(main, ["init", "--sources", "nonexistent/**/*.py"])

        assert result.exit_code == 0
        assert "no modules found" in result.output.lower()


class TestStatusEdgeCases:
    """Additional tests for status command edge cases."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry with various module states."""
        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/main.py",
                status="reviewed",
                last_reviewed_commit="abc123",
                last_reviewed_date="2024-01-15",
                reviewers=["@alice"],
                lines=50,
            )
        )
        registry.set(
            ModuleStatus(
                path="src/in_prog.py",
                status="in_progress",
                review_issue="#42",
                assigned_reviewer="@bob",
                review_started_commit="def456",
                lines=30,
            )
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    def test_status_empty_registry(self, runner, project_dir, monkeypatch):
        """Status with empty registry."""
        # Create empty registry
        registry = Registry()
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status"])

        assert result.exit_code == 0
        assert "no modules" in result.output.lower()

    @patch("glreview.cli.has_changes_since")
    def test_status_single_module_json(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status single module with JSON output."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "src/main.py", "--json"])

        assert result.exit_code == 0
        data = json.loads(result.output)
        # to_dict() doesn't include path, but has status
        assert data["status"] == "reviewed"
        assert data["last_reviewed_commit"] == "abc123"

    @patch("glreview.review_state.has_changes_since")
    def test_status_shows_in_progress(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status shows in-progress modules."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status"])

        assert result.exit_code == 0
        assert "in progress" in result.output.lower()
        assert "@bob" in result.output or "bob" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_status_single_module_stale(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status shows stale warning for module with changes."""
        mock_has_changes.return_value = True  # Module is stale
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "src/main.py"])

        assert result.exit_code == 0
        assert "changes detected" in result.output.lower()

    @patch("glreview.review_state.has_changes_since")
    def test_status_single_module_no_changes(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status shows no changes for module."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "src/main.py"])

        assert result.exit_code == 0
        assert "no changes" in result.output.lower()

    @patch("glreview.review_state.has_changes_since")
    def test_status_single_module_in_progress(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Status shows in-progress module details."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "src/in_prog.py"])

        assert result.exit_code == 0
        assert "issue" in result.output.lower()
        assert "assigned" in result.output.lower()

    @patch("glreview.review_state.ReviewStateChecker.is_module_stale")
    def test_status_single_module_stale_check_error(
        self, mock_is_stale, runner, project_dir, registry_file, monkeypatch
    ):
        """Status handles stale check error."""
        mock_is_stale.return_value = (False, "Could not fetch")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["status", "src/main.py"])

        assert result.exit_code == 0
        assert "could not check" in result.output.lower()


class TestCustomTemplate:
    """Tests for custom issue template loading."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry file."""
        registry = Registry()
        registry.set(ModuleStatus(path="src/main.py", status="needs_review", lines=50))
        save_registry(registry, project_dir / "review_registry.json")

        # Create source file
        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        (src / "main.py").write_text("def main(): pass\n")
        return project_dir / "review_registry.json"

    @patch("glreview.cli.create_issue")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_start_with_custom_template(
        self, mock_commit, mock_gitlab, mock_create,
        runner, project_dir, registry_file, monkeypatch
    ):
        """Start uses custom template when it exists."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = True
        mock_issue = MagicMock()
        mock_issue.number = "100"
        mock_issue.url = "https://gitlab.com/org/project/-/issues/100"
        mock_create.return_value = mock_issue

        # Create custom template
        templates = project_dir / "templates"
        templates.mkdir()
        (templates / "custom_issue.md").write_text("## Custom Template\n{{ path }}\n")

        # Configure custom template in pyproject.toml
        pyproject = project_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]
issue_template = "templates/custom_issue.md"
""")

        monkeypatch.chdir(project_dir)
        result = runner.invoke(main, ["start", "src/main.py"])

        assert result.exit_code == 0
        # Custom template should be loaded and used
        mock_create.assert_called_once()

    @patch("glreview.cli.create_issue")
    @patch("glreview.cli.gitlab_available")
    @patch("glreview.cli.get_current_commit")
    def test_start_with_missing_custom_template(
        self, mock_commit, mock_gitlab, mock_create,
        runner, project_dir, registry_file, monkeypatch
    ):
        """Start falls back to default when custom template missing."""
        mock_commit.return_value = "abc123"
        mock_gitlab.return_value = True
        mock_issue = MagicMock()
        mock_issue.number = "100"
        mock_issue.url = "https://gitlab.com/org/project/-/issues/100"
        mock_create.return_value = mock_issue

        # Configure custom template that doesn't exist
        pyproject = project_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]
issue_template = "templates/nonexistent.md"
""")

        monkeypatch.chdir(project_dir)
        result = runner.invoke(main, ["start", "src/main.py"])

        assert result.exit_code == 0
        # Should show warning about missing template
        assert "warning" in result.output.lower() or "not found" in result.output.lower()
        # But still succeed by using default template
        mock_create.assert_called_once()


class TestReportEdgeCases:
    """Additional tests for report command edge cases."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry with modules and corresponding source files."""
        # Create source files so sync doesn't remove them
        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        (src / "main.py").write_text("# main\n")
        (src / "in_prog.py").write_text("# in progress\n")
        (src / "needs.py").write_text("# needs review\n")

        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/main.py",
                status="reviewed",
                last_reviewed_commit="abc123",
                last_reviewed_date="2024-01-15",
                reviewers=["@alice"],
                lines=100,
            )
        )
        registry.set(
            ModuleStatus(
                path="src/in_prog.py",
                status="in_progress",
                review_issue="https://gitlab.com/org/project/-/issues/42",
                assigned_reviewer="@bob",
                lines=50,
            )
        )
        registry.set(
            ModuleStatus(
                path="src/needs.py",
                status="needs_review",
                priority="critical",
                lines=75,
            )
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    def test_report_empty_registry(self, runner, project_dir, monkeypatch):
        """Report with empty registry."""
        registry = Registry()
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report"])

        assert result.exit_code == 1
        assert "no modules" in result.output.lower()

    @patch("glreview.cli.get_current_commit")
    @patch("glreview.review_state.has_changes_since")
    def test_report_with_sync(
        self, mock_has_changes, mock_commit, runner, project_dir, registry_file, monkeypatch
    ):
        """Report with --sync option."""
        mock_has_changes.return_value = False
        mock_commit.return_value = "abc123"
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--sync"])

        assert result.exit_code == 0
        # Should contain report content
        assert "coverage" in result.output.lower()

    @patch("glreview.review_state.has_changes_since")
    def test_report_markdown_with_in_progress(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Report includes in-progress section."""
        mock_has_changes.return_value = False
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "markdown"])

        assert result.exit_code == 0
        assert "in progress" in result.output.lower()
        assert "@bob" in result.output or "bob" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_report_shows_stale_modules(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Report shows stale (changed since review) modules."""
        mock_has_changes.return_value = True  # Module has changed
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "markdown"])

        assert result.exit_code == 0
        # Should show the changed section
        assert "changed" in result.output.lower() or "stale" in result.output.lower()

    @patch("glreview.review_state.has_changes_since")
    def test_report_badge_high_coverage(
        self, mock_has_changes, runner, project_dir, monkeypatch
    ):
        """Report badge shows green for high coverage."""
        mock_has_changes.return_value = False

        # Create registry with 95% coverage (19/20 lines reviewed)
        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        for i in range(10):
            (src / f"mod{i}.py").write_text(f"# mod {i}\n")

        registry = Registry()
        # 9 reviewed, 1 needs review (90% coverage)
        for i in range(9):
            registry.set(ModuleStatus(
                path=f"src/mod{i}.py", status="reviewed",
                last_reviewed_commit="abc", lines=10
            ))
        registry.set(ModuleStatus(path="src/mod9.py", status="needs_review", lines=10))
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "badge"])

        assert result.exit_code == 0
        assert "brightgreen" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_report_badge_medium_coverage(
        self, mock_has_changes, runner, project_dir, monkeypatch
    ):
        """Report badge shows green for medium coverage."""
        mock_has_changes.return_value = False

        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        for i in range(10):
            (src / f"mod{i}.py").write_text(f"# mod {i}\n")

        registry = Registry()
        # 7 reviewed, 3 needs review (70% coverage)
        for i in range(7):
            registry.set(ModuleStatus(
                path=f"src/mod{i}.py", status="reviewed",
                last_reviewed_commit="abc", lines=10
            ))
        for i in range(7, 10):
            registry.set(ModuleStatus(path=f"src/mod{i}.py", status="needs_review", lines=10))
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "badge"])

        assert result.exit_code == 0
        assert "green" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_report_badge_low_coverage(
        self, mock_has_changes, runner, project_dir, monkeypatch
    ):
        """Report badge shows yellow for low coverage."""
        mock_has_changes.return_value = False

        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        for i in range(10):
            (src / f"mod{i}.py").write_text(f"# mod {i}\n")

        registry = Registry()
        # 5 reviewed, 5 needs review (50% coverage)
        for i in range(5):
            registry.set(ModuleStatus(
                path=f"src/mod{i}.py", status="reviewed",
                last_reviewed_commit="abc", lines=10
            ))
        for i in range(5, 10):
            registry.set(ModuleStatus(path=f"src/mod{i}.py", status="needs_review", lines=10))
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "badge"])

        assert result.exit_code == 0
        assert "yellow" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_report_badge_very_low_coverage(
        self, mock_has_changes, runner, project_dir, monkeypatch
    ):
        """Report badge shows red for very low coverage."""
        mock_has_changes.return_value = False

        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        for i in range(10):
            (src / f"mod{i}.py").write_text(f"# mod {i}\n")

        registry = Registry()
        # 2 reviewed, 8 needs review (20% coverage)
        for i in range(2):
            registry.set(ModuleStatus(
                path=f"src/mod{i}.py", status="reviewed",
                last_reviewed_commit="abc", lines=10
            ))
        for i in range(2, 10):
            registry.set(ModuleStatus(path=f"src/mod{i}.py", status="needs_review", lines=10))
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "badge"])

        assert result.exit_code == 0
        assert "red" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_report_markdown_high_coverage(
        self, mock_has_changes, runner, project_dir, monkeypatch
    ):
        """Report markdown shows brightgreen badge for high coverage."""
        mock_has_changes.return_value = False

        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        for i in range(10):
            (src / f"mod{i}.py").write_text(f"# mod {i}\n")

        registry = Registry()
        # 9 reviewed, 1 needs review (90% coverage)
        for i in range(9):
            registry.set(ModuleStatus(
                path=f"src/mod{i}.py", status="reviewed",
                last_reviewed_commit="abc", lines=10
            ))
        registry.set(ModuleStatus(path="src/mod9.py", status="needs_review", lines=10))
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "markdown"])

        assert result.exit_code == 0
        assert "brightgreen" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_report_markdown_medium_coverage(
        self, mock_has_changes, runner, project_dir, monkeypatch
    ):
        """Report markdown shows green badge for medium coverage."""
        mock_has_changes.return_value = False

        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        for i in range(10):
            (src / f"mod{i}.py").write_text(f"# mod {i}\n")

        registry = Registry()
        # 7 reviewed, 3 needs review (70% coverage)
        for i in range(7):
            registry.set(ModuleStatus(
                path=f"src/mod{i}.py", status="reviewed",
                last_reviewed_commit="abc", lines=10
            ))
        for i in range(7, 10):
            registry.set(ModuleStatus(path=f"src/mod{i}.py", status="needs_review", lines=10))
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "markdown"])

        assert result.exit_code == 0
        assert "green" in result.output

    @patch("glreview.review_state.has_changes_since")
    def test_report_markdown_low_coverage(
        self, mock_has_changes, runner, project_dir, monkeypatch
    ):
        """Report markdown shows yellow badge for low coverage."""
        mock_has_changes.return_value = False

        src = project_dir / "src"
        src.mkdir(exist_ok=True)
        for i in range(10):
            (src / f"mod{i}.py").write_text(f"# mod {i}\n")

        registry = Registry()
        # 5 reviewed, 5 needs review (50% coverage)
        for i in range(5):
            registry.set(ModuleStatus(
                path=f"src/mod{i}.py", status="reviewed",
                last_reviewed_commit="abc", lines=10
            ))
        for i in range(5, 10):
            registry.set(ModuleStatus(path=f"src/mod{i}.py", status="needs_review", lines=10))
        save_registry(registry, project_dir / "review_registry.json")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["report", "--format", "markdown"])

        assert result.exit_code == 0
        assert "yellow" in result.output


class TestCheckEdgeCases:
    """Additional tests for check command edge cases."""

    @pytest.fixture
    def registry_file(self, project_dir):
        """Create a registry with reviewed module marked critical."""
        registry = Registry()
        registry.set(
            ModuleStatus(
                path="src/critical.py",
                status="reviewed",
                priority="critical",
                last_reviewed_commit="abc123",
                lines=50,
            )
        )
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir / "review_registry.json"

    @patch("glreview.review_state.has_changes_since")
    def test_check_critical_module_changed(
        self, mock_has_changes, runner, project_dir, registry_file, monkeypatch
    ):
        """Check reports critical module changes."""
        mock_has_changes.return_value = True
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["check"])

        assert result.exit_code == 1
        assert "critical" in result.output.lower()


class TestClaudeInit:
    """Tests for claude-init command."""

    @pytest.fixture
    def project_with_modules(self, project_dir):
        """Create a project directory with source files and registry."""
        # Source files already created by project_dir fixture
        # Create a registry
        registry = Registry()
        registry.set(ModuleStatus(path="src/main.py", status="needs_review"))
        registry.set(ModuleStatus(path="src/utils.py", status="needs_review"))
        save_registry(registry, project_dir / "review_registry.json")
        return project_dir

    def test_claude_init_context_exists(
        self, runner, project_with_modules, monkeypatch
    ):
        """Claude-init exits when context already exists."""
        project_dir = project_with_modules
        # Create existing context file
        context_dir = project_dir / ".glreview"
        context_dir.mkdir()
        (context_dir / "context.json").write_text("{}")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-init"])

        assert result.exit_code == 1
        assert "already exists" in result.output.lower()

    @patch("glreview.cli.claude_available")
    def test_claude_init_no_claude(
        self, mock_available, runner, project_with_modules, monkeypatch
    ):
        """Claude-init exits when claude CLI not found."""
        mock_available.return_value = False
        monkeypatch.chdir(project_with_modules)

        result = runner.invoke(main, ["claude-init"])

        assert result.exit_code == 1
        assert "not found" in result.output.lower()

    def test_claude_init_no_modules(self, runner, temp_dir, monkeypatch):
        """Claude-init exits when no modules found."""
        # Create pyproject but no source files matching pattern
        pyproject = temp_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["nonexistent/**/*.py"]
""")
        monkeypatch.chdir(temp_dir)

        result = runner.invoke(main, ["claude-init"])

        assert result.exit_code == 1
        assert "no modules" in result.output.lower()

    def test_claude_init_dry_run(
        self, runner, project_with_modules, monkeypatch
    ):
        """Claude-init dry run shows prompt."""
        monkeypatch.chdir(project_with_modules)

        result = runner.invoke(main, ["claude-init", "--dry-run"])

        assert result.exit_code == 0
        assert "prompt" in result.output.lower()
        # Should show module paths in prompt
        assert "src/main.py" in result.output

    @patch("glreview.cli.run_claude_review")
    @patch("glreview.cli.claude_available")
    @patch("glreview.cli.get_current_commit")
    def test_claude_init_success(
        self, mock_commit, mock_available, mock_run,
        runner, project_with_modules, monkeypatch
    ):
        """Claude-init succeeds with valid response."""
        mock_commit.return_value = "abc123"
        mock_available.return_value = True
        mock_run.return_value = (
            json.dumps({
                "project": {
                    "description": "Test project",
                    "conventions": "PEP8",
                },
                "modules": {
                    "src/main.py": {"purpose": "Entry point"}
                }
            }),
            True
        )
        monkeypatch.chdir(project_with_modules)

        result = runner.invoke(main, ["claude-init"])

        assert result.exit_code == 0
        assert "context generated" in result.output.lower()

    @patch("glreview.cli.run_claude_review")
    @patch("glreview.cli.claude_available")
    def test_claude_init_claude_error(
        self, mock_available, mock_run,
        runner, project_with_modules, monkeypatch
    ):
        """Claude-init handles Claude error."""
        mock_available.return_value = True
        mock_run.return_value = ("API Error", False)
        monkeypatch.chdir(project_with_modules)

        result = runner.invoke(main, ["claude-init"])

        assert result.exit_code == 1
        assert "error" in result.output.lower()

    @patch("glreview.cli.run_claude_review")
    @patch("glreview.cli.claude_available")
    def test_claude_init_invalid_json(
        self, mock_available, mock_run,
        runner, project_with_modules, monkeypatch
    ):
        """Claude-init handles invalid JSON response."""
        mock_available.return_value = True
        mock_run.return_value = ("not valid json {{{", True)
        monkeypatch.chdir(project_with_modules)

        result = runner.invoke(main, ["claude-init"])

        assert result.exit_code == 1
        assert "json" in result.output.lower()


class TestClaudeSync:
    """Tests for claude-sync command."""

    @pytest.fixture
    def git_project_with_context(self, temp_git_repo):
        """Create a git project with existing context and commits."""
        import subprocess

        # Create source files
        src = temp_git_repo / "src"
        src.mkdir(exist_ok=True)
        (src / "main.py").write_text("def main(): pass\n")
        (src / "utils.py").write_text("def util(): pass\n")

        # Create pyproject
        pyproject = temp_git_repo / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]
""")

        # Commit initial files
        subprocess.run(["git", "add", "."], cwd=temp_git_repo, check=True)
        subprocess.run(
            ["git", "commit", "-m", "Initial commit"],
            cwd=temp_git_repo, check=True
        )

        # Get commit SHA
        result = subprocess.run(
            ["git", "rev-parse", "--short", "HEAD"],
            cwd=temp_git_repo, capture_output=True, text=True, check=True
        )
        commit_sha = result.stdout.strip()

        # Create context
        context_dir = temp_git_repo / ".glreview"
        context_dir.mkdir()
        context_data = {
            "version": "1",
            "generated_at": "2024-01-01T00:00:00Z",
            "generated_commit": commit_sha,
            "project": {"description": "Test project"},
            "modules": {}
        }
        (context_dir / "context.json").write_text(json.dumps(context_data))

        return temp_git_repo, commit_sha

    def test_claude_sync_no_context(self, runner, project_dir, monkeypatch):
        """Claude-sync exits when no context exists."""
        pyproject = project_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]
""")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-sync"])

        assert result.exit_code == 1
        assert "no context" in result.output.lower()

    def test_claude_sync_up_to_date(
        self, runner, git_project_with_context, monkeypatch
    ):
        """Claude-sync reports when context is up to date."""
        project_dir, commit_sha = git_project_with_context
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-sync"])

        assert result.exit_code == 0
        assert "up to date" in result.output.lower()

    @patch("glreview.cli.claude_available")
    def test_claude_sync_no_claude(
        self, mock_available, runner, git_project_with_context, monkeypatch
    ):
        """Claude-sync exits when claude CLI not found."""
        import subprocess

        project_dir, commit_sha = git_project_with_context
        mock_available.return_value = False

        # Make a change and commit to trigger sync
        (project_dir / "src" / "main.py").write_text("def main(): return 1\n")
        subprocess.run(["git", "add", "."], cwd=project_dir, check=True)
        subprocess.run(
            ["git", "commit", "-m", "Update main"],
            cwd=project_dir, check=True
        )

        monkeypatch.chdir(project_dir)
        result = runner.invoke(main, ["claude-sync"])

        assert result.exit_code == 1
        assert "not found" in result.output.lower()

    def test_claude_sync_dry_run(
        self, runner, git_project_with_context, monkeypatch
    ):
        """Claude-sync dry run shows prompt without running claude."""
        import subprocess

        project_dir, commit_sha = git_project_with_context

        # Make a change and commit to trigger sync
        (project_dir / "src" / "main.py").write_text("def main(): return 1\n")
        subprocess.run(["git", "add", "."], cwd=project_dir, check=True)
        subprocess.run(
            ["git", "commit", "-m", "Update main"],
            cwd=project_dir, check=True
        )

        monkeypatch.chdir(project_dir)
        result = runner.invoke(main, ["claude-sync", "--dry-run"])

        # Dry run should show changes
        assert result.exit_code == 0
        assert "modified" in result.output.lower() or "changes" in result.output.lower()

    @patch("glreview.cli.run_claude_review")
    @patch("glreview.cli.claude_available")
    def test_claude_sync_force_rebuild(
        self, mock_available, mock_run,
        runner, git_project_with_context, monkeypatch
    ):
        """Claude-sync --force triggers full rebuild."""
        project_dir, commit_sha = git_project_with_context
        mock_available.return_value = True
        mock_run.return_value = (
            json.dumps({
                "project": {"description": "Updated project"},
                "modules": {}
            }),
            True
        )
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-sync", "--force"])

        assert result.exit_code == 0


class TestClaudeContext:
    """Tests for claude-context command."""

    @pytest.fixture
    def project_with_context(self, project_dir):
        """Create a project with context."""
        context_dir = project_dir / ".glreview"
        context_dir.mkdir()
        context_data = {
            "version": "1",
            "generated_at": "2024-01-01T00:00:00Z",
            "generated_commit": "abc123",
            "project": {
                "description": "Test project description",
                "conventions": "Use PEP8",
                "architecture": "Layered design",
                "test_patterns": "pytest"
            },
            "modules": {
                "src/main.py": {
                    "purpose": "Main entry point",
                    "test_files": ["tests/test_main.py"],
                    "imports_from": ["src/utils.py"],
                    "imported_by": ["src/cli.py"],
                    "related_files": ["src/config.py"],
                    "notes": "Critical module"
                }
            }
        }
        (context_dir / "context.json").write_text(json.dumps(context_data))

        # Create pyproject
        pyproject = project_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]
""")
        return project_dir

    def test_claude_context_no_context(self, runner, project_dir, monkeypatch):
        """Claude-context exits when no context exists."""
        pyproject = project_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]
""")
        monkeypatch.chdir(project_dir)

        result = runner.invoke(main, ["claude-context"])

        assert result.exit_code == 1
        assert "no context" in result.output.lower()

    def test_claude_context_project(
        self, runner, project_with_context, monkeypatch
    ):
        """Claude-context shows project info."""
        monkeypatch.chdir(project_with_context)

        result = runner.invoke(main, ["claude-context"])

        assert result.exit_code == 0
        assert "project context" in result.output.lower()
        assert "test project description" in result.output.lower()
        assert "pep8" in result.output.lower()
        assert "layered" in result.output.lower()

    def test_claude_context_module(
        self, runner, project_with_context, monkeypatch
    ):
        """Claude-context shows module info."""
        monkeypatch.chdir(project_with_context)

        result = runner.invoke(main, ["claude-context", "src/main.py"])

        assert result.exit_code == 0
        assert "main entry point" in result.output.lower()
        assert "test_main.py" in result.output
        assert "utils.py" in result.output

    def test_claude_context_module_not_found(
        self, runner, project_with_context, monkeypatch
    ):
        """Claude-context handles missing module."""
        monkeypatch.chdir(project_with_context)

        result = runner.invoke(main, ["claude-context", "nonexistent.py"])

        assert result.exit_code == 1
        assert "no context found" in result.output.lower()


class TestReviewersTeams:
    """Tests for reviewers command with teams."""

    @pytest.fixture
    def project_with_teams(self, project_dir):
        """Create a project with team configuration."""
        pyproject = project_dir / "pyproject.toml"
        pyproject.write_text("""[tool.glreview]
sources = ["src/**/*.py"]

[tool.glreview.teams]
core = ["@alice", "@bob"]
frontend = ["@charlie"]
""")
        return project_dir

    @patch("glreview.cli.gitlab_available")
    def test_reviewers_with_teams(
        self, mock_gitlab, runner, project_with_teams, monkeypatch
    ):
        """Reviewers shows configured teams."""
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_with_teams)

        result = runner.invoke(main, ["reviewers"])

        assert result.exit_code == 0
        assert "configured teams" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    def test_reviewers_filter_team(
        self, mock_gitlab, runner, project_with_teams, monkeypatch
    ):
        """Reviewers filters by team."""
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_with_teams)

        result = runner.invoke(main, ["reviewers", "--team", "core"])

        assert result.exit_code == 0
        assert "alice" in result.output.lower() or "bob" in result.output.lower()

    @patch("glreview.cli.gitlab_available")
    def test_reviewers_unknown_team(
        self, mock_gitlab, runner, project_with_teams, monkeypatch
    ):
        """Reviewers fails for unknown team."""
        mock_gitlab.return_value = False
        monkeypatch.chdir(project_with_teams)

        result = runner.invoke(main, ["reviewers", "--team", "unknown"])

        assert result.exit_code == 1
        assert "unknown team" in result.output.lower()
