from AccessControl import Unauthorized
from ploneintranet.api.env import system_timezone_CET
from ploneintranet.notifications.adapters.category_provider import BaseCategory
from ploneintranet.notifications.adapters.category_provider import BaseCategoryProvider
from ploneintranet.notifications.browser.aggregator import AggregatedBase
from ploneintranet.notifications.browser.aggregator import AggregatedNotifications
from ploneintranet.notifications.browser.aggregator import AggregatorView
from ploneintranet.notifications.interfaces import ICategoryProvider
from ploneintranet.notifications.interfaces import ILoggedEvent
from ploneintranet.notifications.interfaces import IProvideContextUID
from ploneintranet.notifications.interfaces import IResolveContextUID
from ploneintranet.notifications.sql.saconfig import LoggedEvent
from ploneintranet.notifications.sql.saconfig import Notification
from ploneintranet.notifications.sql.saconfig import session
from ploneintranet.notifications.testing import SQL_FIXTURE
from unittest import mock
from unittest import TestCase
from unittest.mock import Mock
from zope.component import adapter
from zope.component import getGlobalSiteManager
from zope.interface import implementer
from zope.interface import provider

import datetime

FAKE_OBJ = Mock()


@adapter(Mock)
@implementer(IProvideContextUID)
def mock_event_context_uid(value):
    return "Mock:mock"


@provider(IResolveContextUID)
def MockNotificationResolveContextUID(uid):
    return FAKE_OBJ


class AggregatedMockDigestView(AggregatedBase):
    def process(self):
        """Fake the digest email structure"""
        out = [self.context.recipient_id]
        for app, categories in self.context.grouped_notifications.items():
            out.append(app)
            for category, contexts in categories.items():
                out.append(category)
                for context, notifications in contexts.items():
                    out.append(context)
                    for notification in notifications:
                        out.append(
                            [
                                notification.logged_event.actor_id,
                                notification.logged_event.zope_event,
                            ]
                        )
        return out


class MockCategory(BaseCategory):
    name = "test_category"
    _app_id = "test_app"


class MockApp:
    def getId(self):
        return "test_app"


test_app = MockApp()


@adapter(ILoggedEvent)
@implementer(ICategoryProvider)
class MockCategoryProvider(BaseCategoryProvider):
    category_factories = [MockCategory]


class TestAggregatorUnit(TestCase):

    layer = SQL_FIXTURE
    maxDiff = 1000

    def setUp(self):
        """Custom shared utility setup for tests."""
        self.session = session()
        gsm = getGlobalSiteManager()
        gsm.registerUtility(MockNotificationResolveContextUID, name="Mock")
        gsm.registerAdapter(mock_event_context_uid)

        if not self.session.bind.table_names():
            # XXX there is a test isolation issue that requires us to
            # create the tables (they should have been created by the SQL_FIXTURE layer)
            from ploneintranet.notifications.sql.saconfig import Base

            Base.metadata.create_all(self.session.bind.engine, checkfirst=True)

    def tearDown(self):
        gsm = getGlobalSiteManager()
        gsm.unregisterUtility(MockNotificationResolveContextUID, name="Mock")
        gsm.unregisterAdapter(mock_event_context_uid)

    @system_timezone_CET()
    def test_aggregator_view(self):
        """Test that the aggregator works as expected"""
        fake_obj = Mock()
        with mock.patch("plone.api.content.get", return_value=fake_obj):
            logged_event = LoggedEvent(
                context=fake_obj,
                zope_event="foo",
                time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            )
            self.session.add(logged_event)
            self.session.flush()

        # Create a couple of notifications for the same recipient
        notification1 = Notification(
            recipient_id="allan",
            category="foo",
            channel="desktop",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )
        notification2 = Notification(
            recipient_id="allan",
            category="foo",
            channel="email",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )
        self.session.add(notification1)
        self.session.add(notification2)
        self.session.flush()

        # We have an aggregator view that know who should get notifications
        # And which notifications they should get
        aggregator_view = AggregatorView(None, None)
        self.assertSetEqual(aggregator_view.get_recipient_ids(), {"allan"})
        self.assertSetEqual(
            set(aggregator_view.get_recipient_notifications("allan")),
            {notification1, notification2},
        )

        # We can also get the notifications grouped by channel
        self.assertDictEqual(
            aggregator_view.get_recipient_notifications_by_channel("allan"),
            {"desktop": [notification1], "email": [notification2]},
        )

        with mock.patch("plone.api.content.get_view", return_value=Mock()):
            self.assertListEqual(
                aggregator_view.aggregate(), [notification1, notification2]
            )

            # Calling the aggregator again should not re-notify the
            # same notifications
            self.assertListEqual(aggregator_view.aggregate(), [])

    @system_timezone_CET()
    def test_aggregated_base_and_mock_digest(self):
        """Test that the aggregator works as expected"""

        logged_event = LoggedEvent(
            context=FAKE_OBJ,
            zope_event="event1",
            time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            actor_id="edith",
        )
        self.session.add(logged_event)
        self.session.flush()

        # Create a couple of notifications for the same recipient
        notification1 = Notification(
            recipient_id="allan",
            category="category1",
            channel="record",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )
        notification2 = Notification(
            recipient_id="allan",
            category="category1",
            channel="email",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )

        aggregated_notifications = AggregatedNotifications(
            "allan", [notification1, notification2]
        )
        aggregated_view = AggregatedMockDigestView(aggregated_notifications, None)
        expected = {None: {"category1": {FAKE_OBJ: [notification1, notification2]}}}
        observed = aggregated_view.context.grouped_notifications
        self.assertDictEqual(expected, observed)

        expected = [
            "allan",
            None,
            "category1",
            FAKE_OBJ,
            ["edith", "event1"],
            ["edith", "event1"],
        ]
        self.assertListEqual(expected, aggregated_view.process())

    @mock.patch("ploneintranet.api.apps.get", return_value=test_app)
    @system_timezone_CET()
    def test_aggregated_by_app(self, _):
        gsm = getGlobalSiteManager()
        gsm.registerAdapter(MockCategoryProvider, name="test_event")

        logged_event = LoggedEvent(
            context=FAKE_OBJ,
            zope_event="test_event",
            time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            actor_id="edith",
        )
        self.session.add(logged_event)
        self.session.flush()

        # Create a couple of notifications for the same recipient
        notification1 = Notification(
            recipient_id="allan",
            category="test_category",
            channel="record",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )
        notification2 = Notification(
            recipient_id="allan",
            category="test_category",
            channel="email",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )

        aggregated_notifications = AggregatedNotifications(
            "allan", [notification1, notification2]
        )
        aggregated_view = AggregatedMockDigestView(aggregated_notifications, None)
        expected = {
            test_app: {"test_category": {FAKE_OBJ: [notification1, notification2]}}
        }
        observed = aggregated_view.context.grouped_notifications
        self.assertDictEqual(expected, observed)

    @mock.patch("ploneintranet.api.apps.get", return_value=test_app)
    @system_timezone_CET()
    def test_aggregated_notifications__context(self, _):
        """Test that AggregatedNotifications deals with valid and invalid
        notifications."""
        gsm = getGlobalSiteManager()
        gsm.registerAdapter(MockCategoryProvider, name="test_event")

        logged_event = LoggedEvent(
            context=Mock(),
            zope_event="test_event",
        )
        self.session.add(logged_event)
        self.session.flush()

        # Create a couple of notifications for the same recipient
        notification = Notification(
            recipient_id="allan",
            category="test_category",
            channel="desktop",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )

        aggregated_notifications = AggregatedNotifications("allan", [notification])

        self.assertEqual(len(aggregated_notifications.notifications), 1)

    @system_timezone_CET()
    def test_aggregated_notifications__no_context(self):
        """Test that AggregatedNotifications deals with valid and invalid
        notifications."""
        gsm = getGlobalSiteManager()
        gsm.registerAdapter(MockCategoryProvider, name="test_event")

        logged_event = LoggedEvent(
            context=Mock(),
            zope_event="test_event",
        )
        self.session.add(logged_event)
        self.session.flush()

        # Create a couple of notifications for the same recipient
        notification = Notification(
            recipient_id="allan",
            category="test_category",
            channel="desktop",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )

        aggregated_notifications = AggregatedNotifications("allan", [notification])
        with mock.patch.object(
            LoggedEvent, "context", new_callable=mock.PropertyMock
        ) as logged_event_context:
            logged_event_context.return_value = None
            self.assertEqual(len(aggregated_notifications.notifications), 0)

    @system_timezone_CET()
    @mock.patch(
        "ploneintranet.notifications.sql.saconfig.Notification.get_app",
        return_value=test_app,
    )
    def test_aggregated_notifications__raises_unauthorized(self, _):
        """Test that AggregatedNotifications deals with valid and invalid
        notifications."""
        gsm = getGlobalSiteManager()
        gsm.registerAdapter(MockCategoryProvider, name="test_event")

        logged_event = LoggedEvent(
            context=Mock(),
            zope_event="test_event",
        )
        self.session.add(logged_event)
        self.session.flush()

        # Create a couple of notifications for the same recipient
        notification = Notification(
            recipient_id="allan",
            category="test_category",
            channel="desktop",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )

        aggregated_notifications = AggregatedNotifications("allan", [notification])
        with mock.patch.object(
            Notification,
            "assert_context_is_accessible_for_recipient",
            new_callable=mock.PropertyMock,
        ) as assert_context_is_accessible_for_recipient:
            assert_context_is_accessible_for_recipient.side_effect = Unauthorized
            self.assertEqual(len(aggregated_notifications.grouped_notifications), 0)

    @system_timezone_CET()
    @mock.patch(
        "ploneintranet.notifications.sql.saconfig.Notification.get_app",
        return_value=test_app,
    )
    def test_aggregated_notifications__raises_exception(self, _):
        """Test that AggregatedNotifications deals with valid and invalid
        notifications."""
        gsm = getGlobalSiteManager()
        gsm.registerAdapter(MockCategoryProvider, name="test_event")

        logged_event = LoggedEvent(
            context=Mock(),
            zope_event="test_event",
        )
        self.session.add(logged_event)
        self.session.flush()

        # Create a couple of notifications for the same recipient
        notification = Notification(
            recipient_id="allan",
            category="test_category",
            channel="desktop",
            scheduled_time=datetime.datetime(2023, 1, 1, 1, tzinfo=datetime.UTC),
            logged_event=logged_event,
        )

        aggregated_notifications = AggregatedNotifications("allan", [notification])
        with mock.patch.object(
            Notification,
            "assert_context_is_accessible_for_recipient",
            new_callable=mock.PropertyMock,
        ) as assert_context_is_accessible_for_recipient:
            assert_context_is_accessible_for_recipient.side_effect = AttributeError
            self.assertEqual(len(aggregated_notifications.grouped_notifications), 0)
