from unittest import mock

import pytest
from django.contrib.auth import get_user_model
from django.utils import timezone
from apps.notifications.models import Notification, NotificationTemplate, NotificationPreference
from apps.notifications.handler import NotificationHandler


@pytest.fixture
def user(db):
    User = get_user_model()
    return User.objects.create_user(email="notify@example.com", password="pass123")


@pytest.fixture
def user2(db):
    User = get_user_model()
    return User.objects.create_user(email="notify2@example.com", password="pass123")


@pytest.mark.django_db
class TestNotificationModel:
    def test_create_notification(self, user):
        notification = Notification.objects.create(
            user=user,
            title="Test Notification",
            message="This is a test",
            notification_type="info",
            priority="medium"
        )
        assert notification.title == "Test Notification"
        assert notification.is_read is False
        assert notification.notification_type == "info"

    def test_notification_mark_as_read(self, user):
        notification = Notification.objects.create(
            user=user,
            title="Test",
            message="Test message"
        )
        notification.mark_as_read()
        notification.refresh_from_db()
        assert notification.is_read is True
        assert notification.read_at is not None

    def test_notification_soft_delete(self, user):
        notification = Notification.objects.create(
            user=user,
            title="Test",
            message="Test message"
        )
        notification.mark_as_deleted()
        notification.refresh_from_db()
        assert notification.is_deleted is True

    def test_notification_types(self, user):
        types = ["info", "success", "warning", "error", "task", "event", "chat"]
        for ntype in types:
            Notification.objects.create(
                user=user,
                title=f"{ntype} notification",
                message="Test",
                notification_type=ntype
            )
        assert Notification.objects.filter(notification_type="task").count() == 1
        assert Notification.objects.filter(notification_type="chat").count() == 1

    def test_notification_metadata(self, user):
        metadata = {"task_id": 123, "assigned_by": "admin"}
        notification = Notification.objects.create(
            user=user,
            title="New Task",
            message="You have a new task",
            metadata=metadata
        )
        assert notification.metadata == {"task_id": 123, "assigned_by": "admin"}

    def test_notification_priority(self, user):
        notification = Notification.objects.create(
            user=user,
            title="Urgent",
            message="This is urgent",
            priority="urgent"
        )
        assert notification.priority == "urgent"

    def test_notification_with_link(self, user):
        notification = Notification.objects.create(
            user=user,
            title="New Message",
            message="You have a new message",
            link="/chat/conversation/123"
        )
        assert notification.link == "/chat/conversation/123"


@pytest.mark.django_db
class TestNotificationTemplateModel:
    def test_create_template(self):
        template = NotificationTemplate.objects.create(
            name="task_assigned",
            title_template="New Task: {task_title}",
            message_template="You have been assigned to task #{task_id}",
            notification_type="task",
            icon="task"
        )
        assert template.name == "task_assigned"
        assert "{task_title}" in template.title_template

    def test_template_str(self):
        template = NotificationTemplate.objects.create(
            name="test_template",
            title_template="Test",
            message_template="Test message",
            notification_type="info"
        )
        assert str(template) == "test_template"


@pytest.mark.django_db
class TestNotificationPreferenceModel:
    def test_create_preference(self, user):
        prefs = NotificationPreference.objects.create(user=user)
        assert prefs.email_enabled is True
        assert prefs.push_enabled is True

    def test_type_enabled(self, user):
        prefs = NotificationPreference.objects.create(user=user)
        assert prefs.is_type_enabled("task") is True
        prefs.set_type_enabled("task", False)
        prefs.refresh_from_db()
        assert prefs.is_type_enabled("task") is False

    def test_quiet_hours(self, user):
        from datetime import time
        prefs = NotificationPreference.objects.create(
            user=user,
            quiet_hours_start=time(22, 0),
            quiet_hours_end=time(8, 0)
        )
        assert prefs.quiet_hours_start.hour == 22


@pytest.mark.django_db
class TestNotificationHandler:
    def test_send_notification(self, user):
        notification = NotificationHandler.send(
            user=user,
            title="Handler Test",
            message="Testing the handler",
            notification_type="info"
        )
        assert notification.user == user
        assert notification.title == "Handler Test"

    def test_broadcast_notifications(self, user, user2):
        notifications = NotificationHandler.broadcast(
            users=[user, user2],
            title="Broadcast Test",
            message="This is a broadcast",
            notification_type="info"
        )
        assert len(notifications) == 2

    def test_send_to_role(self, user):
        from django.contrib.auth.models import Group
        group = Group.objects.create(name="testers")
        user.groups.add(group)

        notifications = NotificationHandler.broadcast(
            users=[user],
            title="Role Test",
            message="Testing role-based notification",
            notification_type="system"
        )
        assert len(notifications) == 1

    def test_mark_as_read(self, user):
        notification = Notification.objects.create(
            user=user,
            title="To Mark Read",
            message="Will be marked read"
        )
        success = NotificationHandler.mark_as_read(str(notification.id), user)
        assert success is True
        notification.refresh_from_db()
        assert notification.is_read is True

    def test_mark_all_as_read(self, user):
        Notification.objects.create(user=user, title="N1", message="M1")
        Notification.objects.create(user=user, title="N2", message="M2")
        count = NotificationHandler.mark_all_as_read(user)
        assert count == 2

    def test_get_unread_count(self, user):
        Notification.objects.create(user=user, title="N1", message="M1")
        Notification.objects.create(user=user, title="N2", message="M2")
        count = NotificationHandler.get_unread_count(user)
        assert count == 2

    def test_get_unread(self, user):
        Notification.objects.create(user=user, title="N1", message="M1")
        Notification.objects.create(user=user, title="N2", message="M2")
        unread = NotificationHandler.get_unread(user, limit=10)
        assert len(unread) == 2

    def test_send_submits_one_background_job_not_three(self, user, settings, django_capture_on_commit_callbacks):
        """PERF-004: a single notification.send() used to submit 3 separate
        run_in_background jobs (broadcast + FCM + email) — now it's 1 job
        that runs all three channels sequentially inside it."""
        settings.BACKGROUND_TASKS_EAGER = False
        with mock.patch("apps.core.background.run_in_background") as mock_run, \
             django_capture_on_commit_callbacks(execute=True):
            NotificationHandler.send(
                user=user, title="T", message="M", notification_type="info", broadcast=True,
            )
        assert mock_run.call_count == 1


@pytest.mark.django_db
class TestFCMDevice:
    def test_register_device(self, user, client):
        client.force_login(user)
        from apps.notifications.models import FCMDevice
        FCMDevice.objects.create(user=user, token="tok-abc", platform="web")
        assert FCMDevice.objects.filter(user=user, is_active=True).count() == 1

    def test_send_skips_when_fcm_unconfigured(self, user, settings):
        # No credentials file -> push is a no-op, never raises.
        settings.FIREBASE_CREDENTIALS_FILE = ""
        from apps.notifications import fcm
        from apps.notifications.models import FCMDevice
        FCMDevice.objects.create(user=user, token="tok-xyz", platform="web")
        assert fcm.send_to_user(user, title="Hi", body="There") == 0

    def test_handler_push_respects_disabled_pref(self, user):
        # push_enabled=False must not raise even with a registered device.
        prefs = NotificationPreference.objects.create(user=user, push_enabled=False)
        from apps.notifications.models import FCMDevice
        FCMDevice.objects.create(user=user, token="tok-1", platform="web")
        n = NotificationHandler.send(user=user, title="T", message="M")
        assert n.pk is not None


@pytest.mark.django_db
class TestAdministrationsType:
    def test_administrations_is_a_valid_type(self, user):
        n = Notification.objects.create(
            user=user, title="Ticket assigned", message="M",
            notification_type="administrations",
        )
        assert n.notification_type == "administrations"
        assert Notification.objects.filter(notification_type="administrations").count() == 1


class TestQuietHours:
    def test_no_window_set_is_never_quiet(self):
        prefs = NotificationPreference(quiet_hours_start=None, quiet_hours_end=None)
        assert NotificationHandler._in_quiet_hours(prefs) is False

    def test_non_wrapping_window(self):
        from datetime import time
        from unittest.mock import patch

        prefs = NotificationPreference(quiet_hours_start=time(22, 0), quiet_hours_end=time(23, 0))
        with patch("django.utils.timezone.localtime") as mock_lt:
            mock_lt.return_value.time.return_value = time(22, 30)
            assert NotificationHandler._in_quiet_hours(prefs) is True
            mock_lt.return_value.time.return_value = time(12, 0)
            assert NotificationHandler._in_quiet_hours(prefs) is False

    def test_wrapping_window_past_midnight(self):
        from datetime import time
        from unittest.mock import patch

        prefs = NotificationPreference(quiet_hours_start=time(22, 0), quiet_hours_end=time(6, 0))
        with patch("django.utils.timezone.localtime") as mock_lt:
            mock_lt.return_value.time.return_value = time(23, 30)
            assert NotificationHandler._in_quiet_hours(prefs) is True
            mock_lt.return_value.time.return_value = time(3, 0)
            assert NotificationHandler._in_quiet_hours(prefs) is True
            mock_lt.return_value.time.return_value = time(12, 0)
            assert NotificationHandler._in_quiet_hours(prefs) is False


@pytest.mark.django_db
class TestWebSocketGating:
    """WEBSOCKET_ENABLED=False must suppress the broadcast without affecting
    the notification itself — the client learns about it from its poll.

    Production is WSGI-only (cPanel), so no consumer is ever attached and the
    group_send only serialized a payload into a channel layer nobody read.
    """

    def test_broadcast_skipped_when_disabled(self, user, settings):
        settings.WEBSOCKET_ENABLED = False
        with mock.patch("apps.notifications.handler.get_channel_layer") as layer:
            NotificationHandler.send(user=user, title="Hi", message="There")
        layer.assert_not_called()

    def test_notification_is_still_created_when_disabled(self, user, settings):
        settings.WEBSOCKET_ENABLED = False
        NotificationHandler.send(user=user, title="Hi", message="There")
        assert Notification.objects.filter(user=user, title="Hi").exists()

    def test_broadcast_attempted_when_enabled(self, user, settings):
        # Called directly: send() defers dispatch via transaction.on_commit,
        # which never fires inside the test transaction.
        settings.WEBSOCKET_ENABLED = True
        notification = Notification.objects.create(user=user, title="Hi", message="There")
        with mock.patch("apps.notifications.handler.get_channel_layer") as layer:
            NotificationHandler._broadcast_notification(notification)
        layer.assert_called()

    def test_broadcast_method_is_a_noop_when_disabled(self, user, settings):
        settings.WEBSOCKET_ENABLED = False
        notification = Notification.objects.create(user=user, title="Hi", message="There")
        with mock.patch("apps.notifications.handler.get_channel_layer") as layer:
            NotificationHandler._broadcast_notification(notification)
        layer.assert_not_called()
