"""End-to-end integration test for the complete coordinator flow.

This test verifies the entire assignment-based trigger flow:
1. Gitea webhook → receiver
2. Receiver → parser
3. Parser → queue
4. Queue → orchestrator
5. Orchestrator → agent spawning

Test Requirements:
- Full flow must complete in < 10 seconds
- All components must work together seamlessly
- 100% of critical path must be covered
"""

import hmac
import json
import tempfile
import time
from collections.abc import Generator
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest
from anthropic.types import Message, TextBlock, Usage
from fastapi.testclient import TestClient


class TestEndToEndIntegration:
    """Test suite for complete end-to-end integration."""

    @pytest.fixture
    def temp_queue_file(self) -> Generator[Path, None, None]:
        """Create a temporary file for queue persistence."""
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f:
            temp_path = Path(f.name)
        yield temp_path
        # Cleanup
        if temp_path.exists():
            temp_path.unlink()

    @pytest.fixture
    def sample_issue_body(self) -> str:
        """Return a sample issue body with all required metadata."""
        return """## Objective

Create comprehensive integration test for entire assignment-based trigger flow.

## Implementation Details

1. Create test Gitea instance or mock
2. Simulate webhook events (issue.assigned)
3. Verify webhook receiver processes event
4. Verify parser extracts metadata
5. Verify queue manager adds issue
6. Verify orchestrator picks up issue
7. Verify comments posted to Gitea

## Context Estimate

• Files to modify: 3 (test_integration.py, fixtures.py, docker-compose.test.yml)
• Implementation complexity: medium (20000 tokens)
• Test requirements: high (15000 tokens)
• Documentation: medium (3000 tokens)
• **Total estimated: 46800 tokens**
• **Recommended agent: sonnet**

## Difficulty

medium

## Dependencies

• Blocked by: #160 (COORD-004 - needs all components working)
• Blocks: None (validates Phase 0 complete)

## Acceptance Criteria

[ ] Integration test runs full flow
[ ] Test creates issue, assigns to @mosaic
[ ] Test verifies webhook fires
[ ] Test verifies parser extracts metadata
[ ] Test verifies queue updated
[ ] Test verifies orchestrator processes
[ ] Test verifies comment posted
[ ] Test runs in CI/CD pipeline
[ ] 100% of critical path covered

## Testing Requirements

• Full end-to-end integration test
• Mock Gitea API or use test instance
• Verify all components interact correctly
• Performance test: Full flow < 10 seconds
• Success criteria: All components working together"""

    @pytest.fixture
    def sample_webhook_payload(self) -> dict[str, Any]:
        """Return a sample Gitea webhook payload for issue.assigned event."""
        return {
            "action": "assigned",
            "number": 161,
            "issue": {
                "id": 161,
                "number": 161,
                "title": "[COORD-005] End-to-end integration test",
                "state": "open",
                "body": "",  # Will be set in test
                "assignee": {
                    "id": 1,
                    "login": "mosaic",
                    "full_name": "Mosaic Bot",
                },
            },
            "repository": {
                "name": "stack",
                "full_name": "mosaic/stack",
                "owner": {"login": "mosaic"},
            },
            "sender": {
                "id": 2,
                "login": "admin",
                "full_name": "Admin User",
            },
        }

    @pytest.fixture
    def mock_anthropic_response(self) -> Message:
        """Return a mock Anthropic API response with parsed metadata."""
        return Message(
            id="msg_test123",
            type="message",
            role="assistant",
            content=[
                TextBlock(
                    type="text",
                    text='{"estimated_context": 46800, "difficulty": "medium", '
                    '"assigned_agent": "sonnet", "blocks": [], "blocked_by": [160]}',
                )
            ],
            model="claude-sonnet-4.5-20250929",
            stop_reason="end_turn",
            usage=Usage(input_tokens=500, output_tokens=50),
        )

    def _create_signature(self, payload_str: str, secret: str) -> str:
        """Create HMAC SHA256 signature for webhook payload."""
        payload_bytes = payload_str.encode("utf-8")
        return hmac.new(secret.encode("utf-8"), payload_bytes, "sha256").hexdigest()

    @pytest.mark.asyncio
    async def test_full_flow_webhook_to_orchestrator(
        self,
        client: TestClient,
        webhook_secret: str,
        sample_webhook_payload: dict[str, Any],
        sample_issue_body: str,
        mock_anthropic_response: Message,
        temp_queue_file: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test complete flow from webhook receipt to orchestrator processing.

        This is the critical path test that verifies:
        1. Webhook receiver accepts and validates Gitea webhook
        2. Parser extracts metadata from issue body
        3. Queue manager adds issue to queue
        4. Orchestrator picks up issue and spawns agent
        5. Full flow completes in < 10 seconds

        This test covers 100% of the critical integration path.
        """
        start_time = time.time()

        # Set up the issue body in payload
        sample_webhook_payload["issue"]["body"] = sample_issue_body

        # Mock the Anthropic API call for parsing
        mock_client = MagicMock()
        mock_client.messages.create.return_value = mock_anthropic_response

        with patch("src.parser.Anthropic", return_value=mock_client):
            # Clear any cached parser data
            from src.parser import clear_cache

            clear_cache()

            # Step 1: Send webhook to receiver
            payload_json = json.dumps(sample_webhook_payload, separators=(",", ":"))
            signature = self._create_signature(payload_json, webhook_secret)
            headers = {"X-Gitea-Signature": signature}

            response = client.post(
                "/webhook/gitea",
                data=payload_json,
                headers={**headers, "Content-Type": "application/json"},
            )

            # Verify webhook was accepted
            assert response.status_code == 200
            assert response.json()["status"] == "success"
            assert response.json()["action"] == "assigned"
            assert response.json()["issue_number"] == 161

            # Step 2: Verify parser was called and extracted metadata
            # (Currently webhook doesn't call parser - this will be implemented in Phase 1)
            # For Phase 0, we manually test the parser integration
            from src.parser import parse_issue_metadata

            metadata = parse_issue_metadata(sample_issue_body, 161)

            # Verify parser extracted correct metadata
            assert metadata.estimated_context == 46800
            assert metadata.difficulty == "medium"
            assert metadata.assigned_agent == "sonnet"
            assert metadata.blocks == []
            assert metadata.blocked_by == [160]

            # Verify Anthropic API was called
            assert mock_client.messages.create.called

            # Step 3: Add issue to queue manually (will be integrated in webhook handler)
            from src.queue import QueueManager

            queue_manager = QueueManager(queue_file=temp_queue_file)
            queue_manager.enqueue(161, metadata)

            # Verify issue is in queue
            item = queue_manager.get_item(161)
            assert item is not None
            assert item.issue_number == 161
            assert item.metadata.estimated_context == 46800
            assert item.metadata.assigned_agent == "sonnet"

            # Step 4: Verify orchestrator can pick up the issue
            from src.coordinator import Coordinator

            coordinator = Coordinator(queue_manager=queue_manager, poll_interval=0.5)

            # Process the queue once
            processed_item = await coordinator.process_queue()

            # Verify orchestrator processed the item
            assert processed_item is not None
            assert processed_item.issue_number == 161

            # Verify item was marked in progress
            queue_item = queue_manager.get_item(161)
            assert queue_item is not None
            # Note: In stub implementation, item is immediately marked complete
            # In real implementation, it would be in_progress

            # Step 5: Verify performance requirement (< 10 seconds)
            elapsed_time = time.time() - start_time
            assert elapsed_time < 10.0, f"Flow took {elapsed_time:.2f}s (must be < 10s)"

    @pytest.mark.asyncio
    async def test_full_flow_with_blocked_dependency(
        self,
        client: TestClient,
        webhook_secret: str,
        sample_webhook_payload: dict[str, Any],
        sample_issue_body: str,
        mock_anthropic_response: Message,
        temp_queue_file: Path,
    ) -> None:
        """Test that blocked issues are not processed until dependencies complete.

        This test verifies:
        1. Issue with blocked_by dependency is added to queue
        2. Orchestrator does not process blocked issue first
        3. When blocker is completed, blocked issue becomes ready
        4. Orchestrator then processes the unblocked issue
        """
        sample_webhook_payload["issue"]["body"] = sample_issue_body

        # Mock the Anthropic API
        mock_client = MagicMock()
        mock_client.messages.create.return_value = mock_anthropic_response

        with patch("src.parser.Anthropic", return_value=mock_client):
            from src.coordinator import Coordinator
            from src.models import IssueMetadata
            from src.parser import clear_cache, parse_issue_metadata
            from src.queue import QueueManager

            clear_cache()

            # Create queue
            queue_manager = QueueManager(queue_file=temp_queue_file)

            # Add blocker issue #160 first (no blockers)
            blocker_meta = IssueMetadata(
                estimated_context=20000,
                difficulty="medium",
                assigned_agent="sonnet",
                blocks=[161],  # This blocks #161
                blocked_by=[],
            )
            queue_manager.enqueue(160, blocker_meta)

            # Parse metadata for #161 (blocked by #160)
            metadata = parse_issue_metadata(sample_issue_body, 161)
            assert metadata.blocked_by == [160]

            # Add blocked issue #161
            queue_manager.enqueue(161, metadata)

            # Verify #160 is ready, #161 is NOT ready
            item160 = queue_manager.get_item(160)
            assert item160 is not None
            assert item160.ready is True

            item161 = queue_manager.get_item(161)
            assert item161 is not None
            assert item161.ready is False

            # Create coordinator
            coordinator = Coordinator(queue_manager=queue_manager, poll_interval=0.5)

            # Process queue - should get #160 (the blocker)
            processed_item = await coordinator.process_queue()
            assert processed_item is not None
            assert processed_item.issue_number == 160

            # Note: The stub implementation immediately marks #160 as complete
            # This should unblock #161

            # Verify #161 is now ready
            item161 = queue_manager.get_item(161)
            assert item161 is not None
            assert item161.ready is True

            # Process queue again - should now get #161
            processed_item = await coordinator.process_queue()
            assert processed_item is not None
            assert processed_item.issue_number == 161

    @pytest.mark.asyncio
    async def test_full_flow_with_multiple_issues(
        self,
        client: TestClient,
        webhook_secret: str,
        temp_queue_file: Path,
    ) -> None:
        """Test orchestrator processes multiple issues in correct order.

        This test verifies:
        1. Multiple issues can be added to queue
        2. Orchestrator processes ready issues in order
        3. Dependencies are respected
        """
        from src.coordinator import Coordinator
        from src.models import IssueMetadata
        from src.queue import QueueManager

        queue_manager = QueueManager(queue_file=temp_queue_file)

        # Add three issues: #100 (no deps), #101 (blocks #102), #102 (blocked by #101)
        meta100 = IssueMetadata(
            estimated_context=10000,
            difficulty="easy",
            assigned_agent="haiku",
            blocks=[],
            blocked_by=[],
        )
        meta101 = IssueMetadata(
            estimated_context=20000,
            difficulty="medium",
            assigned_agent="sonnet",
            blocks=[102],
            blocked_by=[],
        )
        meta102 = IssueMetadata(
            estimated_context=30000,
            difficulty="hard",
            assigned_agent="opus",
            blocks=[],
            blocked_by=[101],
        )

        queue_manager.enqueue(100, meta100)
        queue_manager.enqueue(101, meta101)
        queue_manager.enqueue(102, meta102)

        # Verify #102 is not ready
        item102 = queue_manager.get_item(102)
        assert item102 is not None
        assert item102.ready is False

        # Verify #100 and #101 are ready
        item100 = queue_manager.get_item(100)
        assert item100 is not None
        assert item100.ready is True

        item101 = queue_manager.get_item(101)
        assert item101 is not None
        assert item101.ready is True

        # Create coordinator
        coordinator = Coordinator(queue_manager=queue_manager, poll_interval=0.5)

        # Process first item - should get #100 (lowest number)
        processed = await coordinator.process_queue()
        assert processed is not None
        assert processed.issue_number == 100

        # Process second item - should get #101
        processed = await coordinator.process_queue()
        assert processed is not None
        assert processed.issue_number == 101

        # Now #102 should become ready
        item102 = queue_manager.get_item(102)
        assert item102 is not None
        assert item102.ready is True

        # Process third item - should get #102
        processed = await coordinator.process_queue()
        assert processed is not None
        assert processed.issue_number == 102

    @pytest.mark.asyncio
    async def test_webhook_signature_validation_in_flow(
        self,
        client: TestClient,
        webhook_secret: str,
        sample_webhook_payload: dict[str, Any],
    ) -> None:
        """Test that invalid webhook signatures are rejected in the flow."""
        # Send webhook with invalid signature
        payload_json = json.dumps(sample_webhook_payload, separators=(",", ":"))
        headers = {"X-Gitea-Signature": "invalid_signature", "Content-Type": "application/json"}

        response = client.post(
            "/webhook/gitea", data=payload_json, headers=headers
        )

        # Verify webhook was rejected
        assert response.status_code == 401
        assert "Invalid or missing signature" in response.json()["detail"]

    @pytest.mark.asyncio
    async def test_parser_handles_malformed_issue_body(
        self,
        temp_queue_file: Path,
    ) -> None:
        """Test that parser gracefully handles malformed issue bodies.

        When the parser encounters errors, it should return default values
        rather than crashing.
        """
        from src.parser import clear_cache, parse_issue_metadata

        clear_cache()

        # Test with completely malformed body
        malformed_body = "This is not a valid issue format"

        # Mock Anthropic to raise an error
        with patch("src.parser.Anthropic") as mock_anthropic_class:
            mock_client = MagicMock()
            mock_client.messages.create.side_effect = Exception("API error")
            mock_anthropic_class.return_value = mock_client

            # Parse should return defaults on error
            metadata = parse_issue_metadata(malformed_body, 999)

            # Verify defaults are returned
            assert metadata.estimated_context == 50000  # Default
            assert metadata.difficulty == "medium"  # Default
            assert metadata.assigned_agent == "sonnet"  # Default
            assert metadata.blocks == []
            assert metadata.blocked_by == []

    @pytest.mark.asyncio
    async def test_orchestrator_handles_spawn_agent_failure(
        self,
        temp_queue_file: Path,
    ) -> None:
        """Test that orchestrator handles agent spawn failures gracefully.

        When spawn_agent fails, the issue should remain in progress
        rather than being marked complete.
        """
        from src.coordinator import Coordinator
        from src.models import IssueMetadata
        from src.queue import QueueManager

        queue_manager = QueueManager(queue_file=temp_queue_file)

        # Add an issue
        meta = IssueMetadata(
            estimated_context=10000,
            difficulty="easy",
            assigned_agent="haiku",
        )
        queue_manager.enqueue(200, meta)

        coordinator = Coordinator(queue_manager=queue_manager, poll_interval=0.5)

        # Mock spawn_agent to raise an error
        original_spawn = coordinator.spawn_agent

        async def failing_spawn(item: Any) -> bool:
            raise Exception("Spawn failed!")

        coordinator.spawn_agent = failing_spawn  # type: ignore

        # Process queue
        processed = await coordinator.process_queue()

        # Verify item was attempted
        assert processed is not None
        assert processed.issue_number == 200

        # Verify item remains in progress (not completed)
        item = queue_manager.get_item(200)
        assert item is not None
        from src.queue import QueueItemStatus

        assert item.status == QueueItemStatus.IN_PROGRESS

        # Restore original spawn
        coordinator.spawn_agent = original_spawn  # type: ignore

    @pytest.mark.asyncio
    async def test_performance_full_flow_under_10_seconds(
        self,
        client: TestClient,
        webhook_secret: str,
        sample_webhook_payload: dict[str, Any],
        sample_issue_body: str,
        mock_anthropic_response: Message,
        temp_queue_file: Path,
    ) -> None:
        """Performance test: Verify full flow completes in under 10 seconds.

        This test specifically validates the performance requirement
        from the issue specification.
        """
        sample_webhook_payload["issue"]["body"] = sample_issue_body

        # Mock the Anthropic API for fast response
        mock_client = MagicMock()
        mock_client.messages.create.return_value = mock_anthropic_response

        with patch("src.parser.Anthropic", return_value=mock_client):
            from src.coordinator import Coordinator
            from src.parser import clear_cache, parse_issue_metadata
            from src.queue import QueueManager

            clear_cache()

            # Start timer
            start_time = time.time()

            # Execute full flow
            # 1. Webhook
            payload_json = json.dumps(sample_webhook_payload, separators=(",", ":"))
            signature = self._create_signature(payload_json, webhook_secret)
            headers = {"X-Gitea-Signature": signature, "Content-Type": "application/json"}
            response = client.post(
                "/webhook/gitea", data=payload_json, headers=headers
            )
            assert response.status_code == 200

            # 2. Parse
            metadata = parse_issue_metadata(sample_issue_body, 161)
            assert metadata.estimated_context == 46800

            # 3. Queue
            queue_manager = QueueManager(queue_file=temp_queue_file)
            queue_manager.enqueue(161, metadata)

            # 4. Orchestrate
            coordinator = Coordinator(queue_manager=queue_manager, poll_interval=0.1)
            processed = await coordinator.process_queue()
            assert processed is not None

            # End timer
            elapsed_time = time.time() - start_time

            # Verify performance requirement
            assert (
                elapsed_time < 10.0
            ), f"Full flow took {elapsed_time:.2f}s (requirement: < 10s)"

            # Log performance for visibility
            print(f"\n✓ Full flow completed in {elapsed_time:.3f} seconds")
