#  Unit test case for testing HasTraits 'on_trait_change' support.
#
#  Written by: David C. Morrill
#
#  Date: 4/10/2007
#
#  (c) Copyright 2007 by Enthought, Inc.
#
#  This software is provided without warranty under the terms of the BSD
#  license included in /LICENSE.txt and may be redistributed only
#  under the conditions described in the aforementioned license.  The license
#  is also available online at http://www.enthought.com/licenses/BSD.txt

""" Unit test case for testing HasTraits 'on_trait_change' support.
"""

from __future__ import absolute_import

from traits.testing.unittest_tools import unittest

from traits.api import (
    Any,
    Dict,
    HasTraits,
    Instance,
    Int,
    List,
    Property,
    TraitDictEvent,
    TraitError,
    TraitListEvent,
    Undefined,
    cached_property,
    on_trait_change,
    pop_exception_handler,
    push_exception_handler,
)

from traits.trait_handlers import TraitListObject, TraitDictObject


class ArgCheckBase(HasTraits):

    value = Int(0)
    int1 = Int(0, test=True)
    int2 = Int(0)
    int3 = Int(0, test=True)
    tint1 = Int(0)
    tint2 = Int(0, test=True)
    tint3 = Int(0)

    calls = Int(0)
    tc = Any


class ArgCheckSimple(ArgCheckBase):
    def arg_check0(self):
        self.calls += 1

    def arg_check1(self, new):
        self.calls += 1
        self.tc.assertEqual(new, self.value)

    def arg_check2(self, name, new):
        self.calls += 1
        self.tc.assertEqual(name, "value")
        self.tc.assertEqual(new, self.value)

    def arg_check3(self, object, name, new):
        self.calls += 1
        self.tc.assertIs(object, self)
        self.tc.assertEqual(name, "value")
        self.tc.assertEqual(new, self.value)

    def arg_check4(self, object, name, old, new):
        self.calls += 1
        self.tc.assertIs(object, self)
        self.tc.assertEqual(name, "value")
        self.tc.assertEqual(old, (self.value - 1))
        self.tc.assertEqual(new, self.value)


class ArgCheckDecorator(ArgCheckBase):
    @on_trait_change("value")
    def arg_check0(self):
        self.calls += 1

    @on_trait_change("value")
    def arg_check1(self, new):
        self.calls += 1
        self.tc.assertEqual(new, self.value)

    @on_trait_change("value")
    def arg_check2(self, name, new):
        self.calls += 1
        self.tc.assertEqual(name, "value")
        self.tc.assertEqual(new, self.value)

    @on_trait_change("value")
    def arg_check3(self, object, name, new):
        self.calls += 1
        self.tc.assertIs(object, self)
        self.tc.assertEqual(name, "value")
        self.tc.assertEqual(new, self.value)

    @on_trait_change("value")
    def arg_check4(self, object, name, old, new):
        self.calls += 1
        self.tc.assertIs(object, self)
        self.tc.assertEqual(name, "value")
        self.tc.assertEqual(old, (self.value - 1))
        self.tc.assertEqual(new, self.value)


class Instance1(HasTraits):

    ref = Instance(ArgCheckBase, ())

    calls = Int(0)
    exp_object = Any
    exp_name = Any
    dst_name = Any
    exp_old = Any
    exp_new = Any
    dst_new = Any
    tc = Any

    @on_trait_change("ref.value")
    def arg_check0(self):
        self.calls += 1

    @on_trait_change("ref.value")
    def arg_check1(self, new):
        self.calls += 1
        self.tc.assertEqual(new, self.dst_new)

    @on_trait_change("ref.value")
    def arg_check2(self, name, new):
        self.calls += 1
        self.tc.assertEqual(name, self.dst_name)
        self.tc.assertEqual(new, self.dst_new)

    @on_trait_change("ref.value")
    def arg_check3(self, object, name, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(new, self.exp_new)

    @on_trait_change("ref.value")
    def arg_check4(self, object, name, old, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(old, self.exp_old)
        self.tc.assertEqual(new, self.exp_new)


class List1(HasTraits):

    refs = List(ArgCheckBase)
    calls = Int(0)

    exp_object = Any
    exp_name = Any
    type_old = Any
    exp_old = Any
    type_new = Any
    exp_new = Any
    tc = Any

    @on_trait_change("refs.value")
    def arg_check0(self):
        self.calls += 1

    @on_trait_change("refs.value")
    def arg_check3(self, object, name, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        if self.type_new is None:
            self.tc.assertEqual(new, self.exp_new)
        else:
            self.tc.assertIsInstance(new, self.type_new)

    @on_trait_change("refs.value")
    def arg_check4(self, object, name, old, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        if self.type_old is None:
            self.tc.assertEqual(old, self.exp_old)
        else:
            self.tc.assertIsInstance(old, self.type_old)
        if self.type_new is None:
            self.tc.assertEqual(new, self.exp_new)
        else:
            self.tc.assertIsInstance(new, self.type_new)


class List2(HasTraits):

    refs = List(ArgCheckBase)

    calls = Int(0)
    exp_new = Any
    tc = Any

    @on_trait_change("refs.value")
    def arg_check1(self, new):
        self.calls += 1
        self.tc.assertEqual(new, self.exp_new)


class List3(HasTraits):

    refs = List(ArgCheckBase)

    calls = Int(0)
    exp_name = Any
    exp_new = Any
    tc = Any

    @on_trait_change("refs.value")
    def arg_check2(self, name, new):
        self.calls += 1
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(new, self.exp_new)


class Dict1(List1):
    refs = Dict(Int, ArgCheckBase)


class Dict2(HasTraits):

    refs = Dict(Int, ArgCheckBase)

    calls = Int(0)
    exp_new = Any
    tc = Any

    @on_trait_change("refs.value")
    def arg_check1(self, new):
        self.calls += 1
        self.tc.assertEqual(new, self.exp_new)


class Dict3(HasTraits):

    refs = Dict(Int, ArgCheckBase)

    calls = Int(0)
    exp_name = Any
    exp_new = Any
    tc = Any

    @on_trait_change("refs.value")
    def arg_check2(self, name, new):
        self.calls += 1
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(new, self.exp_new)


class Complex(HasTraits):

    int1 = Int(0, test=True)
    int2 = Int(0)
    int3 = Int(0, test=True)
    tint1 = Int(0)
    tint2 = Int(0, test=True)
    tint3 = Int(0)
    ref = Instance(ArgCheckBase, ())

    calls = Int(0)
    exp_object = Any
    exp_name = Any
    dst_name = Any
    exp_old = Any
    exp_new = Any
    dst_new = Any
    tc = Any

    def arg_check0(self):
        self.calls += 1

    def arg_check1(self, new):
        self.calls += 1
        self.tc.assertEqual(new, self.exp_new)

    def arg_check2(self, name, new):
        self.calls += 1
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(new, self.exp_new)

    def arg_check3(self, object, name, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(new, self.exp_new)

    def arg_check4(self, object, name, old, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(old, self.exp_old)
        self.tc.assertEqual(new, self.exp_new)


class Link(HasTraits):

    next = Any
    prev = Any
    value = Int(0)


class LinkTest(HasTraits):

    head = Instance(Link)

    calls = Int(0)
    exp_object = Any
    exp_name = Any
    dst_name = Any
    exp_old = Any
    exp_new = Any
    dst_new = Any
    tc = Any

    def arg_check0(self):
        self.calls += 1

    def arg_check1(self, new):
        self.calls += 1
        self.tc.assertEqual(new, self.exp_new)

    def arg_check2(self, name, new):
        self.calls += 1
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(new, self.exp_new)

    def arg_check3(self, object, name, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(new, self.exp_new)

    def arg_check4(self, object, name, old, new):
        self.calls += 1
        self.tc.assertIs(object, self.exp_object)
        self.tc.assertEqual(name, self.exp_name)
        self.tc.assertEqual(old, self.exp_old)
        self.tc.assertEqual(new, self.exp_new)


class PropertyDependsOn(HasTraits):

    sum = Property(depends_on="ref.[int1,int2,int3]")
    ref = Instance(ArgCheckBase, ())

    pcalls = Int(0)
    calls = Int(0)
    exp_old = Any
    exp_new = Any
    tc = Any

    @cached_property
    def _get_sum(self):
        self.pcalls += 1
        r = self.ref
        return r.int1 + r.int2 + r.int3

    def _sum_changed(self, old, new):
        self.calls += 1
        self.tc.assertEqual(old, self.exp_old)
        self.tc.assertEqual(new, self.exp_new)


class OnTraitChangeTest(unittest.TestCase):
    def setUp(self):
        def ignore(*args):
            pass

        push_exception_handler(handler=ignore, reraise_exceptions=True)

    def tearDown(self):
        pop_exception_handler()

    def test_arg_check_simple(self):
        ac = ArgCheckSimple(tc=self)
        ac.on_trait_change(ac.arg_check0, "value")
        ac.on_trait_change(ac.arg_check1, "value")
        ac.on_trait_change(ac.arg_check2, "value")
        ac.on_trait_change(ac.arg_check3, "value")
        ac.on_trait_change(ac.arg_check4, "value")
        for i in range(3):
            ac.value += 1
        self.assertEqual(ac.calls, (3 * 5))
        ac.on_trait_change(ac.arg_check0, "value", remove=True)
        ac.on_trait_change(ac.arg_check1, "value", remove=True)
        ac.on_trait_change(ac.arg_check2, "value", remove=True)
        ac.on_trait_change(ac.arg_check3, "value", remove=True)
        ac.on_trait_change(ac.arg_check4, "value", remove=True)
        for i in range(3):
            ac.value += 1
        self.assertEqual(ac.calls, (3 * 5))
        self.assertEqual(ac.value, (2 * 3))

    def test_arg_check_decorator(self):
        ac = ArgCheckDecorator(tc=self)
        for i in range(3):
            ac.value += 1
        self.assertEqual(ac.calls, (3 * 5))
        self.assertEqual(ac.value, 3)

    def test_instance1(self):
        i1 = Instance1(tc=self)
        for i in range(3):
            i1.trait_set(
                exp_object=i1.ref,
                exp_name="value",
                dst_name="value",
                exp_old=i,
                exp_new=(i + 1),
                dst_new=(i + 1),
            )
            i1.ref.value = i + 1
        self.assertEqual(i1.calls, (3 * 5))
        self.assertEqual(i1.ref.value, 3)
        ref = ArgCheckBase()
        i1.trait_set(
            exp_object=i1,
            exp_name="ref",
            dst_name="value",
            exp_old=i1.ref,
            exp_new=ref,
            dst_new=0,
        )
        i1.ref = ref
        self.assertEqual(i1.calls, (4 * 5))
        self.assertEqual(i1.ref.value, 0)
        for i in range(3):
            i1.trait_set(
                exp_object=i1.ref,
                exp_name="value",
                dst_name="value",
                exp_old=i,
                exp_new=(i + 1),
                dst_new=(i + 1),
            )
            i1.ref.value = i + 1
        self.assertEqual(i1.calls, (7 * 5))
        self.assertEqual(i1.ref.value, 3)

    def test_list1(self):
        l1 = List1(tc=self)
        for i in range(3):
            ac = ArgCheckBase()
            l1.trait_set(
                exp_object=l1,
                exp_name="refs_items",
                type_old=None,
                exp_old=Undefined,
                type_new=TraitListEvent,
            )
            l1.refs.append(ac)
        # self.assertEqual(l1.calls, (3 * 3))  # FIXME
        for i in range(3):
            self.assertEqual(l1.refs[i].value, 0)
        refs = [ArgCheckBase(), ArgCheckBase(), ArgCheckBase()]
        l1.trait_set(
            exp_object=l1,
            exp_name="refs",
            type_old=None,
            exp_old=l1.refs,
            type_new=TraitListObject,
        )
        l1.refs = refs
        # self.assertEqual(l1.calls, (4 * 3))
        for i in range(3):
            self.assertEqual(l1.refs[i].value, 0)
        for i in range(3):
            for j in range(3):
                l1.trait_set(
                    exp_object=l1.refs[j],
                    exp_name="value",
                    type_old=None,
                    exp_old=i,
                    type_new=None,
                    exp_new=(i + 1),
                )
                l1.refs[j].value = i + 1
        # self.assertEqual(l1.calls, (13 * 3))
        for i in range(3):
            self.assertEqual(l1.refs[i].value, 3)

    def test_list2(self):
        self.check_list(List2(tc=self))

    def test_list3(self):
        self.check_list(List3(tc=self))

    def test_dict1(self):
        d1 = Dict1(tc=self)
        for i in range(3):
            ac = ArgCheckBase()
            d1.trait_set(
                exp_object=d1,
                exp_name="refs_items",
                type_old=None,
                exp_old=Undefined,
                type_new=TraitDictEvent,
            )
            d1.refs[i] = ac
        # self.assertEqual(d1.calls, (3 * 3))  # FIXME
        for i in range(3):
            self.assertEqual(d1.refs[i].value, 0)
        refs = {0: ArgCheckBase(), 1: ArgCheckBase(), 2: ArgCheckBase()}
        d1.trait_set(
            exp_object=d1,
            exp_name="refs",
            type_old=None,
            exp_old=d1.refs,
            type_new=TraitDictObject,
        )
        d1.refs = refs
        # self.assertEqual(d1.calls, (4 * 3))
        for i in range(3):
            self.assertEqual(d1.refs[i].value, 0)
        for i in range(3):
            for j in range(3):
                d1.trait_set(
                    exp_object=d1.refs[j],
                    exp_name="value",
                    type_old=None,
                    exp_old=i,
                    type_new=None,
                    exp_new=(i + 1),
                )
                d1.refs[j].value = i + 1
        # self.assertEqual(d1.calls, (13 * 3))
        for i in range(3):
            self.assertEqual(d1.refs[i].value, 3)

    def test_dict2(self):
        self.check_dict(Dict2(tc=self))

    def test_dict3(self):
        self.check_dict(Dict3(tc=self))

    def test_pattern_list1(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c,
            "int1, int2, int3",
            ["int1", "int2", "int3"],
            ["tint1", "tint2", "tint3"],
        )

    def test_pattern_list2(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c,
            ["int1", "int2", "int3"],
            ["int1", "int2", "int3"],
            ["tint1", "tint2", "tint3"],
        )

    def test_pattern_list3(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c.ref,
            "ref.[int1, int2, int3]",
            ["int1", "int2", "int3"],
            ["tint1", "tint2", "tint3"],
        )

    def test_pattern_list4(self):
        c = Complex(tc=self)
        handlers = [c.arg_check0, c.arg_check3, c.arg_check4]
        n = len(handlers)
        pattern = "ref.[int1,int2,int3]"
        self.multi_register(c, handlers, pattern)
        r0 = c.ref
        r1 = ArgCheckBase()
        c.trait_set(exp_object=c, exp_name="ref", exp_old=r0, exp_new=r1)
        c.ref = r1
        c.trait_set(exp_old=r1, exp_new=r0)
        c.ref = r0
        self.assertEqual(c.calls, 2 * n)
        self.multi_register(c, handlers, pattern, remove=True)
        c.ref = r1
        c.ref = r0
        self.assertEqual(c.calls, 2 * n)

    def test_pattern_list5(self):
        c = Complex(tc=self)
        c.on_trait_change(c.arg_check1, "ref.[int1,int2,int3]")
        self.assertRaises(TraitError, c.trait_set, ref=ArgCheckBase())

    def test_pattern_list6(self):
        c = Complex(tc=self)
        c.on_trait_change(c.arg_check2, "ref.[int1,int2,int3]")
        self.assertRaises(TraitError, c.trait_set, ref=ArgCheckBase())

    def test_pattern_list7(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c,
            "+test",
            ["int1", "int3", "tint2"],
            ["int2", "tint1", "tint3"],
        )

    def test_pattern_list8(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c,
            "int+test",
            ["int1", "int3"],
            ["int2", "tint1", "tint2", "tint3"],
        )

    def test_pattern_list9(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c,
            "int-test",
            ["int2"],
            ["int1", "int3", "tint4", "tint5", "tint6"],
        )

    def test_pattern_list10(self):
        c = Complex(tc=self)
        self.check_complex(
            c, c, "int+", ["int1", "int2", "int3"], ["tint1", "tint2", "tint3"]
        )

    def test_pattern_list11(self):
        c = Complex(tc=self)
        self.check_complex(
            c, c, "int-", ["int1", "int2", "int3"], ["tint1", "tint2", "tint3"]
        )

    def test_pattern_list12(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c,
            "int+test,tint-test",
            ["int1", "int3", "tint1", "tint3"],
            ["int2", "tint2"],
        )

    def test_pattern_list13(self):
        c = Complex(tc=self)
        self.check_complex(
            c,
            c.ref,
            "ref.[int+test,tint-test]",
            ["int1", "int3", "tint1", "tint3"],
            ["int2", "tint2"],
        )

    def test_cycle1(self):
        lt = LinkTest(tc=self, head=self.build_list())
        handlers = [
            lt.arg_check0,
            lt.arg_check1,
            lt.arg_check2,
            lt.arg_check3,
            lt.arg_check4,
        ]
        nh = len(handlers)
        self.multi_register(lt, handlers, "head.next*.value")
        cur = lt.head
        for i in range(4):
            lt.trait_set(
                exp_object=cur,
                exp_name="value",
                exp_old=10 * i,
                exp_new=(10 * i) + 1,
            )
            cur.value = (10 * i) + 1
            cur = cur.next
        self.assertEqual(lt.calls, 4 * nh)
        self.multi_register(lt, handlers, "head.next*.value", remove=True)
        cur = lt.head
        for i in range(4):
            cur.value = (10 * i) + 2
            cur = cur.next
        self.assertEqual(lt.calls, 4 * nh)

    def test_cycle2(self):
        lt = LinkTest(tc=self, head=self.build_list())
        handlers = [
            lt.arg_check0,
            lt.arg_check1,
            lt.arg_check2,
            lt.arg_check3,
            lt.arg_check4,
        ]
        nh = len(handlers)
        self.multi_register(lt, handlers, "head.[next,prev]*.value")
        cur = lt.head
        for i in range(4):
            lt.trait_set(
                exp_object=cur,
                exp_name="value",
                exp_old=10 * i,
                exp_new=(10 * i) + 1,
            )
            cur.value = (10 * i) + 1
            cur = cur.next
        self.assertEqual(lt.calls, 4 * nh)
        self.multi_register(
            lt, handlers, "head.[next,prev]*.value", remove=True
        )
        cur = lt.head
        for i in range(4):
            cur.value = (10 * i) + 2
            cur = cur.next
        self.assertEqual(lt.calls, 4 * nh)

    def test_cycle3(self):
        lt = LinkTest(tc=self, head=self.build_list())
        handlers = [lt.arg_check0, lt.arg_check3, lt.arg_check4]
        nh = len(handlers)
        self.multi_register(lt, handlers, "head.next*.value")
        link = self.new_link(lt, lt.head, 1)
        self.assertEqual(lt.calls, nh)
        link = self.new_link(lt, link, 2)
        self.assertEqual(lt.calls, 2 * nh)
        self.multi_register(lt, handlers, "head.next*.value", remove=True)
        link = self.new_link(lt, link, 3)
        self.assertEqual(lt.calls, 2 * nh)

    def test_property(self):
        pdo = PropertyDependsOn(tc=self)
        sum = pdo.sum
        self.assertEqual(sum, 0)
        for n in ["int1", "int2", "int3"]:
            for i in range(3):
                pdo.trait_set(exp_old=sum, exp_new=sum + 1)
                setattr(pdo.ref, n, i + 1)
                sum += 1
        self.assertEqual(pdo.pcalls, (3 * 3) + 1)
        self.assertEqual(pdo.calls, 3 * 3)
        for i in range(10):
            x = pdo.sum
        self.assertEqual(pdo.pcalls, (3 * 3) + 1)
        pdo.trait_set(exp_old=sum, exp_new=60)
        old_ref = pdo.ref
        pdo.ref = ArgCheckBase(int1=10, int2=20, int3=30)
        self.assertEqual(pdo.pcalls, (3 * 3) + 2)
        self.assertEqual(pdo.calls, (3 * 3) + 1)
        sum = 60
        for n in ["int1", "int2", "int3"]:
            for i in range(3):
                pdo.trait_set(exp_old=sum, exp_new=sum + 1)
                setattr(pdo.ref, n, getattr(pdo.ref, n) + 1)
                sum += 1
        self.assertEqual(pdo.pcalls, (2 * 3 * 3) + 2)
        self.assertEqual(pdo.calls, (2 * 3 * 3) + 1)
        for n in ["int1", "int2", "int3"]:
            for i in range(3):
                setattr(old_ref, n, getattr(old_ref, n) + 1)
        self.assertEqual(pdo.pcalls, (2 * 3 * 3) + 2)
        self.assertEqual(pdo.calls, (2 * 3 * 3) + 1)
        self.assertEqual(pdo.sum, sum)
        self.assertEqual(pdo.pcalls, (2 * 3 * 3) + 2)

    def check_list(self, l):
        for i in range(3):
            ac = ArgCheckBase()
            self.assertRaises(TraitError, l.refs.append, ac)
        self.assertEqual(l.calls, 0)
        for i in range(3):
            self.assertEqual(l.refs[i].value, 0)
        refs = [ArgCheckBase(), ArgCheckBase(), ArgCheckBase()]
        self.assertRaises(TraitError, l.trait_set, refs=refs)
        self.assertEqual(l.calls, 0)
        for i in range(3):
            self.assertEqual(l.refs[i].value, 0)
        for i in range(3):
            for j in range(3):
                l.exp_new = i + 1
                l.refs[j].value = i + 1
        self.assertEqual(l.calls, 0)
        for i in range(3):
            self.assertEqual(l.refs[i].value, 3)

    def check_dict(self, d):
        for i in range(3):
            ac = ArgCheckBase()
            self.assertRaises(TraitError, d.refs.setdefault, i, ac)
        self.assertEqual(d.calls, 0)
        for i in range(3):
            self.assertEqual(d.refs[i].value, 0)
        refs = {0: ArgCheckBase(), 1: ArgCheckBase(), 2: ArgCheckBase()}
        self.assertRaises(TraitError, d.trait_set, refs=refs)
        self.assertEqual(d.calls, 0)
        for i in range(3):
            self.assertEqual(d.refs[i].value, 0)
        for i in range(3):
            for j in range(3):
                d.exp_new = i + 1
                d.refs[j].value = i + 1
        self.assertEqual(d.calls, 0)
        for i in range(3):
            self.assertEqual(d.refs[i].value, 3)

    def check_complex(self, c, r, pattern, names, other=[]):
        handlers = [
            c.arg_check0,
            c.arg_check1,
            c.arg_check2,
            c.arg_check3,
            c.arg_check4,
        ]
        nh = len(handlers)
        nn = len(names)
        self.multi_register(c, handlers, pattern)
        for i in range(3):
            for n in names:
                c.trait_set(
                    exp_object=r, exp_name=n, exp_old=i, exp_new=(i + 1)
                )
                setattr(r, n, i + 1)
            for n in other:
                c.trait_set(
                    exp_object=r, exp_name=n, exp_old=i, exp_new=(i + 1)
                )
                setattr(r, n, i + 1)
        self.assertEqual(c.calls, 3 * nn * nh)
        self.multi_register(c, handlers, pattern, remove=True)
        for i in range(3):
            for n in names:
                setattr(r, n, i + 1)
            for n in other:
                setattr(r, n, i + 1)
        self.assertEqual(c.calls, 3 * nn * nh)

    def multi_register(self, object, handlers, pattern, remove=False):
        for handler in handlers:
            object.on_trait_change(handler, pattern, remove=remove)

    def build_list(self):
        l1 = Link(value=00)
        l2 = Link(value=10)
        l3 = Link(value=20)
        l4 = Link(value=30)
        l1.trait_set(next=l2, prev=l4)
        l2.trait_set(next=l3, prev=l1)
        l3.trait_set(next=l4, prev=l2)
        l4.trait_set(next=l1, prev=l3)
        return l1

    def new_link(self, lt, cur, value):
        link = Link(value=value, next=cur.next, prev=cur)
        cur.next.prev = link
        lt.trait_set(
            exp_object=cur, exp_name="next", exp_old=cur.next, exp_new=link
        )
        cur.next = link
        return link


# Run the unit tests (if invoked from the command line):
if __name__ == "__main__":
    unittest.main()
