# #region Test.GitService.Status [C:3] [TYPE Module] [SEMANTICS test,git,status,diff,history]
# @BRIEF Tests for GitServiceStatusMixin — parse porcelain, get_status, get_diff, get_commit_history.
# @RELATION BINDS_TO -> [GitServiceStatusMixin]
# @TEST_EDGE: empty_porcelain -> no output returns empty lists
# @TEST_EDGE: untracked_files -> ?? prefix parsed as untracked
# @TEST_EDGE: staged_and_modified -> XY prefix correctly split
# @TEST_EDGE: renamed_files -> -> arrow parsed as rename target
# @TEST_EDGE: git_status_failure -> exception caught, warning logged
# @TEST_EDGE: no_commits -> has_commits=false, branch name still returned
# @TEST_EDGE: divergent_branch -> ahead>0 AND behind>0 → DIVERGED
# @TEST_EDGE: ahead_remote -> ahead>0, behind=0 → AHEAD_REMOTE
# @TEST_EDGE: behind_remote -> ahead=0, behind>0 → BEHIND_REMOTE
# @TEST_EDGE: dirty_repo -> changes present → CHANGES
# @TEST_EDGE: clean_synced -> no changes, no ahead/behind → SYNCED
# @TEST_EDGE: diff_with_file -> file_path passed to git diff
# @TEST_EDGE: diff_staged -> --staged flag passed
# @TEST_EDGE: commit_history_empty -> no heads, no remotes returns []
# @TEST_EDGE: commit_history_exception -> exception logged, returns []

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))

import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from src.services.git._status import GitServiceStatusMixin


# ── Helper: create a testable instance of the mixin ──

class TestableGitStatus(GitServiceStatusMixin):
    """Concrete test class providing the minimum _locked and get_repo stubs.

    _locked is a no-op context manager. get_repo returns a pre-set mock.
    """
    def __init__(self, mock_repo=None):
        self._mock_repo = mock_repo or MagicMock()
        self._lock_called = False

    async def get_repo(self, dashboard_id):
        return self._mock_repo

    def _locked(self, dashboard_id):
        import contextlib
        @contextlib.contextmanager
        def _lock():
            self._lock_called = True
            yield
        return _lock()


# ── _parse_status_porcelain ──

class TestParseStatusPorcelain:
    """_parse_status_porcelain — git status --porcelain parser."""

    # #region test_porcelain_empty_output [C:2] [TYPE Function]
    def test_porcelain_empty_output(self):
        """Empty output → all lists empty."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = ""
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        assert staged == []
        assert modified == []
        assert untracked == []
    # #endregion test_porcelain_empty_output

    # #region test_porcelain_untracked_files [C:2] [TYPE Function]
    def test_porcelain_untracked_files(self):
        """?? prefixed lines appear in untracked list."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = "?? new_file.txt\n?? untracked_dir/\n"
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        assert untracked == ["new_file.txt", "untracked_dir/"]
        assert staged == []
        assert modified == []
    # #endregion test_porcelain_untracked_files

    # #region test_porcelain_staged [C:2] [TYPE Function]
    def test_porcelain_staged(self):
        """XY where X!=space → staged."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = "M  staged.txt\nA  added.py\n"
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        assert staged == ["staged.txt", "added.py"]
        assert modified == []
    # #endregion test_porcelain_staged

    # #region test_porcelain_modified [C:2] [TYPE Function]
    def test_porcelain_modified(self):
        """XY where Y!=space → modified."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = " M modified.rs\n"
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        assert modified == ["modified.rs"]
        assert staged == []
    # #endregion test_porcelain_modified

    # #region test_porcelain_staged_and_modified [C:2] [TYPE Function]
    def test_porcelain_staged_and_modified(self):
        """MM → both staged and modified."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = "MM both.txt\n"
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        assert "both.txt" in staged
        assert "both.txt" in modified
    # #endregion test_porcelain_staged_and_modified

    # #region test_porcelain_renamed [C:2] [TYPE Function]
    def test_porcelain_renamed(self):
        """R  old -> new → staged shows new path."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = "R  old_name.py -> new_name.py\n"
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        assert "new_name.py" in staged
        assert "old_name.py" not in str(staged)
    # #endregion test_porcelain_renamed

    # #region test_porcelain_exclude_intent_to_add [C:2] [TYPE Function]
    def test_porcelain_exclude_intent_to_add(self):
        """!! lines (intent-to-add/assume-unchanged) are skipped."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = "!! ignored_pattern\n"
        _, _, untracked = svc._parse_status_porcelain(repo)
        assert untracked == []
    # #endregion test_porcelain_exclude_intent_to_add

    # #region test_porcelain_short_line [C:2] [TYPE Function]
    def test_porcelain_short_line(self):
        """Line with <3 chars is skipped (e.g., empty/broken output)."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.return_value = "A\nBB\n M file.txt\n"
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        # "A\n" and "BB\n" are skipped (< 3 chars). Only " M file.txt" is parsed.
        assert "file.txt" in modified
        assert len(staged) == 0
    # #endregion test_porcelain_short_line

    # #region test_porcelain_git_failure [C:2] [TYPE Function]
    def test_porcelain_git_failure(self):
        """Exception from git.status returns empty lists, no crash."""
        svc = TestableGitStatus()
        repo = MagicMock()
        repo.git.status.side_effect = Exception("git not available")
        staged, modified, untracked = svc._parse_status_porcelain(repo)
        assert staged == []
        assert modified == []
        assert untracked == []
    # #endregion test_porcelain_git_failure


# ── get_status ──

class TestGetStatus:
    """get_status — full repository status computation."""

    # #region test_get_status_no_commits [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_no_commits(self):
        """Repository with no commits: has_commits=false, branch returned."""
        from unittest.mock import PropertyMock
        repo = MagicMock()
        head = MagicMock()
        # Accessing repo.head.commit as a PROPERTY raises ValueError (like real GitPython)
        type(head).commit = PropertyMock(side_effect=ValueError("no commits"))
        repo.head = head
        repo.active_branch.name = "main"
        repo.active_branch.tracking_branch.return_value = None

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert svc._lock_called is True
        assert result["current_branch"] == "main"
        assert result["has_upstream"] is False
    # #endregion test_get_status_no_commits

    # #region test_get_status_diverged [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_diverged(self):
        """ahead>0 and behind>0 → DIVERGED."""
        repo = MagicMock()
        head = MagicMock()
        head.commit = MagicMock()
        repo.head = head
        repo.active_branch.name = "feature"
        tracking = MagicMock()
        tracking.name = "origin/feature"
        repo.active_branch.tracking_branch.return_value = tracking
        # iter_commits for ahead: first call returns [1,2], second returns [3,4,5]
        repo.iter_commits.side_effect = [
            [MagicMock(), MagicMock()],  # ahead: 2 commits
            [MagicMock(), MagicMock(), MagicMock()],  # behind: 3 commits
        ]
        repo.git.status.return_value = ""

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert result["sync_state"] == "DIVERGED"
        assert result["ahead_count"] == 2
        assert result["behind_count"] == 3
        assert result["is_diverged"] is True
    # #endregion test_get_status_diverged

    # #region test_get_status_ahead [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_ahead(self):
        """ahead>0, behind=0 → AHEAD_REMOTE."""
        repo = MagicMock()
        head = MagicMock()
        head.commit = MagicMock()
        repo.head = head
        repo.active_branch.name = "feature"
        tracking = MagicMock()
        repo.active_branch.tracking_branch.return_value = tracking
        repo.iter_commits.side_effect = [
            [MagicMock()],  # ahead: 1 commit
            [],              # behind: 0
        ]
        repo.git.status.return_value = ""

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert result["sync_state"] == "AHEAD_REMOTE"
        assert result["ahead_count"] == 1
        assert result["behind_count"] == 0
    # #endregion test_get_status_ahead

    # #region test_get_status_behind [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_behind(self):
        """ahead=0, behind>0 → BEHIND_REMOTE."""
        repo = MagicMock()
        head = MagicMock()
        head.commit = MagicMock()
        repo.head = head
        repo.active_branch.name = "main"
        tracking = MagicMock()
        repo.active_branch.tracking_branch.return_value = tracking
        repo.iter_commits.side_effect = [
            [],              # ahead: 0
            [MagicMock()],   # behind: 1
        ]
        repo.git.status.return_value = ""

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert result["sync_state"] == "BEHIND_REMOTE"
        assert result["ahead_count"] == 0
        assert result["behind_count"] == 1
    # #endregion test_get_status_behind

    # #region test_get_status_dirty [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_dirty(self):
        """Unstaged changes present → CHANGES."""
        repo = MagicMock()
        head = MagicMock()
        head.commit = MagicMock()
        repo.head = head
        repo.active_branch.name = "main"
        repo.active_branch.tracking_branch.return_value = None
        repo.git.status.return_value = " M modified.txt\n?? new.txt\n"

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert result["sync_state"] == "CHANGES"
        assert result["is_dirty"] is True
        assert "modified.txt" in result["modified_files"]
        assert "new.txt" in result["untracked_files"]
    # #endregion test_get_status_dirty

    # #region test_get_status_synced [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_synced(self):
        """Clean, no divergence → SYNCED."""
        repo = MagicMock()
        head = MagicMock()
        head.commit = MagicMock()
        repo.head = head
        repo.active_branch.name = "main"
        repo.active_branch.tracking_branch.return_value = None
        repo.git.status.return_value = ""

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert result["sync_state"] == "SYNCED"
        assert result["is_dirty"] is False
    # #endregion test_get_status_synced

    # #region test_get_status_tracking_exception [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_tracking_exception(self):
        """Exception in tracking_branch() → has_upstream=false."""
        repo = MagicMock()
        head = MagicMock()
        head.commit = MagicMock()
        repo.head = head
        repo.active_branch.name = "main"
        repo.active_branch.tracking_branch.side_effect = Exception("no remote")
        repo.git.status.return_value = ""

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert result["has_upstream"] is False
        assert result["upstream_branch"] is None
    # #endregion test_get_status_tracking_exception

    # #region test_get_status_iter_commits_exception [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_status_iter_commits_exception(self):
        """Exception in iter_commits → ahead=0, behind=0."""
        repo = MagicMock()
        head = MagicMock()
        head.commit = MagicMock()
        repo.head = head
        repo.active_branch.name = "main"
        tracking = MagicMock()
        repo.active_branch.tracking_branch.return_value = tracking
        repo.iter_commits.side_effect = Exception("git error")

        svc = TestableGitStatus(repo)
        result = await svc.get_status(42)

        assert result["ahead_count"] == 0
        assert result["behind_count"] == 0
    # #endregion test_get_status_iter_commits_exception


# ── get_diff ──

class TestGetDiff:
    """get_diff — diff generation."""

    # #region test_get_diff_no_args [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_diff_no_args(self):
        """Default diff (no file, not staged)."""
        repo = MagicMock()
        repo.git.diff.return_value = "diff --git a/file.txt b/file.txt"
        svc = TestableGitStatus(repo)
        result = await svc.get_diff(42)
        assert result == "diff --git a/file.txt b/file.txt"
        repo.git.diff.assert_called_once_with()
    # #endregion test_get_diff_no_args

    # #region test_get_diff_staged [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_diff_staged(self):
        """--staged flag included."""
        repo = MagicMock()
        repo.git.diff.return_value = "staged diff"
        svc = TestableGitStatus(repo)
        result = await svc.get_diff(42, staged=True)
        assert result == "staged diff"
        repo.git.diff.assert_called_once_with("--staged")
    # #endregion test_get_diff_staged

    # #region test_get_diff_with_file [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_diff_with_file(self):
        """File path passed with -- separator."""
        repo = MagicMock()
        repo.git.diff.return_value = "file diff"
        svc = TestableGitStatus(repo)
        result = await svc.get_diff(42, file_path="src/main.py")
        assert result == "file diff"
        repo.git.diff.assert_called_once_with("--", "src/main.py")
    # #endregion test_get_diff_with_file

    # #region test_get_diff_staged_with_file [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_diff_staged_with_file(self):
        """Both staged and file_path passed."""
        repo = MagicMock()
        repo.git.diff.return_value = "staged file diff"
        svc = TestableGitStatus(repo)
        result = await svc.get_diff(42, file_path="test.py", staged=True)
        assert result == "staged file diff"
        repo.git.diff.assert_called_once_with("--staged", "--", "test.py")
    # #endregion test_get_diff_staged_with_file


# ── get_commit_history ──

class TestGetCommitHistory:
    """get_commit_history — commit log retrieval."""

    # #region test_commit_history_no_heads_no_remotes [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_commit_history_no_heads_no_remotes(self):
        """No heads and no remotes → returns []."""
        repo = MagicMock()
        repo.heads = []
        repo.remotes = []
        svc = TestableGitStatus(repo)
        result = await svc.get_commit_history(42)
        assert result == []
    # #endregion test_commit_history_no_heads_no_remotes

    # #region test_commit_history_returns_commits [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_commit_history_returns_commits(self):
        """Returns parsed commit dicts from iter_commits."""
        import datetime
        repo = MagicMock()
        repo.heads = [MagicMock()]
        repo.remotes = [MagicMock()]

        commit1 = MagicMock()
        commit1.hexsha = "abc123"
        commit1.author.name = "Alice"
        commit1.author.email = "alice@example.com"
        commit1.committed_date = 1700000000
        commit1.message = "First commit\n"
        commit1.stats.files = {"file1.py": {}, "file2.py": {}}

        repo.iter_commits.return_value = [commit1]

        svc = TestableGitStatus(repo)
        result = await svc.get_commit_history(42, limit=10)

        assert len(result) == 1
        assert result[0]["hash"] == "abc123"
        assert result[0]["author"] == "Alice"
        assert result[0]["email"] == "alice@example.com"
        assert result[0]["message"] == "First commit"
        assert "file1.py" in result[0]["files_changed"]
        repo.iter_commits.assert_called_once_with(max_count=10)
    # #endregion test_commit_history_returns_commits

    # #region test_commit_history_exception [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_commit_history_exception(self):
        """Exception during iteration returns []."""
        repo = MagicMock()
        repo.heads = [MagicMock()]
        repo.iter_commits.side_effect = Exception("git error")

        svc = TestableGitStatus(repo)
        result = await svc.get_commit_history(42)

        assert result == []
    # #endregion test_commit_history_exception

    # #region test_commit_history_limit_default [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_commit_history_limit_default(self):
        """Default limit is 50."""
        repo = MagicMock()
        repo.heads = [MagicMock()]
        repo.remotes = [MagicMock()]
        repo.iter_commits.return_value = []

        svc = TestableGitStatus(repo)
        result = await svc.get_commit_history(42)

        repo.iter_commits.assert_called_once_with(max_count=50)
        assert result == []
    # #endregion test_commit_history_limit_default
# #endregion Test.GitService.Status
