"""
Tests for the merge module.

Tests cover:
- Merge readiness checks
- Merge gate with lock mechanism
- Protected branch enforcement
"""

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

from agent_orchestrator.merge.readiness import (
    MergeReadiness,
    ReadinessReport,
    CheckResult,
    CheckStatus,
    CheckSeverity,
)

from agent_orchestrator.merge.gate import (
    MergeGate,
    MergeRequest,
    MergeResult,
    MergeStatus,
    MergeStrategy,
)


# =============================================================================
# Merge Readiness Tests
# =============================================================================

class TestCheckResult:
    """Tests for CheckResult dataclass."""

    def test_create_passed_result(self):
        """Test creating a passed check result."""
        result = CheckResult(
            name="test_check",
            status=CheckStatus.PASSED,
            message="Check passed",
        )

        assert result.name == "test_check"
        assert result.status == CheckStatus.PASSED
        assert result.severity == CheckSeverity.BLOCKING

    def test_create_failed_result(self):
        """Test creating a failed check result."""
        result = CheckResult(
            name="test_check",
            status=CheckStatus.FAILED,
            severity=CheckSeverity.BLOCKING,
            message="Check failed",
            details={"error": "test error"},
        )

        assert result.status == CheckStatus.FAILED
        assert result.details["error"] == "test error"


class TestReadinessReport:
    """Tests for ReadinessReport dataclass."""

    def test_create_empty_report(self):
        """Test creating an empty report."""
        report = ReadinessReport(
            branch="feature/test",
            target_branch="main",
            is_ready=True,
        )

        assert report.branch == "feature/test"
        assert report.target_branch == "main"
        assert report.is_ready is True
        assert report.passed_count == 0

    def test_report_with_checks(self):
        """Test report with check results."""
        checks = [
            CheckResult(name="check1", status=CheckStatus.PASSED, message="OK"),
            CheckResult(name="check2", status=CheckStatus.FAILED, message="Failed"),
        ]

        report = ReadinessReport(
            branch="feature/test",
            target_branch="main",
            is_ready=False,
            checks=checks,
            passed_count=1,
            failed_count=1,
            blocking_issues=["check2: Failed"],
        )

        assert len(report.checks) == 2
        assert report.passed_count == 1
        assert report.failed_count == 1
        assert len(report.blocking_issues) == 1


class TestMergeReadiness:
    """Tests for MergeReadiness class."""

    @pytest.fixture
    def mock_db(self):
        """Create a mock database."""
        db = MagicMock()
        db.get_risks_for_branch.return_value = []
        db.get_latest_status_packet.return_value = {
            "status": "complete",
            "summary": "Test summary",
            "files_changed": ["test.py"],
        }
        return db

    @pytest.fixture
    def readiness(self, mock_db):
        """Create merge readiness checker."""
        return MergeReadiness(
            db=mock_db,
            config={
                "require_tests": False,  # Skip actual test running
                "require_up_to_date": False,
            },
        )

    @pytest.mark.asyncio
    async def test_check_no_critical_risks_pass(self, readiness, mock_db):
        """Test no critical risks check passes."""
        mock_db.get_risks_for_branch.return_value = []

        result = await readiness._check_no_critical_risks("feature/test")

        assert result.status == CheckStatus.PASSED

    @pytest.mark.asyncio
    async def test_check_no_critical_risks_fail(self, readiness, mock_db):
        """Test no critical risks check fails."""
        mock_db.get_risks_for_branch.return_value = [
            {"level": "CRITICAL", "description": "Test risk"}
        ]

        result = await readiness._check_no_critical_risks("feature/test")

        assert result.status == CheckStatus.FAILED
        assert "critical risks" in result.message.lower()

    @pytest.mark.asyncio
    async def test_check_status_packet_complete_pass(self, readiness, mock_db):
        """Test status packet check passes."""
        result = await readiness._check_status_packet_complete("feature/test")

        assert result.status == CheckStatus.PASSED

    @pytest.mark.asyncio
    async def test_check_status_packet_missing(self, readiness, mock_db):
        """Test status packet check fails when missing."""
        mock_db.get_latest_status_packet.return_value = None

        result = await readiness._check_status_packet_complete("feature/test")

        assert result.status == CheckStatus.FAILED

    @pytest.mark.asyncio
    async def test_check_no_pending_approvals_pass(self, readiness):
        """Test pending approvals check passes."""
        mock_queue = MagicMock()
        mock_queue.get_pending_approvals.return_value = []
        readiness.approval_queue = mock_queue

        result = await readiness._check_no_pending_approvals("feature/test")

        assert result.status == CheckStatus.PASSED

    @pytest.mark.asyncio
    async def test_check_no_pending_approvals_fail(self, readiness):
        """Test pending approvals check fails."""
        mock_queue = MagicMock()
        mock_queue.get_pending_approvals.return_value = [
            {"branch": "feature/test", "action": "write"}
        ]
        readiness.approval_queue = mock_queue

        result = await readiness._check_no_pending_approvals("feature/test")

        assert result.status == CheckStatus.FAILED

    def test_register_custom_check(self, readiness):
        """Test registering a custom check."""
        async def custom_check(branch, target):
            return CheckResult(
                name="custom",
                status=CheckStatus.PASSED,
                message="Custom check passed",
            )

        readiness.register_check("custom", custom_check)

        assert "custom" in readiness._custom_checks

    def test_generate_report_text(self, readiness):
        """Test report text generation."""
        report = ReadinessReport(
            branch="feature/test",
            target_branch="main",
            is_ready=True,
            checks=[
                CheckResult(name="check1", status=CheckStatus.PASSED, message="OK"),
                CheckResult(name="check2", status=CheckStatus.WARNING, message="Warning"),
            ],
            passed_count=1,
            warning_count=1,
        )

        text = readiness.generate_report_text(report)

        assert "MERGE READINESS REPORT" in text
        assert "feature/test" in text
        assert "YES" in text  # Ready to merge
        assert "check1" in text
        assert "check2" in text


# =============================================================================
# Merge Gate Tests
# =============================================================================

class TestMergeRequest:
    """Tests for MergeRequest dataclass."""

    def test_create_request(self):
        """Test creating a merge request."""
        request = MergeRequest(
            request_id="merge-0001",
            source_branch="feature/test",
            target_branch="main",
        )

        assert request.request_id == "merge-0001"
        assert request.status == MergeStatus.PENDING
        assert request.strategy == MergeStrategy.MERGE

    def test_request_with_options(self):
        """Test request with custom options."""
        request = MergeRequest(
            request_id="merge-0001",
            source_branch="feature/test",
            target_branch="main",
            strategy=MergeStrategy.SQUASH,
            commit_message="Squash merge",
            delete_source_branch=True,
        )

        assert request.strategy == MergeStrategy.SQUASH
        assert request.delete_source_branch is True


class TestMergeGate:
    """Tests for MergeGate class."""

    @pytest.fixture
    def mock_db(self):
        """Create a mock database."""
        return MagicMock()

    @pytest.fixture
    def mock_readiness(self):
        """Create a mock readiness checker."""
        readiness = AsyncMock(spec=MergeReadiness)
        readiness.check_readiness.return_value = ReadinessReport(
            branch="feature/test",
            target_branch="main",
            is_ready=True,
        )
        return readiness

    @pytest.fixture
    def gate(self, mock_db, mock_readiness):
        """Create merge gate."""
        return MergeGate(
            db=mock_db,
            readiness_checker=mock_readiness,
            config={
                "protected_branches": ["main", "production"],
                "run_tests": False,
            },
        )

    def test_protected_branches(self, gate):
        """Test protected branch configuration."""
        assert gate.is_protected("main") is True
        assert gate.is_protected("production") is True
        assert gate.is_protected("feature/test") is False

    def test_add_remove_protected_branch(self, gate):
        """Test adding and removing protected branches."""
        gate.add_protected_branch("develop")
        assert gate.is_protected("develop") is True

        gate.remove_protected_branch("develop")
        assert gate.is_protected("develop") is False

    def test_get_protected_branches(self, gate):
        """Test getting protected branches."""
        branches = gate.get_protected_branches()

        assert "main" in branches
        assert "production" in branches

    def test_generate_request_id(self, gate):
        """Test request ID generation."""
        id1 = gate._generate_request_id()
        id2 = gate._generate_request_id()

        assert id1 != id2
        assert id1.startswith("merge-")

    @pytest.mark.asyncio
    async def test_request_merge_ready(self, gate, mock_readiness):
        """Test merge request when ready."""
        with patch.object(gate, '_execute_merge_with_lock') as mock_execute:
            mock_execute.return_value = MergeResult(
                success=True,
                request=MergeRequest(
                    request_id="merge-0001",
                    source_branch="feature/test",
                    target_branch="main",
                ),
                merge_commit="abc123",
            )

            result = await gate.request_merge(
                source_branch="feature/test",
                target_branch="main",
            )

            assert result.success is True
            mock_readiness.check_readiness.assert_called_once()

    @pytest.mark.asyncio
    async def test_request_merge_not_ready(self, gate, mock_readiness):
        """Test merge request when not ready."""
        mock_readiness.check_readiness.return_value = ReadinessReport(
            branch="feature/test",
            target_branch="main",
            is_ready=False,
            blocking_issues=["Tests failed"],
        )

        result = await gate.request_merge(
            source_branch="feature/test",
            target_branch="main",
        )

        assert result.success is False
        assert result.error_type == "readiness_failed"

    @pytest.mark.asyncio
    async def test_request_merge_skip_readiness(self, gate, mock_readiness):
        """Test merge request skipping readiness."""
        with patch.object(gate, '_execute_merge_with_lock') as mock_execute:
            mock_execute.return_value = MergeResult(
                success=True,
                request=MagicMock(),
            )

            await gate.request_merge(
                source_branch="feature/test",
                target_branch="main",
                skip_readiness=True,
            )

            mock_readiness.check_readiness.assert_not_called()

    @pytest.mark.asyncio
    async def test_pre_merge_hook_blocks(self, gate, mock_readiness):
        """Test pre-merge hook can block merge."""
        async def blocking_hook(request):
            return False  # Block the merge

        gate.register_pre_merge_hook(blocking_hook)

        result = await gate.request_merge(
            source_branch="feature/test",
            target_branch="main",
        )

        assert result.success is False
        assert result.error_type == "hook_blocked"

    @pytest.mark.asyncio
    async def test_pre_merge_hook_allows(self, gate, mock_readiness):
        """Test pre-merge hook can allow merge."""
        async def allowing_hook(request):
            return True  # Allow the merge

        gate.register_pre_merge_hook(allowing_hook)

        with patch.object(gate, '_execute_merge_with_lock') as mock_execute:
            mock_execute.return_value = MergeResult(success=True, request=MagicMock())

            result = await gate.request_merge(
                source_branch="feature/test",
                target_branch="main",
            )

            assert result.success is True

    def test_cancel_pending_merge(self, gate):
        """Test cancelling pending merge."""
        # Add a pending request
        request = MergeRequest(
            request_id="merge-0001",
            source_branch="feature/test",
            target_branch="main",
            status=MergeStatus.PENDING,
        )
        gate._pending_requests["merge-0001"] = request

        success = gate.cancel_merge("merge-0001")

        assert success is True
        assert "merge-0001" not in gate._pending_requests
        assert request.status == MergeStatus.CANCELLED

    def test_cancel_nonexistent_merge(self, gate):
        """Test cancelling non-existent merge."""
        success = gate.cancel_merge("nonexistent")
        assert success is False

    def test_get_pending_requests(self, gate):
        """Test getting pending requests."""
        request = MergeRequest(
            request_id="merge-0001",
            source_branch="feature/test",
            target_branch="main",
        )
        gate._pending_requests["merge-0001"] = request

        pending = gate.get_pending_requests()

        assert len(pending) == 1
        assert pending[0].request_id == "merge-0001"

    def test_is_merge_in_progress(self, gate):
        """Test checking if merge is in progress."""
        assert gate.is_merge_in_progress() is False

        gate._current_merge = MagicMock()

        assert gate.is_merge_in_progress() is True

    @pytest.mark.asyncio
    async def test_check_branch_protection_push(self, gate):
        """Test branch protection for push."""
        allowed = await gate.check_branch_protection("main", "push")
        assert allowed is False

        allowed = await gate.check_branch_protection("feature/test", "push")
        assert allowed is True

    @pytest.mark.asyncio
    async def test_check_branch_protection_force_push(self, gate):
        """Test branch protection for force push."""
        allowed = await gate.check_branch_protection("main", "force-push")
        assert allowed is False

    @pytest.mark.asyncio
    async def test_check_branch_protection_delete(self, gate):
        """Test branch protection for delete."""
        allowed = await gate.check_branch_protection("main", "delete")
        assert allowed is False

    def test_get_gate_status(self, gate):
        """Test getting gate status."""
        status = gate.get_gate_status()

        assert "protected_branches" in status
        assert "main" in status["protected_branches"]
        assert status["merge_in_progress"] is False
        assert status["pending_requests"] == 0


class TestMergeStrategy:
    """Tests for MergeStrategy enum."""

    def test_strategy_values(self):
        """Test strategy values."""
        assert MergeStrategy.MERGE.value == "merge"
        assert MergeStrategy.SQUASH.value == "squash"
        assert MergeStrategy.REBASE.value == "rebase"
        assert MergeStrategy.FAST_FORWARD.value == "ff-only"
