from plone import api
from ploneintranet import api as pi_api
from ploneintranet.notifications.sql import saconfig
from ploneintranet.notifications.testing import FunctionalTestCase
from ploneintranet.notifications.testing import IntegrationTestCase
from time import sleep
from transaction import commit
from unittest import mock

import datetime
import pika
import threading


class TestPushIntegration(IntegrationTestCase):
    def create_and_send_notifications(self):
        """Crfeate a status update that will fire a LoggedEvent that will be used
        to trigger the creaation ant the dispatch of some notifications
        """
        pi_api.microblog.statusupdate.create("Test")
        # We need to update the time of the notifications to be way in the past
        # so that they are all dispatched now
        session = saconfig.session()
        vary_past_date = datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC)
        session.query(saconfig.LoggedEvent).update(
            {saconfig.LoggedEvent.time: vary_past_date}
        )
        self.send_notifications()

    def send_notifications(self):
        with api.env.adopt_user("admin"):
            api.content.get_view(
                "schedule-notifications", self.portal, self.request.clone()
            )()
            api.content.get_view(
                "dispatch-notifications", self.portal, self.request.clone()
            )()

    def assertMockedCalledWith(self, mocked, calls):
        """We cannot rely on the order of the calls and we cannot cast to
        a set because the set is not hashable,
        so we need a fancy way to check this
        """
        observed = [call.args for call in mocked.mock_calls]
        self.assertEqual(len(observed), len(calls))
        for call in calls:
            self.assertIn(call, observed)

    def test_push_default_settings(self):
        """With default settings we send:
        - a push notification to update the counter
        - a push notification for the browser
        """
        with api.env.adopt_user("admin"):
            pi_api.userprofile.create("foo", approve=True)

        tool = api.portal.get_tool("ploneintranet_notifications")
        tool._notification_settings["foo"] = {"activity_posts": {"desktop"}}

        with mock.patch("ploneintranet.api.push._send") as mocked:
            # Push message are dispatched to everybody
            self.create_and_send_notifications()
            self.assertMockedCalledWith(
                mocked=mocked,
                calls=[
                    ("foo", "browser_new_notification"),
                    ({"foo"}, "notification_counter_changed"),
                ],
            )

    def test_push_settings_interactions_desktop_disabled(self):
        """When in the settings we do not select the desktop option, we send:
        - a push notification to update the counter
        """
        with api.env.adopt_user("admin"):
            pi_api.userprofile.create("foo", approve=True)

        tool = api.portal.get_tool("ploneintranet_notifications")
        tool._notification_settings["foo"] = {"activity_posts": {"bar"}}

        with mock.patch("ploneintranet.api.push._send") as mocked:
            # Push message are dispatched to everybody
            self.create_and_send_notifications()
            self.assertMockedCalledWith(
                mocked=mocked,
                calls=[
                    ({"foo"}, "notification_counter_changed"),
                ],
            )

    def test_push_does_not_keep_sending_notification_counter_changed(self):
        with api.env.adopt_user("admin"):
            pi_api.userprofile.create("foo", approve=True)

        tool = api.portal.get_tool("ploneintranet_notifications")
        tool._notification_settings["foo"] = {"activity_posts": {"desktop"}}

        with mock.patch("ploneintranet.api.push._send") as mocked:
            self.create_and_send_notifications()
            # We actually have a new unread notification
            self.assertEqual(mocked.call_count, 2)
            self.assertMockedCalledWith(
                mocked=mocked,
                calls=[
                    ("foo", "browser_new_notification"),
                    ({"foo"}, "notification_counter_changed"),
                ],
            )

        # Triggering a new send without any new notifications
        # should not trigger the notification_counter_changed
        with mock.patch("ploneintranet.api.push._send") as mocked:
            self.send_notifications()
            self.assertEqual(mocked.call_count, 0)


class TestPushFunctional(FunctionalTestCase):
    def setUp(self):
        self.portal = self.layer["portal"]
        self.request = self.layer["request"]

    def create_and_send_notifications(self):
        """Crfeate a status update that will fire a LoggedEvent that will be used
        to trigger the creaation ant the dispatch of some notifications
        """
        pi_api.microblog.statusupdate.create("Test")
        # We need to update the time of the notifications to be way in the past
        # so that they are all dispatched now
        session = saconfig.session()
        vary_past_date = datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC)
        session.query(saconfig.LoggedEvent).update(
            {saconfig.LoggedEvent.time: vary_past_date}
        )
        with api.env.adopt_user("admin"):
            api.content.get_view(
                "schedule-notifications", self.portal, self.request.clone()
            )()
            api.content.get_view(
                "dispatch-notifications", self.portal, self.request.clone()
            )()

    def test_push_functional(self):
        with api.env.adopt_user("admin"):
            pi_api.userprofile.create("foo", approve=True)

        # Ensure we just have a notification for the counter
        tool = api.portal.get_tool("ploneintranet_notifications")
        tool._notification_settings["foo"] = {"activity_posts": set()}

        # Set up a mock connection to the rabbitmq server and configure the exchange
        connection = pika.BlockingConnection(
            pika.ConnectionParameters(host="localhost")
        )
        channel = connection.channel()
        exchange = api.portal.get_registry_record("ploneintranet.layout.push_exchange")
        channel.exchange_declare(exchange=exchange, exchange_type="topic", durable=True)
        result = channel.queue_declare("", exclusive=True)
        queue_name = result.method.queue
        channel.queue_bind(exchange=exchange, queue=queue_name, routing_key="foo")

        def callback(*args):
            callback.called_with_args = args
            channel = args[0]
            channel.stop_consuming()

        channel.basic_consume(
            queue=queue_name, on_message_callback=callback, auto_ack=True
        )

        thread = threading.Thread(target=channel.start_consuming, daemon=True)
        thread.start()

        self.create_and_send_notifications()

        self.assertRaises(AttributeError, getattr, callback, "called_with_args")
        # The push hook will happen when the transaction is committed
        commit()

        counter = 0
        while thread.is_alive():
            sleep(0.1)
            counter += 1
            self.assertLess(
                counter, 10, "It seems we did not received the message from rabbitmq"
            )

        self.assertEqual(callback.called_with_args[-1], b"notification_counter_changed")
