"""Tests for merge reliability improvements (Phase 7, Stream C)."""

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

import pytest

from loom.skills.merge import (
    _validate_branch,
    _heartbeat_loop,
    _set_merge_phase,
    MergePhase,
    MergeStatus,
)


class TestBranchValidation:
    async def test_valid_branch(self, tmp_path):
        """Branch that exists with commits ahead should pass."""
        with patch("loom.skills.merge._run_git") as mock_git:
            # First call: rev-parse --verify (branch exists)
            # Second call: git log (has commits)
            mock_git.side_effect = [
                (0, "abc123", ""),
                (0, "abc123 some commit\ndef456 another commit", ""),
            ]
            valid, msg = await _validate_branch("feature-1", tmp_path)
            assert valid is True
            assert "2 commit(s)" in msg

    async def test_missing_branch(self, tmp_path):
        """Non-existent branch should fail."""
        with patch("loom.skills.merge._run_git") as mock_git:
            mock_git.return_value = (128, "", "fatal: not a valid ref")
            valid, msg = await _validate_branch("nonexistent", tmp_path)
            assert valid is False
            assert "does not exist" in msg

    async def test_no_new_commits(self, tmp_path):
        """Branch with no new commits should fail."""
        with patch("loom.skills.merge._run_git") as mock_git:
            mock_git.side_effect = [
                (0, "abc123", ""),  # branch exists
                (0, "", ""),  # no new commits
            ]
            valid, msg = await _validate_branch("stale-branch", tmp_path)
            assert valid is False
            assert "no new commits" in msg


class TestMergePhase:
    def test_phase_values(self):
        """All merge phases are defined."""
        assert MergePhase.LOCK_ACQUIRED == "lock_acquired"
        assert MergePhase.MERGED == "merged"
        assert MergePhase.TESTS_RUNNING == "tests_running"
        assert MergePhase.TESTS_PASSED == "tests_passed"
        assert MergePhase.PUSHED == "pushed"
        assert MergePhase.CLEANED_UP == "cleaned_up"


class TestSetMergePhase:
    async def test_sets_phase_in_context(self):
        """Phase should be stored in task context."""
        mock_pool = AsyncMock()
        mock_task = MagicMock()
        mock_task.context = {"branch_name": "feature-1"}

        with patch("loom.skills.merge.store.get_task", return_value=mock_task):
            with patch("loom.skills.merge.store.update_task") as mock_update:
                await _set_merge_phase(mock_pool, "task-1", MergePhase.MERGED)
                mock_update.assert_called_once()
                call_kwargs = mock_update.call_args
                ctx = call_kwargs.kwargs.get("context") or call_kwargs[1].get("context")
                assert ctx["merge_phase"] == "merged"
                assert ctx["branch_name"] == "feature-1"

    async def test_handles_failure_gracefully(self):
        """Phase checkpoint failure should not raise."""
        mock_pool = AsyncMock()
        with patch("loom.skills.merge.store.get_task", side_effect=Exception("db error")):
            # Should not raise
            await _set_merge_phase(mock_pool, "task-1", MergePhase.MERGED)


class TestHeartbeatLoop:
    async def test_heartbeat_calls_set_claim_expiry(self):
        """Heartbeat should call store.set_claim_expiry periodically."""
        mock_pool = AsyncMock()
        with patch("loom.skills.merge.store.set_claim_expiry") as mock_expiry:
            mock_expiry.return_value = MagicMock()
            task = asyncio.create_task(_heartbeat_loop(mock_pool, "task-1", interval=0.1))
            await asyncio.sleep(0.25)  # Should trigger at least 2 heartbeats
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
            assert mock_expiry.call_count >= 2

    async def test_heartbeat_handles_errors(self):
        """Heartbeat should continue even if set_claim_expiry fails."""
        mock_pool = AsyncMock()
        with patch("loom.skills.merge.store.set_claim_expiry", side_effect=Exception("db error")):
            task = asyncio.create_task(_heartbeat_loop(mock_pool, "task-1", interval=0.1))
            await asyncio.sleep(0.15)
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
            # Should not have raised
