"""Unit tests for WebhookExecutor."""

from __future__ import annotations

import hashlib
import hmac
import json
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, patch

import httpx
import pytest

from noteflow.domain.webhooks import WebhookEventType
from noteflow.domain.webhooks.events import WebhookPayloadDict

if TYPE_CHECKING:
    from noteflow.domain.webhooks import WebhookConfig
    from noteflow.infrastructure.webhooks import WebhookExecutor

    from .conftest import HeaderCapture


class TestWebhookExecutorDelivery:
    """Test webhook delivery functionality."""

    @pytest.mark.asyncio
    async def test_deliver_success(
        self, executor: WebhookExecutor, enabled_config: WebhookConfig
    ) -> None:
        """Deliver webhook successfully when server returns 200."""
        payload: WebhookPayloadDict = {"event": "meeting.completed", "meeting_id": "123"}

        mock_response = httpx.Response(200)
        with patch.object(
            httpx.AsyncClient, "post", new_callable=AsyncMock, return_value=mock_response
        ):
            delivery = await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        http_ok = 200
        assert delivery.succeeded is True, "delivery should succeed with HTTP 200"
        assert delivery.status_code == http_ok, "status code should be HTTP OK"
        assert delivery.attempt_count == 1, "should succeed on first attempt"
        assert delivery.error_message is None, "successful delivery has no error"

    @pytest.mark.asyncio
    async def test_deliver_disabled_webhook(
        self, executor: WebhookExecutor, disabled_config: WebhookConfig
    ) -> None:
        """Skip delivery for disabled webhooks."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}

        delivery = await executor.deliver(
            disabled_config,
            WebhookEventType.MEETING_COMPLETED,
            payload,
        )

        assert delivery.succeeded is False, "disabled webhook should fail"
        assert delivery.attempt_count == 0, "no attempts for disabled webhook"
        assert delivery.error_message == "Webhook disabled", "should report disabled error"

    @pytest.mark.asyncio
    async def test_deliver_unsubscribed_event(
        self, executor: WebhookExecutor, enabled_config: WebhookConfig
    ) -> None:
        """Skip delivery for events not in subscription list."""
        payload: WebhookPayloadDict = {"event": "recording.started"}

        delivery = await executor.deliver(
            enabled_config,
            WebhookEventType.RECORDING_STARTED,
            payload,
        )

        assert delivery.succeeded is False, "unsubscribed event should fail"
        assert delivery.attempt_count == 0, "no attempts for unsubscribed event"
        error_msg = str(delivery.error_message)
        assert "not subscribed" in error_msg, "should report not subscribed"

    @pytest.mark.asyncio
    async def test_deliver_retries_on_timeout(
        self, executor: WebhookExecutor, enabled_config: WebhookConfig
    ) -> None:
        """Retry delivery when request times out."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}

        with patch.object(
            httpx.AsyncClient,
            "post",
            new_callable=AsyncMock,
            side_effect=httpx.TimeoutException("Timeout"),
        ):
            delivery = await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        assert delivery.succeeded is False, "timeout should fail"
        assert delivery.attempt_count == 3, "should exhaust max_retries"
        error_msg = str(delivery.error_message)
        assert "Max retries exceeded" in error_msg, "should report max retries"

    @pytest.mark.asyncio
    async def test_deliver_retries_on_connection_error(
        self, executor: WebhookExecutor, enabled_config: WebhookConfig
    ) -> None:
        """Retry delivery when connection fails."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}

        with patch.object(
            httpx.AsyncClient,
            "post",
            new_callable=AsyncMock,
            side_effect=httpx.ConnectError("Connection refused"),
        ):
            delivery = await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        assert delivery.succeeded is False, "connection error should fail"
        assert delivery.attempt_count == 3, "should exhaust retries on connection error"
        error_msg = str(delivery.error_message)
        assert "Max retries exceeded" in error_msg, "should report max retries"


class TestHmacSignature:
    """Test HMAC signature generation."""

    @pytest.mark.asyncio
    async def test_hmac_signature_generation(
        self,
        executor: WebhookExecutor,
        signed_config: WebhookConfig,
        header_capture: HeaderCapture,
    ) -> None:
        """Generate valid HMAC-SHA256 signature when secret is configured."""
        payload: WebhookPayloadDict = {"event": "meeting.completed", "meeting_id": "123"}

        with patch.object(
            httpx.AsyncClient,
            "post",
            new_callable=AsyncMock,
            side_effect=header_capture.capture_request,
        ):
            await executor.deliver(
                signed_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        assert "X-NoteFlow-Signature" in header_capture.headers, "Should include signature"
        assert "X-NoteFlow-Timestamp" in header_capture.headers, "Should include timestamp"
        assert "X-NoteFlow-Delivery" in header_capture.headers, "Should include delivery ID"

        signature_header = header_capture.headers["X-NoteFlow-Signature"]
        timestamp = header_capture.headers["X-NoteFlow-Timestamp"]
        delivery_id = header_capture.headers["X-NoteFlow-Delivery"]

        assert signature_header.startswith("sha256="), "Should use sha256 algorithm"

        # Verify signature is correct with new format: {timestamp}.{delivery_id}.{body}
        expected_body = json.dumps(payload, separators=(",", ":"), sort_keys=True)
        signature_base = f"{timestamp}.{delivery_id}.{expected_body}"
        assert signed_config.secret is not None, "Test requires signed config"
        expected_signature = hmac.new(
            signed_config.secret.encode(),
            signature_base.encode(),
            hashlib.sha256,
        ).hexdigest()

        assert signature_header == f"sha256={expected_signature}", "Signature should match"

    @pytest.mark.asyncio
    async def test_no_signature_without_secret(
        self,
        executor: WebhookExecutor,
        enabled_config: WebhookConfig,
        header_capture: HeaderCapture,
    ) -> None:
        """Omit signature header when secret is not configured."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}

        with patch.object(
            httpx.AsyncClient,
            "post",
            new_callable=AsyncMock,
            side_effect=header_capture.capture_request,
        ):
            await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        assert "X-NoteFlow-Signature" not in header_capture.headers, "no signature without secret"


class TestWebhookHeaders:
    """Test webhook request headers."""

    @pytest.mark.asyncio
    async def test_includes_event_header(
        self,
        executor: WebhookExecutor,
        enabled_config: WebhookConfig,
        header_capture: HeaderCapture,
    ) -> None:
        """Include event type in X-NoteFlow-Event header."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}

        with patch.object(
            httpx.AsyncClient,
            "post",
            new_callable=AsyncMock,
            side_effect=header_capture.capture_request,
        ):
            await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        assert header_capture.headers.get("X-NoteFlow-Event") == "meeting.completed", "event header should match"

    @pytest.mark.asyncio
    async def test_includes_delivery_id_header(
        self,
        executor: WebhookExecutor,
        enabled_config: WebhookConfig,
        header_capture: HeaderCapture,
    ) -> None:
        """Include unique delivery ID in X-NoteFlow-Delivery header."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}

        with patch.object(
            httpx.AsyncClient,
            "post",
            new_callable=AsyncMock,
            side_effect=header_capture.capture_request,
        ):
            await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        assert "X-NoteFlow-Delivery" in header_capture.headers, "delivery ID header should be present"
        # Verify it's a valid UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
        delivery_id = header_capture.headers["X-NoteFlow-Delivery"]
        uuid_with_hyphens_length = 36
        assert len(delivery_id) == uuid_with_hyphens_length, "delivery ID should be UUID format"

    @pytest.mark.asyncio
    async def test_delivery_id_header_matches_returned_record(
        self,
        executor: WebhookExecutor,
        enabled_config: WebhookConfig,
        header_capture: HeaderCapture,
    ) -> None:
        """Delivery ID in X-NoteFlow-Delivery header matches returned WebhookDelivery.id."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}

        with patch.object(
            httpx.AsyncClient,
            "post",
            new_callable=AsyncMock,
            side_effect=header_capture.capture_request,
        ):
            delivery = await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )

        header_delivery_id = header_capture.headers.get("X-NoteFlow-Delivery")
        record_delivery_id = str(delivery.id)

        assert header_delivery_id is not None, (
            "X-NoteFlow-Delivery header should be present"
        )
        assert header_delivery_id == record_delivery_id, (
            f"Header delivery ID '{header_delivery_id}' should match "
            f"returned record ID '{record_delivery_id}'"
        )


class TestExecutorCleanup:
    """Test executor resource cleanup."""

    @pytest.mark.asyncio
    async def test_close_cleans_up_client(
        self, executor: WebhookExecutor, enabled_config: WebhookConfig
    ) -> None:
        """Close method cleans up HTTP client and allows reuse."""
        payload: WebhookPayloadDict = {"event": "meeting.completed"}
        mock_response: httpx.Response = httpx.Response(200)

        with patch.object(
            httpx.AsyncClient, "post", new_callable=AsyncMock, return_value=mock_response
        ):
            # Trigger client creation through public API
            delivery = await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )
            assert delivery.succeeded is True, "initial delivery should succeed"

            # Close should not raise
            await executor.close()

            # Close again should be idempotent (no error)
            await executor.close()

            # Executor should still be functional after close (new client created)
            delivery2 = await executor.deliver(
                enabled_config,
                WebhookEventType.MEETING_COMPLETED,
                payload,
            )
            assert delivery2.succeeded is True, "delivery after close should succeed"
