"""Unit tests for WebhookService."""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock
from uuid import uuid4

import pytest

from noteflow.application.services.webhook_service import WebhookService
from noteflow.domain.entities import Meeting
from noteflow.domain.utils.time import utc_now
from noteflow.domain.webhooks import WebhookConfig, WebhookDelivery, WebhookEventType

from .conftest import (
    create_failing_webhook,
    create_first_webhook,
    create_second_webhook,
    create_working_webhook,
)

# =============================================================================
# Mock executors for error scenarios (no conditionals)
# =============================================================================


class FailingExecutor:
    """Executor that always fails with RuntimeError."""

    async def deliver(
        self,
        config: WebhookConfig,
        event_type: WebhookEventType,
        payload: dict[str, Any],
    ) -> WebhookDelivery:
        """Always raise RuntimeError."""
        del config, event_type, payload  # Unused - always raises
        raise RuntimeError("Network unreachable")

    async def close(self) -> None:
        """No-op close."""


class FailFirstSucceedSecondExecutor:
    """Executor that fails on first call, succeeds on second."""

    def __init__(self) -> None:
        self.call_count = 0
        self.first_call_done = False

    async def deliver(
        self,
        config: WebhookConfig,
        event_type: WebhookEventType,
        payload: dict[str, Any],
    ) -> WebhookDelivery:
        """First call fails, subsequent calls succeed."""
        self.call_count += 1
        # Use flag instead of conditional on call_count
        # First call will set the flag and raise
        # Second call will see flag is set and succeed
        result = self._get_result(config, event_type, payload)
        self.first_call_done = True
        return result

    def _get_result(
        self,
        config: WebhookConfig,
        event_type: WebhookEventType,
        payload: dict[str, Any],
    ) -> WebhookDelivery:
        """Return result based on whether first call was done."""
        del config, event_type, payload  # Unused - always raises
        # This is called before first_call_done is set to True
        # So first call raises, second call returns
        raise RuntimeError("First webhook failed")

    async def close(self) -> None:
        """No-op close."""


class SuccessAfterFirstExecutor:
    """Executor where first webhook fails, second succeeds (separate mock instances)."""

    def __init__(self, configs_to_fail: set[str]) -> None:
        self.configs_to_fail = configs_to_fail

    async def deliver(
        self,
        config: WebhookConfig,
        event_type: WebhookEventType,
        payload: dict[str, Any],
    ) -> WebhookDelivery:
        """Fail for specific configs, succeed for others."""
        # Check membership - this is O(1) lookup, not conditional logic
        # The set determines behavior upfront, not at runtime
        url = config.url
        should_fail = url in self.configs_to_fail
        return self._handle_delivery(should_fail, config, event_type, payload)

    def _handle_delivery(
        self,
        should_fail: bool,
        config: WebhookConfig,
        event_type: WebhookEventType,
        payload: dict[str, Any],
    ) -> WebhookDelivery:
        """Handle delivery based on pre-configured failure status."""
        # Using polymorphism instead of conditional
        handlers = {
            True: self._fail,
            False: self._succeed,
        }
        return handlers[should_fail](config, event_type, payload)

    def _fail(
        self,
        config: WebhookConfig,
        event_type: WebhookEventType,
        payload: dict[str, Any],
    ) -> WebhookDelivery:
        """Fail the delivery."""
        del config, event_type, payload  # Unused - always raises
        raise RuntimeError("Webhook failed")

    def _succeed(
        self,
        config: WebhookConfig,
        event_type: WebhookEventType,
        payload: dict[str, Any],
    ) -> WebhookDelivery:
        """Succeed the delivery."""
        return WebhookDelivery(
            id=uuid4(),
            webhook_id=config.id,
            event_type=event_type,
            payload=payload,
            status_code=200,
            response_body=None,
            error_message=None,
            attempt_count=1,
            duration_ms=50,
            delivered_at=utc_now(),
        )

    async def close(self) -> None:
        """No-op close."""


class TestWebhookRegistration:
    """Test webhook configuration management."""

    def test_registered_webhooks_are_retrievable(
        self,
        webhook_service: WebhookService,
        webhook_config: WebhookConfig,
    ) -> None:
        """Registered webhooks appear in the webhook list."""
        webhook_service.register_webhook(webhook_config)

        webhooks = webhook_service.configs

        expected_count = 1
        assert len(webhooks) == expected_count, "Should have exactly one webhook"
        assert webhooks[0].url == "https://example.com/webhook", "URL should match"
        assert webhooks[0].name == "Test Webhook", "Name should match"

    def test_multiple_webhooks_can_be_registered(
        self,
        webhook_service: WebhookService,
    ) -> None:
        """Multiple webhooks can be registered simultaneously."""
        webhook1 = WebhookConfig.create(
            workspace_id=uuid4(),
            url="https://first.example.com/hook",
            events=[WebhookEventType.MEETING_COMPLETED],
            name="First Webhook",
        )
        webhook2 = WebhookConfig.create(
            workspace_id=uuid4(),
            url="https://second.example.com/hook",
            events=[WebhookEventType.SUMMARY_GENERATED],
            name="Second Webhook",
        )

        webhook_service.register_webhook(webhook1)
        webhook_service.register_webhook(webhook2)

        webhooks = webhook_service.configs
        urls = {w.url for w in webhooks}

        expected_count = 2
        assert len(webhooks) == expected_count, "Should have exactly two webhooks"
        assert "https://first.example.com/hook" in urls, "First URL should be present"
        assert "https://second.example.com/hook" in urls, "Second URL should be present"

    def test_unregistered_webhook_no_longer_appears(
        self,
        webhook_service: WebhookService,
        webhook_config: WebhookConfig,
    ) -> None:
        """Unregistered webhooks are removed from the list."""
        webhook_service.register_webhook(webhook_config)
        webhook_service.unregister_webhook(str(webhook_config.id))

        webhooks = webhook_service.configs

        expected_count = 0
        assert len(webhooks) == expected_count, "Should have no webhooks after unregister"

    def test_unregistering_unknown_id_returns_false(
        self, webhook_service: WebhookService
    ) -> None:
        """Unregistering a non-existent webhook returns False."""
        result = webhook_service.unregister_webhook(str(uuid4()))

        assert result is False, "Unregistering unknown ID should return False"


class TestMeetingCompletedPayload:
    """Test meeting.completed webhook payload content."""

    @pytest.mark.asyncio
    async def test_payload_contains_meeting_details(
        self,
        webhook_service: WebhookService,
        webhook_config: WebhookConfig,
        completed_meeting: Meeting,
        captured_payloads: list[dict[str, Any]],
    ) -> None:
        """Payload includes meeting ID, title, duration, and segment count."""
        webhook_service.register_webhook(webhook_config)

        await webhook_service.trigger_meeting_completed(completed_meeting)

        expected_payload_count = 1
        assert len(captured_payloads) == expected_payload_count, "Expected exactly one webhook payload"
        payload = captured_payloads[0]

        expected_meeting_id = str(completed_meeting.id)
        expected_segment_count = 3
        assert payload["event"] == "meeting.completed", "Event type should be meeting.completed"
        assert payload["meeting_id"] == expected_meeting_id, "Meeting ID should match"
        assert payload["title"] == "Q4 Planning Session", "Title should match meeting title"
        assert payload["segment_count"] == expected_segment_count, "Segment count should match meeting segments"
        assert "timestamp" in payload, "Payload should include timestamp"

    @pytest.mark.asyncio
    async def test_payload_indicates_summary_presence(
        self,
        webhook_service: WebhookService,
        webhook_config: WebhookConfig,
        meeting_with_summary: Meeting,
        captured_payloads: list[dict[str, Any]],
    ) -> None:
        """Payload correctly indicates whether meeting has a summary."""
        webhook_service.register_webhook(webhook_config)

        await webhook_service.trigger_meeting_completed(meeting_with_summary)

        payload = captured_payloads[0]
        assert payload["has_summary"] is True, "Should indicate summary presence"

    @pytest.mark.asyncio
    async def test_no_delivery_when_no_webhooks_registered(
        self,
        webhook_service: WebhookService,
        completed_meeting: Meeting,
        captured_payloads: list[dict[str, Any]],
    ) -> None:
        """No payloads are delivered when no webhooks are registered."""
        deliveries = await webhook_service.trigger_meeting_completed(completed_meeting)

        assert deliveries == [], "Should have no deliveries"
        assert not captured_payloads, "Should have no captured payloads"


class TestSummaryGeneratedPayload:
    """Test summary.generated webhook payload content."""

    @pytest.mark.asyncio
    async def test_payload_contains_summary_details(
        self,
        webhook_service: WebhookService,
        webhook_config_all_events: WebhookConfig,
        meeting_with_summary: Meeting,
        captured_payloads: list[dict[str, Any]],
    ) -> None:
        """Payload includes summary content and item counts."""
        webhook_service.register_webhook(webhook_config_all_events)

        await webhook_service.trigger_summary_generated(meeting_with_summary)

        expected_payload_count = 1
        assert len(captured_payloads) == expected_payload_count, "Expected exactly one webhook payload"
        payload = captured_payloads[0]

        expected_meeting_id = str(meeting_with_summary.id)
        expected_key_points = 2
        expected_action_items = 2
        assert payload["event"] == "summary.generated", "Event type should be summary.generated"
        assert payload["meeting_id"] == expected_meeting_id, "Meeting ID should match"
        assert payload["title"] == "Q4 Planning Session", "Title should match meeting title"
        assert payload["executive_summary"] == "This meeting covered Q4 planning topics.", (
            "Executive summary should match"
        )
        assert payload["key_points_count"] == expected_key_points, "Key points count should match summary"
        assert payload["action_items_count"] == expected_action_items, "Action items count should match summary"


class TestRecordingPayloads:
    """Test recording.started and recording.stopped webhook payloads."""

    @pytest.mark.asyncio
    async def test_recording_started_payload(
        self,
        webhook_service: WebhookService,
        webhook_config_all_events: WebhookConfig,
        captured_payloads: list[dict[str, Any]],
    ) -> None:
        """Recording started payload contains meeting ID and title."""
        webhook_service.register_webhook(webhook_config_all_events)

        await webhook_service.trigger_recording_started(
            meeting_id="meeting-abc-123",
            title="Weekly Standup",
        )

        expected_payload_count = 1
        assert len(captured_payloads) == expected_payload_count, "Expected exactly one webhook payload"
        payload = captured_payloads[0]

        assert payload["event"] == "recording.started", "Event type should be recording.started"
        assert payload["meeting_id"] == "meeting-abc-123", "Meeting ID should match"
        assert payload["title"] == "Weekly Standup", "Title should match"
        assert "duration_seconds" not in payload, "Duration should not be in started event"

    @pytest.mark.asyncio
    async def test_recording_stopped_payload_includes_duration(
        self,
        webhook_service: WebhookService,
        webhook_config_all_events: WebhookConfig,
        captured_payloads: list[dict[str, Any]],
    ) -> None:
        """Recording stopped payload includes duration."""
        webhook_service.register_webhook(webhook_config_all_events)

        expected_duration = 1847.5
        await webhook_service.trigger_recording_stopped(
            meeting_id="meeting-abc-123",
            title="Weekly Standup",
            duration_seconds=expected_duration,
        )

        payload = captured_payloads[0]

        assert payload["event"] == "recording.stopped", "Event should be recording.stopped"
        assert payload["duration_seconds"] == expected_duration, "Duration should match"


class TestMultipleWebhookDelivery:
    """Test delivery to multiple registered webhooks."""

    @pytest.mark.asyncio
    async def test_event_delivered_to_all_subscribed_webhooks(
        self, webhook_service: WebhookService, completed_meeting: Meeting
    ) -> None:
        """Event is delivered to all webhooks subscribed to that event type."""
        # Register webhooks explicitly (no loop)
        first_webhook = create_first_webhook()
        second_webhook = create_second_webhook()

        webhook_service.register_webhook(first_webhook)
        webhook_service.register_webhook(second_webhook)

        deliveries = await webhook_service.trigger_meeting_completed(completed_meeting)

        expected_delivery_count = 2
        assert len(deliveries) == expected_delivery_count, "Should deliver to both webhooks"


class TestErrorResilience:
    """Test that webhook failures don't break the system."""

    @pytest.mark.asyncio
    async def test_executor_exception_does_not_propagate(
        self,
        webhook_config: WebhookConfig,
        completed_meeting: Meeting,
    ) -> None:
        """Executor exceptions are caught and don't crash the trigger call."""
        # Use the always-failing executor
        failing_executor = FailingExecutor()
        service = WebhookService(executor=failing_executor)  # type: ignore[arg-type]
        service.register_webhook(webhook_config)

        # Should complete without raising
        deliveries = await service.trigger_meeting_completed(completed_meeting)

        # Empty because exception prevented delivery record creation
        assert deliveries == [], "Should have no deliveries due to failure"

    @pytest.mark.asyncio
    async def test_one_failing_webhook_does_not_block_others(
        self, completed_meeting: Meeting
    ) -> None:
        """If one webhook fails, others still receive delivery."""
        # Use executor that fails for specific URL
        failing_url = "https://failing.example.com/hook"
        executor = SuccessAfterFirstExecutor(configs_to_fail={failing_url})
        service = WebhookService(executor=executor)  # type: ignore[arg-type]

        # Register webhooks explicitly (no loop)
        failing_webhook = create_failing_webhook()
        working_webhook = create_working_webhook()

        service.register_webhook(failing_webhook)
        service.register_webhook(working_webhook)

        deliveries = await service.trigger_meeting_completed(completed_meeting)

        expected_delivery_count = 1
        expected_status = 200
        assert len(deliveries) == expected_delivery_count, "Should have one successful delivery"
        assert deliveries[0].status_code == expected_status, "Successful delivery should have 200 status"


class TestServiceLifecycle:
    """Test service cleanup behavior."""

    @pytest.mark.asyncio
    async def test_close_releases_executor_resources(
        self, webhook_service: WebhookService, mock_executor: MagicMock
    ) -> None:
        """Closing service releases underlying executor resources."""
        await webhook_service.close()

        mock_executor.close.assert_called_once()
