from unittest.mock import patch

from odoo import Command
from odoo.tests import Form, TransactionCase
from odoo.tools.misc import submap

from odoo.addons.base.tests.common import SavepointCaseWithUserDemo


def strip_prefix(prefix, names):
    size = len(prefix)
    return [name[size:] for name in names if name.startswith(prefix)]


class TestOnchange(SavepointCaseWithUserDemo):

    def setUp(self):
        super().setUp()
        self.Discussion = self.env['test_orm.discussion']
        self.Message = self.env['test_orm.message']
        self.EmailMessage = self.env['test_orm.emailmessage']

    def test_default_get(self):
        """ checking values returned by default_get() """
        fields = ['name', 'categories', 'participants', 'messages']
        values = self.Discussion.default_get(fields)
        self.assertEqual(values, {})

        user = self.env.user
        fields_spec = self.env['test_orm.message']._get_fields_spec()
        values = self.env['test_orm.message'].onchange({}, [], fields_spec)['value']
        self.assertEqual(values['discussion'], False)
        self.assertEqual(values['body'], False)
        self.assertEqual(values['author'], {'id': user.id, 'display_name': user.display_name})
        self.assertEqual(values['name'], f'[] {user.name}')
        self.assertEqual(values['size'], 0)

    def test_default_x2many(self):
        """ checking default values for x2many fields """
        tag = self.env['test_orm.multi.tag'].create({'name': 'alpha'})
        model = self.env['test_orm.multi'].with_context(default_tags=[Command.set(tag.ids)])

        values = model.default_get(['tags'])
        self.assertEqual(values, {'tags': [Command.set(tag.ids)]})

        fields_spec = {'tags': {}}
        result = model.onchange({}, [], fields_spec)
        self.assertEqual(result['value'], {'tags': [(Command.LINK, tag.id, {'id': tag.id})]})

    def test_get_field(self):
        """ checking that accessing an unknown attribute does nothing special """
        with self.assertRaises(AttributeError):
            self.Discussion.not_really_a_method()

    def test_onchange(self):
        """ test the effect of onchange() """
        discussion = self.env.ref('test_orm.discussion_0')
        BODY = "What a beautiful day!"
        USER = self.env.user

        fields_spec = self.Message._get_fields_spec()
        self.assertEqual(
            submap(fields_spec, ('author', 'body', 'discussion')),
            {
                'author': {'fields': {'display_name': {}}},
                'body': {},
                'discussion': {'fields': {'display_name': {}}},
            },
        )

        # changing 'discussion' should recompute 'name'
        values = {
            'discussion': discussion.id,
            'name': f"[] {USER.name}",
            'body': False,
            'author': USER.id,
            'size': 0,
        }
        self.env.invalidate_all()
        result = self.Message.onchange(values, ['discussion'], fields_spec)
        self.assertEqual(result['value'], {
            'name': f"[{discussion.name}] {USER.name}",
        })

        # changing 'body' should recompute 'size'
        values = {
            'discussion': discussion.id,
            'name': f"[{discussion.name}] {USER.name}",
            'body': BODY,
            'author': USER.id,
            'size': 0,
        }
        self.env.invalidate_all()
        result = self.Message.onchange(values, ['body'], fields_spec)
        self.assertEqual(result['value'], {
            'size': len(BODY),
        })

        # changing 'body' should not recompute 'name', even if 'discussion' and
        # 'name' are not consistent with each other
        values = {
            'discussion': discussion.id,
            'name': False,
            'body': BODY,
            'author': USER.id,
            'size': 0,
        }
        self.env.invalidate_all()
        result = self.Message.onchange(values, ['body'], fields_spec)
        self.assertNotIn('name', result['value'])

    def test_onchange_many2one(self):
        Category = self.env['test_orm.category']

        fields_spec = Category._get_fields_spec()
        self.assertEqual(fields_spec, {
            'name': {},
            'parent': {'fields': {'display_name': {}}},
            'root_categ': {'fields': {'display_name': {}}},
            'dummy': {},
            'color': {},
        })

        root = Category.create(dict(name='root'))

        # set 'parent' to root, and check that 'root_categ' is computed as expected
        values = {
            'name': 'test',
            'parent': root.id,
            'root_categ': False,
        }
        self.env.invalidate_all()
        result = Category.onchange(values, ['parent'], fields_spec)
        self.assertEqual(result['value'], {
            'root_categ': {'id': root.id, 'display_name': root.name},
        })

        # set 'parent' to False, and check that 'root_categ' is computed as expected
        values = {
            'name': 'test',
            'parent': False,
            'root_categ': root.id,
        }
        self.env.invalidate_all()
        result = Category.onchange(values, ['parent'], fields_spec)
        self.assertEqual(result['value'], {
            'root_categ': False,
        })

    def test_onchange_one2many(self):
        """ test the effect of onchange() on one2many fields """
        USER = self.env.user

        # create an independent message
        message1 = self.Message.create({'body': "ABC"})
        message2 = self.Message.create({'body': "ABC"})
        self.assertEqual(message1.name, "[%s] %s" % ('', USER.name))

        fields_spec = self.Discussion._get_fields_spec()
        self.assertEqual(
            submap(fields_spec, ('name', 'messages')),
            {
                'name': {},
                'messages': {'fields': {
                    'author': {'fields': {'display_name': {}}},
                    'body': {},
                    'name': {},
                    'size': {},
                    'important': {},
                }},
            },
        )

        # modify discussion name
        values = {
            'name': "Foo",
            'categories': [],
            'moderator': False,
            'participants': [],
            'messages': [
                Command.link(message1.id),
                Command.link(message2.id),
                Command.update(message2.id, {'body': "XYZ"}),
                (Command.CREATE, "virtual3", {
                    'name': f"[] {USER.name}",
                    'body': "ABC",
                    'author': USER.id,
                    'size': 3,
                    'important': False,
                }),
            ],
        }
        self.env.invalidate_all()
        result = self.Discussion.onchange(values, ['name'], fields_spec)
        self.assertIn('messages', result['value'])
        self.assertEqual(result['value']['messages'], [
            Command.update(message1.id, {'name': f"[Foo] {USER.name}"}),
            Command.update(message2.id, {'name': f"[Foo] {USER.name}"}),
            Command.update("virtual3", {'name': f"[Foo] {USER.name}"}),
        ])

        # ensure onchange changing one2many without subfield works
        one_level_fields_spec = {field_name: {} for field_name in fields_spec}
        values = dict(values, name='{generate_dummy_message}')
        result = self.Discussion.with_context(generate_dummy_message=True).onchange(values, ['name'], one_level_fields_spec)
        self.assertEqual(result['value']['messages'], [
            Command.create({}),
        ])

    def test_onchange_one2many_reference(self):
        """ test the effect of onchange() on one2many fields with line references """
        BODY = "What a beautiful day!"
        USER = self.env.user

        fields_spec = self.Discussion._get_fields_spec()
        self.assertEqual(
            submap(fields_spec, ('name', 'messages')),
            {
                'name': {},
                'messages': {'fields': {
                    'author': {'fields': {'display_name': {}}},
                    'body': {},
                    'name': {},
                    'size': {},
                    'important': {},
                }},
            },
        )

        # modify discussion name, and check that the reference of the new line
        # is returned
        values = {
            'name': "Foo",
            'categories': [],
            'moderator': False,
            'participants': [],
            'messages': [
                (Command.CREATE, 'virtual1', {
                    'name': f"[] {USER.name}",
                    'body': BODY,
                    'author': USER.id,
                    'size': len(BODY),
                    'important': False,
                }),
            ],
        }
        self.env.invalidate_all()
        result = self.Discussion.onchange(values, ['name'], fields_spec)
        self.assertIn('messages', result['value'])
        self.assertItemsEqual(result['value']['messages'], [
            (Command.UPDATE, 'virtual1', {'name': f"[Foo] {USER.name}"}),
        ])

    def test_onchange_one2many_multi(self):
        """ test the effect of multiple onchange methods on one2many fields """
        partner1 = self.env['res.partner'].create({'name': 'A partner'})
        multi = self.env['test_orm.multi'].create({'partner': partner1.id})
        line1 = multi.lines.create({'multi': multi.id})

        self.assertEqual(multi.partner, partner1)
        self.assertEqual(multi.name, partner1.name)
        self.assertEqual(multi.lines, line1)
        self.assertEqual(line1.partner, partner1)
        self.assertEqual(line1.name, False)

        fields_spec = multi._get_fields_spec()
        self.assertEqual(fields_spec, {
            'name': {},
            'partner': {'fields': {'display_name': {}}},
            'lines': {
                'fields': {
                    'name': {},
                    'partner': {'fields': {'display_name': {}}},
                    'tags': {'fields': {'name': {}}},
                },
            },
        })

        # modify 'partner'
        #   -> set 'partner' on all lines
        #   -> recompute 'name'
        #       -> set 'name' on all lines
        partner2 = self.env['res.partner'].create({'name': 'A second partner'})
        values = {
            'name': partner1.name,
            'partner': partner2.id,             # this one just changed
            'lines': [
                (Command.CREATE, 'virtual2', {'name': False, 'partner': False, 'tags': [Command.clear()]}),
            ],
        }
        self.env.invalidate_all()

        result = multi.onchange(values, ['partner'], fields_spec)
        self.assertEqual(result['value'], {
            'name': partner2.name,
            'lines': [
                Command.update(line1.id, {
                    'name': partner2.name,
                    'partner': {'id': partner2.id, 'display_name': partner2.name},
                }),
                Command.update('virtual2', {
                    'name': partner2.name,
                    'partner': {'id': partner2.id, 'display_name': partner2.name},
                }),
            ],
        })

        # do it again, but this time with a new tag on the second line
        values = {
            'name': partner1.name,
            'partner': partner2.id,             # this one just changed
            'lines': [
                (Command.CREATE, 'virtual2', {
                    'name': False,
                    'partner': False,
                    'tags': [Command.create({'name': 'Tag'})],
                }),
            ],
        }
        self.env.invalidate_all()
        result = multi.onchange(values, ['partner'], fields_spec)
        expected_value = {
            'name': partner2.name,
            'lines': [
                Command.update(line1.id, {
                    'name': partner2.name,
                    'partner': {'id': partner2.id, 'display_name': partner2.name},
                }),
                Command.update('virtual2', {
                    'name': partner2.name,
                    'partner': {'id': partner2.id, 'display_name': partner2.name},
                }),
            ],
        }
        self.assertEqual(result['value'], expected_value)

        # ensure ID is not returned when asked and a many2many record is set to be created
        self.env.invalidate_all()

        fields_spec = multi._get_fields_spec()
        fields_spec['lines']['fields']['tags']['fields']['id'] = {}
        result = multi.onchange(values, ['partner'], fields_spec)
        self.assertEqual(result['value'], expected_value)

        # ensure inverse of one2many field is not returned
        self.env.invalidate_all()

        fields_spec = multi._get_fields_spec()
        fields_spec['lines']['fields']['multi'] = {}
        result = multi.onchange(values, ['partner'], fields_spec)
        self.assertEqual(result['value'], expected_value)

    def test_onchange_one2many_default(self):
        """ test onchange on x2many field with default value in context """
        default_messages = [Command.create({'body': 'A'})]
        model = self.Discussion.with_context(default_messages=default_messages)

        # the command CREATE below must be applied on the empty value, instead
        # of the default value above
        values = {
            'name': 'Stuff',
            'messages': [(Command.CREATE, 'virtual1', {'name': '[] ', 'body': 'B'})],
        }
        fields_spec = {
            'name': {},
            'messages': {'fields': {
                'name': {},
                'body': {},
            }},
        }
        result = model.onchange(values, ['name'], fields_spec)
        self.assertEqual(result['value'], {
            'messages': [Command.update('virtual1', {'name': f'[Stuff] {self.env.user.name}'})],
        })

    def test_fields_specific(self):
        """ test the effect of field-specific onchange method """
        discussion = self.env.ref('test_orm.discussion_0')
        demo = self.user_demo

        fields_spec = self.Discussion._get_fields_spec()
        self.assertEqual(fields_spec.get('moderator'), {'fields': {'display_name': {}}})
        self.assertEqual(fields_spec.get('participants'), {'fields': {'display_name': {}}})

        # first remove demo user from participants
        discussion.participants -= demo
        self.assertNotIn(demo, discussion.participants)

        # check that demo_user is added to participants when set as moderator
        values = {
            'moderator': demo.id,
        }
        self.env.invalidate_all()
        result = discussion.onchange(values, ['moderator'], fields_spec)

        self.assertIn('participants', result['value'])
        self.assertItemsEqual(
            result['value']['participants'],
            [(Command.LINK, demo.id, {'id': demo.id, 'display_name': demo.display_name})],
        )

    def test_onchange_default(self):
        """ test the effect of a conditional user-default on a field """
        Foo = self.env['test_orm.foo']
        fields_spec = Foo._get_fields_spec()
        self.assertTrue(type(Foo).value1.change_default)
        self.assertIn('value1', Foo._onchange_methods)

        # create a user-defined default based on 'value1'
        self.env['ir.default'].set('test_orm.foo', 'value2', 666, condition='value1=42')

        # setting 'value1' to 42 should trigger the change of 'value2'
        self.env.invalidate_all()
        values = {'name': 'X', 'value1': 42, 'value2': False}
        result = Foo.onchange(values, ['value1'], fields_spec)
        self.assertEqual(result['value'], {'value2': 666})

        # setting 'value1' to 24 should not trigger the change of 'value2'
        self.env.invalidate_all()
        values = {'name': 'X', 'value1': 24, 'value2': False}
        result = Foo.onchange(values, ['value1'], fields_spec)
        self.assertEqual(result['value'], {})

    def test_onchange_one2many_first(self):
        partner = self.env['res.partner'].create({
            'name': 'X',
            'country_id': self.env.ref('base.be').id,
        })
        with Form(self.env['test_orm.multi']) as form:
            form.partner = partner
            self.assertEqual(form.partner, partner)
            self.assertEqual(form.name, partner.name)
            with form.lines.new() as line:
                # the first onchange() must have computed partner
                self.assertEqual(line.partner, partner)

    def test_onchange_one2many_value(self):
        """ test the value of the one2many field inside the onchange """
        discussion = self.env.ref('test_orm.discussion_0')
        demo = self.user_demo

        fields_spec = self.Discussion._get_fields_spec()
        self.assertEqual(fields_spec, {
            'important_emails': {'fields': {
                'author': {'fields': {'display_name': {}}},
                'body': {},
                'email_to': {},
                'important': {},
                'name': {},
                'size': {},
            }},
            'message_concat': {},
            'messages': {'fields': {
                'author': {'fields': {'display_name': {}}},
                'body': {},
                'important': {},
                'name': {},
                'size': {},
            }},
            'moderator': {'fields': {'display_name': {}}},
            'name': {},
            'participants': {'fields': {'display_name': {}}},
        })

        self.assertEqual(len(discussion.messages), 3)
        messages = [Command.link(msg.id) for msg in discussion.messages]
        messages[0] = (1, messages[0][1], {'body': 'test onchange'})
        lines = ["%s:%s" % (m.name, m.body) for m in discussion.messages]
        lines[0] = "%s:%s" % (discussion.messages[0].name, 'test onchange')
        values = {
            'name': discussion.name,
            'moderator': demo.id,
            'categories': [Command.link(cat.id) for cat in discussion.categories],
            'messages': messages,
            'participants': [Command.link(usr.id) for usr in discussion.participants],
            'message_concat': False,
        }
        result = discussion.onchange(values, ['messages'], fields_spec)
        self.assertIn('message_concat', result['value'])
        self.assertEqual(result['value']['message_concat'], "\n".join(lines))

    def test_onchange_one2many_with_domain_on_related_field(self):
        """ test the value of the one2many field when defined with a domain on a related field"""
        discussion = self.env.ref('test_orm.discussion_0')
        demo = self.user_demo

        # mimic UI behaviour, so we get subfields
        # (we need at least subfield: 'important_emails.important')
        view_info = self.Discussion.get_view(self.env.ref('test_orm.discussion_form').id, 'form')
        fields_spec = self.Discussion._get_fields_spec(view_info=view_info)
        self.assertEqual(fields_spec, {
            'name': {},
            'moderator': {'fields': {'display_name': {}}},
            'messages': {'fields': {
                'name': {},
                'body': {},
                'important': {},
                'author': {'fields': {'display_name': {}}},
                'size': {},
            }},
            'important_emails': {'fields': {
                'name': {},
                'body': {},
                'important': {},
                'email_to': {},
                'author': {'fields': {'display_name': {}}},
                'size': {}},
            },
            'participants': {'fields': {'display_name': {}}},
            'message_concat': {},
        })

        BODY = "What a beautiful day!"
        USER = self.env.user

        # create standalone email
        email = self.EmailMessage.create({
            'discussion': discussion.id,
            'name': f"[] {USER.name}",
            'body': BODY,
            'author': USER.id,
            'important': False,
            'email_to': demo.email,
        })

        # check if server-side cache is working correctly
        self.env.invalidate_all()
        self.assertIn(email, discussion.emails)
        self.assertNotIn(email, discussion.important_emails)
        email.important = True
        self.assertIn(email, discussion.important_emails)

        # check that when trigger an onchange, we don't reset important emails
        # (force `invalidate` as but appear in onchange only when we get a cache
        # miss)
        self.env.invalidate_all()
        self.assertEqual(len(discussion.messages), 4)
        values = {
            'name': "Foo Bar",
            'moderator': demo.id,
            'important_messages': [Command.set(discussion.important_messages.ids)],
            'important_emails': [Command.set(discussion.important_emails.ids)],
        }
        self.env.invalidate_all()
        result = discussion.onchange(values, ['name'], fields_spec)

        self.assertEqual(
            result['value']['important_emails'],
            [Command.update(email.id, {
                'name': f'[Foo Bar] {USER.name}',
            })],
        )

    def test_onchange_related(self):
        user = self.env.user

        values = {
            'message': 1,
            'message_name': False,
            'message_currency': 2,
        }
        fields_spec = {
            'message': {'fields': {'display_name': {}}},
            'message_name': {},
            'message_currency': {'fields': {'display_name': {}}},
        }

        expected = {
            'message_name': 'Hey dude!',
            'message_currency': {'id': user.id, 'display_name': user.display_name},
        }

        self.env.invalidate_all()
        Message = self.env['test_orm.related']
        result = Message.onchange(values, ['message'], fields_spec)

        self.assertEqual(result['value'], expected)

        self.env.invalidate_all()
        Message = self.env(user=self.user_demo.id)['test_orm.related']
        result = Message.onchange(values, ['message'], fields_spec)

        self.assertEqual(result['value'], expected)

    def test_onchange_many2one_one2many(self):
        """ Setting a many2one field should not read the inverse one2many. """
        discussion = self.env.ref('test_orm.discussion_0')
        fields_spec = self.Message._get_fields_spec()
        self.assertEqual(fields_spec, {
            'discussion': {'fields': {'display_name': {}}},
            'name': {},
            'author': {'fields': {'display_name': {}}},
            'size': {},
            'attributes': {},
            'body': {},
        })

        values = {
            'discussion': discussion.id,
            'name': "[%s] %s" % ('', self.env.user.name),
            'body': False,
            'author': self.env.uid,
            'size': 0,
        }

        called = [False]
        orig_read = type(discussion).read

        def mock_read(self, fields=None, load='_classic_read'):
            if discussion in self and 'messages' in (fields or ()):
                called[0] = True
            return orig_read(self, fields, load)

        # changing 'discussion' on message should not read 'messages' on discussion
        with patch.object(type(discussion), 'read', mock_read, create=True):
            self.env.invalidate_all()
            self.Message.onchange(values, ['discussion'], fields_spec)

        self.assertFalse(called[0], "discussion.messages has been read")

    def test_onchange_one2many_many2one_in_form(self):
        order = self.env['test_orm.monetary_order'].create({
            'currency_id': self.env.ref('base.USD').id,
        })

        # this call to onchange() is made when creating a new line in field
        # order.line_ids; check what happens when the line's form view contains
        # the inverse many2one field
        values = {'order_id': {'id': order.id, 'currency_id': order.currency_id.id}}
        fields_spec = {
            'order_id': {},
            'subtotal': {},
        }
        result = self.env['test_orm.monetary_order_line'].onchange(values, [], fields_spec)

        self.assertEqual(result['value']['order_id'], order.id)

    def test_onchange_inherited(self):
        """ Setting an inherited field should assign the field on the parent record. """
        foo, bar = self.env['test_orm.multi.tag'].create([{'name': 'Foo'}, {'name': 'Bar'}])
        view = self.env['ir.ui.view'].create({
            'name': 'Payment form view',
            'model': 'test_orm.payment',
            'arch': """
                <form>
                    <field name="move_id" readonly="1" required="0"/>
                    <field name="tag_id"/>
                    <field name="tag_name"/>
                    <field name="tag_repeat"/>
                    <field name="tag_string"/>
                </form>
            """,
        })

        # both fields 'tag_id' and 'tag_name' are inherited through 'move_id';
        # assigning 'tag_id' should modify 'move_id.tag_id' accordingly, which
        # should in turn recompute `move.tag_name` and `tag_name`
        form = Form(self.env['test_orm.payment'], view)
        self.assertEqual(form.tag_name, False)
        form.tag_id = foo
        self.assertEqual(form.tag_name, 'Foo')
        self.assertEqual(form.tag_string, '')
        form.tag_repeat = 2
        self.assertEqual(form.tag_name, 'Foo')
        self.assertEqual(form.tag_string, 'FooFoo')

        payment = form.save()
        self.assertEqual(payment.tag_id, foo)
        self.assertEqual(payment.tag_name, 'Foo')
        self.assertEqual(payment.tag_repeat, 2)
        self.assertEqual(payment.tag_string, 'FooFoo')

        with Form(payment, view) as form:
            form.tag_id = bar
            self.assertEqual(form.tag_name, 'Bar')
            self.assertEqual(form.tag_string, 'BarBar')
            form.tag_repeat = 3
            self.assertEqual(form.tag_name, 'Bar')
            self.assertEqual(form.tag_string, 'BarBarBar')

        self.assertEqual(payment.tag_id, bar)
        self.assertEqual(payment.tag_name, 'Bar')
        self.assertEqual(payment.tag_repeat, 3)
        self.assertEqual(payment.tag_string, 'BarBarBar')

    def test_onchange_inherited_in_one2many(self):
        move = self.env['test_orm.move'].create({})
        view = self.env["ir.ui.view"].create({
            "model": "test_orm.move",
            "type": "form",
            "arch": """<form>
                <field name="payment_ids">
                    <form>
                        <!-- tag_repeat is inherited from move model -->
                        <field name="tag_repeat" />
                    </form>
                </field>
            </form>""",
        })

        with Form(move, view) as form:
            with form.payment_ids.new() as line:
                line.tag_repeat = 1
            self.assertEqual(len(form.payment_ids), 1)

        self.assertEqual(len(move.payment_ids), 1)
        self.assertEqual(move.payment_ids.move_id, move)
        self.assertEqual(move.payment_ids.tag_repeat, 1)

    def test_onchange_inherited_computed(self):
        # 'test_orm.payment' inherits from 'test_orm.move', which has a computed
        # field that depends on the corresponding payment's amount
        view = self.env['ir.ui.view'].create({
            'name': 'Payment form view',
            'model': 'test_orm.payment',
            'arch': '<form><field name="amount"/><field name="payment_amount"/></form>',
        })
        with Form(self.env['test_orm.payment'], view) as form:
            self.assertEqual(form.amount, 0)
            self.assertEqual(form.payment_amount, 0)
            form.amount = 10
            self.assertEqual(form.payment_amount, 10)
            form.amount = 0
            self.assertEqual(form.payment_amount, 0)

        # same thing, but with 'test_orm.payment' as lines in a one2many in the
        # form of a 'test_orm.move'
        view = self.env['ir.ui.view'].create({
            'name': 'Move form view',
            'model': 'test_orm.move',
            'arch': '''<form>
                <field name="payment_amount"/>
                <field name="payment_ids">
                    <list editable="bottom"><field name="amount"/></list>
                </field>
            </form>''',
        })
        with Form(self.env['test_orm.move'], view) as form:
            self.assertEqual(form.payment_amount, 0)
            with form.payment_ids.new() as payment:
                payment.amount = 10
            self.assertEqual(form.payment_amount, 10)
            with form.payment_ids.edit(0) as payment:
                payment.amount = 0
            self.assertEqual(form.payment_amount, 0)

    def test_display_name(self):
        self.env['ir.ui.view'].create({
            'name': 'test_orm.multi.tag form view',
            'model': 'test_orm.multi.tag',
            'arch': """
                <form>
                    <field name="name"/>
                    <field name="display_name"/>
                </form>
            """,
        })

        form = Form(self.env['test_orm.multi.tag'])
        self.assertEqual(form.name, False)
        self.assertEqual(form.display_name, "")

        record = form.save()
        self.assertEqual(record.name, False)
        self.assertEqual(record.display_name, "")

    def test_reading_one2many_and_inverse_is_not_supported(self):
        # when reading a relation one2many, sometime the relation itself defines
        # the many2one toward the parent model. Adding this to the fields_spec
        # is not supported
        USER = self.env.user

        fields_spec = {
            'name': {},
            'messages': {'fields': {
                'name': {},
                'author': {'fields': {'display_name': {}}},
                # add the inverse of field 'messages' into its subfields
                'discussion': {'fields': {'display_name': {}}},
            }},
        }

        # modify discussion name with a special value that adds a new message
        values = {
            'name': "{generate_dummy_message}",
        }

        result = self.Discussion.with_context(generate_dummy_message=True).onchange(values, ['name'], fields_spec)
        self.assertEqual(result['value']['messages'], [
            Command.create({
                'name': f'[{{generate_dummy_message}}] {USER.name}',
                'author': {'id': 1, 'display_name': f'{USER.name}'},
                'discussion': False,  # this value is False because the main record does not exist yet
            }),
        ])

    def test_reading_many2one_extra_fields(self):
        Category = self.env['test_orm.category']
        root = Category.create(dict(name='root'))

        fields_spec = {
            'name': {},
            'parent': {'fields': {'display_name': {}, 'color': {}}},
            'root_categ': {'fields': {'display_name': {}, 'color': {}}},
        }

        # set 'parent' to root, and check that 'root_categ' is computed as expected
        values = {
            'name': 'test',
            'parent': root.id,
            'root_categ': False,
        }
        self.env.invalidate_all()
        result = Category.onchange(values, ['parent'], fields_spec)
        self.assertEqual(result['value'], {
            'root_categ': {'id': root.id, 'display_name': root.name, 'color': root.color},
        })

    def test_one2many_field_with_context_many2one(self):
        """ test many2one fields with a context on their one2many container field """
        multi = self.env['test_orm.multi'].create({})
        line = multi.lines.create({'multi': multi.id})

        self.assertFalse(multi.partner)
        self.assertFalse(multi.name)
        self.assertEqual(multi.lines, line)
        self.assertFalse(line.partner)
        self.assertFalse(line.name)

        fields_spec = multi._get_fields_spec()
        self.assertEqual(fields_spec, {
            'name': {},
            'partner': {'fields': {'display_name': {}}},
            'lines': {
                'fields': {
                    'name': {},
                    'partner': {'fields': {'display_name': {}}},
                    'tags': {'fields': {'name': {}}},
                },
            },
        })
        # add a context on field 'lines' to change the display_name of subfield 'partner'
        fields_spec['lines']['context'] = {'show_email': True}

        # create a partner (for a change)
        partner = self.env['res.partner'].create({
            'name': 'A partner',
            'email': 'foo@example.com',
        })
        display_name = partner.display_name
        display_name_email = partner.with_context(show_email=True).display_name
        self.assertNotEqual(display_name_email, display_name)

        # modify 'partner'
        #   -> set 'partner' on all lines
        #   -> recompute 'name'
        #       -> set 'name' on all lines
        values = {
            'partner': partner.id,             # this one just changed
        }
        self.env.invalidate_all()

        result = multi.onchange(values, ['partner'], fields_spec)
        self.assertEqual(result['value'], {
            'name': partner.name,
            'lines': [
                Command.update(line.id, {
                    'name': partner.name,
                    'partner': {'id': partner.id, 'display_name': display_name_email},
                }),
            ],
        })

    def test_one2many_field_with_context_many2many(self):
        """ test relational fields with a context on their one2many container field """
        partner = self.env['res.partner'].create({'name': 'A partner'})
        multi = self.env['test_orm.multi'].create({'partner': partner.id})
        line = multi.lines.create({'multi': multi.id})

        self.assertEqual(multi.partner, partner)
        self.assertEqual(multi.name, partner.name)
        self.assertEqual(multi.lines, line)
        self.assertEqual(line.partner, partner)
        self.assertEqual(line.name, False)

        fields_spec = {
            'name': {},
            'partner': {'fields': {'display_name': {}}},
            'lines': {
                # this context should change the 'display_name' of 'tags'
                'context': {'special_tag': True},
                'fields': {
                    'name': {},
                    'partner': {'fields': {'display_name': {}}},
                    'tags': {'fields': {'display_name': {}}},
                },
            },
            'tags': {'fields': {'display_name': {}}},
        }

        # set field 'tags': this should modify 'tags' on all lines
        tag = self.env['test_orm.multi.tag'].create({'name': 'tag'})
        self.assertEqual(tag.display_name, 'tag')
        self.assertEqual(tag.with_context(special_tag=True).display_name, 'tag!')

        values = {
            'tags': [Command.link(tag.id)],
        }

        self.env.invalidate_all()
        result = multi.onchange(values, ['tags'], fields_spec)
        self.assertEqual(result['value'], {
            'lines': [
                Command.update(line.id, {
                    'tags': [(Command.LINK, tag.id, {'id': tag.id, 'display_name': 'tag!'})],
                }),
            ],
        })

    def test_one2many_field_with_many2many_subfield(self):
        """ test relational fields with a context on their one2many container field """
        tag1 = self.env['test_orm.multi.tag'].create({'name': 'a1'})
        tag2 = self.env['test_orm.multi.tag'].create({'name': 'a2'})
        multi = self.env['test_orm.multi'].create({})
        line = multi.lines.create({
            'multi': multi.id,
            'tags': [Command.set(tag1.ids)],
        })

        # having 'lines' below force the prefetching of the subfields 'name'
        # and 'tags'; this test ensures that the ids of 'tags' are "newified"
        # in the new record
        values = {
            'lines': [(Command.CREATE, 'virtual1', {'name': 'X', 'tags': []})],
            'tags': [Command.link(tag2.id)],
        }
        fields_spec = {
            'name': {},
            'lines': {'fields': {
                'name': {},
                'tags': {},
            }},
            'tags': {},
        }
        self.env.invalidate_all()
        result = multi.onchange(values, ['tags'], fields_spec)
        self.assertEqual(result['value'], {
            'lines': [
                Command.update(line.id, {
                    'tags': [(Command.LINK, tag2.id, {'id': tag2.id})],
                }),
                Command.update('virtual1', {
                    'tags': [(Command.LINK, tag2.id, {'id': tag2.id})],
                }),
            ],
        })


class TestComputeOnchange2(TransactionCase):

    def test_create(self):
        model = self.env['test_orm.compute.onchange']

        # compute 'bar' (readonly) and 'baz' (editable)
        record = model.create({'active': True})
        self.assertEqual(record.bar, "r")
        self.assertEqual(record.baz, "z")

        # compute 'bar' and 'baz'
        record = model.create({'active': True, 'foo': "foo"})
        self.assertEqual(record.bar, "foor")
        self.assertEqual(record.baz, "fooz")

        # compute 'bar' but not 'baz'
        record = model.create({'active': True, 'foo': "foo", 'bar': "bar", 'baz': "baz"})
        self.assertEqual(record.bar, "foor")
        self.assertEqual(record.baz, "baz")

        # compute 'bar' and 'baz', but do not change its value
        record = model.create({'active': False, 'foo': "foo"})
        self.assertEqual(record.bar, "foor")
        self.assertEqual(record.baz, False)

        # compute 'bar' but not 'baz'
        record = model.create({'active': False, 'foo': "foo", 'bar': "bar", 'baz': "baz"})
        self.assertEqual(record.bar, "foor")
        self.assertEqual(record.baz, "baz")

    def test_copy(self):
        Model = self.env['test_orm.compute.onchange']

        # create tags
        tag_foo, tag_bar = self.env['test_orm.multi.tag'].create([
            {'name': 'foo1'},
            {'name': 'bar1'},
        ])

        # compute 'bar' (readonly), 'baz', 'line_ids' and 'tag_ids' (editable)
        record = Model.create({'active': True, 'foo': "foo1"})
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "foo1z")
        self.assertEqual(record.line_ids.mapped('foo'), ['foo1'])
        self.assertEqual(record.tag_ids, tag_foo)

        # manually update 'baz' and 'lines' to test copy attribute
        record.write({
            'baz': "baz1",
            'line_ids': [Command.create({'foo': 'bar'})],
            'tag_ids': [Command.link(tag_bar.id)],
        })
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "baz1")
        self.assertEqual(record.line_ids.mapped('foo'), ['foo1', 'bar'])
        self.assertEqual(record.tag_ids, tag_foo + tag_bar)

        # copy the record, and check results
        copied = record.copy()
        self.assertEqual(copied.foo, "foo1 (copy)")   # copied and modified
        self.assertEqual(copied.bar, "foo1 (copy)r")  # computed
        self.assertEqual(copied.baz, "baz1")          # copied
        self.assertEqual(record.line_ids.mapped('foo'), ['foo1', 'bar'])  # copied
        self.assertEqual(record.tag_ids, tag_foo + tag_bar)  # copied

    def test_copy_batch(self):
        partners = self.env['test_orm.partner'].create([
            {'name': f'Partner {index}'} for index in range(5)
        ])

        # warmup
        partners.copy()

        with self.assertQueryCount(1):
            new_partners = partners.copy()

        for old_partner, new_partner in zip(partners, new_partners):
            self.assertEqual(new_partner.name, old_partner.name)

    def test_write(self):
        model = self.env['test_orm.compute.onchange']
        record = model.create({'active': True, 'foo': "foo"})
        self.assertEqual(record.bar, "foor")
        self.assertEqual(record.baz, "fooz")

        # recompute 'bar' (readonly) and 'baz' (editable)
        record.write({'foo': "foo1"})
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "foo1z")

        # recompute 'bar' but not 'baz'
        record.write({'foo': "foo2", 'bar': "bar2", 'baz': "baz2"})
        self.assertEqual(record.bar, "foo2r")
        self.assertEqual(record.baz, "baz2")

        # recompute 'bar' and 'baz', but do not change its value
        record.write({'active': False, 'foo': "foo3"})
        self.assertEqual(record.bar, "foo3r")
        self.assertEqual(record.baz, "baz2")

        # recompute 'bar' but not 'baz'
        record.write({'active': False, 'foo': "foo4", 'bar': "bar4", 'baz': "baz4"})
        self.assertEqual(record.bar, "foo4r")
        self.assertEqual(record.baz, "baz4")

    def test_set(self):
        model = self.env['test_orm.compute.onchange']
        record = model.create({'active': True, 'foo': "foo"})
        self.assertEqual(record.bar, "foor")
        self.assertEqual(record.baz, "fooz")

        # recompute 'bar' (readonly) and 'baz' (editable)
        record.foo = "foo1"
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "foo1z")

        # do not recompute 'baz'
        record.baz = "baz2"
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "baz2")

        # recompute 'baz', but do not change its value
        record.active = False
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "baz2")

        # recompute 'baz', but do not change its value
        record.foo = "foo3"
        self.assertEqual(record.bar, "foo3r")
        self.assertEqual(record.baz, "baz2")

        # do not recompute 'baz'
        record.baz = "baz4"
        self.assertEqual(record.bar, "foo3r")
        self.assertEqual(record.baz, "baz4")

    def test_set_new(self):
        model = self.env['test_orm.compute.onchange']
        record = model.new({'active': True})
        self.assertEqual(record.bar, "r")
        self.assertEqual(record.baz, "z")

        # recompute 'bar' (readonly) and 'baz' (editable)
        record.foo = "foo1"
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "foo1z")

        # do not recompute 'baz'
        record.baz = "baz2"
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "baz2")

        # recompute 'baz', but do not change its value
        record.active = False
        self.assertEqual(record.bar, "foo1r")
        self.assertEqual(record.baz, "baz2")

        # recompute 'baz', but do not change its value
        record.foo = "foo3"
        self.assertEqual(record.bar, "foo3r")
        self.assertEqual(record.baz, "baz2")

        # do not recompute 'baz'
        record.baz = "baz4"
        self.assertEqual(record.bar, "foo3r")
        self.assertEqual(record.baz, "baz4")

    def test_onchange(self):
        # check computations of 'bar' (readonly) and 'baz' (editable)
        form = Form(self.env['test_orm.compute.onchange'])
        self.assertEqual(form.bar, "r")
        self.assertEqual(form.baz, False)
        self.assertEqual(form.quux, "quux")
        form.active = True
        self.assertEqual(form.bar, "r")
        self.assertEqual(form.baz, "z")
        form.foo = "foo1"
        self.assertEqual(form.bar, "foo1r")
        self.assertEqual(form.baz, "foo1z")
        form.baz = "baz2"
        self.assertEqual(form.bar, "foo1r")
        self.assertEqual(form.baz, "baz2")
        form.active = False
        self.assertEqual(form.bar, "foo1r")
        self.assertEqual(form.baz, "baz2")
        form.foo = "foo3"
        self.assertEqual(form.bar, "foo3r")
        self.assertEqual(form.baz, "baz2")
        form.active = True
        self.assertEqual(form.bar, "foo3r")
        self.assertEqual(form.baz, "foo3z")

        with form.line_ids.new() as line:
            # check computation of 'bar' (readonly)
            self.assertEqual(line.foo, False)
            self.assertEqual(line.bar, "r")
            line.foo = "foo"
            self.assertEqual(line.foo, "foo")
            self.assertEqual(line.bar, "foor")

        record = form.save()
        self.assertEqual(record.bar, "foo3r")
        self.assertEqual(record.baz, "foo3z")
        self.assertEqual(record.quux, "quux")

        form = Form(record)
        self.assertEqual(form.bar, "foo3r")
        self.assertEqual(form.baz, "foo3z")
        self.assertEqual(form.quux, "quux")
        form.foo = "foo4"
        self.assertEqual(form.bar, "foo4r")
        self.assertEqual(form.baz, "foo4z")
        form.baz = "baz5"
        self.assertEqual(form.bar, "foo4r")
        self.assertEqual(form.baz, "baz5")
        form.active = False
        self.assertEqual(form.bar, "foo4r")
        self.assertEqual(form.baz, "baz5")
        form.foo = "foo6"
        self.assertEqual(form.bar, "foo6r")
        self.assertEqual(form.baz, "baz5")

    def test_onchange_default(self):
        form = Form(self.env['test_orm.compute.onchange'].with_context(
            default_active=True, default_foo="foo", default_baz="baz", default_quux="no",
        ))
        # 'baz' is computed editable, so when given a default value it should
        # 'not be recomputed, even if a dependency also has a default value
        self.assertEqual(form.foo, "foo")
        self.assertEqual(form.bar, "foor")
        self.assertEqual(form.baz, "baz")
        self.assertEqual(form.quux, "no")

    def test_onchange_once(self):
        """ Modifies `foo` field which will trigger an onchange method and
        checks it was triggered only one time. """
        form = Form(self.env['test_orm.compute.onchange'].with_context(default_foo="oof"))
        record = form.save()
        self.assertEqual(record.foo, "oof")
        self.assertEqual(record.count, 1, "value onchange must be called only one time")

    def test_onchange_one2many(self):
        record = self.env['test_orm.model_parent_m2o'].create({
            'name': 'Family',
            'child_ids': [
                Command.create({'name': 'W', 'cost': 10}),
                Command.create({'name': 'X', 'cost': 10}),
                Command.create({'name': 'Y'}),
                Command.create({'name': 'Z'}),
            ],
        })
        self.env.flush_all()
        self.assertEqual(record.child_ids.mapped('name'), list('WXYZ'))
        self.assertEqual(record.cost, 22)

        # modifying a line should not recompute the cost on other lines
        with Form(record) as form:
            with form.child_ids.edit(1) as line:
                line.name = 'XXX'
            self.assertEqual(form.cost, 15)

            with form.child_ids.edit(1) as line:
                line.cost = 20
            self.assertEqual(form.cost, 32)

            with form.child_ids.edit(2) as line:
                line.cost = 30
            self.assertEqual(form.cost, 61)

    def test_onchange_editable_compute_one2many(self):
        # create a record with a computed editable field ('edit') on lines
        record = self.env['test_orm.compute_editable'].create({
            'line_ids': [Command.create({'value': 7})],
        })
        self.env.flush_all()
        line = record.line_ids
        self.assertRecordValues(line, [{'value': 7, 'edit': 7, 'count': 0}])

        # retrieve the onchange spec for calling 'onchange'
        fields_spec = record._get_fields_spec()

        # The onchange on 'line_ids' should increment 'count' and keep the value
        # of 'edit' (this field should not be recomputed), whatever the order of
        # the fields in the dictionary.  This ensures that the value set by the
        # user on a computed editable field on a line is not lost.
        line_ids = [
            Command.update(line.id, {'value': 8, 'edit': 9, 'count': 0}),
            (Command.CREATE, 'virtual2', {'value': 8, 'edit': 9, 'count': 0}),
        ]
        result = record.onchange({'line_ids': line_ids}, ['line_ids'], fields_spec)
        expected = {'value': {
            'line_ids': [
                Command.update(line.id, {'count': 8}),
                Command.update('virtual2', {'count': 8}),
            ],
        }}
        self.assertEqual(result, expected)

        # change dict order in lines, and try again
        line_ids = [
            (op, id_, dict(reversed(list(vals.items()))))
            for op, id_, vals in line_ids
        ]
        result = record.onchange({'line_ids': line_ids}, ['line_ids'], fields_spec)
        self.assertEqual(result, expected)

    def test_computed_editable_one2many_domain(self):
        """ Test a computed, editable one2many field with a domain. """
        record = self.env['test_orm.one2many'].create({'name': 'foo'})
        self.assertRecordValues(record.line_ids, [
            {'name': 'foo', 'count': 1},
        ])

        # trigger recomputation by changing name
        record.name = 'bar'
        self.assertRecordValues(record.line_ids, [
            {'name': 'foo', 'count': 1},
            {'name': 'bar', 'count': 1},
        ])

        # manually adding a line should not trigger recomputation
        record.line_ids.create({'name': 'baz', 'container_id': record.id})
        self.assertRecordValues(record.line_ids, [
            {'name': 'foo', 'count': 1},
            {'name': 'bar', 'count': 1},
            {'name': 'baz', 'count': 1},
        ])

        # changing the field in the domain should not trigger recomputation...
        record.line_ids[-1].count = 2
        self.assertRecordValues(record.line_ids, [
            {'name': 'foo', 'count': 1},
            {'name': 'bar', 'count': 1},
            {'name': 'baz', 'count': 2},
        ])

        # ...and may show cache inconsistencies
        record.line_ids[-1].count = 0
        self.assertRecordValues(record.line_ids, [
            {'name': 'foo', 'count': 1},
            {'name': 'bar', 'count': 1},
            {'name': 'baz', 'count': 0},
        ])
        self.env.flush_all()
        self.env.invalidate_all()
        self.assertRecordValues(record.line_ids, [
            {'name': 'foo', 'count': 1},
            {'name': 'bar', 'count': 1},
        ])

    def test_one2many_compute(self):
        """ Test a computed, editable one2many field with a domain. """
        record = self.env['test_orm.compute_editable'].create(
            {'line_ids': [Command.create({})]},
        )
        with Form(record) as form:
            form.precision_rounding = 0.0001

    def test_new_one2many_on_existing_record(self):
        discussion = self.env['test_orm.discussion'].create({
            'messages': [Command.create({'body': 'Required Body', 'important': True})],
            'name': 'Required field',
            'participants': [Command.set(self.env.user.ids)],
        })

        with Form(discussion, 'test_orm.discussion_form_2') as discussion_form:
            self.assertEqual(len(discussion_form.messages), 1)
            with discussion_form.messages.new() as message_form:
                # this actually checks that during the onchange, the new message
                # has access to its sibling messages through the discussion
                self.assertEqual(message_form.has_important_sibling, True)
                message_form.body = 'Required Body'
            self.assertEqual(len(discussion_form.messages), 2)

        # Test the same flow but when the important message is archived.
        # The _compute_has_important_sibling should take in account archived siblings.
        discussion = self.env['test_orm.discussion'].create({
            'messages': [
                Command.create({'body': 'Archived Important sibling', 'important': True, 'active': False}),
            ],
            'name': 'Required field',
            'participants': [Command.set(self.env.user.ids)],
        })

        with Form(discussion, 'test_orm.discussion_form_2') as discussion_form:
            self.assertEqual(len(discussion_form.messages), 0)
            with discussion_form.messages.new() as message_form:
                # this actually checks that during the onchange, the new message
                # has access to its archived sibling messages through the discussion
                self.assertEqual(message_form.has_important_sibling, True)
                message_form.body = 'Required Body'
            self.assertEqual(len(discussion_form.messages), 1)

    def test_protection_shared_compute(self):
        model = self.env['test_orm.shared.compute']
        START = 5

        with Form(model) as form:
            self.assertEqual(form.start, 0)
            self.assertEqual(form.end, 10)

            form.start = START
            self.assertEqual(form.start, START, "updating 'start' should not recompute it")
            self.assertEqual(form.end, 10, "updating 'start' should not recompute 'end'")

            form.name = f"{START}->{START + 20}"
            self.assertEqual(form.start, START, "updating 'name' should recompute 'start'")
            self.assertEqual(form.end, START + 20, "updating 'name' should recompute 'end'")

    def test_new_one2many_traversing_many2one_second_onchange(self):
        discussion = self.env['test_orm.discussion'].create({
            'name': 'Required field',
            'messages': [Command.create({'body': 'Msg1: Existing', 'important': True})],
            'participants': [Command.set(self.env.user.ids)],
        })

        with Form(discussion, 'test_orm.discussion_form_2') as discussion_form:
            self.assertEqual(len(discussion_form.messages), 1)
            with discussion_form.messages.new() as message_form:
                # check that Msg1 is visible in the one2many during onchange()
                self.assertEqual(message_form.has_important_sibling, True)
                message_form.body = 'Msg2: New'

            self.assertEqual(len(discussion_form.messages), 2)
            with discussion_form.messages.new() as message_form:
                # check that Msg1 is visible in the one2many during onchange()
                self.assertEqual(message_form.has_important_sibling, True)
                message_form.body = 'Msg3: New'

    def test_absent_dependencies_fields(self):
        model = self.env['test_orm.onchange.partial.view']
        created_record = model.create({})
        self.assertEqual(created_record.currency_id, self.env.company.currency_id)

        with Form(self.env['test_orm.onchange.partial.view']) as record_form:
            self.assertEqual(record_form.currency_id, self.env.company.currency_id)
            saved_record = record_form.save()

        self.assertEqual(saved_record.currency_id, self.env.company.currency_id)
