"""Tests for event-driven drift handler."""

from __future__ import annotations

import hashlib
import hmac
import json
import time
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from backend.drift.event_handler import (
    _detect_cloud,
    _parse_signature_header,
    handle_drift_event,
)


def _make_request(
    body: dict[str, Any],
    *,
    signature: str = "",
    timestamp: int | None = None,
    secret: bytes = b"test-secret-32-bytes-0123456789ab",
) -> MagicMock:
    """Build a mock Flask Request."""
    raw_body = json.dumps(body)
    ts = str(timestamp or int(time.time()))

    if not signature:
        signed = f"{ts}.{raw_body}".encode()
        signature = hmac.new(secret, signed, hashlib.sha256).hexdigest()

    mock = MagicMock()
    mock.headers = {"X-Drift-Signature": f"t={ts},v1={signature}"}
    mock.get_data.return_value = raw_body
    return mock


_GCP_INSTANCE_NAME = (
    "//compute.googleapis.com/projects/my-proj/zones/us-central1-a/instances/vm1"
)

GCP_EVENT = {
    "asset": {
        "name": _GCP_INSTANCE_NAME,
        "assetType": "compute.googleapis.com/Instance",
        "resource": {"data": {}},
    },
    "priorAsset": None,
    "window": {"startTime": "2026-01-01T00:00:00Z"},
}

AWS_EVENT = {
    "source": "aws.config",
    "detail-type": "Config Configuration Item Change",
    "detail": {
        "configurationItem": {
            "resourceType": "AWS::EC2::Instance",
            "resourceId": "i-123",
            "awsAccountId": "123456789012",
            "configurationItemCaptureTime": "2026-01-01T00:00:00Z",
        },
        "configurationItemDiff": {"changeType": "CREATE"},
    },
}

AZURE_EVENT = {
    "eventType": "Microsoft.Resources.ResourceWriteSuccess",
    "data": {
        "resourceUri": (
            "/subscriptions/sub-1/resourceGroups/rg"
            "/providers/Microsoft.Compute"
            "/virtualMachines/vm1"
        ),
        "operationName": ("Microsoft.Compute/virtualMachines/write"),
        "status": "Succeeded",
    },
    "eventTime": "2026-01-01T00:00:00Z",
}


class TestSignatureParsing:
    """Signature header parsing tests."""

    def test_valid_hmac_accepted(self) -> None:
        ts, sig = _parse_signature_header("t=12345,v1=abcdef")
        assert ts == "12345"
        assert sig == "abcdef"

    def test_missing_signature_header_401(self) -> None:
        mock = MagicMock()
        mock.headers = {}
        result = handle_drift_event.__wrapped__(mock)
        assert result == ("", 401)

    def test_malformed_signature_header_401(self) -> None:
        mock = MagicMock()
        mock.headers = {"X-Drift-Signature": "malformed-garbage"}
        result = handle_drift_event.__wrapped__(mock)
        assert result == ("", 401)

    def test_expired_timestamp_401(self) -> None:
        mock = MagicMock()
        old_ts = str(int(time.time()) - 600)
        mock.headers = {"X-Drift-Signature": f"t={old_ts},v1=abc123"}
        mock.get_data.return_value = json.dumps(GCP_EVENT)
        result = handle_drift_event.__wrapped__(mock)
        assert result == ("", 401)

    def test_timestamp_within_window_accepted(self) -> None:
        ts = str(int(time.time()) - 100)
        _, sig = _parse_signature_header(f"t={ts},v1=test")
        # Just verifying parsing works for recent timestamps
        assert ts


class TestCloudDetection:
    """Cloud provider detection tests."""

    def test_cloud_detection_gcp(self) -> None:
        assert _detect_cloud(GCP_EVENT) == "gcp"

    def test_cloud_detection_aws(self) -> None:
        assert _detect_cloud(AWS_EVENT) == "aws"

    def test_cloud_detection_azure(self) -> None:
        assert _detect_cloud(AZURE_EVENT) == "azure"

    def test_cloud_detection_unknown_raises(self) -> None:
        with pytest.raises(ValueError, match="Cannot detect"):
            _detect_cloud({"random": "data"})


class TestEventProcessing:
    """Full event processing tests."""

    def test_wrong_signature_401(self) -> None:
        mock = MagicMock()
        ts = str(int(time.time()))
        mock.headers = {"X-Drift-Signature": f"t={ts},v1=wrong_signature"}
        mock.get_data.return_value = json.dumps(GCP_EVENT)

        # Mock the async processing
        with patch(
            "backend.drift.event_handler._process_event",
            new_callable=AsyncMock,
            return_value=("", 401),
        ):
            result = handle_drift_event.__wrapped__(mock)
            # The handler should try processing and get 401 from HMAC check
            assert result[1] in (401, 500) or result == ("", 401)

    @patch("backend.drift.event_handler._process_event", new_callable=AsyncMock)
    def test_duplicate_event_returns_200(self, mock_process: AsyncMock) -> None:
        mock_process.return_value = ("duplicate", 200)
        mock = _make_request(GCP_EVENT)
        result = handle_drift_event.__wrapped__(mock)
        assert result == ("duplicate", 200)

    @patch("backend.drift.event_handler._process_event", new_callable=AsyncMock)
    def test_score_drop_triggers_alert(self, mock_process: AsyncMock) -> None:
        mock_process.return_value = ("ok", 200)
        mock = _make_request(GCP_EVENT)
        result = handle_drift_event.__wrapped__(mock)
        assert result == ("ok", 200)

    @patch("backend.drift.event_handler._process_event", new_callable=AsyncMock)
    def test_score_unchanged_no_alert(self, mock_process: AsyncMock) -> None:
        mock_process.return_value = ("ok", 200)
        mock = _make_request(GCP_EVENT)
        result = handle_drift_event.__wrapped__(mock)
        assert result == ("ok", 200)

    @patch("backend.drift.event_handler._process_event", new_callable=AsyncMock)
    def test_drift_history_document_written(self, mock_process: AsyncMock) -> None:
        mock_process.return_value = ("ok", 200)
        mock = _make_request(GCP_EVENT)
        result = handle_drift_event.__wrapped__(mock)
        assert result[1] == 200

    @patch("backend.drift.event_handler._process_event", new_callable=AsyncMock)
    def test_project_not_found_404(self, mock_process: AsyncMock) -> None:
        mock_process.return_value = ("project not found", 404)
        mock = _make_request(GCP_EVENT)
        result = handle_drift_event.__wrapped__(mock)
        assert result == ("project not found", 404)
