# # -*- coding: utf-8 -*-
# # Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import sys
from unittest.mock import patch

from odoo import Command
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.exceptions import AccessError, ValidationError
from odoo.tests import Form, common, tagged
from odoo.tools import mute_logger


def create_automation(self, **kwargs):
    """
    Create a transient automation with the given data and actions
    The created automation is cleaned up at the end of the calling test
    """
    vals = {'name': 'Automation'}
    vals.update(kwargs)
    actions_data = vals.pop('_actions', [])
    if not isinstance(actions_data, list):
        actions_data = [actions_data]
    automation_id = self.env['base.automation'].create(vals)
    action_ids = self.env['ir.actions.server'].create(
        [
            {
                'name': 'Action',
                'base_automation_id': automation_id.id,
                'model_id': automation_id.model_id.id,
                'usage': 'base_automation',
                **action,
            }
            for action in actions_data
        ]
    )
    action_ids.flush_recordset()
    automation_id.write({'action_server_ids': [Command.set(action_ids.ids)]})
    self.addCleanup(automation_id.unlink)
    return automation_id


@tagged('post_install', '-at_install')
class BaseAutomationTest(TransactionCaseWithUserDemo):
    def setUp(self):
        super(BaseAutomationTest, self).setUp()
        self.user_root = self.env.ref('base.user_root')
        self.user_admin = self.env.ref('base.user_admin')
        self.lead_model = self.env.ref('test_base_automation.model_base_automation_lead_test')
        self.project_model = self.env.ref('test_base_automation.model_test_base_automation_project')
        self.test_mail_template_automation = self.env['mail.template'].create(
            {
                'name': 'Template Automation',
                'model_id': self.env['ir.model']._get_id("base.automation.lead.thread.test"),
                'body_html': """&lt;div&gt;Email automation&lt;/div&gt;""",
            }
        )
        self.res_partner_1 = self.env['res.partner'].create({'name': 'My Partner'})

    def create_lead(self, **kwargs):
        vals = {
            'name': "Lead Test",
            'user_id': self.user_root.id,
        }
        vals.update(kwargs)
        lead = self.env['base.automation.lead.test'].create(vals)
        self.addCleanup(lead.unlink)
        return lead

    def create_line(self, **kwargs):
        vals = {
            'name': 'Line Test',
            'user_id': self.user_root.id,
        }
        vals.update(kwargs)
        line = self.env['base.automation.line.test'].create(vals)
        self.addCleanup(line.unlink)
        return line

    def create_project(self, **kwargs):
        vals = {'name': 'Project Test'}
        vals.update(kwargs)
        project = self.env['test_base_automation.project'].create(vals)
        self.addCleanup(project.unlink)
        return project

    def create_stage(self, **kwargs):
        vals = {'name': 'Stage Test'}
        vals.update(kwargs)
        stage = self.env['test_base_automation.stage'].create(vals)
        self.addCleanup(stage.unlink)
        return stage

    def create_tag(self, **kwargs):
        vals = {'name': 'Tag Test'}
        vals.update(kwargs)
        tag = self.env['test_base_automation.tag'].create(vals)
        self.addCleanup(tag.unlink)
        return tag

    def test_000_on_create_or_write(self):
        """
        Test case: on save, simple case
        - trigger: on_create_or_write
        """
        # --- Without the automation ---
        lead = self.create_lead()
        self.assertEqual(lead.state, 'draft')
        self.assertEqual(lead.user_id, self.user_root)

        # --- With the automation ---
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            _actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
        )

        # Write a lead should trigger the automation
        lead.write({'state': 'open'})
        self.assertEqual(lead.state, 'open')
        self.assertEqual(lead.user_id, self.user_demo)

        # Create a lead should trigger the automation
        lead2 = self.create_lead()
        self.assertEqual(lead2.state, 'draft')
        self.assertEqual(lead2.user_id, self.user_demo)

    def test_001_on_create_or_write(self):
        """
        Test case: on save, with filter_domain
        - trigger: on_create_or_write
        - apply when: state is 'draft'
        """
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            filter_domain="[('state', '=', 'draft')]",
            _actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
        )

        # Create a lead with state=open should not trigger the automation
        lead = self.create_lead(state='open')
        self.assertEqual(lead.state, 'open')
        self.assertEqual(lead.user_id, self.user_root)

        # Write a lead to state=draft should trigger the automation
        lead.write({'state': 'draft'})
        self.assertEqual(lead.state, 'draft')
        self.assertEqual(lead.user_id, self.user_demo)

        # Create a lead with state=draft should trigger the automation
        lead_2 = self.create_lead()
        self.assertEqual(lead_2.state, 'draft')
        self.assertEqual(lead_2.user_id, self.user_demo)

    def test_002_on_create_or_write(self):
        """
        Test case: on save, with filter_pre_domain and filter_domain
        - trigger: on_create_or_write
        - before update filter: state is 'open'
        - apply when: state is 'done'
        """
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            filter_pre_domain="[('state', '=', 'open')]",
            filter_domain="[('state', '=', 'done')]",
            _actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
        )

        # Create a lead with state=open should not trigger the automation
        lead = self.create_lead(state='open')
        self.assertEqual(lead.state, 'open')
        self.assertEqual(lead.user_id, self.user_root)

        # Write a lead to state=pending THEN to state=done should not trigger the automation
        lead.write({'state': 'pending'})
        self.assertEqual(lead.state, 'pending')
        self.assertEqual(lead.user_id, self.user_root)
        lead.write({'state': 'done'})
        self.assertEqual(lead.state, 'done')
        self.assertEqual(lead.user_id, self.user_root)

        # Write a lead from state=open to state=done should trigger the automation
        lead.write({'state': 'open'})
        self.assertEqual(lead.state, 'open')
        self.assertEqual(lead.user_id, self.user_root)
        lead.write({'state': 'done'})
        self.assertEqual(lead.state, 'done')
        self.assertEqual(lead.user_id, self.user_demo)

        # Create a lead with state=open then write it to state=done should trigger the automation
        lead_2 = self.create_lead(state='open')
        self.assertEqual(lead_2.state, 'open')
        self.assertEqual(lead_2.user_id, self.user_root)
        lead_2.write({'state': 'done'})
        self.assertEqual(lead_2.state, 'done')
        self.assertEqual(lead_2.user_id, self.user_demo)

        # Create a lead with state=done should trigger the automation,
        # as verifying the filter_pre_domain does not make sense on create
        lead_3 = self.create_lead(state='done')
        self.assertEqual(lead_3.state, 'done')
        self.assertEqual(lead_3.user_id, self.user_demo)

    def test_003_on_create_or_write(self):
        """ Check that the on_create_or_write trigger works as expected with trigger fields. """
        lead_state_field = self.env.ref('test_base_automation.field_base_automation_lead_test__state')
        automation = create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            trigger_field_ids=[Command.link(lead_state_field.id)],
            _actions={
                'state': 'code',
                'code': """
if env.context.get('old_values', None): # on write only
    record = model.browse(env.context['active_id'])
    record['name'] = record.name + 'X'""",
            },
        )

        lead = self.create_lead(name="X")
        lead.priority = True
        partner1 = self.res_partner_1
        lead.partner_id = partner1
        self.assertEqual(lead.name, 'X', "No update until now.")

        lead.state = 'open'
        self.assertEqual(lead.name, 'XX', "One update should have happened.")
        lead.state = 'done'
        self.assertEqual(lead.name, 'XXX', "One update should have happened.")
        lead.state = 'done'
        self.assertEqual(lead.name, 'XXX', "No update should have happened.")
        lead.state = 'cancel'
        self.assertEqual(lead.name, 'XXXX', "One update should have happened.")

        # change the rule to trigger on partner_id
        lead_partner_id_field = self.env.ref('test_base_automation.field_base_automation_lead_test__partner_id')
        automation.write({'trigger_field_ids': [Command.set([lead_partner_id_field.id])]})

        partner2 = self.env['res.partner'].create({'name': 'A new partner'})
        lead.name = 'X'
        lead.state = 'open'
        self.assertEqual(lead.name, 'X', "No update should have happened.")
        lead.partner_id = partner2
        self.assertEqual(lead.name, 'XX', "One update should have happened.")
        lead.partner_id = partner2
        self.assertEqual(lead.name, 'XX', "No update should have happened.")
        lead.partner_id = partner1
        self.assertEqual(lead.name, 'XXX', "One update should have happened.")
        lead.partner_id = partner1
        self.assertEqual(lead.name, 'XXX', "No update should have happened.")

    def test_010_recompute(self):
        """
        Test case: automation is applied whenever a field is recomputed
                   after a change on another model.
        - trigger: on_create_or_write
        - apply when: employee is True
        """
        partner = self.res_partner_1
        partner.write({'employee': False})

        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            filter_domain="[('employee', '=', True)]",
            _actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
        )

        lead = self.create_lead(partner_id=partner.id)
        self.assertEqual(lead.partner_id, partner)
        self.assertEqual(lead.employee, False)
        self.assertEqual(lead.user_id, self.user_root)

        # change partner, recompute on lead should trigger the rule
        partner.write({'employee': True})
        self.env.flush_all()  # ensures the recomputation is done
        self.assertEqual(lead.partner_id, partner)
        self.assertEqual(lead.employee, True)
        self.assertEqual(lead.user_id, self.user_demo)

    def test_011_recompute(self):
        """
        Test case: automation is applied whenever a field is recomputed.
                   The context contains the target field.
        - trigger: on_create_or_write
        """
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            _actions={
                'state': 'code',
                'code': """
if env.context.get('old_values', None):  # on write
    if 'user_id' in env.context['old_values'][record.id]:
        record.write({'is_assigned_to_admin': (record.user_id.id == 1)})""",
                },
            )

        partner = self.res_partner_1
        lead = self.create_lead(state='draft', partner_id=partner.id)
        self.assertEqual(lead.deadline, False)
        self.assertEqual(lead.is_assigned_to_admin, False)

        # change priority and user; this triggers deadline recomputation, and
        # the server action should set is_assigned_to_admin field to True
        lead.write({'priority': True, 'user_id': self.user_root.id})
        self.assertNotEqual(lead.deadline, False)
        self.assertEqual(lead.is_assigned_to_admin, True)

    def test_012_recompute(self):
        """
        Test case: automation is applied whenever a field is recomputed.
        - trigger: on_create_or_write
        - if updating fields: [deadline]
        """
        active_field = self.env.ref("test_base_automation.field_base_automation_lead_test__active")
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            trigger_field_ids=[Command.link(active_field.id)],
            _actions={
                'state': 'code',
                'code': """
if not env.context.get('old_values', None):  # on create
    record.write({'state': 'open'})
else:
    record.write({'priority': not record.priority})""",
            },
        )

        lead = self.create_lead(state='draft', priority=False)
        self.assertEqual(lead.state, 'open')  # the rule has set the state to open on create
        self.assertEqual(lead.priority, False)

        # change state; the rule should not be triggered
        lead.write({'state': 'pending'})
        self.assertEqual(lead.state, 'pending')
        self.assertEqual(lead.priority, False)

        # change active; the rule should be triggered
        lead.write({'active': False})
        self.assertEqual(lead.state, 'pending')
        self.assertEqual(lead.priority, True)

        # change active again; the rule should still be triggered
        lead.write({'active': True})
        self.assertEqual(lead.state, 'pending')
        self.assertEqual(lead.priority, False)

    def test_013_recompute(self):
        """
        Test case: automation is applied whenever a field is recomputed
        - trigger: on_create_or_write
        - if updating fields: [deadline]
        - before update filter: deadline is not set
        - apply when: deadline is set
        """
        deadline_field = self.env.ref("test_base_automation.field_base_automation_lead_test__deadline")
        create_automation(
            self,
            model_id=self.env['ir.model']._get_id('base.automation.lead.thread.test'),
            trigger='on_create_or_write',
            trigger_field_ids=[Command.link(deadline_field.id)],
            filter_pre_domain="[('deadline', '=', False)]",
            filter_domain="[('deadline', '!=', False)]",
            _actions={
                'state': 'mail_post',
                'mail_post_method': 'email',
                'template_id': self.test_mail_template_automation.id,
            },
        )

        send_mail_count = 0

        def _patched_send_mail(*args, **kwargs):
            nonlocal send_mail_count
            send_mail_count += 1

        patcher = patch('odoo.addons.mail.models.mail_template.MailTemplate.send_mail', _patched_send_mail)
        self.startPatcher(patcher)

        lead = self.env['base.automation.lead.thread.test'].create({
            'name': "Lead Test",
            'user_id': self.user_root.id,
        })
        self.addCleanup(lead.unlink)
        self.assertEqual(lead.priority, False)
        self.assertEqual(lead.deadline, False)
        self.assertEqual(send_mail_count, 0)

        lead.write({'priority': True})
        self.assertEqual(lead.priority, True)
        self.assertNotEqual(lead.deadline, False)
        self.assertEqual(send_mail_count, 1)

    def test_020_recursive(self):
        """ Check that a rule is executed recursively by a secondary change. """
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            _actions={
                'state': 'code',
                'code': """
if env.context.get('old_values', None):  # on write
    if 'partner_id' in env.context['old_values'][record.id]:
        record.write({'state': 'draft'})""",
            },
        )
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            filter_domain="[('state', '=', 'draft')]",
            _actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
        )

        lead = self.create_lead(state='open')
        self.assertEqual(lead.state, 'open')
        self.assertEqual(lead.user_id, self.user_root)

        # change partner; this should trigger the rule that modifies the state
        # and then the rule that modifies the user
        partner = self.res_partner_1
        lead.write({'partner_id': partner.id})
        self.assertEqual(lead.state, 'draft')
        self.assertEqual(lead.user_id, self.user_demo)

    def test_021_recursive(self):
        """ Check what it does with a recursive infinite loop """
        automations = [
            create_automation(
                self,
                model_id=self.lead_model.id,
                trigger='on_create_or_write',
                filter_domain="[('state', '=', 'draft')]",
                _actions={'state': 'code', 'code': "record.write({'state': 'pending'})"},
            ),
            create_automation(
                self,
                model_id=self.lead_model.id,
                trigger='on_create_or_write',
                filter_domain="[('state', '=', 'pending')]",
                _actions={'state': 'code', 'code': "record.write({'state': 'open'})"},
            ),
            create_automation(
                self,
                model_id=self.lead_model.id,
                trigger='on_create_or_write',
                filter_domain="[('state', '=', 'open')]",
                _actions={'state': 'code', 'code': "record.write({'state': 'done'})"},
            ),
            create_automation(
                self,
                model_id=self.lead_model.id,
                trigger='on_create_or_write',
                filter_domain="[('state', '=', 'done')]",
                _actions={'state': 'code', 'code': "record.write({'state': 'draft'})"},
            ),
        ]

        def _patch(*args, **kwargs):
            self.assertEqual(args[0], automations.pop(0))

        patcher = patch('odoo.addons.base_automation.models.base_automation.BaseAutomation._process', _patch)
        self.startPatcher(patcher)

        lead = self.create_lead(state='draft')
        self.assertEqual(lead.state, 'draft')
        self.assertEqual(len(automations), 0)  # all automations have been processed # CHECK if proper assertion ?

    def test_030_submodel(self):
        """ Check that a rule on a submodel is executed when the parent is modified. """
        # --- Without the automations ---
        line = self.create_line()
        self.assertEqual(line.user_id, self.user_root)

        lead = self.create_lead(line_ids=[(0, 0, {'name': 'Line', 'user_id': self.user_root.id})])
        self.assertEqual(lead.user_id, self.user_root)
        self.assertEqual(lead.line_ids.user_id, self.user_root)

        # --- With the automations ---
        comodel = self.env.ref('test_base_automation.model_base_automation_line_test')
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            _actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
        )
        create_automation(
            self,
            model_id=comodel.id,
            trigger='on_create_or_write',
            _actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
        )

        line = self.create_line(user_id=self.user_root.id)
        self.assertEqual(line.user_id, self.user_demo)  # rule on secondary model

        lead = self.create_lead(line_ids=[(0, 0, {'name': 'Line', 'user_id': self.user_root.id})])
        self.assertEqual(lead.user_id, self.user_demo)  # rule on primary model
        self.assertEqual(lead.line_ids.user_id, self.user_demo)  # rule on secondary model

    def test_040_modelwithoutaccess(self):
        """
        Ensure a domain on a M2O without user access doesn't fail.
        We create a base automation with a filter on a model the user haven't access to
        - create a group
        - restrict acl to this group and set only admin in it
        - create base.automation with a filter
        - create a record in the restricted model in admin
        - create a record in the non restricted model in demo
        """
        Model = self.env['base.automation.link.test']
        model_id = self.env.ref('test_base_automation.model_base_automation_link_test')
        Comodel = self.env['base.automation.linked.test']
        comodel_access = self.env.ref('test_base_automation.access_base_automation_linked_test')
        comodel_access.group_id = self.env['res.groups'].create({
            'name': "Access to base.automation.linked.test",
            "users": [Command.link(self.user_admin.id)],
        })

        # sanity check: user demo has no access to the comodel of 'linked_id'
        with self.assertRaises(AccessError):
            Comodel.with_user(self.user_demo).check_access('read')

        # check base automation with filter that performs Comodel.search()
        create_automation(
            self,
            model_id=model_id.id,
            trigger='on_create_or_write',
            filter_pre_domain="[('linked_id.another_field', '=', 'something')]",
            _actions={'state': 'code', 'code': 'action = [rec.name for rec in records]'},
        )
        Comodel.create([
            {'name': 'a first record', 'another_field': 'something'},
            {'name': 'another record', 'another_field': 'something different'},
        ])
        rec1 = Model.create({'name': 'a record'})
        rec1.write({'name': 'a first record'})
        rec2 = Model.with_user(self.user_demo).create({'name': 'another record'})
        rec2.write({'name': 'another value'})

        # check base automation with filter that performs Comodel.name_search()
        create_automation(
            self,
            model_id=model_id.id,
            trigger='on_create_or_write',
            filter_pre_domain="[('linked_id', '=', 'whatever')]",
            _actions={'state': 'code', 'code': 'action = [rec.name for rec in records]'},
        )
        rec3 = Model.create({'name': 'a random record'})
        rec3.write({'name': 'a first record'})
        rec4 = Model.with_user(self.user_demo).create({'name': 'again another record'})
        rec4.write({'name': 'another value'})

    def test_050_on_create_or_write_with_create_record(self):
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_create_or_write',
            _actions={
                'state': 'object_create',
                'crud_model_id': self.project_model.id,
                'value': 'foo',
            },
        )
        lead = self.create_lead()
        search_result = self.env['test_base_automation.project'].name_search('foo')
        self.assertEqual(len(search_result), 1, 'One record on the project model should have been created')

        lead.write({'name': 'renamed lead'})
        search_result = self.env['test_base_automation.project'].name_search('foo')
        self.assertEqual(len(search_result), 2, 'Another record on the project model should have been created')

        # ----------------------------
        # The following does not work properly as it is a known
        # limitation of the implementation since at least 14.0
        # -> AssertionError: 4 != 3 : Another record on the secondary model should have been created

        # # write on a field that is a dependency of another computed field
        # lead.write({'priority': True})
        # search_result = self.env['test_base_automation.project'].name_search('foo')
        # self.assertEqual(len(search_result), 3, 'Another record on the secondary model should have been created')
        # ----------------------------

    def test_060_on_stage_set(self):
        stage_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.project_model.id),
            ('name', '=', 'stage_id'),
        ])
        stage1 = self.create_stage()
        stage2 = self.create_stage()
        create_automation(
            self,
            model_id=self.project_model.id,
            trigger='on_stage_set',
            trigger_field_ids=[stage_field.id],
            filter_domain="[('stage_id', '=', %s)]" % stage1.id,
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )
        project = self.create_project()
        self.assertEqual(project.name, 'Project Test')
        project.write({'stage_id': stage1.id})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'stage_id': stage1.id})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'stage_id': stage2.id})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'stage_id': False})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'stage_id': stage1.id})
        self.assertEqual(project.name, 'Project Test!!')

    def test_070_on_user_set(self):
        user_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.lead_model.id),
            ('name', '=', 'user_id'),
        ])
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_user_set',
            trigger_field_ids=[user_field.id],
            filter_domain="[('user_id', '!=', False)]",
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )

        lead = self.create_lead()
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'user_id': self.user_demo.id})
        self.assertEqual(lead.name, 'Lead Test!!')
        lead.write({'user_id': self.user_demo.id})
        self.assertEqual(lead.name, 'Lead Test!!')
        lead.write({'user_id': self.user_admin.id})
        self.assertEqual(lead.name, 'Lead Test!!!')
        lead.write({'user_id': False})
        self.assertEqual(lead.name, 'Lead Test!!!')
        lead.write({'user_id': self.user_demo.id})
        self.assertEqual(lead.name, 'Lead Test!!!!')

    def test_071_on_user_set(self):
        # same test as above but with the user_ids many2many on a project
        user_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.project_model.id),
            ('name', '=', 'user_ids'),
        ])
        create_automation(
            self,
            model_id=self.project_model.id,
            trigger='on_user_set',
            trigger_field_ids=[user_field.id],
            filter_domain="[('user_ids', '!=', False)]",
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )

        project = self.create_project()
        self.assertEqual(project.name, 'Project Test')
        project.write({'user_ids': [Command.set([self.user_demo.id])]})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'user_ids': [Command.set([self.user_demo.id])]})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'user_ids': [Command.link(self.user_admin.id)]})
        self.assertEqual(project.name, 'Project Test!!')
        # Unlinking a user while there are still other users does trigger the automation
        # This behavior could be changed in the future but needs a bit of investigation
        project.write({'user_ids': [Command.unlink(self.user_admin.id)]})
        self.assertEqual(project.name, 'Project Test!!!')
        project.write({'user_ids': [Command.set([])]})
        self.assertEqual(project.name, 'Project Test!!!')
        project.write({'user_ids': [Command.set([self.user_demo.id])]})
        self.assertEqual(project.name, 'Project Test!!!!')

    def test_080_on_tag_set(self):
        tag_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.project_model.id),
            ('name', '=', 'tag_ids'),
        ])
        tag1 = self.create_tag()
        create_automation(
            self,
            model_id=self.project_model.id,
            trigger='on_tag_set',
            trigger_field_ids=[tag_field.id],
            filter_pre_domain="[('tag_ids', 'not in', [%s])]" % tag1.id,
            filter_domain="[('tag_ids', 'in', [%s])]" % tag1.id,
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )
        project = self.create_project()
        self.assertEqual(project.name, 'Project Test')
        project.write({'tag_ids': [Command.set([tag1.id])]})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'tag_ids': [Command.set([tag1.id])]})
        self.assertEqual(project.name, 'Project Test!')

        tag2 = self.create_tag()
        project.write({'tag_ids': [Command.link(tag2.id)]})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'tag_ids': [Command.clear()]})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'tag_ids': [Command.set([tag2.id])]})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'tag_ids': [Command.link(tag1.id)]})
        self.assertEqual(project.name, 'Project Test!!')

    def test_090_on_state_set(self):
        state_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.lead_model.id),
            ('name', '=', 'state'),
        ])

        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_state_set',
            trigger_field_ids=[state_field.id],
            filter_domain="[('state', '=', 'done')]",
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )

        lead = self.create_lead()
        self.assertEqual(lead.name, 'Lead Test')
        lead.write({'state': 'open'})
        self.assertEqual(lead.name, 'Lead Test')
        lead.write({'state': 'done'})
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'state': 'done'})
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'state': 'open'})
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'state': 'done'})
        self.assertEqual(lead.name, 'Lead Test!!')

    def test_100_on_priority_set(self):
        priority_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.project_model.id),
            ('name', '=', 'priority'),
        ])
        create_automation(
            self,
            model_id=self.project_model.id,
            trigger='on_priority_set',
            trigger_field_ids=[priority_field.id],
            filter_domain="[('priority', '=', '2')]",
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )
        project = self.create_project()
        self.assertEqual(project.name, 'Project Test')
        self.assertEqual(project.priority, '1')
        project.write({'priority': '0'})
        self.assertEqual(project.name, 'Project Test')
        project.write({'priority': '2'})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'priority': '2'})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'priority': '0'})
        self.assertEqual(project.name, 'Project Test!')
        project.write({'priority': '2'})
        self.assertEqual(project.name, 'Project Test!!')

    def test_110_on_archive(self):
        active_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.lead_model.id),
            ('name', '=', 'active'),
        ])
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_archive',
            trigger_field_ids=[active_field.id],
            filter_domain="[('active', '=', False)]",
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )
        lead = self.create_lead()
        self.assertEqual(lead.name, 'Lead Test')
        lead.write({'active': False})
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'active': True})
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'active': False})
        self.assertEqual(lead.name, 'Lead Test!!')
        lead.write({'active': False})
        self.assertEqual(lead.name, 'Lead Test!!')

    def test_110_on_unarchive(self):
        active_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.lead_model.id),
            ('name', '=', 'active'),
        ])
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_unarchive',
            trigger_field_ids=[active_field.id],
            filter_domain="[('active', '=', True)]",
            _actions={'state': 'object_write', 'evaluation_type': 'equation', 'update_path': 'name', 'value': "record.name + '!'"},
        )
        lead = self.create_lead()
        self.assertEqual(lead.name, 'Lead Test')
        lead.write({'active': False})
        self.assertEqual(lead.name, 'Lead Test')
        lead.write({'active': True})
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'active': False})
        self.assertEqual(lead.name, 'Lead Test!')
        lead.write({'active': True})
        self.assertEqual(lead.name, 'Lead Test!!')
        lead.write({'active': True})
        self.assertEqual(lead.name, 'Lead Test!!')

    def test_120_on_change(self):
        Model = self.env.get(self.lead_model.model)
        lead_name_field = self.env['ir.model.fields'].search([
            ('model_id', '=', self.lead_model.id),
            ('name', '=', 'name'),
        ])
        self.assertEqual(lead_name_field.name in Model._onchange_methods, False)
        create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_change',
            on_change_field_ids=[lead_name_field.id],
            _actions={'state': 'code', 'code': ""},
        )
        self.assertEqual(lead_name_field.name in Model._onchange_methods, True)

    def test_130_on_unlink(self):
        automation = create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_unlink',
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )

        called_count = 0

        def _patch(*args, **kwargs):
            nonlocal called_count
            called_count += 1
            self.assertEqual(args[0], automation)

        patcher = patch('odoo.addons.base_automation.models.base_automation.BaseAutomation._process', _patch)
        self.startPatcher(patcher)

        lead = self.create_lead()
        self.assertEqual(called_count, 0)
        lead.unlink()
        self.assertEqual(called_count, 1)

    def test_004_check_method(self):
        model = self.env["ir.model"]._get("base.automation.lead.test")
        TIME_TRIGGERS = [
           'on_time',
           'on_time_created',
           'on_time_updated',
        ]
        self.env["base.automation"].search([('trigger', 'in', TIME_TRIGGERS)]).active = False

        automation = self.env["base.automation"].create({
            "name": "Cron BaseAuto",
            "trigger": "on_time",
            "model_id": model.id,
        })
        self.assertFalse(automation.last_run)
        self.env["base.automation"]._check(False)
        self.assertTrue(automation.last_run)

    def test_005_check_model_with_different_rec_name_char(self):
        model = self.env["ir.model"]._get("base.automation.model.with.recname.char")

        create_automation(
            self,
            model_id=self.project_model.id,
            trigger='on_create_or_write',
            _actions={
                'state': 'object_create',
                'crud_model_id': model.id,
                'value': "Test _rec_name Automation",
            },
        )

        self.create_project()
        record_count = self.env[model.model].search_count([('description', '=', 'Test _rec_name Automation')])
        self.assertEqual(record_count, 1, "Only one record should have been created")

    def test_006_check_model_with_different_m2o_name_create(self):
        model = self.env["ir.model"]._get("base.automation.model.with.recname.m2o")

        create_automation(
            self,
            model_id=self.project_model.id,
            trigger='on_create_or_write',
            _actions={
                'state': 'object_create',
                'crud_model_id': model.id,
                'value': "Test _rec_name Automation",
            },
        )

        self.create_project()
        record_count = self.env[model.model].search_count([('user_id', '=', 'Test _rec_name Automation')])
        self.assertEqual(record_count, 1, "Only one record should have been created")

    def test_140_copy_should_copy_actions(self):
        """ Copying an automation should copy its actions. """
        automation = create_automation(
            self,
            model_id=self.lead_model.id,
            trigger='on_change',
            _actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
        )
        action_ids = automation.action_server_ids

        copy_automation = automation.copy()
        copy_action_ids = copy_automation.action_server_ids
        # Same number of actions but id should be different
        self.assertEqual(len(action_ids), 1)
        self.assertEqual(len(copy_action_ids), len(action_ids))
        self.assertNotEqual(copy_action_ids, action_ids)


@common.tagged('post_install', '-at_install')
class TestCompute(common.TransactionCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.env.ref('base.user_admin').write({
            'email': 'mitchell.admin@example.com',
        })

    def test_automation_form_view(self):
        automation_form = Form(self.env['base.automation'], view='base_automation.view_base_automation_form')

        # Initialize some fields
        automation_form.name = "Test Automation"
        automation_form.model_id = self.env.ref('test_base_automation.model_test_base_automation_project')
        automation_form.trigger = 'on_create_or_write'
        self.assertEqual(automation_form.trigger_field_ids.ids, [])
        self.assertEqual(automation_form.filter_domain, False)
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        # Changing the model must reset the trigger
        automation_form.model_id = self.env.ref('test_base_automation.model_base_automation_lead_test')
        self.assertEqual(automation_form.trigger, False)
        self.assertEqual(automation_form.trigger_field_ids.ids, [])
        self.assertEqual(automation_form.filter_domain, False)

        # Some triggers must preset a filter_domain and trigger_field_ids
        ## State is set to...
        automation_form.trigger = 'on_state_set'
        state_field_id = self.env.ref('test_base_automation.field_base_automation_lead_test__state').id
        self.assertEqual(automation_form.trigger_field_ids.ids, [state_field_id])
        self.assertEqual(automation_form.filter_domain, False)
        automation_form.trg_selection_field_id = self.env['ir.model.fields.selection'].search([
            ('field_id', '=', state_field_id),
            ('value', '=', 'pending'),
        ])
        self.assertEqual(automation_form.trigger_field_ids.ids, [state_field_id])
        self.assertEqual(automation_form.filter_domain, repr([('state', '=', 'pending')]))
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## Priority is set to...
        automation_form.model_id = self.env.ref('test_base_automation.model_test_base_automation_project')
        automation_form.trigger = 'on_priority_set'
        priority_field_id = self.env.ref('test_base_automation.field_test_base_automation_project__priority').id
        self.assertEqual(automation_form.trigger_field_ids.ids, [priority_field_id])
        self.assertEqual(automation_form.filter_domain, False)
        automation_form.trg_selection_field_id = self.env['ir.model.fields.selection'].search([
            ('field_id', '=', priority_field_id),
            ('value', '=', '2'),
        ])
        self.assertEqual(automation_form.trigger_field_ids.ids, [priority_field_id])
        self.assertEqual(automation_form.filter_domain, repr([('priority', '=', '2')]))
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## Stage is set to...
        automation_form.model_id = self.env.ref('test_base_automation.model_base_automation_lead_test')
        automation_form.trigger = 'on_stage_set'
        stage_field_id = self.env.ref('test_base_automation.field_base_automation_lead_test__stage_id').id
        self.assertEqual(automation_form.trigger_field_ids.ids, [stage_field_id])
        self.assertEqual(automation_form.filter_domain, False)
        new_lead_stage = self.env['test_base_automation.stage'].create({'name': 'New'})
        automation_form.trg_field_ref = new_lead_stage.id
        self.assertEqual(automation_form.filter_domain, repr([('stage_id', '=', new_lead_stage.id)]))
        self.assertEqual(automation_form.trigger_field_ids.ids, [stage_field_id])
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## User is set
        automation_form.trigger = 'on_user_set'
        self.assertEqual(automation_form.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__user_id').id
        ])
        self.assertEqual(automation_form.filter_domain, repr([('user_id', '!=', False)]))
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## On archive
        automation_form.trigger = 'on_archive'
        self.assertEqual(automation_form.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__active').id
        ])
        self.assertEqual(automation_form.filter_domain, repr([('active', '=', False)]))
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## On unarchive
        automation_form.trigger = 'on_unarchive'
        self.assertEqual(automation_form.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__active').id
        ])
        self.assertEqual(automation_form.filter_domain, repr([('active', '=', True)]))
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## Tag is set to...
        automation_form.trigger = 'on_tag_set'
        a_lead_tag = self.env['test_base_automation.tag'].create({'name': '*AWESOME*'})
        automation_form.trg_field_ref = a_lead_tag.id
        self.assertEqual(automation_form.filter_domain, repr([('tag_ids', 'in', [a_lead_tag.id])]))
        self.assertEqual(automation_form.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__tag_ids').id
        ])
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, repr([('tag_ids', 'not in', [a_lead_tag.id])]))

    def test_automation_form_view_on_change_filter_domain(self):
        a_lead_tag = self.env['test_base_automation.tag'].create({'name': '*AWESOME*'})
        automation = self.env['base.automation'].create({
            'name': 'Test Automation',
            'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
            'trigger': 'on_tag_set',
            'trg_field_ref': a_lead_tag.id,
        })
        self.assertEqual(automation.filter_pre_domain, repr([('tag_ids', 'not in', [a_lead_tag.id])]))
        self.assertEqual(automation.filter_domain, repr([('tag_ids', 'in', [a_lead_tag.id])]))
        self.assertEqual(automation.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__tag_ids').id
        ])
        self.assertEqual(automation.on_change_field_ids.ids, [])

        # Change the trigger to "On save" will erase the domains and the trigger fields
        automation_form = Form(automation, view='base_automation.view_base_automation_form')
        automation_form.trigger = 'on_create_or_write'
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)
        self.assertEqual(automation.filter_domain, False)
        self.assertEqual(automation.trigger_field_ids.ids, [])
        self.assertEqual(automation.on_change_field_ids.ids, [])

        # Change the domain will append each used field to the trigger fields
        automation_form.filter_domain = repr([('priority', '=', True), ('employee', '=', False)])
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)
        self.assertEqual(automation.filter_domain, repr([('priority', '=', True), ('employee', '=', False)]))
        self.assertSetEqual(set(automation.trigger_field_ids.ids), {
            self.env.ref('test_base_automation.field_base_automation_lead_test__priority').id,
            self.env.ref('test_base_automation.field_base_automation_lead_test__employee').id,
        })
        self.assertEqual(automation.on_change_field_ids.ids, [])

        # Change the trigger fields will not change the domain
        automation_form.trigger_field_ids.set(
            self.env.ref('test_base_automation.field_base_automation_lead_test__tag_ids')
        )
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)
        self.assertEqual(automation.filter_domain, repr([('priority', '=', True), ('employee', '=', False)]))
        self.assertEqual(automation.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__tag_ids').id
        ])
        self.assertEqual(automation.on_change_field_ids.ids, [])

        # Erase the domain will not change the trigger fields
        automation_form.filter_domain = False
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)
        self.assertEqual(automation.filter_domain, False)
        self.assertEqual(automation.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__tag_ids').id
        ])
        self.assertEqual(automation.on_change_field_ids.ids, [])

    def test_automation_form_view_time_triggers(self):
        # Starting from a "On save" automation
        on_save_automation = self.env['base.automation'].create({
            'name': 'Test Automation',
            'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
            'trigger': 'on_create_or_write',
            'filter_domain': repr([('employee', '=', False)]),
            'trigger_field_ids': self.env.ref('test_base_automation.field_base_automation_lead_test__employee')
        })

        automation = on_save_automation.copy()
        self.assertEqual(automation.filter_pre_domain, False)
        self.assertEqual(automation.filter_domain, repr([('employee', '=', False)]))
        self.assertEqual(automation.trg_date_id.id, False)
        self.assertEqual(automation.trigger_field_ids.ids, [
            self.env.ref('test_base_automation.field_base_automation_lead_test__employee').id
        ])

        # Changing to a time trigger must erase domains and trigger fields
        ## Change the trigger to "On time created"
        automation_form = Form(automation, view='base_automation.view_base_automation_form')
        automation_form.trigger = 'on_time_created'
        self.assertEqual(automation_form.filter_domain, False)
        self.assertEqual(automation_form.trg_date_id, self.env.ref('test_base_automation.field_base_automation_lead_test__create_date'))
        self.assertEqual(automation_form.trigger_field_ids.ids, [])
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## Change the trigger to "On time updated"
        automation = on_save_automation.copy()
        automation_form = Form(automation, view='base_automation.view_base_automation_form')
        automation_form.trigger = 'on_time_updated'
        self.assertEqual(automation_form.filter_domain, False)
        self.assertEqual(automation_form.trg_date_id, self.env.ref('test_base_automation.field_base_automation_lead_test__write_date'))
        self.assertEqual(automation_form.trigger_field_ids.ids, [])
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

        ## Change the trigger to "On time"
        automation = on_save_automation.copy()
        automation_form = Form(automation, view='base_automation.view_base_automation_form')
        automation_form.trigger = 'on_time'
        automation_form.trg_date_id = self.env.ref('test_base_automation.field_base_automation_lead_test__create_date')
        self.assertEqual(automation_form.filter_domain, False)
        self.assertEqual(automation_form.trg_date_id, self.env.ref('test_base_automation.field_base_automation_lead_test__create_date'))
        self.assertEqual(automation_form.trigger_field_ids.ids, [])
        automation = automation_form.save()
        self.assertEqual(automation.filter_pre_domain, False)

    def test_automation_form_view_with_default_values_in_context(self):
        # Use case where default model, trigger and filter_domain in context
        context = {
            'default_name': 'Test Automation',
            'default_model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
            'default_trigger': 'on_create_or_write',
            'default_filter_domain': repr([('state', '=', 'draft')]),
        }
        # Create form should be pre-filled with the default values
        automation = self.env['base.automation'].with_context(context)
        default_trigger_field_ids = [self.env.ref('test_base_automation.field_base_automation_lead_test__state').id]
        automation_form = Form(automation, view='base_automation.view_base_automation_form')
        self.assertEqual(automation_form.name, context.get('default_name'))
        self.assertEqual(automation_form.model_id.id, context.get('default_model_id'))
        self.assertEqual(automation_form.trigger, context.get('default_trigger'))
        self.assertEqual(automation_form.trigger_field_ids.ids, default_trigger_field_ids,
            'trigger_field_ids should match the fields in the default filter domain.')

        automation_form.trigger = 'on_stage_set'
        self.assertNotEqual(automation_form.trigger_field_ids.ids, default_trigger_field_ids,
            'When user changes trigger, the trigger_field_ids field should be updated')

    def test_inversion(self):
        """ If a stored field B depends on A, an update to the trigger for A
        should trigger the recomputaton of A, then B.

        However if a search() is performed during the computation of A
        ??? and _order is affected ??? a flush will be triggered, forcing the
        computation of B, based on the previous A.

        This happens if a rule has a non-empty filter_pre_domain, even if
        it's an empty list (``'[]'`` as opposed to ``False``).
        """
        company1 = self.env['res.partner'].create({
            'name': "Gorofy",
            'is_company': True,
        })
        company2 = self.env['res.partner'].create({
            'name': "Awiclo",
            'is_company': True
        })
        r = self.env['res.partner'].create({
            'name': 'Bob',
            'is_company': False,
            'parent_id': company1.id
        })
        self.assertEqual(r.display_name, 'Gorofy, Bob')
        r.parent_id = company2
        self.assertEqual(r.display_name, 'Awiclo, Bob')

        create_automation(
            self,
            model_id=self.env.ref('base.model_res_partner').id,
            filter_pre_domain=False,
            trigger='on_create_or_write',
            _actions={'state': 'code'},  # no-op action
        )
        r.parent_id = company1
        self.assertEqual(r.display_name, 'Gorofy, Bob')

        create_automation(
            self,
            model_id=self.env.ref('base.model_res_partner').id,
            filter_pre_domain='[]',
            trigger='on_create_or_write',
            _actions={'state': 'code'},  # no-op action
        )
        r.parent_id = company2
        self.assertEqual(r.display_name, 'Awiclo, Bob')

    def test_recursion(self):
        project = self.env['test_base_automation.project'].create({})

        # this action is executed every time a task is assigned to project
        create_automation(
            self,
            model_id=self.env.ref('test_base_automation.model_test_base_automation_task').id,
            trigger='on_create_or_write',
            filter_domain=repr([('project_id', '=', project.id)]),
            _actions={'state': 'code'},  # no-op action
        )

        # create one task in project with 10 subtasks; all the subtasks are
        # automatically assigned to project, too
        task = self.env['test_base_automation.task'].create({'project_id': project.id})
        subtasks = task.create([{'parent_id': task.id} for _ in range(10)])
        subtasks.flush_model()

        # This test checks what happens when a stored recursive computed field
        # is marked to compute on many records, and automation rules are
        # triggered depending on that field.  In this case, we trigger the
        # recomputation of 'project_id' on 'subtasks' by deleting their parent
        # task.
        #
        # An issue occurs when the domain of automation rules is evaluated by
        # method search(), because the latter flushes the fields to search on,
        # which are also the ones being recomputed.  Combined with the fact
        # that recursive fields are not computed in batch, this leads to a huge
        # amount of recursive calls between the automation rule and flush().
        #
        # The execution of task.unlink() looks like this:
        # - mark 'project_id' to compute on subtasks
        # - delete task
        # - flush()
        #   - recompute 'project_id' on subtask1
        #     - call compute on subtask1
        #     - in action, search([('id', 'in', subtask1.ids), ('project_id', '=', pid)])
        #       - flush(['id', 'project_id'])
        #         - recompute 'project_id' on subtask2
        #           - call compute on subtask2
        #           - in action search([('id', 'in', subtask2.ids), ('project_id', '=', pid)])
        #             - flush(['id', 'project_id'])
        #               - recompute 'project_id' on subtask3
        #                 - call compute on subtask3
        #                 - in action, search([('id', 'in', subtask3.ids), ('project_id', '=', pid)])
        #                   - flush(['id', 'project_id'])
        #                     - recompute 'project_id' on subtask4
        #                       ...
        limit = sys.getrecursionlimit()
        try:
            sys.setrecursionlimit(100)
            task.unlink()
        finally:
            sys.setrecursionlimit(limit)

    def test_mail_triggers(self):
        lead_model = self.env["ir.model"]._get("base.automation.lead.test")
        with self.assertRaises(ValidationError):
            create_automation(self, trigger="on_message_sent", model_id=lead_model.id)

        lead_thread_model = self.env["ir.model"]._get("base.automation.lead.thread.test")
        automation = create_automation(self, trigger="on_message_sent", model_id=lead_thread_model.id, _actions={
            "state": "object_write",
            "update_path": "active",
            "update_boolean_value": "false"
        })

        ext_partner = self.env["res.partner"].create({"name": "ext", "email": "email@server.com"})
        internal_partner = self.env["res.users"].browse(2).partner_id

        obj = self.env["base.automation.lead.thread.test"].create({"name": "test"})
        obj.message_subscribe([ext_partner.id, internal_partner.id])

        obj.message_post(author_id=internal_partner.id, message_type="comment", subtype_xmlid="mail.mt_comment")
        self.assertFalse(obj.active)

        obj.active = True
        obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment")
        self.assertTrue(obj.active)

        obj.message_post(author_id=ext_partner.id, message_type="comment")
        self.assertTrue(obj.active)

        obj.message_post(author_id=internal_partner.id, message_type="comment")
        self.assertTrue(obj.active)
        obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
        self.assertFalse(obj.active)

        obj.active = True
        # message doesn't have author_id, so it should be considered as external the automation should't be triggered
        obj.message_post(author_id=False, email_from="test_abla@test.test", message_type="email", subtype_xmlid="mail.mt_comment")
        self.assertTrue(obj.active)

        automation.trigger = "on_message_received"
        obj.active = True
        obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
        self.assertTrue(obj.active)

        obj.message_post(author_id=ext_partner.id, message_type="comment")
        self.assertTrue(obj.active)

        obj.message_post(author_id=ext_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
        self.assertFalse(obj.active)

        obj.active = True
        obj.message_post(author_id=ext_partner.id, subtype_xmlid="mail.mt_comment")
        self.assertTrue(obj.active)

        obj.message_post(author_id=False, email_from="test_abla@test.test", message_type="email", subtype_xmlid="mail.mt_comment")
        self.assertFalse(obj.active)

    def test_multiple_mail_triggers(self):
        lead_model = self.env["ir.model"]._get("base.automation.lead.test")
        with self.assertRaises(ValidationError):
            create_automation(self, trigger="on_message_sent", model_id=lead_model.id)

        lead_thread_model = self.env["ir.model"]._get("base.automation.lead.thread.test")

        create_automation(self, trigger="on_message_sent", model_id=lead_thread_model.id, _actions={
            "state": "object_write",
            "update_path": "active",
            "update_boolean_value": "false"
        })
        create_automation(self, trigger="on_message_sent", model_id=lead_thread_model.id, _actions={
            "state": "object_write",
            "evaluation_type": "equation",
            "update_path": "name",
            "value": "record.name + '!'"
        })

        ext_partner = self.env["res.partner"].create({"name": "ext", "email": "email@server.com"})
        internal_partner = self.env["res.users"].browse(2).partner_id

        obj = self.env["base.automation.lead.thread.test"].create({"name": "test"})
        obj.message_subscribe([ext_partner.id, internal_partner.id])

        obj.message_post(author_id=internal_partner.id, message_type="comment", subtype_xmlid="mail.mt_comment")
        self.assertFalse(obj.active)
        self.assertEqual(obj.name, "test!")

    def test_compute_on_create(self):
        lead_model = self.env['ir.model']._get('base.automation.lead.test')
        stage_field = self.env['ir.model.fields']._get('base.automation.lead.test', 'stage_id')
        new_stage = self.env['test_base_automation.stage'].create({'name': 'New'})

        create_automation(
            self,
            model_id=lead_model.id,
            trigger='on_stage_set',
            trigger_field_ids=[stage_field.id],
            _actions={
                'state': 'object_create',
                'crud_model_id': self.env['ir.model']._get('res.partner').id,
                'value': "Test Partner Automation",
            },
            filter_domain=repr([('stage_id', '=', new_stage.id)]),
        )

        # Tricky case: the record is created with 'stage_id' being false, and
        # the field is marked for recomputation.  The field is then recomputed
        # while evaluating 'filter_domain', which causes the execution of the
        # automation.  And as the domain is satisfied, the automation is
        # processed again, but it must detect that it has just been run!
        self.env['base.automation.lead.test'].create({
            'name': 'Test Lead',
        })

        # check that the automation has been run once
        partner_count = self.env['res.partner'].search_count([('name', '=', 'Test Partner Automation')])
        self.assertEqual(partner_count, 1, "Only one partner should have been created")

    def test_00_form_save_update_related_model_id(self):
        with Form(self.env['ir.actions.server'], view="base.view_server_action_form") as f:
            f.name = "Test Action"
            f.model_id = self.env["ir.model"]._get("res.partner")
            f.state = "object_write"
            f.update_path = "user_id"
            f.evaluation_type = "value"
            f.resource_ref = "res.users,2"

        res_users_model = self.env["ir.model"]._get("res.users")
        self.assertEqual(f.update_related_model_id, res_users_model)

    def test_01_form_object_write_o2m_field(self):
        aks_partner = self.env["res.partner"].create({"name": "A Kind Shepherd"})
        bs_partner = self.env["res.partner"].create({"name": "Black Sheep"})

        # test the 'object_write' type shows a resource_ref field for o2many
        f = Form(self.env['ir.actions.server'], view="base.view_server_action_form")
        f.name = "Adopt The Black Sheep"
        f.model_id = self.env["ir.model"]._get("res.partner")
        f.state = "object_write"
        f.evaluation_type = "value"
        f.update_path = "child_ids"
        self.assertEqual(f.update_m2m_operation, "add")
        self.assertEqual(f.value_field_to_show, "resource_ref")
        f.resource_ref = f"res.partner,{bs_partner.id}"
        action = f.save()

        # test the action runs correctly
        action.with_context(
            active_model="res.partner",
            active_id=aks_partner.id,
        ).run()
        self.assertEqual(aks_partner.child_ids, bs_partner)
        self.assertEqual(bs_partner.parent_id, aks_partner)

        # also check with 'remove' operation
        f.update_m2m_operation = "remove"
        action = f.save()
        action.with_context(
            active_model="res.partner",
            active_id=aks_partner.id,
        ).run()
        self.assertEqual(aks_partner.child_ids.ids, [])
        self.assertEqual(bs_partner.parent_id.id, False)

@common.tagged("post_install", "-at_install")
class TestHttp(common.HttpCase):
    def test_webhook_trigger(self):
        model = self.env["ir.model"]._get("base.automation.linked.test")
        record_getter = "model.search([('name', '=', payload['name'])]) if payload.get('name') else None"
        automation = create_automation(self, trigger="on_webhook", model_id=model.id, record_getter=record_getter, _actions={
            "state": "object_write",
            "update_path": "another_field",
            "value": "written"
        })

        obj = self.env[model.model].create({"name": "some name"})
        response = self.url_open(automation.url, data=json.dumps({"name": "some name"}))
        self.assertEqual(response.json(), {"status": "ok"})
        self.assertEqual(response.status_code, 200)
        self.assertEqual(obj.another_field, "written")

        obj.another_field = False
        with mute_logger("odoo.addons.base_automation.models.base_automation"):
            response = self.url_open(automation.url, data=json.dumps({}))
        self.assertEqual(response.json(), {"status": "error"})
        self.assertEqual(response.status_code, 500)
        self.assertEqual(obj.another_field, False)

        response = self.url_open("/web/hook/0123456789", data=json.dumps({"name": "some name"}))
        self.assertEqual(response.json(), {"status": "error"})
        self.assertEqual(response.status_code, 404)

    def test_payload_in_action_server(self):
        model = self.env["ir.model"]._get("base.automation.linked.test")
        record_getter = "model.search([('name', '=', payload['name'])]) if payload.get('name') else None"
        automation = create_automation(self, trigger="on_webhook", model_id=model.id, record_getter=record_getter, _actions={
            "state": "code",
            "code": "record.write({'another_field': json.dumps(payload)})"
        })

        obj = self.env[model.model].create({"name": "some name"})
        self.url_open(automation.url, data=json.dumps({"name": "some name", "test_key": "test_value"}), headers={"Content-Type": "application/json"})
        self.assertEqual(json.loads(obj.another_field), {
            "name": "some name",
            "test_key": "test_value",
        })

        obj.another_field = ""
        self.url_open(automation.url + "?test_param=test_value&name=some%20name")
        self.assertEqual(json.loads(obj.another_field), {
            "name": "some name",
            "test_param": "test_value",
        })

    def test_webhook_send_and_receive(self):
        model = self.env["ir.model"]._get("base.automation.linked.test")
        obj = self.env[model.model].create({"name": "some name"})

        automation_receiver = create_automation(self, trigger="on_webhook", model_id=model.id, _actions={
            "state": "code",
            "code": "record.write({'another_field': json.dumps(payload)})"
        })
        name_field_id = self.env.ref("test_base_automation.field_base_automation_linked_test__name")
        automation_sender = create_automation(self, trigger="on_write", model_id=model.id, trigger_field_ids=[(6, 0, [name_field_id.id])], _actions={
            "state": "webhook",
            "webhook_url": automation_receiver.url,
        })

        obj.name = "new_name"
        self.cr.flush()
        self.cr.clear()
        self._wait_remaining_requests()  # just in case the request timeouts
        self.assertEqual(json.loads(obj.another_field), {
            '_action': f'Send Webhook Notification(#{automation_sender.action_server_ids[0].id})',
            "_id": obj.id,
            "_model": obj._name,
        })

    def test_on_change_get_views_cache(self):
        model_name = "base.automation.lead.test"
        my_view = self.env["ir.ui.view"].create({
            "name": "My View",
            "model": model_name,
            "type": "form",
            "arch": "<form><field name='active'/></form>",
        })
        self.assertEqual(
            self.env[model_name].get_view(my_view.id)["arch"],
            '<form><field name="active"/></form>'
        )
        model = self.env["ir.model"]._get(model_name)
        active_field = self.env["ir.model.fields"]._get(model_name, "active")
        create_automation(self, trigger="on_change", model_id=model.id, on_change_field_ids=[Command.set([active_field.id])], _actions={
            "state": "code",
            "code": "",
        })
        self.assertEqual(
            self.env[model_name].get_view(my_view.id)["arch"],
            '<form><field name="active" on_change="1"/></form>'
        )
