# Copyright 2018 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from contextlib import contextmanager

import odoo.tests
from odoo import http
from odoo.exceptions import AccessError, ValidationError
from odoo.tests import users
from odoo.tests.common import Form

from odoo.addons.mail.tests.common import mail_new_test_user


class ActivityCase(odoo.tests.HttpCase):
    def setUp(self):
        super().setUp()
        self.cron = self.env.ref("privacy_consent.cron_auto_consent")
        self.cron_mail_queue = self.env.ref("mail.ir_cron_mail_scheduler_action")
        self.sync_blacklist = self.env.ref("privacy_consent.sync_blacklist")
        self.mt_consent_consent_new = self.env.ref(
            "privacy_consent.mt_consent_consent_new"
        )
        self.mt_consent_acceptance_changed = self.env.ref(
            "privacy_consent.mt_consent_acceptance_changed"
        )
        self.mt_consent_state_changed = self.env.ref(
            "privacy_consent.mt_consent_state_changed"
        )
        # Some partners to ask for consent
        self.partners = self.env["res.partner"].create(
            [
                {"name": "consent-partner-0", "email": "partner0@example.com"},
                {"name": "consent-partner-1", "email": "partner1@example.com"},
                {"name": "consent-partner-2", "email": "partner2@example.com"},
                # Partner without email, on purpose
                {"name": "consent-partner-3"},
                # Partner with wrong email, on purpose
                {"name": "consent-partner-4", "email": "wrong-mail"},
            ]
        )
        # Blacklist some partners
        self.blacklists = self.env["mail.blacklist"]
        self.blacklists += self.blacklists._add("partner1@example.com")
        # Activity without consent
        self.activity_noconsent = self.env["privacy.activity"].create(
            {"name": "activity_noconsent", "description": "I'm activity 1"}
        )
        # Activity with auto consent, for all partners
        self.activity_auto = self.env["privacy.activity"].create(
            {
                "name": "activity_auto",
                "description": "I'm activity auto",
                "subject_find": True,
                "subject_domain": repr([("id", "in", self.partners.ids)]),
                "consent_required": "auto",
                "default_consent": True,
                "server_action_id": self.sync_blacklist.id,
            }
        )
        # Activity with manual consent, skipping partner 0
        self.activity_manual = self.env["privacy.activity"].create(
            {
                "name": "activity_manual",
                "description": "I'm activity 3",
                "subject_find": True,
                "subject_domain": repr([("id", "in", self.partners[1:].ids)]),
                "consent_required": "manual",
                "default_consent": False,
                "server_action_id": self.sync_blacklist.id,
            }
        )

    @contextmanager
    def _patch_build(self):
        self._built_messages = []
        IMS = self.env["ir.mail_server"]

        def _build_email(_self, email_from, email_to, subject, body, *args, **kwargs):
            self._built_messages.append(body)
            return _build_email.origin(
                _self,
                email_from,
                email_to,
                subject,
                body,
                *args,
                **kwargs,
            )

        try:
            IMS._patch_method("build_email", _build_email)
            yield
        finally:
            IMS._revert_method("build_email")


@odoo.tests.tagged("post_install", "-at_install")
class ActivityFlow(ActivityCase):
    def check_activity_auto_properly_sent(self):
        """Check emails sent by ``self.activity_auto``."""
        # Check pending mails
        for consent in self.activity_auto.consent_ids:
            self.assertEqual(consent.state, "draft")
            self.assertEqual(len(consent.message_ids), 2)
        # Check sent mails
        with self._patch_build():
            self.cron_mail_queue.method_direct_trigger()
        for consent in self.activity_auto.consent_ids:
            good_email = "@" in (consent.partner_id.email or "")
            self.assertEqual(
                consent.state,
                "sent" if good_email else "draft",
            )
            self.assertEqual(len(consent.message_ids), 2)
            # message notifies creation
            self.assertTrue(
                self.mt_consent_consent_new in consent.message_ids.mapped("subtype_id")
            )
            # message notifies subject
            # Placeholder links should be logged
            message_subject = consent.message_ids.filtered(
                lambda x: x.subtype_id != self.mt_consent_consent_new
            )
            self.assertIn("/privacy/consent/accept/", message_subject.body)
            self.assertIn("/privacy/consent/reject/", message_subject.body)
            # Tokenized links shouldn't be logged
            self.assertNotIn(consent._url(True), message_subject.body)
            self.assertNotIn(consent._url(False), message_subject.body)
            # without state change (only in test mode)
            self.assertTrue(
                self.mt_consent_state_changed
                not in consent.message_ids.mapped("subtype_id")
            )
            # Partner's is_blacklisted should be synced with default consent
            self.assertFalse(consent.partner_id.is_blacklisted)
            # Check the sent message was built properly tokenized
            accept_url, reject_url = map(consent._url, (True, False))
            for body in self._built_messages:
                if accept_url in body and reject_url in body:
                    self._built_messages.remove(body)
                    break
            else:
                raise AssertionError("Some message body should have these urls")

    def test_default_template(self):
        """We have a good mail template by default."""
        good = self.env.ref("privacy_consent.template_consent")
        self.assertEqual(
            self.activity_noconsent.consent_template_id,
            good,
        )
        self.assertEqual(
            self.activity_noconsent.consent_template_default_body_html,
            good.body_html,
        )
        self.assertEqual(
            self.activity_noconsent.consent_template_default_subject,
            good.subject,
        )

    def test_find_subject_if_consent_required(self):
        """If user wants to require consent, it needs subjects."""
        # Test the onchange helper
        onchange_activity1 = self.env["privacy.activity"].new(
            self.activity_noconsent.copy_data()[0]
        )
        self.assertFalse(onchange_activity1.subject_find)
        onchange_activity1.consent_required = "auto"
        onchange_activity1._onchange_consent_required_subject_find()
        self.assertTrue(onchange_activity1.subject_find)
        # Test very dumb user that forces an error
        with self.assertRaises(ValidationError):
            self.activity_noconsent.consent_required = "manual"

    def test_template_required_auto(self):
        """Automatic consent activities need a template."""
        self.activity_noconsent.subject_find = True
        self.activity_noconsent.consent_template_id = False
        self.activity_noconsent.consent_required = "manual"
        with self.assertRaises(ValidationError):
            self.activity_noconsent.consent_required = "auto"

    def test_generate_manually(self):
        """Manually-generated consents work as expected."""
        for partner in self.partners:
            if "@" in (partner.email or ""):
                self.blacklists._remove(partner.email)
        result = self.activity_manual.action_new_consents()
        self.assertEqual(result["res_model"], "privacy.consent")
        consents = self.env[result["res_model"]].search(result["domain"])
        self.assertEqual(consents.mapped("state"), ["draft"] * 3)
        self.assertEqual(
            consents.mapped("partner_id.is_blacklisted"),
            [False] * 3,
        )
        self.assertEqual(consents.mapped("accepted"), [False] * 3)
        self.assertEqual(consents.mapped("last_metadata"), [False] * 3)
        # Check sent mails
        messages = consents.mapped("message_ids")
        self.assertEqual(len(messages), 3)
        subtypes = messages.mapped("subtype_id")
        self.assertTrue(subtypes & self.mt_consent_consent_new)
        self.assertFalse(subtypes & self.mt_consent_acceptance_changed)
        self.assertFalse(subtypes & self.mt_consent_state_changed)
        # Send one manual request
        action = consents[0].action_manual_ask()
        self.assertEqual(action["res_model"], "mail.compose.message")
        Composer = self.env[action["res_model"]].with_context(
            active_ids=consents[0].ids,
            active_model=consents._name,
            **action["context"],
        )
        composer_wizard = Form(Composer)
        self.assertIn(consents[0].partner_id.name, composer_wizard.body)
        composer_record = composer_wizard.save()
        with self._patch_build():
            composer_record.action_send_mail()
        # Check the sent message was built properly tokenized
        body = self._built_messages[0]
        self.assertIn(consents[0]._url(True), body)
        self.assertIn(consents[0]._url(False), body)
        messages = consents.mapped("message_ids") - messages
        self.assertEqual(len(messages), 1)
        self.assertNotEqual(messages.subtype_id, self.mt_consent_state_changed)
        self.assertEqual(consents.mapped("state"), ["sent", "draft", "draft"])
        self.assertEqual(
            consents.mapped("partner_id.is_blacklisted"),
            [True, False, False],
        )
        # Placeholder links should be logged
        self.assertTrue("/privacy/consent/accept/" in messages.body)
        self.assertTrue("/privacy/consent/reject/" in messages.body)
        # Tokenized links shouldn't be logged
        accept_url = consents[0]._url(True)
        reject_url = consents[0]._url(False)
        self.assertNotIn(accept_url, messages.body)
        self.assertNotIn(reject_url, messages.body)
        # Visit tokenized accept URL
        self.authenticate("portal", "portal")
        http.root.session_store.save(self.session)
        result = self.url_open(accept_url).text
        self.assertIn("accepted", result)
        self.assertIn(reject_url, result)
        self.assertIn(self.activity_manual.name, result)
        self.assertIn(self.activity_manual.description, result)
        consents.invalidate_recordset()
        self.assertEqual(consents.mapped("accepted"), [True, False, False])
        self.assertTrue(consents[0].last_metadata)
        self.assertFalse(consents[0].partner_id.is_blacklisted)
        self.assertEqual(consents.mapped("state"), ["answered", "draft", "draft"])
        self.assertEqual(
            consents[0].message_ids[0].subtype_id,
            self.mt_consent_acceptance_changed,
        )
        # Visit tokenized reject URL
        result = self.url_open(reject_url).text
        self.assertIn("rejected", result)
        self.assertIn(accept_url, result)
        self.assertIn(self.activity_manual.name, result)
        self.assertIn(self.activity_manual.description, result)
        consents.invalidate_recordset()
        self.assertEqual(consents.mapped("accepted"), [False, False, False])
        self.assertTrue(consents[0].last_metadata)
        self.assertTrue(consents[0].partner_id.is_blacklisted)
        self.assertEqual(consents.mapped("state"), ["answered", "draft", "draft"])
        self.assertEqual(
            consents[0].message_ids[0].subtype_id,
            self.mt_consent_acceptance_changed,
        )
        self.assertFalse(consents[1].last_metadata)

    def test_generate_automatically(self):
        """Automatically-generated consents work as expected."""
        result = self.activity_auto.action_new_consents()
        self.assertEqual(result["res_model"], "privacy.consent")
        self.check_activity_auto_properly_sent()

    def test_generate_cron(self):
        """Cron-generated consents work as expected."""
        self.cron.method_direct_trigger()
        self.check_activity_auto_properly_sent()

    def test_mail_template_without_links(self):
        """Cannot create mail template without needed links."""
        with self.assertRaises(ValidationError):
            self.activity_manual.consent_template_id.body_html = "No links :("


@odoo.tests.tagged("post_install", "-at_install")
class ActivitySecurity(ActivityCase):
    @classmethod
    def setUpClass(cls):
        """Create users based on privacy groups to tests ACLs"""
        super().setUpClass()

        # users
        cls.user_admin = cls.env.ref("base.user_admin")
        cls.user_privacy_user = mail_new_test_user(
            cls.env,
            company_id=cls.user_admin.company_id.id,
            country_id=cls.env.ref("base.be").id,
            groups="base.group_user,privacy.group_data_protection_user",
            login="user_privacy_user",
            name="Patrick Privacy User",
            notification_type="inbox",
            signature="--\nPatrick",
        )
        cls.user_privacy_manager = mail_new_test_user(
            cls.env,
            company_id=cls.user_admin.company_id.id,
            country_id=cls.env.ref("base.be").id,
            groups="base.group_user,privacy.group_data_protection_manager",
            login="user_privacy_manager",
            name="Patricia Privacy Manager",
            notification_type="inbox",
            signature="--\nPatricia",
        )

    @users("user_privacy_user")
    def test_consent_acl_user(self):
        """Users can read"""
        activity_manual = self.activity_manual.with_user(self.env.user)
        activity_manual.read(["name"])

        # can't create consents
        with self.assertRaises(AccessError):
            activity_manual.action_new_consents()

    @users("user_privacy_manager")
    def test_consent_acl_manager(self):
        """Managers have all rights"""
        activity_manual = self.activity_manual.with_user(self.env.user)
        activity_manual.read(["name"])

        result = activity_manual.action_new_consents()
        consents = self.env[result["res_model"]].search(result["domain"], limit=1)
        consents.read(["state"])
        consents.unlink()

    def test_consent_controller_security_noaccess(self):
        """Test no access granted scenarios, should raise a NotFound and no
        crash / error"""
        result = self.activity_manual.action_new_consents()
        consent = self.env[result["res_model"]].search(result["domain"], limit=1)

        for res_id, token in [
            (-1, consent._token()),
            (-1, ""),
            (consent.id, ""),
        ]:
            response = self.url_open(f"/privacy/consent/accept/{res_id}/{token}")
            self.assertEqual(response.status_code, 404)
