"""Tests for the Zapier / Make.com automation integration.

Covers:
- Automation hook utility functions (payload builder, DB queries, dispatch)
- Automation hook Celery task
- REST hooks API endpoints (subscribe, unsubscribe, list, sample, events)
- Incoming action endpoints (upload)
- Integration with existing webhook dispatch
"""

import json
import time
from unittest.mock import MagicMock

import pytest

from app.models import AutomationHook
from app.utils.automation_hooks import (
    SAMPLE_PAYLOADS,
    build_zapier_payload,
    dispatch_automation_hooks,
    get_active_hooks_for_event,
)
from app.utils.webhook import VALID_EVENTS

# ---------------------------------------------------------------------------
# Unit tests – build_zapier_payload
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestBuildZapierPayload:
    """Tests for the Zapier-compatible payload builder."""

    def test_contains_required_keys(self):
        """Payload must contain id, event, timestamp, plus data fields."""
        payload = build_zapier_payload("document.uploaded", {"document_id": 1})
        assert "id" in payload
        assert "event" in payload
        assert "timestamp" in payload
        assert "document_id" in payload

    def test_id_starts_with_evt(self):
        """ID field must start with 'evt_' for Zapier deduplication."""
        payload = build_zapier_payload("document.uploaded", {"document_id": 1})
        assert payload["id"].startswith("evt_")

    def test_event_matches_input(self):
        """Event field must match the event argument."""
        payload = build_zapier_payload("document.processed", {"document_id": 2})
        assert payload["event"] == "document.processed"

    def test_timestamp_is_recent(self):
        """Timestamp should be close to current time."""
        before = time.time()
        payload = build_zapier_payload("document.uploaded", {})
        after = time.time()
        assert before <= payload["timestamp"] <= after

    def test_data_is_flat(self):
        """Data fields should be merged into top level (flat, no nested 'data' key)."""
        payload = build_zapier_payload("document.uploaded", {"filename": "test.pdf", "size": 1024})
        assert payload["filename"] == "test.pdf"
        assert payload["size"] == 1024
        assert "data" not in payload

    def test_unique_ids(self):
        """Each call should produce a unique ID."""
        ids = {build_zapier_payload("document.uploaded", {})["id"] for _ in range(50)}
        assert len(ids) == 50


# ---------------------------------------------------------------------------
# Unit tests – SAMPLE_PAYLOADS
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestSamplePayloads:
    """Tests for the sample payloads used by Zapier field mapping."""

    def test_all_events_have_samples(self):
        """Every valid event should have a sample payload."""
        for event in VALID_EVENTS:
            assert event in SAMPLE_PAYLOADS, f"Missing sample payload for {event}"

    def test_samples_contain_id_and_event(self):
        """Each sample should contain id and event keys."""
        for event, sample in SAMPLE_PAYLOADS.items():
            assert "id" in sample, f"Sample for {event} missing 'id'"
            assert sample["event"] == event


# ---------------------------------------------------------------------------
# Unit tests – get_active_hooks_for_event
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetActiveHooksForEvent:
    """Tests for querying active automation hooks from the database."""

    def test_returns_matching_hooks(self, mocker):
        """Only hooks subscribed to the event should be returned."""
        hook = MagicMock(
            id=1,
            target_url="https://hooks.zapier.com/1234",
            secret="abc",
            events=json.dumps(["document.uploaded"]),
            is_active=True,
        )
        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.all.return_value = [hook]
        mocker.patch("app.utils.automation_hooks.SessionLocal", return_value=mock_session)

        result = get_active_hooks_for_event("document.uploaded")
        assert len(result) == 1
        assert result[0]["target_url"] == "https://hooks.zapier.com/1234"

    def test_excludes_non_matching_hooks(self, mocker):
        """Hooks for different events should not be returned."""
        hook = MagicMock(
            id=1,
            target_url="https://hooks.zapier.com/1234",
            secret=None,
            events=json.dumps(["document.processed"]),
            is_active=True,
        )
        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.all.return_value = [hook]
        mocker.patch("app.utils.automation_hooks.SessionLocal", return_value=mock_session)

        result = get_active_hooks_for_event("document.uploaded")
        assert len(result) == 0

    def test_empty_when_no_hooks(self, mocker):
        """Empty list returned when no hooks exist."""
        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.all.return_value = []
        mocker.patch("app.utils.automation_hooks.SessionLocal", return_value=mock_session)

        result = get_active_hooks_for_event("document.uploaded")
        assert result == []


# ---------------------------------------------------------------------------
# Unit tests – dispatch_automation_hooks
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestDispatchAutomationHooks:
    """Tests for the automation hook dispatch function."""

    def test_ignores_unknown_events(self, mocker):
        """Unknown events should be silently ignored."""
        mocker.patch("app.utils.automation_hooks.settings", MagicMock(automation_hooks_enabled=True))
        mock_get = mocker.patch("app.utils.automation_hooks.get_active_hooks_for_event")
        dispatch_automation_hooks("bad.event", {})
        mock_get.assert_not_called()

    def test_skips_when_disabled(self, mocker):
        """No hooks should fire when automation_hooks_enabled is False."""
        mocker.patch("app.utils.automation_hooks.settings", MagicMock(automation_hooks_enabled=False))
        mock_get = mocker.patch("app.utils.automation_hooks.get_active_hooks_for_event")
        dispatch_automation_hooks("document.uploaded", {"file_id": 1})
        mock_get.assert_not_called()

    def test_queues_celery_task_for_each_hook(self, mocker):
        """A Celery task is queued for each matching hook."""
        mocker.patch("app.utils.automation_hooks.settings", MagicMock(automation_hooks_enabled=True))
        mocker.patch(
            "app.utils.automation_hooks.get_active_hooks_for_event",
            return_value=[
                {"id": 1, "target_url": "https://hooks.zapier.com/a", "secret": "s", "events": ["document.uploaded"]},
                {"id": 2, "target_url": "https://hooks.zapier.com/b", "secret": None, "events": ["document.uploaded"]},
            ],
        )
        mock_task = mocker.patch("app.tasks.automation_tasks.deliver_automation_hook_task.delay")

        dispatch_automation_hooks("document.uploaded", {"file_id": 42})

        assert mock_task.call_count == 2

    def test_no_tasks_when_no_hooks(self, mocker):
        """No tasks should be queued when there are no matching hooks."""
        mocker.patch("app.utils.automation_hooks.settings", MagicMock(automation_hooks_enabled=True))
        mocker.patch("app.utils.automation_hooks.get_active_hooks_for_event", return_value=[])
        mock_task = mocker.patch("app.tasks.automation_tasks.deliver_automation_hook_task.delay")

        dispatch_automation_hooks("document.uploaded", {})

        mock_task.assert_not_called()


# ---------------------------------------------------------------------------
# Unit tests – deliver_automation_hook_task
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestDeliverAutomationHookTask:
    """Tests for the automation hook Celery task."""

    def test_returns_success_dict(self, mocker):
        """Successful delivery returns status dict."""
        mocker.patch("app.tasks.automation_tasks.deliver_webhook", return_value=True)

        from app.tasks.automation_tasks import deliver_automation_hook_task

        deliver_automation_hook_task.request.retries = 0

        result = deliver_automation_hook_task.__wrapped__("https://hooks.zapier.com/test", {"event": "test"}, None)
        assert result["status"] == "delivered"
        assert result["url"] == "https://hooks.zapier.com/test"

    def test_raises_on_failure(self, mocker):
        """Failed delivery raises RuntimeError for Celery retry."""
        mocker.patch("app.tasks.automation_tasks.deliver_webhook", return_value=False)

        from app.tasks.automation_tasks import deliver_automation_hook_task

        deliver_automation_hook_task.request.retries = 0

        with pytest.raises(RuntimeError, match="Automation hook delivery"):
            deliver_automation_hook_task.__wrapped__("https://hooks.zapier.com/test", {"event": "test"}, None)


# ---------------------------------------------------------------------------
# Integration tests – webhook dispatch integration
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestWebhookDispatchIntegration:
    """Test that dispatch_webhook_event also triggers automation hooks."""

    def test_dispatch_triggers_automation_hooks(self, mocker):
        """dispatch_webhook_event should also call dispatch_automation_hooks."""
        mocker.patch("app.utils.webhook.get_active_webhooks_for_event", return_value=[])
        mock_auto = mocker.patch("app.utils.automation_hooks.dispatch_automation_hooks")

        from app.utils.webhook import dispatch_webhook_event

        dispatch_webhook_event("document.uploaded", {"file_id": 1})

        mock_auto.assert_called_once_with("document.uploaded", {"file_id": 1})


# ---------------------------------------------------------------------------
# Integration tests – API endpoints
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestAutomationAPI:
    """Tests for the /api/automation/ endpoints."""

    def _with_auth(self, client):
        """Override auth dependency to simulate an authenticated user."""
        from app.api.automation import _require_api_user

        client.app.dependency_overrides[_require_api_user] = lambda: {
            "id": "testuser",
            "email": "test@example.com",
            "preferred_username": "testuser",
            "is_admin": False,
        }
        return client

    # ── Subscribe / Unsubscribe ──────────────────────────────────────

    def test_subscribe_hook(self, client):
        """POST /api/automation/hooks/subscribe creates a new hook."""
        self._with_auth(client)
        resp = client.post(
            "/api/automation/hooks/subscribe",
            json={
                "target_url": "https://hooks.zapier.com/test",
                "events": ["document.uploaded"],
                "hook_type": "zapier",
                "description": "My Zap",
            },
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["target_url"] == "https://hooks.zapier.com/test"
        assert data["events"] == ["document.uploaded"]
        assert data["is_active"] is True
        assert data["hook_type"] == "zapier"

    def test_subscribe_with_secret(self, client):
        """POST /api/automation/hooks/subscribe with secret masks it."""
        self._with_auth(client)
        resp = client.post(
            "/api/automation/hooks/subscribe",
            json={
                "target_url": "https://hooks.zapier.com/secret",
                "events": ["document.processed"],
                "secret": "my-signing-secret",
            },
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["has_secret"] is True
        assert "secret" not in data

    def test_subscribe_invalid_event(self, client):
        """POST /api/automation/hooks/subscribe rejects invalid events."""
        self._with_auth(client)
        resp = client.post(
            "/api/automation/hooks/subscribe",
            json={
                "target_url": "https://hooks.zapier.com/bad",
                "events": ["bad.event"],
            },
        )
        assert resp.status_code == 422

    def test_unsubscribe_hook(self, client, db_session):
        """DELETE /api/automation/hooks/{id} removes the hook."""
        self._with_auth(client)
        hook = AutomationHook(
            target_url="https://hooks.zapier.com/del",
            events=json.dumps(["document.uploaded"]),
            is_active=True,
            hook_type="zapier",
        )
        db_session.add(hook)
        db_session.commit()
        hook_id = hook.id

        resp = client.delete(f"/api/automation/hooks/{hook_id}")
        assert resp.status_code == 204

    def test_unsubscribe_not_found(self, client):
        """DELETE /api/automation/hooks/9999 returns 404."""
        self._with_auth(client)
        resp = client.delete("/api/automation/hooks/9999")
        assert resp.status_code == 404

    # ── List hooks ───────────────────────────────────────────────────

    def test_list_hooks(self, client, db_session):
        """GET /api/automation/hooks returns all hooks."""
        self._with_auth(client)
        hook = AutomationHook(
            target_url="https://hooks.zapier.com/list",
            events=json.dumps(["document.processed"]),
            is_active=True,
            hook_type="make",
        )
        db_session.add(hook)
        db_session.commit()

        resp = client.get("/api/automation/hooks")
        assert resp.status_code == 200
        items = resp.json()
        assert any(h["target_url"] == "https://hooks.zapier.com/list" for h in items)

    # ── Sample trigger data ──────────────────────────────────────────

    def test_trigger_sample(self, client):
        """GET /api/automation/triggers/sample/{event} returns sample data."""
        self._with_auth(client)
        resp = client.get("/api/automation/triggers/sample/document.uploaded")
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["event"] == "document.uploaded"
        assert "id" in data[0]

    def test_trigger_sample_unknown_event(self, client):
        """GET /api/automation/triggers/sample/bad returns 404."""
        self._with_auth(client)
        resp = client.get("/api/automation/triggers/sample/bad.event")
        assert resp.status_code == 404

    # ── Events listing ───────────────────────────────────────────────

    def test_list_events(self, client):
        """GET /api/automation/events returns valid event types."""
        self._with_auth(client)
        resp = client.get("/api/automation/events")
        assert resp.status_code == 200
        events = resp.json()
        assert "document.uploaded" in events
        assert "document.processed" in events
        assert "document.failed" in events

    # ── Incoming action: upload ──────────────────────────────────────

    def test_action_upload(self, client, mocker):
        """POST /api/automation/actions/upload accepts a file."""
        self._with_auth(client)
        mock_task = MagicMock()
        mock_task.id = "task-123"
        mocker.patch("app.tasks.process_document.process_document.delay", return_value=mock_task)

        resp = client.post(
            "/api/automation/actions/upload",
            files={"file": ("test.pdf", b"fake-pdf-content", "application/pdf")},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "accepted"
        assert data["filename"] == "test.pdf"
        assert data["task_id"] == "task-123"

    def test_action_upload_no_filename(self, client):
        """POST /api/automation/actions/upload rejects empty filename."""
        self._with_auth(client)
        resp = client.post(
            "/api/automation/actions/upload",
            files={"file": ("", b"content", "application/pdf")},
        )
        # FastAPI/Starlette may return 422 (multipart validation) or 400
        # (our explicit check) depending on how the empty filename is
        # parsed by the underlying multipart parser version.
        assert resp.status_code in (400, 422)

    # ── Auth required ────────────────────────────────────────────────

    def test_requires_auth(self, client):
        """Endpoints return 401 without authentication."""
        from app.api.automation import _require_api_user

        client.app.dependency_overrides.pop(_require_api_user, None)

        resp = client.get("/api/automation/hooks")
        assert resp.status_code == 401
