from DateTime import DateTime
from plone import api
from plone.app.testing import helpers
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer
from ploneintranet import api as pi_api
from ploneintranet.calendar.testing import INTEGRATION_TESTING
from ploneintranet.notifications.sql.saconfig import LoggedEvent
from ploneintranet.notifications.sql.saconfig import Notification
from ploneintranet.notifications.sql.saconfig import session
from ploneintranet.notifications.sql.saconfig import StaleNotificationError
from ploneintranet.notifications.testing import SQL_FIXTURE
from zope.i18nmessageid import Message
from zope.lifecycleevent import Attributes
from zope.lifecycleevent import modified

import unittest


class PloneintranetCalendarNotificationsLayer(PloneSandboxLayer):
    defaultBases = (SQL_FIXTURE, INTEGRATION_TESTING)

    def setUpZope(self, app, configurationContext):
        import ploneintranet.notifications.tests

        self.loadZCML(package=ploneintranet.notifications.tests)

    def setUpPloneSite(self, portal):
        helpers.applyProfile(portal, "ploneintranet.notifications.tests:testing")

        with api.env.adopt_user("admin"):
            pi_api.userprofile.create(
                "maria_manager",
                approve=True,
                properties={
                    "first_name": "Maria",
                    "last_name": "Manager",
                },
            )
            pi_api.userprofile.create(
                "otto_organizer",
                approve=True,
                properties={
                    "first_name": "Otto",
                    "last_name": "Organizer",
                },
            )
            api.user.grant_roles(
                username="maria_manager", obj=portal, roles=["Manager"]
            )
            api.user.grant_roles(
                username="otto_organizer",
                obj=portal.workspaces["event-import-workspace"],
                roles=["TeamManager", "TeamMember"],
            )

        with api.env.adopt_user("maria_manager"):
            api.content.create(
                container=portal.calendars,
                title="My calendar",
                type="ploneintranet.calendar.central_calendar",
            )

        with api.env.adopt_user("otto_organizer"):
            api.content.create(
                container=portal.workspaces["event-import-workspace"],
                title="My event",
                id="my-event",
                start=DateTime(),
                end=DateTime() + 1,
                whole_day=True,
                type="Event",
            )


FIXTURE = PloneintranetCalendarNotificationsLayer()
LAYER = IntegrationTesting(
    bases=(FIXTURE,), name="PloneintranetCalendarNotificationsLayer:Integration"
)


class TestCalendarNotifications(unittest.TestCase):
    layer = LAYER

    def setUp(self):
        super().setUp()
        self.portal = self.layer["portal"]
        self.request = self.layer["request"]
        self.workspace = self.portal.workspaces["event-import-workspace"]
        self.event = self.workspace["my-event"]
        self.calendar = self.portal.calendars["my-calendar"]

    def assertUserReceivesNotificationsOnChannels(self, user_id, channels):
        """Check that the user has the right number of notifications"""
        notifications = session.query(Notification).filter_by(recipient_id=user_id)
        self.assertSetEqual(
            {notification.recipient_id for notification in notifications},
            {user_id},
        )
        self.assertEqual(notifications.count(), len(channels))
        self.assertSetEqual(
            {notification.channel for notification in notifications},
            channels,
        )

    def assertInUserNotification(self, user_id, text):
        """Check that the user gets the right notification text"""
        notifications = session.query(Notification).filter_by(recipient_id=user_id)
        notification = notifications.first()

        self.assertEqual(len(notification.logged_event.category_provider.categories), 1)
        category = notification.logged_event.category_provider.categories[0]
        observed = category.get_message_for(notification)
        if isinstance(observed, Message):
            observed = api.portal.translate(observed)
        self.assertIn(text, observed)

    def assertStaleUserNotification(self, user_id):
        """Check that the user does not get a notification text"""
        notifications = session.query(Notification).filter_by(recipient_id=user_id)
        notification = notifications.first()

        self.assertEqual(len(notification.logged_event.category_provider.categories), 1)
        category = notification.logged_event.category_provider.categories[0]
        with self.assertRaises(StaleNotificationError):
            category.get_message_for(notification)

    def add_event_to_calendar_as_otto(self):
        with api.env.adopt_user("otto_organizer"):
            api.relation.create(self.event, self.calendar, "shared_calendar")
            modified(self.event, Attributes(None, "shared_calendar"))
            self.assertEqual(session.query(LoggedEvent).count(), 1)
            logged_event = session.query(LoggedEvent).one()
            self.assertEqual(logged_event.actor_id, "otto_organizer")
        return logged_event

    def test_auto_accepted_calendar_setting(self):
        self.calendar.automatically_accept = True
        logged_event = self.add_event_to_calendar_as_otto()

        logged_event.notify_all()
        self.assertUserReceivesNotificationsOnChannels(
            "maria_manager", {"desktop", "email", "record"}
        )
        self.assertInUserNotification(
            "maria_manager",
            "Otto Organizer added an event to a calendar you are maintaining: My calendar.",  # noqa: E501
        )

    def test_auto_accepted_organizer_is_calendar_moderator(self):
        api.user.grant_roles(
            username="otto_organizer", obj=self.portal, roles=["Manager"]
        )
        logged_event = self.add_event_to_calendar_as_otto()

        logged_event.notify_all()
        self.assertUserReceivesNotificationsOnChannels(
            "maria_manager", {"desktop", "email", "record"}
        )
        self.assertInUserNotification(
            "maria_manager",
            "Otto Organizer added an event to a calendar you are maintaining: My calendar.",  # noqa: E501
        )

        # invalidate notification
        with api.env.adopt_user("otto_organizer"):
            self.event.shared_calendar = None
            modified(self.event, Attributes(None, "shared_calendar"))
        self.assertStaleUserNotification("maria_manager")

    def test_need_approval(self):
        logged_event = self.add_event_to_calendar_as_otto()

        logged_event.notify_all()
        self.assertUserReceivesNotificationsOnChannels(
            "maria_manager", {"desktop", "email", "record"}
        )
        self.assertInUserNotification(
            "maria_manager",
            "Otto Organizer requested adding an event to a calendar you are maintaining: My calendar.",  # noqa: E501
        )

        # invalidate notification
        with api.env.adopt_user("otto_organizer"):
            self.event.shared_calendar = None
            modified(self.event, Attributes(None, "shared_calendar"))
        self.assertStaleUserNotification("maria_manager")

    def test_approval_accepted(self):
        with api.env.adopt_user("otto_organizer"):
            api.relation.create(self.event, self.calendar, "shared_calendar")
            api.content.transition(self.event, "submit")

        with api.env.adopt_user("maria_manager"):
            request = self.request.clone()
            request.method = "POST"
            request.form["accept"] = True
            request.form["uid"] = self.event.UID()

            view = api.content.get_view("panel-shared-event", self.calendar, request)
            view()

            logged_event = session.query(LoggedEvent).one()
            self.assertEqual(logged_event.actor_id, "maria_manager")

        logged_event.notify_all()

        self.assertUserReceivesNotificationsOnChannels(
            "otto_organizer", {"desktop", "email", "record"}
        )
        self.assertInUserNotification(
            "otto_organizer",
            "Maria Manager added your event 'My event' to a calendar: My calendar.",
        )

        # invalidate notification
        with api.env.adopt_user("otto_organizer"):
            self.event.shared_calendar = None
            modified(self.event, Attributes(None, "shared_calendar"))
        self.assertStaleUserNotification("otto_organizer")

    def test_approval_declined(self):
        with api.env.adopt_user("otto_organizer"):
            api.relation.create(self.event, self.calendar, "shared_calendar")
            api.content.transition(self.event, "submit")

        with api.env.adopt_user("maria_manager"):
            request = self.request.clone()
            request.method = "POST"
            request.form["decline"] = True
            request.form["uid"] = self.event.UID()

            view = api.content.get_view("panel-shared-event", self.calendar, request)
            view()

            logged_event = session.query(LoggedEvent).one()
            self.assertEqual(logged_event.actor_id, "maria_manager")

        logged_event.notify_all()

        self.assertUserReceivesNotificationsOnChannels(
            "otto_organizer", {"desktop", "email", "record"}
        )
        self.assertInUserNotification(
            "otto_organizer",
            "Maria Manager declined your request to add your event 'My event' to a calendar: My calendar.",  # noqa: E501
        )
        # invalidate notification
        with api.env.adopt_user("otto_organizer"):
            api.relation.create(self.event, self.calendar, "shared_calendar")
            api.content.transition(self.event, "submit")
        self.assertStaleUserNotification("otto_organizer")

    def test_update_event_workflow(self):
        with api.env.adopt_user("maria_manager"):
            other_calendar = api.content.create(
                container=self.portal.calendars,
                title="Other calendar",
                type="ploneintranet.calendar.central_calendar",
            )

        with api.env.adopt_user("otto_organizer"):
            self.assertEqual(api.content.get_state(self.event), "published")

            api.relation.create(self.event, self.calendar, "shared_calendar")
            modified(self.event, Attributes(None, "shared_calendar"))
            self.assertEqual(api.content.get_state(self.event), "pending")

            self.event.shared_calendar = None
            modified(self.event, Attributes(None, "shared_calendar"))
            self.assertEqual(api.content.get_state(self.event), "published")

            api.relation.create(self.event, other_calendar, "shared_calendar")
            modified(self.event, Attributes(None, "shared_calendar"))
            self.assertEqual(api.content.get_state(self.event), "pending")

    def test_demonstrate_plone_api_bug_on_retract(self):
        # demonstrate a limitation in plone.api.content
        with api.env.adopt_user("otto_organizer"):
            api.relation.create(self.event, self.calendar, "shared_calendar")
            api.content.transition(self.event, "submit")

            with self.assertRaises(api.exc.InvalidParameterError) as exc:
                api.content.transition(self.event, to_state="published")
            self.assertEqual(
                str(exc.exception),
                "Could not find workflow to set state to published on <Event at my-event>",  # noqa: E501
            )
            self.assertEqual(api.content.get_state(self.event), "pending")
            api.content.transition(self.event, transition="retract")
            self.assertEqual(api.content.get_state(self.event), "published")

    def test_event_modified(self):
        with api.env.adopt_user("maria_manager"):
            other_calendar = api.content.create(
                container=self.portal.calendars,
                title="Other calendar",
                type="ploneintranet.calendar.central_calendar",
            )

        with api.env.adopt_user("otto_organizer"):
            self.assertEqual(api.content.get_state(self.event), "published")

            api.relation.create(self.event, self.calendar, "shared_calendar")
            modified(self.event, Attributes(None, "shared_calendar"))
            self.assertEqual(api.content.get_state(self.event), "pending")

            self.event.shared_calendar = None
            modified(self.event, Attributes(None, "shared_calendar"))
            self.assertEqual(api.content.get_state(self.event), "published")

            api.relation.create(self.event, other_calendar, "shared_calendar")
            modified(self.event, Attributes(None, "shared_calendar"))
            self.assertEqual(api.content.get_state(self.event), "pending")

            # demonstrate a limitation in plone.api.content
            with self.assertRaises(api.exc.InvalidParameterError) as exc:
                api.content.transition(self.event, to_state="published")
            self.assertEqual(
                str(exc.exception),
                "Could not find workflow to set state to published on <Event at my-event>",  # noqa: E501
            )
            self.assertEqual(api.content.get_state(self.event), "pending")
            api.content.transition(self.event, transition="retract")
            self.assertEqual(api.content.get_state(self.event), "published")
