"""Tests for the notification service layer."""

from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest

import notifications.config as cfg
from notifications.config import NotificationConfig
from notifications.models import (
    CreateNotification,
    Notification,
    NotificationType,
)
from notifications.service import (
    get_notifications,
    mark_all_read,
    mark_read,
    send_bulk,
    send_notification,
)


@pytest.fixture()
def sample_notification() -> Notification:
    """Return a sample notification for test assertions."""
    return Notification(
        id=uuid4(),
        user_id="user-123",
        type=NotificationType.INFO,
        title="Test",
        message="Hello world",
        action_url=None,
        read=False,
        created_at=datetime.now(timezone.utc),
    )


class TestSendNotification:
    """Tests for service.send_notification()."""

    @patch("notifications.service.insert", new_callable=AsyncMock)
    async def test_sends_single_notification(
        self,
        mock_insert: AsyncMock,
        initialized_config: NotificationConfig,
        sample_notification: Notification,
    ):
        mock_insert.return_value = sample_notification

        result = await send_notification(
            user_id="user-123",
            type="info",
            title="Test",
            message="Hello world",
        )

        assert result.user_id == "user-123"
        assert result.title == "Test"
        mock_insert.assert_called_once()

        payload = mock_insert.call_args[0][0]
        assert isinstance(payload, CreateNotification)
        assert payload.user_id == "user-123"
        assert payload.type == NotificationType.INFO

    @patch("notifications.service.insert", new_callable=AsyncMock)
    async def test_accepts_enum_type(
        self,
        mock_insert: AsyncMock,
        initialized_config: NotificationConfig,
        sample_notification: Notification,
    ):
        mock_insert.return_value = sample_notification

        await send_notification(
            user_id="user-123",
            type=NotificationType.ERROR,
            title="Error",
            message="Something broke",
        )

        payload = mock_insert.call_args[0][0]
        assert payload.type == NotificationType.ERROR

    @patch("notifications.service.insert", new_callable=AsyncMock)
    async def test_passes_action_url(
        self,
        mock_insert: AsyncMock,
        initialized_config: NotificationConfig,
        sample_notification: Notification,
    ):
        mock_insert.return_value = sample_notification

        await send_notification(
            user_id="user-123",
            type="success",
            title="Done",
            message="All good",
            action_url="https://example.com/action",
        )

        payload = mock_insert.call_args[0][0]
        assert payload.action_url == "https://example.com/action"


class TestSendBulk:
    """Tests for service.send_bulk()."""

    @patch("notifications.service.insert_bulk", new_callable=AsyncMock)
    async def test_sends_to_multiple_users(
        self,
        mock_insert_bulk: AsyncMock,
        initialized_config: NotificationConfig,
    ):
        now = datetime.now(timezone.utc)
        mock_insert_bulk.return_value = [
            Notification(
                id=uuid4(),
                user_id=uid,
                type=NotificationType.WARNING,
                title="Maintenance",
                message="Downtime at midnight",
                read=False,
                created_at=now,
            )
            for uid in ["user-1", "user-2", "user-3"]
        ]

        results = await send_bulk(
            user_ids=["user-1", "user-2", "user-3"],
            type="warning",
            title="Maintenance",
            message="Downtime at midnight",
        )

        assert len(results) == 3
        mock_insert_bulk.assert_called_once()
        payloads = mock_insert_bulk.call_args[0][0]
        assert len(payloads) == 3
        assert {p.user_id for p in payloads} == {"user-1", "user-2", "user-3"}


class TestGetNotifications:
    """Tests for service.get_notifications()."""

    @patch("notifications.service.get_unread_count", new_callable=AsyncMock)
    @patch("notifications.service.get_total_count", new_callable=AsyncMock)
    @patch("notifications.service.get_by_user", new_callable=AsyncMock)
    async def test_returns_paginated_result(
        self,
        mock_get_by_user: AsyncMock,
        mock_get_total: AsyncMock,
        mock_get_unread: AsyncMock,
        initialized_config: NotificationConfig,
        sample_notification: Notification,
    ):
        mock_get_by_user.return_value = [sample_notification]
        mock_get_total.return_value = 1
        mock_get_unread.return_value = 1

        result = await get_notifications("user-123", page=1)

        assert result["items"] == [sample_notification]
        assert result["total"] == 1
        assert result["unread_count"] == 1
        assert result["page"] == 1
        assert result["per_page"] == 20  # from test config

    @patch("notifications.service.get_unread_count", new_callable=AsyncMock)
    @patch("notifications.service.get_total_count", new_callable=AsyncMock)
    @patch("notifications.service.get_by_user", new_callable=AsyncMock)
    async def test_page_offset_calculation(
        self,
        mock_get_by_user: AsyncMock,
        mock_get_total: AsyncMock,
        mock_get_unread: AsyncMock,
        initialized_config: NotificationConfig,
    ):
        mock_get_by_user.return_value = []
        mock_get_total.return_value = 50
        mock_get_unread.return_value = 10

        await get_notifications("user-123", page=3)

        # Page 3 with per_page=20 means offset=40
        call_args = mock_get_by_user.call_args
        assert call_args.kwargs.get("offset", call_args[1].get("offset")) == 40


class TestMarkRead:
    """Tests for service.mark_read() and service.mark_all_read()."""

    @patch("notifications.service.store_mark_read", new_callable=AsyncMock)
    async def test_mark_read_with_string_id(
        self,
        mock_store_mark_read: AsyncMock,
        initialized_config: NotificationConfig,
    ):
        mock_store_mark_read.return_value = True
        nid = uuid4()

        result = await mark_read(str(nid))

        assert result is True
        mock_store_mark_read.assert_called_once_with(nid)

    @patch("notifications.service.store_mark_read", new_callable=AsyncMock)
    async def test_mark_read_with_uuid(
        self,
        mock_store_mark_read: AsyncMock,
        initialized_config: NotificationConfig,
    ):
        mock_store_mark_read.return_value = True
        nid = uuid4()

        result = await mark_read(nid)

        assert result is True
        mock_store_mark_read.assert_called_once_with(nid)

    @patch("notifications.service.store_mark_all_read", new_callable=AsyncMock)
    async def test_mark_all_read(
        self,
        mock_store_mark_all: AsyncMock,
        initialized_config: NotificationConfig,
    ):
        mock_store_mark_all.return_value = 7

        count = await mark_all_read("user-123")

        assert count == 7
        mock_store_mark_all.assert_called_once_with("user-123")
