from collective.workspace.interfaces import IWorkspace
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.microblog.interfaces import IMicroblogContext
from ploneintranet.notifications.browser.scheduler import SchedulerView
from ploneintranet.notifications.sql import saconfig
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 ploneintranet.suite.testing import PLONEINTRANET_SUITE_BASE_INTEGRATION_LAYER
from ploneintranet.testing import temporary_registry_record
from ploneintranet.workspace.config import INTRANET_USERS_GROUP_ID
from ploneintranet.workspace.interfaces import IWorkspaceFolder
from Products.CMFPlone.utils import get_installer
from zope.event import notify
from zope.interface import alsoProvides
from zope.lifecycleevent import ObjectModifiedEvent

import datetime
import DateTime
import ploneintranet.notifications.tests
import pytz
import unittest

# Note that the integration tests here use mock workspaces,
# that do NOT FULLY IMPLEMENT the workspace security policies.


class PloneintranetSuiteNotificationsLayer(PloneSandboxLayer):
    defaultBases = (
        SQL_FIXTURE,
        PLONEINTRANET_SUITE_BASE_INTEGRATION_LAYER,
    )

    def setUpZope(self, app, configurationContext):
        self.loadZCML(package=ploneintranet.notifications.tests)

    def setUpPloneSite(self, portal):
        helpers.applyProfile(portal, "ploneintranet.notifications.tests:testing")
        pi_api.userprofile.create(
            email="a@example.com",
            username="arnold",
            properties={"first_name": "Arnold", "last_name": "A"},
            approve=True,
        )
        pi_api.userprofile.create(
            email="b@example.com",
            username="bernard",
            properties={"first_name": "Bernard", "last_name": "B"},
            approve=True,
        )
        pi_api.userprofile.create(
            email="c@example.com",
            username="claire",
            properties={"first_name": "Claire", "last_name": "C"},
            approve=True,
        )
        pi_api.userprofile.create(
            email="d@example.com",
            username="damian",
            properties={"first_name": "Damian", "last_name": "D"},
            approve=True,
        )
        pi_api.userprofile.create(
            email="e@example.com",
            username="edgar",
            properties={"first_name": "Edgar", "last_name": "E"},
            approve=True,
        )
        pi_api.userprofile.create(
            email="z@example.com",
            username="zoe",
            properties={"first_name": "Zoe", "last_name": "Z"},
            approve=False,
        )


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


class NotificationsTestCase(unittest.TestCase):
    layer = LAYER

    def setUp(self):
        super().setUp()
        self.session = session()
        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()


class TestRecipientsChannels(NotificationsTestCase):
    maxDiff = None

    def readable_recipients_channels(self, logged_event):
        observed = logged_event.recipients_channels
        helper_dict = {}
        for recipient_id in observed:
            helper_dict[recipient_id] = {
                channel: observed[recipient_id][channel].name
                for channel in observed[recipient_id]
            }
        return helper_dict

    def test_recipients_channels_activity_post_nomentions_default(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create("My first post")
            self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {
                "record": "activity_posts",
                "desktop": "activity_posts",
            },
            "claire": {
                "record": "activity_posts",
                "desktop": "activity_posts",
            },
            "damian": {
                "record": "activity_posts",
                "desktop": "activity_posts",
            },
            "edgar": {
                "record": "activity_posts",
                "desktop": "activity_posts",
            },
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_deleted_post_notification(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post", mention_ids=["claire"]
            )
            self.session.flush()

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

            # delete the post
            logged_event.context.delete()

            logged_event.notify_all()
            self.session.flush()

            self.assertEqual(self.session.query(Notification).count(), 0)

    def test_recipients_channels_activity_post_mentions_default(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post", mention_ids=["claire"]
            )
            self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()

        expected = {
            "bernard": {
                "record": "activity_posts",
                "desktop": "activity_posts",
            },
            "claire": {
                "record": "interactions",
                "desktop": "interactions",
                "email": "interactions",
            },
            "damian": {
                "record": "activity_posts",
                "desktop": "activity_posts",
            },
            "edgar": {
                "record": "activity_posts",
                "desktop": "activity_posts",
            },
        }

        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_activity_post_mentions_digest_on(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post", mention_ids=["claire"]
            )
            self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()

        expected = {
            "bernard": {
                "record": "activity_posts",
                "desktop": "activity_posts",
                "digest_daily": "activity_posts",
            },
            "claire": {
                "record": "interactions",
                "desktop": "interactions",
                "email": "interactions",
                "digest_daily": "interactions",
            },
            "damian": {
                "record": "activity_posts",
                "desktop": "activity_posts",
                "digest_daily": "activity_posts",
            },
            "edgar": {
                "record": "activity_posts",
                "desktop": "activity_posts",
                "digest_daily": "activity_posts",
            },
        }
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_workspace_member_post_mention_default(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = False

        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post", microblog_context=mock_ws, mention_ids=["claire"]
            )
            self.session.flush()

            logged_event = self.session.query(LoggedEvent).one()
            expected = {
                "bernard": {
                    "record": "workspace_member",
                    "desktop": "workspace_member",
                },
                "claire": {
                    "record": "interactions",
                    "desktop": "interactions",
                    "email": "interactions",
                },
            }
            observed = self.readable_recipients_channels(logged_event)
            self.assertDictEqual(expected, observed)

    def test_recipients_channels_workspace_open_post_mention_default(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = True

        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post", microblog_context=mock_ws, mention_ids=["claire"]
            )
            self.session.flush()
            logged_event = self.session.query(LoggedEvent).one()
            expected = {
                "bernard": {
                    "record": "workspace_member",
                    "desktop": "workspace_member",
                },
                "claire": {
                    "record": "interactions",
                    "desktop": "interactions",
                    "email": "interactions",
                },
                "damian": {"record": "workspace_open"},
                "edgar": {"record": "workspace_open"},
            }
            observed = self.readable_recipients_channels(logged_event)
            self.assertDictEqual(expected, observed)

    def test_recipients_channels_workspace_member_document_comment_default(self):
        """In a workspace, a comment is notified to both interactors and
        workspace members"""
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire", "damian"]
            for user_id in mock_ws.members:
                api.user.grant_roles(
                    username=user_id, obj=mock_ws, roles=["Reader", "Contributor"]
                )
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = False

        with api.env.adopt_user("arnold"):
            doc = api.content.create(
                container=mock_ws, type="Document", title="Arnold's document"
            )
            # this is normally fired by microblog.subscribers.content_created
            created = pi_api.microblog.statusupdate.create(
                content_context=doc, action_verb="created"
            )

        # verify that the content update is not notified
        # NB if you add a direct added handler, this test may break
        self.assertEqual(0, self.session.query(LoggedEvent).count())

        # Now add a comment
        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create(
                "My comment",
                microblog_context=mock_ws,
                thread_id=created.id,
                mention_ids=["claire"],
            )
            self.session.flush()
            logged_event = self.session.query(LoggedEvent).one()
            expected = {
                # arnold is the original author
                "arnold": {
                    "record": "interactions",
                    "desktop": "interactions",
                    "email": "interactions",
                },
                # claire was explicitly mentioned
                "claire": {
                    "record": "interactions",
                    "desktop": "interactions",
                    "email": "interactions",
                },
                "damian": {
                    "record": "workspace_member",
                    "desktop": "workspace_member",
                },
            }

            observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_workspace_content_created_published(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            mock_ws.remote_admin = None
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            alsoProvides(mock_ws, IWorkspaceFolder)
            mock_ws.accessible_to_intranet_users = True
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                doc1 = api.content.create(
                    container=mock_ws,
                    type="Document",
                    id="doc1",
                    title="Arnold's awesome document",
                )
                doc1.indexObject()  # or we don't have a UUID
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {"desktop": "workspace_member", "record": "workspace_member"}
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(LoggedEvent).delete()
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Reader", "Reviewer", "Member"]):
                api.content.transition(doc1, "publish")
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {"desktop": "workspace_member", "record": "workspace_member"},
            "claire": {"record": "workspace_open"},
            "damian": {"record": "workspace_open"},
            "edgar": {"record": "workspace_open"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_workspace_content_following(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            mock_ws.remote_admin = None
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            alsoProvides(mock_ws, IWorkspaceFolder)
            mock_ws.accessible_to_intranet_users = True
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.watch("workspace", mock_ws.UID(), "bernard")
        ploneintranet_network.follow("user", "arnold", "claire")
        ploneintranet_network.follow("tag", "foo", "damian")

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                doc1 = api.content.create(
                    container=mock_ws,
                    type="Document",
                    id="doc1",
                    subject=("foo"),
                    title="Arnold's awesome document",
                )
                doc1.indexObject()  # or we don't have a UUID
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        expected = {"bernard": {"desktop": "following", "record": "following"}}
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(LoggedEvent).delete()
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Reader", "Reviewer", "Member"]):
                api.content.transition(doc1, "publish")
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {"desktop": "following", "record": "following"},
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"desktop": "following", "record": "following"},
            "edgar": {"record": "workspace_open"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_activity_comment_nomentions_default(self):
        """Outside of a workspace, only thread participants are notified of comments"""
        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create("My first post")
            self.session.flush()
        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create("My comment", thread_id=parent.id)
            self.session.flush()

        self.assertEqual(2, self.session.query(LoggedEvent).count())
        # discard the post, check only the comment
        logged_event = self.session.query(LoggedEvent).all()[1]
        expected = {
            "arnold": {
                "record": "interactions",
                "desktop": "interactions",
                "email": "interactions",
            },
            "claire": {"record": "activity_comments"},
            "damian": {"record": "activity_comments"},
            "edgar": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_activity_mentions_some_users(self):
        """Outside of a workspace, only thread participants are notified of comments"""
        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create(
                "My first post", mention_ids=["claire", "damian"]
            )
            self.session.flush()

        # discard the post, check only the comment
        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create("My comment", thread_id=parent.id)
            self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "damian": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "edgar": {"record": "activity_comments"},
        }

        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_activity_mentions_too_many_users(self):
        many_user_ids = set()
        for count in range(30):
            with api.env.adopt_user("admin"):
                pi_api.userprofile.create(
                    email=f"x{count}@example.com",
                    username=f"x{count}",
                    properties={"first_name": f"X{count}", "last_name": "X"},
                    approve=True,
                )
                many_user_ids.add(f"x{count}")

        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create(
                "My first post", mention_ids=many_user_ids
            )
            self.session.flush()

        # discard the post, check only the comment
        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create("My comment", thread_id=parent.id)
            self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            # only the author
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # too many mentions, all are dropped from interactions
            # instead they receive a comment notification
            "claire": {"record": "activity_comments"},
            "damian": {"record": "activity_comments"},
            "edgar": {"record": "activity_comments"},
            "x0": {"record": "activity_comments"},
            "x1": {"record": "activity_comments"},
            "x10": {"record": "activity_comments"},
            "x11": {"record": "activity_comments"},
            "x12": {"record": "activity_comments"},
            "x13": {"record": "activity_comments"},
            "x14": {"record": "activity_comments"},
            "x15": {"record": "activity_comments"},
            "x16": {"record": "activity_comments"},
            "x17": {"record": "activity_comments"},
            "x18": {"record": "activity_comments"},
            "x19": {"record": "activity_comments"},
            "x2": {"record": "activity_comments"},
            "x20": {"record": "activity_comments"},
            "x21": {"record": "activity_comments"},
            "x22": {"record": "activity_comments"},
            "x23": {"record": "activity_comments"},
            "x24": {"record": "activity_comments"},
            "x25": {"record": "activity_comments"},
            "x26": {"record": "activity_comments"},
            "x27": {"record": "activity_comments"},
            "x28": {"record": "activity_comments"},
            "x29": {"record": "activity_comments"},
            "x3": {"record": "activity_comments"},
            "x4": {"record": "activity_comments"},
            "x5": {"record": "activity_comments"},
            "x6": {"record": "activity_comments"},
            "x7": {"record": "activity_comments"},
            "x8": {"record": "activity_comments"},
            "x9": {"record": "activity_comments"},
        }

        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_activity_mentions_comment_reply_thread(self):
        """In case of a deep tree of replies/comments, notifications fall
        into different categories depending on the recipient."""
        # we need extra users for the complexity of this test
        pi_api.userprofile.create(
            email="f@example.com",
            username="fred",
            properties={"first_name": "Fred", "last_name": "F"},
            approve=True,
        )
        pi_api.userprofile.create(
            email="g@example.com",
            username="gary",
            properties={"first_name": "Gary", "last_name": "G"},
            approve=True,
        )
        pi_api.userprofile.create(
            email="h@example.com",
            username="henry",
            properties={"first_name": "Henry", "last_name": "H"},
            approve=True,
        )

        # a. post
        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create(
                "My first post", mention_ids=["bernard"]
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"desktop": "activity_posts", "record": "activity_posts"},
            "damian": {"desktop": "activity_posts", "record": "activity_posts"},
            "edgar": {"desktop": "activity_posts", "record": "activity_posts"},
            "fred": {"desktop": "activity_posts", "record": "activity_posts"},
            "gary": {"desktop": "activity_posts", "record": "activity_posts"},
            "henry": {"desktop": "activity_posts", "record": "activity_posts"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # a.1 comment
        with api.env.adopt_user("claire"):
            comment_c = pi_api.microblog.statusupdate.create(
                "My comment", mention_ids=["damian"], thread_id=parent.id
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            # level1 author
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level1 mentioned
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # author claire of this comment receives nothing
            # level2 mentioned
            "damian": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "edgar": {"record": "activity_comments"},
            "fred": {"record": "activity_comments"},
            "gary": {"record": "activity_comments"},
            "henry": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # a.2 comment
        with api.env.adopt_user("edgar"):
            pi_api.microblog.statusupdate.create("My comment", thread_id=parent.id)
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            # level1 post author
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level1 post mentioned
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # siblinb level2 comment author
            "claire": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # sibling level2 comment mentioned
            "damian": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # author edgar gets nothing
            "fred": {"record": "activity_comments"},
            "gary": {"record": "activity_comments"},
            "henry": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # a.1.x reply
        with api.env.adopt_user("fred"):
            comment_c.add_comment(text="My reply", mention_ids=["gary"])
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            # level1 a. author
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level1 a. mentioned
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level2 a.1 author
            "claire": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level2 a.1 mentions
            "damian": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level2 a.2 edgar is in another subthread
            "edgar": {"record": "activity_comments"},
            # fred is the reply author, gets nothing
            # level 3 a.1.x mentioned
            "gary": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # henry is not involved in the thread at all
            "henry": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # a.1.y reply
        with api.env.adopt_user("arnold"):
            comment_c.add_comment(text="My reply")
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            # level1 a. author is also the reply author, gets nothing
            # level1 a. mentioned
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level2 a.1 author
            "claire": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level2 a.1 mentions
            "damian": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level2 a.2 edgar is in another subthread
            "edgar": {"record": "activity_comments"},
            # level 3 a.1.x sibling author
            "fred": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # level 3 a.1.x sibling mentioned
            "gary": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            # henry is not involved in the thread at all
            "henry": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_recipients_channels_newsitem_published_default(self):
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                newsitem = api.content.create(
                    container=api.portal.get(),
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                self.session.flush()
                self.assertEqual(0, self.session.query(LoggedEvent).count())

                api.content.transition(newsitem, "publish")
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()

        self.assertListEqual(
            [category.name for category in logged_event.categories],
            ["news_published"],
        )

        expected = {
            "bernard": {
                "record": "news_published",
                "desktop": "news_published",
            },
            "claire": {
                "record": "news_published",
                "desktop": "news_published",
            },
            "damian": {
                "record": "news_published",
                "desktop": "news_published",
            },
            "edgar": {
                "record": "news_published",
                "desktop": "news_published",
            },
        }

        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_following_user_post_comment_reply(self):
        """Following *always* overlaps with other categories."""
        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.follow("user", "arnold", "claire")

        # post
        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create(
                "My first post",
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {"desktop": "activity_posts", "record": "activity_posts"},
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"desktop": "activity_posts", "record": "activity_posts"},
            "edgar": {"desktop": "activity_posts", "record": "activity_posts"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # comment
        with api.env.adopt_user("bernard"):
            comment = pi_api.microblog.statusupdate.create(
                "My comment", thread_id=parent.id
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"record": "activity_comments"},
            "damian": {"record": "activity_comments"},
            "edgar": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # reply
        with api.env.adopt_user("arnold"):
            comment.add_comment(text="My reply")
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"record": "activity_comments"},
            "edgar": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_following_tag_post_comment_reply(self):
        """Following *always* overlaps with other categories."""
        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.follow("tag", "foo", "claire")
        ploneintranet_network.follow("tag", "fubar", "claire")

        # post
        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create(
                "My first post is tagged #foo", tags={"foo"}
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {"desktop": "activity_posts", "record": "activity_posts"},
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"desktop": "activity_posts", "record": "activity_posts"},
            "edgar": {"desktop": "activity_posts", "record": "activity_posts"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # comment
        with api.env.adopt_user("bernard"):
            comment = pi_api.microblog.statusupdate.create(
                "My comment tagged #bar", tags={"bar"}, thread_id=parent.id
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"record": "activity_comments"},
            "damian": {"record": "activity_comments"},
            "edgar": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # reply
        with api.env.adopt_user("arnold"):
            comment.add_comment(text="My reply tagged #fubar", tags={"fubar"})
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"record": "activity_comments"},
            "edgar": {"record": "activity_comments"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_following_workspace_post_comment_reply(self):
        """Following *always* overlaps with other categories."""
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = True
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.watch("workspace", mock_ws.UID(), "claire")

        # post
        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create(
                "My first post", microblog_context=mock_ws
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {"desktop": "workspace_member", "record": "workspace_member"},
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"record": "workspace_open"},
            "edgar": {"record": "workspace_open"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # comment
        with api.env.adopt_user("bernard"):
            comment = pi_api.microblog.statusupdate.create(
                "My comment", thread_id=parent.id
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"record": "workspace_open"},
            "edgar": {"record": "workspace_open"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # reply
        with api.env.adopt_user("arnold"):
            comment.add_comment(text="My reply")
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"record": "workspace_open"},
            "edgar": {"record": "workspace_open"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

    def test_following_document_comment_reply(self):
        """Following *always* overlaps with other categories."""
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire"]
            for user_id in mock_ws.members:
                api.user.grant_roles(
                    username=user_id, obj=mock_ws, roles=["Reader", "Contributor"]
                )
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = True
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        with api.env.adopt_user("arnold"):
            doc = api.content.create(
                container=mock_ws, type="Document", title="Arnold's document"
            )
            # this is normally fired by microblog.subscribers.content_created
            parent = pi_api.microblog.statusupdate.create(
                content_context=doc, action_verb="created"
            )

        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.follow("content", doc.UID(), "claire")

        # comment
        with api.env.adopt_user("bernard"):
            comment = pi_api.microblog.statusupdate.create(
                "My comment", thread_id=parent.id
            )
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "arnold": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"record": "workspace_open"},
            "edgar": {"record": "workspace_open"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)

        self.session.query(Notification).delete()
        self.session.query(LoggedEvent).delete()

        # reply
        with api.env.adopt_user("arnold"):
            comment.add_comment(text="My reply")
            self.session.flush()
        logged_event = self.session.query(LoggedEvent).one()
        expected = {
            "bernard": {
                "desktop": "interactions",
                "email": "interactions",
                "record": "interactions",
            },
            "claire": {"desktop": "following", "record": "following"},
            "damian": {"record": "workspace_open"},
            "edgar": {"record": "workspace_open"},
        }
        observed = self.readable_recipients_channels(logged_event)
        self.assertDictEqual(expected, observed)


class TestSubscribers(NotificationsTestCase):
    def test_statusupdate_added(self):
        """Log a dashboard post."""
        with api.env.adopt_user("arnold"):
            su = pi_api.microblog.statusupdate.create("My first post")
            self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        self.assertEqual("arnold", logged_event.actor_id)
        self.assertEqual(su, logged_event.context)
        self.assertEqual("statusupdate_added", logged_event.zope_event)

    def test_content_update_added_does_nothing(self):
        """Log a dashboard post."""
        with api.env.adopt_user("admin"):
            doc = api.content.create(
                container=api.portal.get(), type="Document", title="Arnold's document"
            )
            # this is normally fired by microblog.subscribers.content_created
            pi_api.microblog.statusupdate.create(
                content_context=doc, action_verb="created"
            )

        # verify that the content update is not notified
        # NB if you add a direct added handler, this test may break
        self.assertEqual(0, self.session.query(LoggedEvent).count())

    def test_newsitem_published(self):
        """Publish a news item"""
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                newsitem = api.content.create(
                    container=api.portal.get(),
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                self.session.flush()
                self.assertEqual(0, self.session.query(LoggedEvent).count())

                api.content.transition(newsitem, "publish")
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        self.assertEqual("arnold", logged_event.actor_id)
        self.assertEqual(newsitem, logged_event.context)
        self.assertEqual("newsitem_published", logged_event.zope_event)

    def test_workspace_content_created_published(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            alsoProvides(mock_ws, IWorkspaceFolder)
            mock_ws.accessible_to_intranet_users = True
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                doc1 = api.content.create(
                    container=mock_ws,
                    type="Document",
                    id="doc1",
                    title="Arnold's awesome document",
                )
                doc1.indexObject()  # or we don't have a UUID
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        self.assertEqual("arnold", logged_event.actor_id)
        self.assertEqual(doc1, logged_event.context)
        self.assertEqual("workspace_content", logged_event.zope_event)
        self.assertEqual("created", logged_event.data.get("event_type"))

        self.session.query(LoggedEvent).delete()
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Reader", "Reviewer", "Member"]):
                api.content.transition(doc1, "publish")
                self.session.flush()

        logged_event = self.session.query(LoggedEvent).one()
        self.assertEqual("arnold", logged_event.actor_id)
        self.assertEqual(doc1, logged_event.context)
        self.assertEqual("workspace_content", logged_event.zope_event)
        self.assertEqual("published", logged_event.data.get("event_type"))


class TestScheduler(NotificationsTestCase):
    """Test turning logged events into scheduled notifications"""

    def test_activity_post_nomentions_default(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create("My first post")
            self.session.flush()

        view = SchedulerView(None, None)
        view.create_notifications()
        self.session.flush()
        query = self.session.query(Notification)
        expected = {
            ("bernard", "record"),
            ("bernard", "desktop"),
            ("claire", "record"),
            ("claire", "desktop"),
            ("damian", "record"),
            ("damian", "desktop"),
            ("edgar", "record"),
            ("edgar", "desktop"),
        }

        self.assertSetEqual(
            {
                (notification.recipient_id, notification.channel)
                for notification in query
            },
            expected,
        )
        self.assertEqual(len(expected), query.count())

    def test_activity_post_mentions_default(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post", mention_ids=["claire"]
            )
            self.session.flush()

        view = SchedulerView(None, None)
        view.create_notifications()
        self.session.flush()
        query = self.session.query(Notification)
        expected = {
            ("bernard", "record"),
            ("bernard", "desktop"),
            ("claire", "record"),
            ("claire", "desktop"),
            ("claire", "email"),
            ("damian", "record"),
            ("damian", "desktop"),
            ("edgar", "desktop"),
            ("edgar", "record"),
        }

        self.assertSetEqual(
            {
                (notification.recipient_id, notification.channel)
                for notification in query
            },
            expected,
        )
        self.assertEqual(len(expected), query.count())


class TestCategoryMessages(NotificationsTestCase):
    def get_translated_message(self, notification, with_text=True):
        category_provider = notification.logged_event.category_provider
        for category in category_provider.categories:
            if category.name == notification.category:
                return api.portal.translate(
                    category.get_message_for(notification, with_text)
                )
        return "No get_message_for"

    def get_observed_messages(self, with_text=True, purge=True, channel="record"):
        if purge:
            self.session.query(Notification).delete()
        self.session.flush()
        view = SchedulerView(None, None)
        view.create_notifications()
        self.session.flush()
        notifications = self.session.query(Notification).filter(
            saconfig.Notification.channel == channel,
        )
        observed = {
            (
                notification.recipient_id,
                notification.category,
                self.get_translated_message(notification, with_text),
            )
            for notification in notifications
        }
        return observed

    def test_messages_activity_post_nomentions_short(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create("My first post")
            self.session.flush()

        expected = {
            (
                "bernard",
                "activity_posts",
                "Arnold A created a new post.",
            ),
            (
                "claire",
                "activity_posts",
                "Arnold A created a new post.",
            ),
            (
                "damian",
                "activity_posts",
                "Arnold A created a new post.",
            ),
            (
                "edgar",
                "activity_posts",
                "Arnold A created a new post.",
            ),
        }
        self.assertSetEqual(expected, self.get_observed_messages(with_text=False))

    def test_messages_activity_post_nomentions_default(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post",
            )
            self.session.flush()

        expected = {
            (
                "bernard",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
            (
                "claire",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
            (
                "damian",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
            (
                "edgar",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_activity_post_mentions_default(self):
        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post",
                mention_ids=["claire"],
                time=datetime.datetime(2021, 5, 4, 10, 20, tzinfo=pytz.UTC),
            )
            self.session.flush()
        expected = {
            (
                "bernard",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
            (
                "damian",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                "Arnold A mentioned you in the post “My first post” of May 4, 2021.",
            ),
            (
                "edgar",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_member_post_mention_default(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = False

        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post",
                microblog_context=mock_ws,
                mention_ids=["claire"],
                time=datetime.datetime(2021, 5, 4, 10, 20, tzinfo=pytz.UTC),
            )
            self.session.flush()

        expected = {
            (
                "bernard",
                "workspace_member",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                (
                    "Arnold A mentioned you in the post “My first post” of "
                    "May 4, 2021."
                ),
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_open_post_mention_default(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = True

        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post",
                microblog_context=mock_ws,
                mention_ids=["claire"],
                time=datetime.datetime(2021, 5, 4, 10, 20, tzinfo=pytz.UTC),
            )
            self.session.flush()
        expected = {
            (
                "bernard",
                "workspace_member",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                (
                    "Arnold A mentioned you in the post “My first post” of "
                    "May 4, 2021."
                ),
            ),
            (
                "damian",
                "workspace_open",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
            (
                "edgar",
                "workspace_open",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
        }
        # schedule and get the digest channel
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            with temporary_registry_record(
                "ploneintranet.notifications.enhanced_security",
                True,
            ):
                observed = self.get_observed_messages(channel="digest_daily")
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_member_post_mention_secure(self):
        """In secure mode, hide details of what's going on in private workspaces"""
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = False

        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post",
                microblog_context=mock_ws,
                mention_ids=["claire"],
                time=datetime.datetime(2021, 5, 4, 10, 20, tzinfo=pytz.UTC),
            )
            self.session.flush()

        # the record channel is unchanged
        expected = {
            (
                "bernard",
                "workspace_member",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                (
                    "Arnold A mentioned you in the post “My first post” of "
                    "May 4, 2021."
                ),
            ),
        }

        # schedule all channels, get the record channel
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            with temporary_registry_record(
                "ploneintranet.notifications.enhanced_security",
                True,
            ):
                observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        # the email and digest channels are secured
        expected = {
            (
                "bernard",
                "workspace_member",
                "Arnold A created a new post in a private workspace.",
            ),
            (
                "claire",
                "interactions",
                ("Arnold A mentioned you in a post of May 4, 2021."),
            ),
        }
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="digest_daily")

        self.assertSetEqual(expected, observed)
        expected = {
            (
                "claire",
                "interactions",
                ("Arnold A mentioned you in a post of May 4, 2021."),
            ),
        }
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="email")

    def test_messages_workspace_open_post_mention_secure(self):
        """The secure setting has no effect in open workspaces"""
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = True

        with api.env.adopt_user("arnold"):
            pi_api.microblog.statusupdate.create(
                "My first post",
                microblog_context=mock_ws,
                mention_ids=["claire"],
                time=datetime.datetime(2021, 5, 4, 10, 20, tzinfo=pytz.UTC),
            )
            self.session.flush()
        expected = {
            (
                "bernard",
                "workspace_member",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                (
                    "Arnold A mentioned you in the post “My first post” of "
                    "May 4, 2021."
                ),
            ),
            (
                "damian",
                "workspace_open",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
            (
                "edgar",
                "workspace_open",
                "Arnold A created the following post in Mock Workspace: “My first post”.",
            ),
        }
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            with temporary_registry_record(
                "ploneintranet.notifications.enhanced_security",
                True,
            ):
                # schedule all channels, get the record channel
                observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        # Same expected as before: no change
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="digest_daily")
        self.assertSetEqual(expected, observed)

        expected = {
            (
                "claire",
                "interactions",
                (
                    "Arnold A mentioned you in the post “My first post” of "
                    "May 4, 2021."
                ),
            ),
        }
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="email")
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_member_document_autoextract_mentions(self):
        """
        Content posts for created/published are not handled via statusupdates.
        HOWEVER if ploneintranet.microblog.extract.auto==True and a workspace
        post has an attachment, the attachment is stored in the workspace and we
        now have a workspace post with a content context.
        """
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire", "damian"]
            for user_id in mock_ws.members:
                api.user.grant_roles(
                    username=user_id, obj=mock_ws, roles=["Reader", "Contributor"]
                )
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = False

        with api.env.adopt_user("arnold"):
            doc = api.content.create(
                container=mock_ws, type="Document", title="Arnold's document"
            )
            # Not a content created event, but a post with an attachment
            posted = pi_api.microblog.statusupdate.create(
                "Hey guys what do you think about this file?",
                content_context=doc,
                time=datetime.datetime(2021, 5, 4, 10, 20, tzinfo=pytz.UTC),
            )
            self.session.flush()
            observed = self.get_observed_messages()

        # verify that unlike a content update, this content post is properly notified.
        # NB if you add a direct added handler, this test SHOULD NOT break.
        self.assertEqual(1, self.session.query(LoggedEvent).count())

        # Show post text, not attachment title
        expected = {
            (
                "bernard",
                "workspace_member",
                (
                    "Arnold A created the following post in Mock Workspace: "
                    "“Hey guys what do you think about this file?”."
                ),
            ),
            (
                "claire",
                "workspace_member",
                (
                    "Arnold A created the following post in Mock Workspace: "
                    "“Hey guys what do you think about this file?”."
                ),
            ),
            (
                "damian",
                "workspace_member",
                (
                    "Arnold A created the following post in Mock Workspace: "
                    "“Hey guys what do you think about this file?”."
                ),
            ),
        }
        self.assertSetEqual(expected, observed)

        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create(
                "My comment",
                microblog_context=mock_ws,
                content_context=doc,
                thread_id=posted.id,
                mention_ids=["claire"],
            )
            self.session.flush()
            observed = self.get_observed_messages()

        # Show post text, not attachment title
        expected = {
            (
                "arnold",
                "interactions",
                "Bernard B commented on your post “Hey guys what do you think about this file?” in Mock Workspace.",
            ),
            (
                "claire",
                "interactions",
                "Bernard B mentioned you in a comment on the post “Hey guys what do you think about this file?” in Mock Workspace.",
            ),
            (
                "damian",
                "workspace_member",
                "Bernard B commented on the post “Hey guys what do you think about this file?” in Mock Workspace.",
            ),
        }
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_member_post_comment_mention_default(self):
        """In a workspace, a comment is notified to both interactors and
        workspace members"""
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire", "damian"]
            for user_id in mock_ws.members:
                api.user.grant_roles(
                    username=user_id,
                    obj=mock_ws,
                    roles=["Reader", "Contributor", "Member"],
                )
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = False

        with api.env.adopt_user("arnold"):
            posted = pi_api.microblog.statusupdate.create(
                "My awesome post",
                microblog_context=mock_ws,
            )
            # we're not interested in the post messages, discard them
            self.session.flush()
            observed = self.get_observed_messages()

        # Now add a comment
        with api.env.adopt_user("bernard"):
            # Our mock workspace is not properly indexed. Bypass security.
            with unittest.mock.patch(
                "ploneintranet.microblog.statuscontainer.BaseStatusContainer._blacklist_microblogcontext_uuids",
                return_value=set(),
            ):
                pi_api.microblog.statusupdate.create(
                    "My comment",
                    microblog_context=mock_ws,
                    thread_id=posted.id,
                    mention_ids=["claire"],
                )
                self.session.flush()
                observed = self.get_observed_messages()
        expected = {
            (
                "arnold",
                "interactions",
                "Bernard B commented on your post “My awesome post” in Mock Workspace.",
            ),
            (
                "claire",
                "interactions",
                "Bernard B mentioned you in a comment on the post “My awesome post” in Mock Workspace.",
            ),
            (
                "damian",
                "workspace_member",
                "Bernard B commented on the post “My awesome post” in Mock Workspace.",
            ),
        }

        self.assertSetEqual(expected, observed)

    def test_messages_workspace_member_document_comment_mention_default(self):
        """In a workspace, a comment is notified to both interactors and
        workspace members"""
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire", "damian"]
            for user_id in mock_ws.members:
                api.user.grant_roles(
                    username=user_id, obj=mock_ws, roles=["Reader", "Contributor"]
                )
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            mock_ws.accessible_to_intranet_users = False

        with api.env.adopt_user("arnold"):
            doc = api.content.create(
                container=mock_ws, type="Document", title="Arnold's document"
            )
            # this is normally fired by microblog.subscribers.content_created
            created = pi_api.microblog.statusupdate.create(
                content_context=doc, action_verb="created"
            )

        # verify that the content update is not notified
        # NB if you add a direct added handler, this test may break
        self.assertEqual(0, self.session.query(LoggedEvent).count())

        # Now add a comment
        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create(
                "My comment",
                microblog_context=mock_ws,
                thread_id=created.id,
                mention_ids=["claire"],
            )
            self.session.flush()
            observed = self.get_observed_messages()

        expected = {
            # arnold is the document author
            (
                "arnold",
                "interactions",
                "Bernard B commented on your document “Arnold's document” in Mock Workspace.",
            ),
            # claire was explicitly mentioned
            (
                "claire",
                "interactions",
                "Bernard B mentioned you in a comment on the document “Arnold's document” in Mock Workspace.",
            ),
            (
                "damian",
                "workspace_member",
                "Bernard B commented on the document “Arnold's document” in Mock Workspace.",
            ),
        }
        self.assertSetEqual(expected, observed)

    def test_messages_activity_comment_reply_thread_with_post_mention(self):
        with api.env.adopt_user("arnold"):
            parent = pi_api.microblog.statusupdate.create(
                "My first post",
                mention_ids=["claire"],
                time=datetime.datetime(2021, 5, 4, 10, 20, tzinfo=pytz.UTC),
            )
            self.session.flush()
        expected = {
            (
                "bernard",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                "Arnold A mentioned you in the post “My first post” of May 4, 2021.",
            ),
            (
                "damian",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
            (
                "edgar",
                "activity_posts",
                "Arnold A created the following post: “My first post”.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create(
                "My first comment",
                thread_id=parent.id,
            )
            self.session.flush()
        expected = {
            (
                "arnold",
                "interactions",
                "Bernard B commented on your post: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                "Bernard B commented on the post “My first post” by Arnold A.",
            ),
            (
                "damian",
                "activity_comments",
                "Bernard B commented on the post “My first post” by Arnold A.",
            ),
            (
                "edgar",
                "activity_comments",
                "Bernard B commented on the post “My first post” by Arnold A.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        with api.env.adopt_user("damian"):
            damian_comment = pi_api.microblog.statusupdate.create(
                "My comment", thread_id=parent.id
            )
            self.session.flush()

        expected = {
            (
                "arnold",
                "interactions",
                "Damian D commented on your post: “My first post”.",
            ),
            (
                "bernard",
                "interactions",
                "Damian D commented on the post “My first post” by Arnold A.",
            ),
            (
                "claire",
                "interactions",
                "Damian D commented on the post “My first post” by Arnold A.",
            ),
            (
                "edgar",
                "activity_comments",
                "Damian D commented on the post “My first post” by Arnold A.",
            ),
        }

        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create(
                "My second comment",
                thread_id=parent.id,
            )
            self.session.flush()

        expected = {
            (
                "arnold",
                "interactions",
                "Bernard B commented on your post: “My first post”.",
            ),
            (
                "claire",
                "interactions",
                "Bernard B commented on the post “My first post” by Arnold A.",
            ),
            (
                "damian",
                "interactions",
                "Bernard B commented on the post “My first post” by Arnold A.",
            ),
            (
                "edgar",
                "activity_comments",
                "Bernard B commented on the post “My first post” by Arnold A.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        with api.env.adopt_user("edgar"):
            damian_comment.add_comment(text="My reply to Damian's comment")
            self.session.flush()

        expected = {
            # Toplevel post author
            (
                "arnold",
                "interactions",
                "Edgar E commented on your post: “My first post”.",
            ),
            # Bernard is not in this subthread
            (
                "bernard",
                "activity_comments",
                "Edgar E commented on the post “My first post” by Arnold A.",
            ),
            # Toplevel post mentioned
            (
                "claire",
                "interactions",
                "Edgar E commented on the post “My first post” by Arnold A.",
            ),
            # Ancestor comment author
            (
                "damian",
                "interactions",
                "Edgar E commented on the post “My first post” by Arnold A.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_newsitem_published(self):
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Member"]):
                mock_section = api.content.create(
                    container=api.portal.get(),
                    type="Folder",
                    id="product-announcements",
                    title="Product Announcements",
                )
                newsitem = api.content.create(
                    container=mock_section,
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                self.session.flush()
                self.assertEqual(0, self.session.query(LoggedEvent).count())

        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "publish")
                self.session.flush()

        expected = {
            (
                "arnold",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "claire",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "damian",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "edgar",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_newsitem_published_future_retracted(self):
        # prepare an unpublished newsitem
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Member"]):
                mock_section = api.content.create(
                    container=api.portal.get(),
                    type="Folder",
                    id="product-announcements",
                    title="Product Announcements",
                )
                newsitem = api.content.create(
                    container=mock_section,
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                newsitem.setEffectiveDate(DateTime.DateTime("2099-12-31 01:00 UTC"))
        self.session.flush()
        self.assertEqual(0, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())
        self.assertEqual(0, self.session.query(Notification).count())

        # publishing a news item in the future logs an event, but does not
        # dispatch notifications
        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "publish")
        self.session.flush()
        self.assertEqual(1, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())
        self.assertFalse(self.session.query(LoggedEvent).first().processed)

        # retracting the news item removes the unprocessed logged event
        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "reject")
        self.session.flush()
        self.assertEqual(0, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())

    def test_messages_newsitem_published_future_removed(self):
        # prepare an unpublished newsitem
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Member"]):
                mock_section = api.content.create(
                    container=api.portal.get(),
                    type="Folder",
                    id="product-announcements",
                    title="Product Announcements",
                )
                newsitem = api.content.create(
                    container=mock_section,
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                newsitem.setEffectiveDate(DateTime.DateTime("2099-12-31 01:00 UTC"))
        self.session.flush()
        self.assertEqual(0, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())
        self.assertEqual(0, self.session.query(Notification).count())

        # publishing a news item in the future logs an event, but does not
        # dispatch notifications
        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "publish")
        self.session.flush()
        self.assertEqual(1, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())
        self.assertFalse(self.session.query(LoggedEvent).first().processed)

        # deleting the news item removes the unprocessed logged event
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Owner", "Member"]):
                api.content.delete(newsitem)
        self.session.flush()
        self.assertEqual(0, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())

    def test_messages_newsitem_published_future_full_lifecycle(self):
        # prepare an unpublished newsitem
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Member"]):
                mock_section = api.content.create(
                    container=api.portal.get(),
                    type="Folder",
                    id="product-announcements",
                    title="Product Announcements",
                )
                newsitem = api.content.create(
                    container=mock_section,
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                newsitem.setEffectiveDate(DateTime.DateTime("2099-12-31 01:00 UTC"))
        self.session.flush()
        self.assertEqual(0, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())
        self.assertEqual(0, self.session.query(Notification).count())

        # publishing a news item in the future logs an event, but does not
        # dispatch notifications
        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "publish")
        self.session.flush()
        self.assertEqual(1, self.session.query(LoggedEvent).count())
        self.assertEqual(set(), self.get_observed_messages())

        # modifying the news item into the past does nothing by itself...
        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                newsitem.setEffectiveDate(DateTime.DateTime("1999-12-31 01:00 UTC"))
                notify(ObjectModifiedEvent(newsitem))
        self.session.flush()
        self.assertEqual(1, self.session.query(LoggedEvent).count())
        # ... but on the next dispatch call, it will dispatch notifications
        expected = {
            (
                "arnold",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "claire",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "damian",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "edgar",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
        }
        self.assertEqual(expected, self.get_observed_messages())
        # we have 4 notifications in the web channel, + 4 in the desktop push channel
        self.assertEqual(
            expected, self.get_observed_messages(purge=False, channel="desktop")
        )
        self.assertEqual(8, self.session.query(Notification).count())

        # The logged event is now marked as processed and will not be removed
        self.assertTrue(self.session.query(LoggedEvent).first().processed)

        # retracting the news item does not remove the already processed event and notifications
        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "reject")
        self.session.flush()
        self.assertEqual(1, self.session.query(LoggedEvent).count())
        self.assertEqual(expected, self.get_observed_messages(purge=False))
        self.assertEqual(8, self.session.query(Notification).count())

        # deleting the news item does not remove the already processed event and notifications
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Owner", "Member"]):
                api.content.delete(newsitem)
        self.session.flush()
        self.assertEqual(1, self.session.query(LoggedEvent).count())
        # Force a dispatch. Note that the test helper does not execute the normal
        # code path that would throw and handle BrokenNotificationError
        with self.assertRaises(AttributeError):
            self.get_observed_messages(purge=False)
        # We still retain the "old" notifications for the now-deleted news item
        self.assertEqual(8, self.session.query(Notification).count())

        # These notifications will not be shown, because the web view
        # catches MissingContextError(BrokenNotificationError)
        with self.assertRaises(saconfig.MissingContextError):
            self.session.query(Notification).first().get_message()

    def test_messages_newsitem_published_thread(self):
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Member"]):
                mock_section = api.content.create(
                    container=api.portal.get().news,
                    type="ploneintranet.news.section",
                    id="product-announcements",
                    title="Product Announcements",
                )
                newsitem = api.content.create(
                    container=mock_section,
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                self.session.flush()
                # content creation is not logged
                self.assertEqual(0, self.session.query(LoggedEvent).count())

        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "publish")
                content_update = pi_api.microblog.statusupdate.create(
                    "", content_context=newsitem
                )
                self.session.flush()
                # only the transition is logged, not the content update
                self.assertEqual(1, self.session.query(LoggedEvent).count())

        with api.env.adopt_user("claire"):
            pi_api.microblog.statusupdate.create(
                "Hey that's great", thread_id=content_update.id
            )
            self.session.flush()

        expected = {
            (
                "arnold",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "arnold",
                "interactions",
                "Claire C commented on your document “Arnold's awesome news” in "
                "Product Announcements.",
            ),
            (
                "bernard",
                "interactions",
                (
                    "Claire C commented on the document “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "claire",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "damian",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "damian",
                "activity_comments",
                "Claire C commented on the document “Arnold's awesome news” in "
                "Product Announcements.",
            ),
            (
                "edgar",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "edgar",
                "activity_comments",
                "Claire C commented on the document “Arnold's awesome news” in "
                "Product Announcements.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_newsitem_published_thread_folder(self):
        """Same as `test_messages_newsitem_published_thread` but the news items
        are within a folder instead of a news section."""
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Member"]):
                mock_section = api.content.create(
                    container=api.portal.get(),
                    type="Folder",
                    id="product-announcements",
                    title="Product Announcements",
                )
                newsitem = api.content.create(
                    container=mock_section,
                    type="News Item",
                    id="news1",
                    title="Arnold's awesome news",
                )
                self.session.flush()
                # content creation is not logged
                self.assertEqual(0, self.session.query(LoggedEvent).count())

        with api.env.adopt_user("bernard"):
            with api.env.adopt_roles(["Reviewer", "Member"]):
                api.content.transition(newsitem, "publish")
                content_update = pi_api.microblog.statusupdate.create(
                    "", content_context=newsitem
                )
                self.session.flush()
                # only the transition is logged, not the content update
                self.assertEqual(1, self.session.query(LoggedEvent).count())

        with api.env.adopt_user("claire"):
            pi_api.microblog.statusupdate.create(
                "Hey that's great", thread_id=content_update.id
            )
            self.session.flush()

        expected = {
            (
                "arnold",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "arnold",
                "interactions",
                "Claire C commented on your document “Arnold's awesome news” in "
                "Product Announcements.",
            ),
            (
                "bernard",
                "interactions",
                (
                    "Claire C commented on the document “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "claire",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "damian",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "damian",
                "activity_comments",
                "Claire C commented on the document “Arnold's awesome news” in "
                "Product Announcements.",
            ),
            (
                "edgar",
                "news_published",
                (
                    "Bernard B published the news item “Arnold's awesome news” in "
                    "Product Announcements."
                ),
            ),
            (
                "edgar",
                "activity_comments",
                "Claire C commented on the document “Arnold's awesome news” in "
                "Product Announcements.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_open_mention(self):
        with api.env.adopt_user("admin"):
            portal = self.layer["portal"]
            installer = get_installer(portal)
            installer.install_product("ploneintranet.workspace")
            workgroup1 = pi_api.workgroup.create(
                title="Admins group", members=["edgar"]
            )
            workgroup2 = pi_api.workgroup.create(
                title="Wrapping group", members=[workgroup1.getGroupId()]
            )

        with api.env.adopt_user("arnold"):
            ws = api.content.create(
                portal.workspaces,
                "ploneintranet.workspace.workspacefolder",
                "open-workspace",
                title="Open workspace",
                # accessible_to_intranet_users=True,
                # accessible_to_intranet_users=True,
            )
            ws.accessible_to_intranet_users = True
            ws.accessible_to_intranet_users = True
            IWorkspace(ws).add_to_team(workgroup2.getGroupId())

            # Update the graph
            api.content.get_view("pas-graph-init", portal)()

            pi_api.microblog.statusupdate.create(
                "Hey that's great",
                content_context=ws,
            )
            observed = self.get_observed_messages()
        expected = {
            (
                "bernard",
                "workspace_open",
                "Arnold A created the following post in Open workspace: “Hey that's great”.",
            ),
            (
                "claire",
                "workspace_open",
                "Arnold A created the following post in Open workspace: “Hey that's great”.",
            ),
            (
                "damian",
                "workspace_open",
                "Arnold A created the following post in Open workspace: “Hey that's great”.",
            ),
            (
                "edgar",
                "workspace_member",
                "Arnold A created the following post in Open workspace: “Hey that's great”.",
            ),
        }
        self.assertSetEqual(expected, observed)

        with api.env.adopt_user("bernard"):
            pi_api.microblog.statusupdate.create(
                "Arnold did a great job!",
                content_context=ws,
                mention_ids=["claire"],
                time=datetime.datetime(2023, 1, 1, 0, 00, tzinfo=pytz.UTC),
            )
            observed = self.get_observed_messages()
        expected = {
            (
                "arnold",
                "interactions",
                "Bernard B created the following post in Open workspace: “Arnold did a great job!”.",
            ),
            (
                "claire",
                "interactions",
                "Bernard B mentioned you in the post “Arnold did a great job!” of January 1, 2023.",
            ),
            (
                "damian",
                "workspace_open",
                "Bernard B created the following post in Open workspace: “Arnold did a great "
                "job!”.",
            ),
            (
                "edgar",
                "workspace_member",
                "Bernard B created the following post in Open workspace: “Arnold did a great job!”.",
            ),
        }
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_open_content(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            mock_ws.remote_admin = None
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            alsoProvides(mock_ws, IWorkspaceFolder)
            mock_ws.accessible_to_intranet_users = True
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.watch("workspace", mock_ws.UID(), "bernard")
        ploneintranet_network.follow("user", "arnold", "claire")
        ploneintranet_network.follow("tag", "foo", "damian")

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                doc1 = api.content.create(
                    container=mock_ws,
                    type="Document",
                    id="doc1",
                    subject=("foo"),
                    title="Arnold's awesome document",
                )
                doc1.indexObject()  # or we don't have a UUID
                self.session.flush()

        expected = {
            (
                "bernard",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            )
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        self.session.query(LoggedEvent).delete()
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Reader", "Reviewer", "Member"]):
                api.content.transition(doc1, "publish")
                self.session.flush()

        expected = {
            (
                "bernard",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "edgar",
                "workspace_open",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_member_content(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire", "damian", "edgar"]
            mock_ws.remote_admin = None
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            alsoProvides(mock_ws, IWorkspaceFolder)
            mock_ws.accessible_to_intranet_users = False
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.watch("workspace", mock_ws.UID(), "bernard")
        ploneintranet_network.follow("user", "arnold", "claire")
        ploneintranet_network.follow("tag", "foo", "damian")

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                doc1 = api.content.create(
                    container=mock_ws,
                    type="Document",
                    id="doc1",
                    subject=("foo"),
                    title="Arnold's awesome document",
                )
                doc1.indexObject()  # or we don't have a UUID
                self.session.flush()

        expected = {
            (
                "bernard",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "edgar",
                "workspace_member",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

        self.session.query(LoggedEvent).delete()
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Reader", "Reviewer", "Member"]):
                api.content.transition(doc1, "publish")
                self.session.flush()

        expected = {
            (
                "bernard",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "edgar",
                "workspace_member",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
        }
        observed = self.get_observed_messages()
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_open_content_secure(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard"]
            mock_ws.remote_admin = None
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            alsoProvides(mock_ws, IWorkspaceFolder)
            mock_ws.accessible_to_intranet_users = True
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.watch("workspace", mock_ws.UID(), "bernard")
        ploneintranet_network.follow("user", "arnold", "claire")
        ploneintranet_network.follow("tag", "foo", "damian")

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                doc1 = api.content.create(
                    container=mock_ws,
                    type="Document",
                    id="doc1",
                    subject=("foo"),
                    title="Arnold's awesome document",
                )
                doc1.indexObject()  # or we don't have a UUID
                self.session.flush()

        # schedule and get the digest channel
        expected = {
            (
                "bernard",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            )
        }
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            with temporary_registry_record(
                "ploneintranet.notifications.enhanced_security",
                True,
            ):
                observed = self.get_observed_messages(channel="digest_daily")
        self.assertSetEqual(expected, observed)

        # record is unchanged also
        expected = {
            (
                "bernard",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            )
        }
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="record")
        self.assertSetEqual(expected, observed)

        # Now publish the document

        self.session.query(LoggedEvent).delete()
        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Reader", "Reviewer", "Member"]):
                api.content.transition(doc1, "publish")
                self.session.flush()

        # digest is unchanged in an open workspace
        expected = {
            (
                "bernard",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "edgar",
                "workspace_open",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
        }
        # schedule and get the digest channel
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            with temporary_registry_record(
                "ploneintranet.notifications.enhanced_security",
                True,
            ):
                observed = self.get_observed_messages(channel="digest_daily")
        self.assertSetEqual(expected, observed)

        # the record channel is unchanged always
        expected = {
            (
                "bernard",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "edgar",
                "workspace_open",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
        }
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="record")
        self.assertSetEqual(expected, observed)

    def test_messages_workspace_member_content_secure(self):
        with api.env.adopt_user("admin"):
            mock_ws = api.content.create(
                type="Folder",
                id="mock-ws",
                title="Mock Workspace",
                container=api.portal.get(),
            )
            mock_ws.members = ["arnold", "bernard", "claire", "damian", "edgar"]
            mock_ws.remote_admin = None
            for user_id in mock_ws.members:
                api.user.grant_roles(username=user_id, obj=mock_ws, roles=["Reader"])
            alsoProvides(mock_ws, IMicroblogContext)
            alsoProvides(mock_ws, IWorkspace)
            alsoProvides(mock_ws, IWorkspaceFolder)
            mock_ws.accessible_to_intranet_users = False
            mock_ws.reindexObject()  # or else the security index is not up-to-date

        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        ploneintranet_network.watch("workspace", mock_ws.UID(), "bernard")
        ploneintranet_network.follow("user", "arnold", "claire")
        ploneintranet_network.follow("tag", "foo", "damian")

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Contributor", "Reviewer", "Member"]):
                doc1 = api.content.create(
                    container=mock_ws,
                    type="Document",
                    id="doc1",
                    subject=("foo"),
                    title="Arnold's awesome document",
                )
                doc1.indexObject()  # or we don't have a UUID
                self.session.flush()

        expected = {
            (
                "bernard",
                "following",
                "Arnold A created a document in a private workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A created a document in a private workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A created a document in a private workspace.",
            ),
            (
                "edgar",
                "workspace_member",
                "Arnold A created a document in a private workspace.",
            ),
        }
        # schedule and get the digest channel
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            with temporary_registry_record(
                "ploneintranet.notifications.enhanced_security",
                True,
            ):
                observed = self.get_observed_messages(channel="digest_daily")
        self.assertSetEqual(expected, observed)

        # record channel is unchanged always
        expected = {
            (
                "bernard",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "edgar",
                "workspace_member",
                "Arnold A created the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
        }
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="record")
        self.assertSetEqual(expected, observed)

        # Publish the document

        with api.env.adopt_user("arnold"):
            with api.env.adopt_roles(["Reader", "Reviewer", "Member"]):
                api.content.transition(doc1, "publish")
                self.session.flush()

        expected = {
            (
                "bernard",
                "following",
                "Arnold A published a document in a private workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A published a document in a private workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A published a document in a private workspace.",
            ),
            (
                "edgar",
                "workspace_member",
                "Arnold A published a document in a private workspace.",
            ),
        }
        # schedule and get the digest channel
        with temporary_registry_record(
            "ploneintranet.notifications.default_digest_frequency",
            "daily",
        ):
            with temporary_registry_record(
                "ploneintranet.notifications.enhanced_security",
                True,
            ):
                observed = self.get_observed_messages(channel="digest_daily")
        self.assertSetEqual(expected, observed)

        # the record channel is unchanged always
        expected = {
            (
                "bernard",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "claire",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "damian",
                "following",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
            (
                "edgar",
                "workspace_member",
                "Arnold A published the document “Arnold's awesome document” in Mock "
                "Workspace.",
            ),
        }
        with temporary_registry_record(
            "ploneintranet.notifications.enhanced_security",
            True,
        ):
            observed = self.get_observed_messages(purge=False, channel="record")
        self.assertSetEqual(expected, observed)

    def test_category_workspace_members_user_ids(self):
        from ploneintranet.notifications.adapters.category_provider import BaseCategory

        with api.env.adopt_user("admin"):
            portal = self.layer["portal"]
            installer = get_installer(portal)
            installer.install_product("ploneintranet.workspace")
            workgroup1 = pi_api.workgroup.create(title="workgroup1", members=["edgar"])
            workgroup2 = pi_api.workgroup.create(
                title="workgroup2", members=[workgroup1.getGroupId(), "damian"]
            )
            pi_api.workgroup.create(
                title=INTRANET_USERS_GROUP_ID,
                members=["arnold", "bernard", "claire", "damian", "edgar"],
            )

        with api.env.adopt_user("arnold"):
            ws = api.content.create(
                portal.workspaces,
                "ploneintranet.workspace.workspacefolder",
                "open-workspace",
                title="Open workspace",
            )
            ws.accessible_to_intranet_users = True
            ws.accessible_to_intranet_users = True
            # arnold is automatically a member as creator/admin
            # add claire directly
            IWorkspace(ws).add_to_team("claire")
            # add damian via workgroup2, and edgar via workgroup1
            IWorkspace(ws).add_to_team(workgroup2.getGroupId())
            IWorkspace(ws).add_to_team(INTRANET_USERS_GROUP_ID)

            # Update the graph
            api.content.get_view("pas-graph-init", portal)()

        member_ids = IWorkspace(ws).members
        self.assertSetEqual(
            # bernard is in all intranet users, but not a member
            {"arnold", "claire", "damian", "edgar"},
            BaseCategory().expand_principal_ids(member_ids),
        )
