"""
Tests for the interrupt module.

Tests cover:
- CLI interrupt handler
- Async interrupt handler
- Approval queue
- Integration with autonomy gate
"""

import asyncio
import io
import pytest
from datetime import datetime, timedelta
from unittest.mock import MagicMock, AsyncMock, patch

from agent_orchestrator.interrupt.cli_handler import (
    CLIInterruptHandler,
    CLIConfig,
    ApprovalDecision,
    ApprovalResponse,
    NonInteractiveHandler,
)

from agent_orchestrator.interrupt.async_handler import (
    AsyncInterruptHandler,
    WebhookConfig,
    NotificationChannel,
)

from agent_orchestrator.interrupt.approval_queue import (
    ApprovalQueue,
    QueueConfig,
    ApprovalPriority,
    HandlerMode,
)


# =============================================================================
# CLI Interrupt Handler Tests
# =============================================================================

class TestCLIInterruptHandler:
    """Tests for CLIInterruptHandler."""

    @pytest.fixture
    def mock_db(self):
        """Create a mock database."""
        db = MagicMock()
        db.generate_approval_id.return_value = "approval-123"
        db.get_pending_approvals.return_value = []
        return db

    @pytest.fixture
    def handler(self, mock_db):
        """Create a CLI handler with mock I/O."""
        input_stream = io.StringIO()
        output_stream = io.StringIO()

        config = CLIConfig(
            timeout_seconds=0,  # No timeout for tests
            use_colors=False,
            input_stream=input_stream,
            output_stream=output_stream,
        )

        handler = CLIInterruptHandler(db=mock_db, config=config)
        handler._input = input_stream
        handler._output = output_stream

        return handler

    @pytest.mark.asyncio
    async def test_approval_response_approve(self):
        """Test ApprovalResponse.approve factory."""
        response = ApprovalResponse.approve(reason="Test")
        assert response.approved is True
        assert response.decision == ApprovalDecision.APPROVED
        assert response.reason == "Test"

    @pytest.mark.asyncio
    async def test_approval_response_reject(self):
        """Test ApprovalResponse.reject factory."""
        response = ApprovalResponse.reject(reason="Test")
        assert response.approved is False
        assert response.decision == ApprovalDecision.REJECTED
        assert response.reason == "Test"

    @pytest.mark.asyncio
    async def test_approval_response_timeout(self):
        """Test ApprovalResponse.timeout factory."""
        response = ApprovalResponse.timeout()
        assert response.approved is False
        assert response.decision == ApprovalDecision.TIMEOUT
        assert response.decided_by == "system"

    @pytest.mark.asyncio
    async def test_cli_handler_approve(self, handler, mock_db):
        """Test CLI handler with 'y' input."""
        # Set up input
        handler._input = io.StringIO("y\n")

        response = await handler.request_approval(
            agent_id="test-agent",
            action_type="command",
            target="npm test",
            risk_level="medium",
        )

        assert response.approved is True
        assert response.decision == ApprovalDecision.APPROVED
        mock_db.create_approval.assert_called_once()
        mock_db.update_approval.assert_called_once()

    @pytest.mark.asyncio
    async def test_cli_handler_reject(self, handler, mock_db):
        """Test CLI handler with 'n' input."""
        handler._input = io.StringIO("n\n")

        response = await handler.request_approval(
            agent_id="test-agent",
            action_type="command",
            target="npm test",
            risk_level="medium",
        )

        assert response.approved is False
        assert response.decision == ApprovalDecision.REJECTED

    @pytest.mark.asyncio
    async def test_cli_handler_skip(self, handler, mock_db):
        """Test CLI handler with 's' input."""
        handler._input = io.StringIO("s\n")

        response = await handler.request_approval(
            agent_id="test-agent",
            action_type="command",
            target="npm test",
            risk_level="medium",
        )

        assert response.approved is False
        assert response.decision == ApprovalDecision.SKIPPED

    @pytest.mark.asyncio
    async def test_cli_handler_yes_variations(self, handler):
        """Test various 'yes' inputs."""
        for input_val in ["y", "yes", "approve", "Y", "YES"]:
            handler._input = io.StringIO(f"{input_val}\n")
            response = await handler._get_user_input()
            assert response.approved is True

    @pytest.mark.asyncio
    async def test_cli_handler_no_variations(self, handler):
        """Test various 'no' inputs."""
        for input_val in ["n", "no", "reject", "N", "NO"]:
            handler._input = io.StringIO(f"{input_val}\n")
            response = await handler._get_user_input()
            assert response.approved is False

    def test_display_prompt_contains_info(self, handler):
        """Test that prompt displays all required info."""
        handler._display_prompt(
            agent_id="test-agent",
            action_type="command",
            target="git push",
            risk_level="high",
            context={"reason": "Test reason"},
            diff=None,
        )

        output = handler._output.getvalue()
        assert "APPROVAL REQUIRED" in output
        assert "test-agent" in output
        assert "command" in output
        assert "git push" in output
        assert "HIGH" in output

    def test_display_prompt_with_diff(self, handler):
        """Test that diff is displayed when provided."""
        diff = "+new line\n-old line"

        handler._display_prompt(
            agent_id="test-agent",
            action_type="file_edit",
            target="test.py",
            risk_level="medium",
            context={},
            diff=diff,
        )

        output = handler._output.getvalue()
        assert "+new line" in output
        assert "-old line" in output


class TestNonInteractiveHandler:
    """Tests for NonInteractiveHandler."""

    @pytest.fixture
    def mock_db(self):
        db = MagicMock()
        db.generate_approval_id.return_value = "approval-123"
        return db

    @pytest.mark.asyncio
    async def test_auto_rejects(self, mock_db):
        """Test that non-interactive handler auto-rejects."""
        handler = NonInteractiveHandler(mock_db)

        response = await handler.request_approval(
            agent_id="test-agent",
            action_type="command",
            target="git push",
            risk_level="high",
        )

        assert response.approved is False
        assert response.decided_by == "system"
        assert "non-interactive" in response.reason.lower()


# =============================================================================
# Async Interrupt Handler Tests
# =============================================================================

class TestAsyncInterruptHandler:
    """Tests for AsyncInterruptHandler."""

    @pytest.fixture
    def mock_db(self):
        db = MagicMock()
        db.generate_approval_id.return_value = "approval-123"
        db.get_pending_approvals.return_value = []
        return db

    @pytest.fixture
    def handler(self, mock_db):
        """Create async handler with console notification."""
        config = WebhookConfig(
            channel=NotificationChannel.CONSOLE,
            timeout_seconds=1,  # Short timeout for tests
            poll_interval_seconds=0.1,
        )
        return AsyncInterruptHandler(db=mock_db, config=config)

    @pytest.mark.asyncio
    async def test_request_approval_timeout(self, handler):
        """Test that approval request times out."""
        response = await handler.request_approval(
            agent_id="test-agent",
            action_type="command",
            target="git push",
            risk_level="high",
        )

        assert response.approved is False
        assert response.decision == ApprovalDecision.TIMEOUT

    @pytest.mark.asyncio
    async def test_handle_callback_approve(self, handler, mock_db):
        """Test handling approval callback."""
        # Create a pending approval first
        handler._pending["test-approval"] = MagicMock()
        handler._pending["test-approval"].approval_id = "test-approval"

        success = await handler.handle_callback(
            approval_id="test-approval",
            decision="approve",
            decided_by="user@test.com",
            notes="Looks good",
        )

        assert success is True

        # Check response was queued
        assert not handler._response_queue.empty()

    @pytest.mark.asyncio
    async def test_handle_callback_reject(self, handler, mock_db):
        """Test handling rejection callback."""
        handler._pending["test-approval"] = MagicMock()

        success = await handler.handle_callback(
            approval_id="test-approval",
            decision="reject",
            decided_by="user@test.com",
            notes="Not approved",
        )

        assert success is True

    @pytest.mark.asyncio
    async def test_handle_callback_unknown_approval(self, handler):
        """Test handling callback for unknown approval."""
        success = await handler.handle_callback(
            approval_id="unknown-approval",
            decision="approve",
            decided_by="user@test.com",
        )

        assert success is False

    def test_build_slack_blocks(self, handler):
        """Test Slack block building."""
        from agent_orchestrator.interrupt.async_handler import PendingApproval

        pending = PendingApproval(
            approval_id="test-123",
            agent_id="claude-code",
            action_type="command",
            target="git push origin main",
            risk_level="high",
            created_at=datetime.now(),
            expires_at=datetime.now() + timedelta(minutes=5),
        )

        blocks = handler._build_slack_blocks(pending)

        assert len(blocks) > 0
        assert blocks[0]["type"] == "header"

    def test_verify_callback_signature(self, handler):
        """Test callback signature verification."""
        handler.config.callback_secret = "test-secret"

        payload = '{"test": "data"}'
        signature = handler._sign_payload(payload)

        assert handler.verify_callback_signature(payload, signature) is True
        assert handler.verify_callback_signature(payload, "invalid") is False

    def test_verify_callback_signature_no_secret(self, handler):
        """Test that signature verification passes without secret."""
        handler.config.callback_secret = None

        assert handler.verify_callback_signature("any", "any") is True


# =============================================================================
# Approval Queue Tests
# =============================================================================

class TestApprovalQueue:
    """Tests for ApprovalQueue."""

    @pytest.fixture
    def mock_db(self):
        db = MagicMock()
        db.generate_approval_id.return_value = "approval-123"
        db.get_pending_approvals.return_value = []
        return db

    @pytest.fixture
    def mock_cli_handler(self):
        """Create mock CLI handler that auto-approves."""
        handler = AsyncMock()
        handler.request_approval.return_value = ApprovalResponse.approve(
            reason="Auto-approved"
        )
        return handler

    @pytest.fixture
    def queue(self, mock_db, mock_cli_handler):
        """Create approval queue."""
        config = QueueConfig(
            default_timeout_seconds=1,
            handler_mode=HandlerMode.CLI,
        )
        return ApprovalQueue(
            db=mock_db,
            cli_handler=mock_cli_handler,
            config=config,
        )

    @pytest.mark.asyncio
    async def test_submit_approval(self, queue, mock_cli_handler):
        """Test submitting approval request."""
        await queue.start()

        response = await queue.submit(
            agent_id="test-agent",
            action_type="command",
            target="npm test",
            risk_level="medium",
        )

        assert response.approved is True
        mock_cli_handler.request_approval.assert_called_once()

        await queue.stop()

    @pytest.mark.asyncio
    async def test_queue_stats_tracking(self, queue, mock_cli_handler):
        """Test that queue tracks statistics."""
        await queue.start()

        await queue.submit(
            agent_id="test-agent",
            action_type="command",
            target="npm test",
            risk_level="medium",
        )

        stats = queue.get_stats()
        assert stats.total_submitted == 1
        assert stats.total_approved == 1

        await queue.stop()

    @pytest.mark.asyncio
    async def test_queue_overflow_rejection(self, mock_db, mock_cli_handler):
        """Test that queue rejects when full."""
        config = QueueConfig(
            max_queue_size=1,
            handler_mode=HandlerMode.CLI,
        )
        queue = ApprovalQueue(
            db=mock_db,
            cli_handler=mock_cli_handler,
            config=config,
        )

        # Don't call handler so request stays pending
        async def slow_handler(*args, **kwargs):
            await asyncio.sleep(10)  # Block forever
        mock_cli_handler.request_approval = AsyncMock(side_effect=slow_handler)

        await queue.start()

        # First request should succeed (but block)
        task1 = asyncio.create_task(queue.submit(
            agent_id="test-agent",
            action_type="command",
            target="first",
            risk_level="medium",
        ))

        await asyncio.sleep(0.1)  # Let first request start

        # Second request should fail due to capacity
        response = await queue.submit(
            agent_id="test-agent",
            action_type="command",
            target="second",
            risk_level="medium",
        )

        assert response.approved is False
        assert "queue full" in response.reason.lower()

        task1.cancel()
        await queue.stop()

    def test_risk_to_priority(self, queue):
        """Test risk level to priority conversion."""
        assert queue._risk_to_priority("critical") == ApprovalPriority.CRITICAL
        assert queue._risk_to_priority("high") == ApprovalPriority.HIGH
        assert queue._risk_to_priority("medium") == ApprovalPriority.MEDIUM
        assert queue._risk_to_priority("low") == ApprovalPriority.LOW

    @pytest.mark.asyncio
    async def test_cancel_pending(self, queue, mock_cli_handler):
        """Test cancelling pending approval."""
        # Block the handler
        async def slow_handler(*args, **kwargs):
            await asyncio.sleep(10)  # Block forever
        mock_cli_handler.request_approval = AsyncMock(side_effect=slow_handler)

        await queue.start()

        # Submit (will block)
        task = asyncio.create_task(queue.submit(
            agent_id="test-agent",
            action_type="command",
            target="test",
            risk_level="medium",
        ))

        await asyncio.sleep(0.1)  # Let it start

        # Get the approval ID
        pending = queue.get_pending_approvals()
        assert len(pending) == 1

        # Cancel it
        cancelled = queue.cancel(pending[0]["approval_id"])
        assert cancelled is True

        # Wait for task to complete
        try:
            response = await asyncio.wait_for(task, timeout=1.0)
            assert response.approved is False
        except asyncio.TimeoutError:
            task.cancel()

        await queue.stop()

    @pytest.mark.asyncio
    async def test_callbacks(self, queue, mock_cli_handler):
        """Test callback registration and triggering."""
        on_approval_called = False

        async def on_approval(response):
            nonlocal on_approval_called
            on_approval_called = True

        queue.on_approval(on_approval)

        await queue.start()

        await queue.submit(
            agent_id="test-agent",
            action_type="command",
            target="test",
            risk_level="medium",
        )

        await asyncio.sleep(0.1)  # Let callback fire

        assert on_approval_called is True

        await queue.stop()


# =============================================================================
# Integration Tests
# =============================================================================

class TestIntegration:
    """Integration tests for interrupt handlers."""

    @pytest.fixture
    def mock_db(self):
        db = MagicMock()
        db.generate_approval_id.return_value = "approval-123"
        db.get_pending_approvals.return_value = []
        return db

    @pytest.mark.asyncio
    async def test_cli_to_async_fallback(self, mock_db):
        """Test fallback from CLI to async handler."""
        cli_handler = None  # Not configured
        async_handler = AsyncMock()
        async_handler.request_approval.return_value = ApprovalResponse.approve()

        config = QueueConfig(
            handler_mode=HandlerMode.AUTO,
        )

        queue = ApprovalQueue(
            db=mock_db,
            cli_handler=cli_handler,
            async_handler=async_handler,
            config=config,
        )

        await queue.start()

        # Should use async handler since CLI not available
        response = await queue.submit(
            agent_id="test-agent",
            action_type="command",
            target="test",
            risk_level="medium",
        )

        async_handler.request_approval.assert_called_once()

        await queue.stop()

    @pytest.mark.asyncio
    async def test_integrated_autonomy_gate(self, mock_db):
        """Test IntegratedAutonomyGate with interrupt handler."""
        from agent_orchestrator.control.autonomy_gate import IntegratedAutonomyGate

        mock_handler = AsyncMock()
        mock_handler.request_approval.return_value = ApprovalResponse.approve(
            reason="Approved by test"
        )

        gate = IntegratedAutonomyGate(
            db=mock_db,
            interrupt_handler=mock_handler,
        )

        # High-risk action should trigger approval
        decision = await gate.evaluate_and_wait(
            action_type="file_write",
            target=".env",  # High risk file
            agent_id="test-agent",
        )

        # Should be approved after handler returns approval
        assert decision.allowed is True
        mock_handler.request_approval.assert_called_once()

    @pytest.mark.asyncio
    async def test_integrated_autonomy_gate_rejection(self, mock_db):
        """Test IntegratedAutonomyGate with rejection."""
        from agent_orchestrator.control.autonomy_gate import IntegratedAutonomyGate

        mock_handler = AsyncMock()
        mock_handler.request_approval.return_value = ApprovalResponse.reject(
            reason="Not approved"
        )

        gate = IntegratedAutonomyGate(
            db=mock_db,
            interrupt_handler=mock_handler,
        )

        decision = await gate.evaluate_and_wait(
            action_type="file_write",
            target=".env",
            agent_id="test-agent",
        )

        assert decision.allowed is False
        assert decision.rejected is True
