"""Tests for the core review pipeline: process_file and run_review."""

import types
from unittest.mock import MagicMock

from prlens.reviewer import process_file, run_review

# A minimal patch with one added line at new-file line 2.
# get_diff_positions produces {2: 2} for this patch.
SIMPLE_PATCH = "@@ -1,2 +1,3 @@\n line1\n+new line\n line2\n"


def make_file(filename="src/foo.py", status="modified"):
    return types.SimpleNamespace(filename=filename, status=status)


class StubReviewer:
    def __init__(self, comments):
        self._comments = comments

    def review(self, **kwargs):
        return self._comments


# ---------------------------------------------------------------------------
# process_file
# ---------------------------------------------------------------------------


class TestProcessFile:
    def test_skips_deleted_file(self):
        result = process_file(StubReviewer([]), "", "", make_file(status="removed"), SIMPLE_PATCH, "", [])
        assert result == []

    def test_skips_empty_patch(self):
        result = process_file(StubReviewer([]), "", "", make_file(), "", "", [])
        assert result == []

    def test_valid_comment_included(self):
        reviewer = StubReviewer([{"line": 2, "severity": "minor", "comment": "bad name"}])
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [])
        assert len(result) == 1
        assert result[0]["path"] == "src/foo.py"
        assert result[0]["severity"] == "minor"
        assert "**[MINOR]**" in result[0]["body"]
        assert result[0]["line"] == 2

    def test_comment_for_line_not_in_diff_skipped(self):
        reviewer = StubReviewer([{"line": 99, "severity": "minor", "comment": "bad"}])
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [])
        assert result == []

    def test_comment_missing_line_skipped(self):
        reviewer = StubReviewer([{"severity": "minor", "comment": "bad"}])
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [])
        assert result == []

    def test_comment_missing_text_skipped(self):
        reviewer = StubReviewer([{"line": 2, "severity": "minor"}])
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [])
        assert result == []

    def test_invalid_severity_defaults_to_minor(self):
        reviewer = StubReviewer([{"line": 2, "severity": "blocker", "comment": "issue"}])
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [])
        assert result[0]["severity"] == "minor"
        assert "**[MINOR]**" in result[0]["body"]

    def test_duplicate_in_queued_skipped(self):
        reviewer = StubReviewer([{"line": 2, "severity": "minor", "comment": "bad name"}])
        queued = {("src/foo.py", 2, "bad name")}
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [], queued)
        assert result == []

    def test_comment_added_to_queued_set(self):
        reviewer = StubReviewer([{"line": 2, "severity": "minor", "comment": "bad name"}])
        queued = set()
        process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [], queued)
        assert ("src/foo.py", 2, "bad name") in queued

    def test_duplicate_existing_github_comment_skipped(self):
        reviewer = StubReviewer([{"line": 2, "severity": "minor", "comment": "bad name"}])
        existing = MagicMock()
        existing.path = "src/foo.py"
        existing.line = 2
        existing.body = "bad name"
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [existing])
        assert result == []

    def test_existing_comment_with_none_line_uses_original_line(self):
        """Comments whose line is None (after force-push) should still be detected via original_line."""
        reviewer = StubReviewer([{"line": 2, "severity": "minor", "comment": "bad name"}])
        existing = MagicMock()
        existing.path = "src/foo.py"
        existing.line = None
        existing.original_line = 2
        existing.body = "bad name"
        result = process_file(reviewer, "", "", make_file(), SIMPLE_PATCH, "", [existing])
        assert result == []


# ---------------------------------------------------------------------------
# run_review — early-exit paths
# ---------------------------------------------------------------------------


def _base_config():
    return {
        "github_token": "tok",
        "model": "anthropic",
        "anthropic_api_key": "key",
        "openai_api_key": None,
        "review_draft_prs": False,
        "exclude": [],
        "max_chars_per_file": 20000,
        "batch_limit": 60,
        "guidelines": None,
    }


class TestRunReviewEarlyExits:
    def test_skips_draft_pr(self, mocker):
        mock_pr = MagicMock()
        mock_pr.draft = True
        mock_repo = MagicMock()
        mocker.patch("prlens.reviewer.get_pull", return_value=mock_pr)
        mocker.patch("prlens.reviewer.get_last_reviewed_sha", return_value=None)

        run_review("owner/repo", 1, _base_config(), repo_obj=mock_repo)

        mock_pr.create_review.assert_not_called()

    def test_no_new_commits_returns_early(self, mocker):
        sha = "a" * 40
        mock_pr = MagicMock()
        mock_pr.draft = False
        mock_pr.head.sha = sha
        mock_repo = MagicMock()
        mocker.patch("prlens.reviewer.get_pull", return_value=mock_pr)
        mocker.patch("prlens.reviewer.get_last_reviewed_sha", return_value=sha)

        run_review("owner/repo", 1, _base_config(), repo_obj=mock_repo)

        mock_pr.create_review.assert_not_called()

    def test_shadow_mode_does_not_post(self, mocker):
        sha = "a" * 40
        mock_pr = MagicMock()
        mock_pr.draft = False
        mock_pr.head.sha = sha
        mock_pr.body = ""
        mock_pr.get_review_comments.return_value = []

        mock_file = MagicMock()
        mock_file.filename = "src/foo.py"
        mock_file.status = "modified"
        mock_file.patch = SIMPLE_PATCH

        content_mock = MagicMock()
        content_mock.decoded_content = b"file content"
        mock_repo = MagicMock()
        mock_repo.get_contents.return_value = content_mock

        mocker.patch("prlens.reviewer.get_pull", return_value=mock_pr)
        mocker.patch("prlens.reviewer.get_last_reviewed_sha", return_value=None)
        mocker.patch("prlens.reviewer.get_diff", return_value=[mock_file])
        mocker.patch("prlens.reviewer.load_guidelines", return_value="guidelines")
        mock_reviewer = MagicMock()
        mock_reviewer.review.return_value = []
        mocker.patch("prlens.reviewer._get_reviewer", return_value=mock_reviewer)

        run_review("owner/repo", 1, _base_config(), shadow=True, repo_obj=mock_repo)

        mock_pr.create_review.assert_not_called()

    def test_full_review_bypasses_incremental(self, mocker):
        """--full-review should skip SHA lookup and review all files."""
        sha = "a" * 40
        mock_pr = MagicMock()
        mock_pr.draft = False
        mock_pr.head.sha = sha
        mock_pr.body = ""
        mock_pr.get_review_comments.return_value = []

        mock_file = MagicMock()
        mock_file.filename = "src/foo.py"
        mock_file.status = "modified"
        mock_file.patch = SIMPLE_PATCH

        content_mock = MagicMock()
        content_mock.decoded_content = b"content"
        mock_repo = MagicMock()
        mock_repo.get_contents.return_value = content_mock

        get_last_sha = mocker.patch("prlens.reviewer.get_last_reviewed_sha")
        mocker.patch("prlens.reviewer.get_pull", return_value=mock_pr)
        mocker.patch("prlens.reviewer.get_diff", return_value=[mock_file])
        mocker.patch("prlens.reviewer.load_guidelines", return_value="guidelines")
        mock_reviewer = MagicMock()
        mock_reviewer.review.return_value = []
        mocker.patch("prlens.reviewer._get_reviewer", return_value=mock_reviewer)

        run_review("owner/repo", 1, _base_config(), shadow=True, force_full=True, repo_obj=mock_repo)

        get_last_sha.assert_not_called()
