import unittest
import tkinter
from tkinter import TclError
from test.support import requires
from test.test_tkinter.support import setUpModule  # noqa: F401
from test.test_tkinter.support import AbstractTkTest, requires_tk

requires('gui')

class TextTest(AbstractTkTest, unittest.TestCase):

    def setUp(self):
        super().setUp()
        self.text = tkinter.Text(self.root)
        self.text.pack()

    def test_debug(self):
        text = self.text
        olddebug = text.debug()
        try:
            text.debug(False)
            self.assertIs(text.debug(), False)
            text.debug(True)
            self.assertIs(text.debug(), True)
        finally:
            text.debug(olddebug)
            self.assertEqual(text.debug(), olddebug)

    def test_sync(self):
        text = self.text
        # sync() returns None and brings line metrics up to date.
        self.assertIsNone(text.sync())
        self.assertIs(text.pendingsync(), False)

        # sync(command) schedules a one-shot callback.
        events = []
        text.sync(command=lambda: events.append('synced'))
        text.update()
        self.assertEqual(events, ['synced'])

    def test_index(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
        self.assertEqual(text.index('1.0'), '1.0')
        self.assertEqual(text.index('1.end'), '1.11')
        self.assertEqual(text.index('end'), '3.0')
        self.assertEqual(text.index('2.5'), '2.5')
        # Index past the end of a line is clamped to its end.
        self.assertEqual(text.index('1.100'), '1.11')
        self.assertRaises(TclError, text.index, 'invalid')
        self.assertRaises(TypeError, text.index)
        self.assertRaises(TypeError, text.index, '1.0', '2.5')

    def test_compare(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
        self.assertIs(text.compare('1.0', '<', '2.0'), True)
        self.assertIs(text.compare('2.0', '<', '1.0'), False)
        self.assertIs(text.compare('1.5', '==', '1.5'), True)
        self.assertIs(text.compare('1.5', '!=', '1.5'), False)
        self.assertIs(text.compare('2.0', '>=', '2.0'), True)
        self.assertIs(text.compare('1.0', '<=', 'end'), True)
        self.assertRaises(TclError, text.compare, '1.0', 'invalid', '2.0')
        self.assertRaises(TypeError, text.compare, '1.0', '<')
        self.assertRaises(TypeError, text.compare, '1.0', '<', '2.0', '3.0')

    def test_insert_get_delete(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum')
        self.assertEqual(text.get('1.0', 'end'), 'Lorem ipsum\n')
        self.assertEqual(text.get('1.0', '1.5'), 'Lorem')
        self.assertEqual(text.get('1.6'), 'i')  # single character

        # insert before an existing index
        text.insert('1.0', '*** ')
        self.assertEqual(text.get('1.0', 'end'), '*** Lorem ipsum\n')

        text.delete('1.0', '1.4')
        self.assertEqual(text.get('1.0', 'end'), 'Lorem ipsum\n')
        text.delete('1.5')  # delete a single character
        self.assertEqual(text.get('1.0', 'end'), 'Loremipsum\n')
        self.assertRaises(TypeError, text.get)
        self.assertRaises(TypeError, text.get, '1.0', '1.5', 'end')
        self.assertRaises(TypeError, text.delete, '1.0', '1.5', 'end')

    def test_insert_with_tags(self):
        text = self.text
        text.insert('1.0', 'hello', 'a', ' ', ('a', 'b'), 'world', 'b')
        self.assertEqual(text.get('1.0', 'end'), 'hello world\n')
        self.assertEqual([str(i) for i in text.tag_ranges('a')],
                         ['1.0', '1.6'])
        self.assertEqual([str(i) for i in text.tag_ranges('b')],
                         ['1.5', '1.11'])

    def test_replace(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum')
        text.replace('1.0', '1.5', 'Hello')
        self.assertEqual(text.get('1.0', 'end'), 'Hello ipsum\n')
        text.replace('1.6', 'end - 1 char', 'world')
        self.assertEqual(text.get('1.0', 'end'), 'Hello world\n')
        self.assertRaises(TclError, text.replace, 'invalid', '1.5', 'x')
        # The first index must not be after the second.
        self.assertRaises(TclError, text.replace, '1.5', '1.0', 'x')
        self.assertRaises(TypeError, text.replace, '1.0', '1.5')

    def test_mark(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
        # 'insert' and 'current' marks always exist.
        self.assertIn('insert', text.mark_names())
        self.assertIn('current', text.mark_names())

        text.mark_set('here', '1.5')
        self.assertIn('here', text.mark_names())
        self.assertEqual(text.index('here'), '1.5')
        text.mark_set('here', '2.3')
        self.assertEqual(text.index('here'), '2.3')

        text.mark_unset('here')
        self.assertNotIn('here', text.mark_names())

        # Unsetting a non-existent mark is not an error.
        text.mark_unset('nonexistent')
        self.assertRaises(TclError, text.mark_set, 'm', 'invalid')
        self.assertRaises(TypeError, text.mark_set, 'm')
        self.assertRaises(TypeError, text.mark_set, 'm', '1.0', '2.0')

    def test_mark_gravity(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum')
        text.mark_set('here', '1.5')
        # The default gravity is right.
        self.assertEqual(text.mark_gravity('here'), 'right')
        text.mark_gravity('here', 'left')
        self.assertEqual(text.mark_gravity('here'), 'left')
        # With left gravity the mark stays before inserted text.
        text.insert('here', 'XXX')
        self.assertEqual(text.index('here'), '1.5')
        # With right gravity the mark moves after inserted text.
        text.mark_gravity('here', 'right')
        text.insert('here', 'YYY')
        self.assertEqual(text.index('here'), '1.8')

        self.assertRaises(TclError, text.mark_gravity, 'nonexistent')
        self.assertRaises(TclError, text.mark_gravity, 'here', 'invalid')
        self.assertRaises(TypeError, text.mark_gravity)
        self.assertRaises(TypeError, text.mark_gravity, 'here', 'left', 'extra')

    def test_mark_next_previous(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum')
        # Keep the builtin 'insert' and 'current' marks at 1.0 so they do
        # not interfere with the queries below.
        text.mark_set('insert', '1.0')
        text.mark_set('current', '1.0')
        text.mark_set('m1', '1.3')
        text.mark_set('m2', '1.7')
        # mark_next finds a mark at a position at or after the index,
        # mark_previous finds one at a position strictly before it.
        self.assertEqual(text.mark_next('1.1'), 'm1')
        self.assertEqual(text.mark_next('1.3'), 'm1')
        self.assertEqual(text.mark_next('1.4'), 'm2')
        self.assertIsNone(text.mark_next('1.8'))
        self.assertEqual(text.mark_previous('1.4'), 'm1')
        self.assertEqual(text.mark_previous('1.7'), 'm1')
        self.assertEqual(text.mark_previous('end'), 'm2')
        self.assertIsNone(text.mark_previous('1.0'))

        self.assertRaises(TclError, text.mark_next, 'invalid')
        self.assertRaises(TclError, text.mark_previous, 'invalid')
        self.assertRaises(TypeError, text.mark_next)
        self.assertRaises(TypeError, text.mark_previous)
        self.assertRaises(TypeError, text.mark_next, '1.0', '2.0')
        self.assertRaises(TypeError, text.mark_previous, '1.0', '2.0')

    def test_tag_add_remove_ranges(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
        self.assertEqual(text.tag_ranges('sel'), ())

        text.tag_add('big', '1.0', '1.5')
        self.assertEqual([str(i) for i in text.tag_ranges('big')],
                         ['1.0', '1.5'])
        # Add a second, disjoint range.
        text.tag_add('big', '2.0', '2.5')
        self.assertEqual([str(i) for i in text.tag_ranges('big')],
                         ['1.0', '1.5', '2.0', '2.5'])

        text.tag_remove('big', '1.0', '1.3')
        self.assertEqual([str(i) for i in text.tag_ranges('big')],
                         ['1.3', '1.5', '2.0', '2.5'])

        # tag_ranges of an undefined tag is empty, not an error.
        self.assertEqual(text.tag_ranges('nonexistent'), ())
        self.assertRaises(TclError, text.tag_add, 'big', 'invalid')
        self.assertRaises(TypeError, text.tag_add, 'big')
        self.assertRaises(TclError, text.tag_remove, 'big', 'invalid')
        self.assertRaises(TypeError, text.tag_remove, 'big', '1.0', '2.0', '3.0')
        self.assertRaises(TypeError, text.tag_ranges, 'big', 'extra')

    def test_tag_names(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum')
        text.tag_add('a', '1.0', '1.5')
        text.tag_add('b', '1.3', '1.8')
        # 'sel' always exists; order reflects priority (creation order).
        self.assertEqual(set(text.tag_names()), {'sel', 'a', 'b'})
        # Tags applied to the character at the given index.
        self.assertEqual(set(text.tag_names('1.4')), {'a', 'b'})
        self.assertEqual(set(text.tag_names('1.0')), {'a'})
        self.assertEqual(set(text.tag_names('1.10')), set())
        self.assertRaises(TclError, text.tag_names, 'invalid')
        self.assertRaises(TypeError, text.tag_names, '1.0', '2.0')

    def test_tag_nextrange_prevrange(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum dolor')
        text.tag_add('a', '1.0', '1.5')
        text.tag_add('a', '1.12', '1.17')

        self.assertEqual([str(i) for i in text.tag_nextrange('a', '1.0')],
                         ['1.0', '1.5'])
        self.assertEqual([str(i) for i in text.tag_nextrange('a', '1.5')],
                         ['1.12', '1.17'])
        self.assertEqual(text.tag_nextrange('a', '1.17'), ())

        self.assertEqual([str(i) for i in text.tag_prevrange('a', 'end')],
                         ['1.12', '1.17'])
        self.assertEqual([str(i) for i in text.tag_prevrange('a', '1.12')],
                         ['1.0', '1.5'])
        self.assertEqual(text.tag_prevrange('a', '1.0'), ())

        # An undefined tag has no ranges, but the index must be valid.
        self.assertEqual(text.tag_nextrange('nonexistent', '1.0'), ())
        self.assertRaises(TclError, text.tag_nextrange, 'a', 'invalid')
        self.assertRaises(TclError, text.tag_prevrange, 'a', 'invalid')
        self.assertRaises(TypeError, text.tag_nextrange, 'a')
        self.assertRaises(TypeError, text.tag_prevrange, 'a')
        self.assertRaises(TypeError, text.tag_nextrange, 'a', '1.0', '2.0', '3.0')
        self.assertRaises(TypeError, text.tag_prevrange, 'a', '1.0', '2.0', '3.0')

    def test_tag_delete(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum')
        text.tag_add('a', '1.0', '1.5')
        text.tag_add('b', '1.6', '1.11')
        self.assertEqual(set(text.tag_names()), {'sel', 'a', 'b'})
        text.tag_delete('a', 'b')
        self.assertEqual(set(text.tag_names()), {'sel'})
        self.assertEqual(text.tag_ranges('a'), ())

    def test_tag_raise_lower(self):
        text = self.text
        text.tag_configure('a', foreground='red')
        text.tag_configure('b', foreground='green')
        text.tag_configure('c', foreground='blue')
        # Creation order is lowest-to-highest priority; tag_names lists
        # them in increasing priority order.
        self.assertEqual(text.tag_names(), ('sel', 'a', 'b', 'c'))
        text.tag_raise('a')
        self.assertEqual(text.tag_names(), ('sel', 'b', 'c', 'a'))
        text.tag_lower('a')
        self.assertEqual(text.tag_names(), ('a', 'sel', 'b', 'c'))
        text.tag_raise('a', 'b')
        self.assertEqual(text.tag_names(), ('sel', 'b', 'a', 'c'))
        text.tag_lower('c', 'b')
        self.assertEqual(text.tag_names(), ('sel', 'c', 'b', 'a'))

        self.assertRaises(TclError, text.tag_raise, 'nonexistent')
        self.assertRaises(TclError, text.tag_lower, 'nonexistent')
        self.assertRaises(TclError, text.tag_raise, 'a', 'nonexistent')
        self.assertRaises(TypeError, text.tag_raise)
        self.assertRaises(TypeError, text.tag_raise, 'a', 'b', 'c')
        self.assertRaises(TypeError, text.tag_lower, 'a', 'b', 'c')

    def test_tag_cget_configure(self):
        text = self.text
        text.tag_configure('a', foreground='red', underline=True)
        self.assertEqual(str(text.tag_cget('a', 'foreground')), 'red')
        self.assertIn(text.tag_cget('a', 'underline'), (1, '1', True))
        # tag_cget normalizes the option name (no leading hyphen needed).
        self.assertEqual(str(text.tag_cget('a', '-foreground')), 'red')
        # configure() query returns the full option spec.
        self.assertEqual(text.tag_configure('a', 'foreground')[-1], 'red')
        text.tag_configure('a', foreground='blue')
        self.assertEqual(str(text.tag_cget('a', 'foreground')), 'blue')

        self.assertRaises(TclError, text.tag_cget, 'nonexistent',
                          'foreground')
        self.assertRaises(TypeError, text.tag_cget, 'a')
        self.assertRaises(TypeError, text.tag_cget, 'a', 'foreground', 'extra')

    def test_edit_modified(self):
        text = self.text
        self.assertIs(text.edit_modified(), False)
        text.insert('1.0', 'spam')
        self.assertIs(text.edit_modified(), True)
        text.edit_modified(False)
        self.assertIs(text.edit_modified(), False)
        text.edit_modified(True)
        self.assertIs(text.edit_modified(), True)

    def test_edit_undo_redo(self):
        text = self.text
        text.configure(undo=True)
        text.insert('1.0', 'spam')
        text.edit_separator()
        text.insert('end', ' eggs')
        self.assertEqual(text.get('1.0', 'end'), 'spam eggs\n')

        text.edit_undo()
        self.assertEqual(text.get('1.0', 'end'), 'spam\n')
        text.edit_redo()
        self.assertEqual(text.get('1.0', 'end'), 'spam eggs\n')

        # An empty undo stack raises.
        text.edit_reset()
        self.assertRaises(TclError, text.edit_undo)

    def test_edit_canundo_canredo(self):
        text = self.text
        text.configure(undo=True)

        self.assertIs(text.edit_canundo(), False)
        self.assertIs(text.edit_canredo(), False)

        text.insert('1.0', 'spam')
        self.assertIs(text.edit_canundo(), True)
        self.assertIs(text.edit_canredo(), False)

        text.edit_undo()
        self.assertIs(text.edit_canundo(), False)
        self.assertIs(text.edit_canredo(), True)

        text.edit_redo()
        self.assertIs(text.edit_canundo(), True)
        self.assertIs(text.edit_canredo(), False)

        text.edit_reset()
        self.assertIs(text.edit_canundo(), False)
        self.assertIs(text.edit_canredo(), False)

    def test_dump(self):
        text = self.text
        text.insert('1.0', 'hello')
        text.tag_add('a', '1.0', '1.3')
        text.mark_set('here', '1.2')

        dump = text.dump('1.0', 'end')
        self.assertIsInstance(dump, list)
        for item in dump:
            self.assertIsInstance(item, tuple)
            self.assertEqual(len(item), 3)
        kinds = {item[0] for item in dump}
        self.assertIn('text', kinds)
        self.assertIn('tagon', kinds)
        self.assertIn('mark', kinds)

        # Filtering by kind.
        text_only = text.dump('1.0', 'end', text=True)
        self.assertEqual({item[0] for item in text_only}, {'text'})
        self.assertEqual(''.join(item[1] for item in text_only), 'hello\n')

        # The command callback receives each triple and suppresses the result.
        collected = []
        result = text.dump('1.0', 'end', text=True,
                           command=lambda *a: collected.append(a))
        self.assertIsNone(result)
        self.assertEqual(''.join(key_value_index[1]
                                 for key_value_index in collected), 'hello\n')

        self.assertRaises(TclError, text.dump, 'invalid')
        self.assertRaises(TypeError, text.dump)
        self.assertRaises(TypeError, text.dump, '1.0', 'end', None, 'extra')

    def test_image(self):
        text = self.text
        image = tkinter.PhotoImage(master=self.root, width=10, height=10)
        text.insert('1.0', 'AB')
        name = text.image_create('1.1', image=image)
        self.assertIsInstance(name, str)
        self.assertIn(name, self.root.splitlist(text.image_names()))
        self.assertEqual(str(text.image_cget(name, 'image')), str(image))

        text.image_configure(name, align='top')
        self.assertEqual(str(text.image_cget(name, 'align')), 'top')

        # An embedded image occupies a single index position.
        self.assertEqual(text.index('end - 1 char'), '1.3')

        # The image name can be used as an index; it is matched as a whole.
        self.assertEqual(text.index(name), '1.1')

        # Either a name or an image is required, and the index must be valid.
        self.assertRaises(TclError, text.image_create, '1.0')
        self.assertRaises(TclError, text.image_create, 'invalid',
                          image=image)
        self.assertRaises(TypeError, text.image_cget, name)
        self.assertRaises(TypeError, text.image_cget, name, 'image', 'extra')

    def test_window(self):
        text = self.text
        button = tkinter.Button(text, text='ok')
        text.insert('1.0', 'AB')
        text.window_create('1.1', window=button)
        self.assertEqual(self.root.splitlist(text.window_names()),
                         (str(button),))
        self.assertEqual(text.window_cget('1.1', 'window'), str(button))

        # The window can be addressed by its name where an index is expected;
        # the name is matched as a whole rather than parsed (gh-143070).
        self.assertEqual(text.index(str(button)), '1.1')
        self.assertEqual(text.window_cget(str(button), 'window'), str(button))

        text.window_configure('1.1', padx=5)
        self.assertEqual(text.window_cget('1.1', 'padx'), 5)

        # No embedded window at this index, and the index must be valid.
        self.assertRaises(TclError, text.window_cget, '1.0', 'window')
        self.assertRaises(TclError, text.window_cget, 'invalid', 'window')
        self.assertRaises(TypeError, text.window_cget, '1.1')
        self.assertRaises(TypeError, text.window_cget, '1.1', 'window', 'extra')
        button.destroy()

    def test_tag_configure(self):
        text = self.text
        tag = 'a'
        getint = text.tk.getint
        getboolean = text.tk.getboolean

        # Color options.
        for opt in ('foreground', 'background'):
            text.tag_configure(tag, **{opt: '#ff0000'})
            self.assertEqual(str(text.tag_cget(tag, opt)), '#ff0000')

        # Stipple (bitmap) options.
        for opt in ('bgstipple', 'fgstipple'):
            text.tag_configure(tag, **{opt: 'gray50'})
            self.assertEqual(str(text.tag_cget(tag, opt)), 'gray50')

        # Enumerated options.
        for opt, value in (('justify', 'center'), ('wrap', 'word'),
                           ('relief', 'raised'), ('tabstyle', 'wordprocessor'),
                           ('offset', '5')):
            text.tag_configure(tag, **{opt: value})
        self.assertEqual(str(text.tag_cget(tag, 'justify')), 'center')
        self.assertEqual(str(text.tag_cget(tag, 'wrap')), 'word')
        self.assertEqual(str(text.tag_cget(tag, 'relief')), 'raised')
        self.assertEqual(str(text.tag_cget(tag, 'tabstyle')), 'wordprocessor')

        # Boolean options.
        for opt in ('underline', 'overstrike', 'elide'):
            text.tag_configure(tag, **{opt: True})
            self.assertIs(getboolean(text.tag_cget(tag, opt)), True)
            text.tag_configure(tag, **{opt: False})
            self.assertIs(getboolean(text.tag_cget(tag, opt)), False)

        # Screen-distance (pixel) options.
        for opt in ('borderwidth', 'lmargin1', 'lmargin2', 'rmargin',
                    'spacing1', 'spacing2', 'spacing3'):
            text.tag_configure(tag, **{opt: 7})
            self.assertEqual(getint(text.tag_cget(tag, opt)), 7)

        # Other options.
        text.tag_configure(tag, font='Helvetica 12')
        self.assertEqual(str(text.tag_cget(tag, 'font')), 'Helvetica 12')
        text.tag_configure(tag, tabs=(10.2, '1i'))
        self.assertEqual([str(x) for x in text.tag_ranges('sel')], [])

        self.assertRaises(TclError, text.tag_cget, tag, 'spam')

    @requires_tk(8, 6, 6)
    def test_tag_configure_colors(self):
        # Tag color options added in Tk 8.6.6.
        text = self.text
        tag = 'a'
        for opt in ('selectforeground', 'selectbackground',
                    'lmargincolor', 'rmargincolor',
                    'underlinefg', 'overstrikefg'):
            text.tag_configure(tag, **{opt: '#00ff00'})
            self.assertEqual(str(text.tag_cget(tag, opt)), '#00ff00')

    def test_tag_configure_query(self):
        text = self.text
        tag = 'a'
        # Querying all options returns a dict keyed by option name.
        cnf = text.tag_configure(tag)
        self.assertIsInstance(cnf, dict)
        self.assertIn('foreground', cnf)
        # The value is the full 5-tuple option specification.
        self.assertEqual(len(cnf['foreground']), 5)

        # Querying a single option returns its specification.
        spec = text.tag_configure(tag, 'foreground')
        self.assertEqual(spec[0], 'foreground')
        self.assertEqual(spec[-1], '')  # unset by default

        # Setting via keyword arguments and via a dict are equivalent.
        text.tag_configure(tag, foreground='red')
        self.assertEqual(str(text.tag_configure(tag, 'foreground')[-1]), 'red')
        text.tag_configure(tag, {'foreground': 'blue'})
        self.assertEqual(str(text.tag_cget(tag, 'foreground')), 'blue')

        # tag_config is an alias of tag_configure.
        self.assertEqual(text.tag_config, text.tag_configure)

    def test_image_configure(self):
        text = self.text
        image = tkinter.PhotoImage(master=self.root, width=10, height=10)
        text.insert('1.0', 'AB')
        name = text.image_create('1.1', image=image, name='img')
        self.assertEqual(name, 'img')

        self.assertEqual(str(text.image_cget(name, 'name')), 'img')
        self.assertEqual(str(text.image_cget(name, 'image')), str(image))
        for value in ('top', 'center', 'bottom', 'baseline'):
            text.image_configure(name, align=value)
            self.assertEqual(text.image_cget(name, 'align'), value)
        text.image_configure(name, padx=3, pady=4)
        self.assertEqual(text.image_cget(name, 'padx'),
                         3 if self.wantobjects else '3')
        self.assertEqual(text.image_cget(name, 'pady'),
                         4 if self.wantobjects else '4')

        # Querying returns the full option set.
        cnf = text.image_configure(name)
        self.assertIsInstance(cnf, dict)
        self.assertIn('align', cnf)
        self.assertRaises(TclError, text.image_cget, name, 'spam')

    def test_window_configure(self):
        text = self.text
        button = tkinter.Button(text, text='ok')
        text.insert('1.0', 'AB')
        text.window_create('1.1', window=button)

        self.assertEqual(text.window_cget('1.1', 'window'), str(button))
        for value in ('top', 'center', 'bottom', 'baseline'):
            text.window_configure('1.1', align=value)
            self.assertEqual(text.window_cget('1.1', 'align'), value)
        text.window_configure('1.1', padx=3, pady=4)
        self.assertEqual(text.window_cget('1.1', 'padx'),
                         3 if self.wantobjects else '3')
        self.assertEqual(text.window_cget('1.1', 'pady'),
                         4 if self.wantobjects else '4')
        text.window_configure('1.1', stretch=True)
        self.assertIs(text.tk.getboolean(text.window_cget('1.1', 'stretch')),
                      True)

        cnf = text.window_configure('1.1')
        self.assertIsInstance(cnf, dict)
        self.assertIn('stretch', cnf)
        self.assertRaises(TclError, text.window_cget, '1.1', 'spam')
        self.assertEqual(text.window_config, text.window_configure)
        button.destroy()

    def test_peer(self):
        text = self.text
        text.insert('1.0', 'Lorem ipsum')
        self.assertEqual(text.peer_names(), ())

        text.peer_create('.peer1')
        names = self.root.splitlist(text.tk.call('winfo', 'children', '.'))
        self.assertIn('.peer1', [str(n) for n in names])
        self.assertEqual([str(p) for p in text.peer_names()], ['.peer1'])
        # Peers share content.
        self.assertEqual(text.tk.call('.peer1', 'get', '1.0', 'end'),
                         'Lorem ipsum\n')
        text.tk.call('destroy', '.peer1')

    def test_bbox(self):
        text = self.text
        text.insert('1.0', 'hello')
        text.update()
        bbox = text.bbox('1.0')
        self.assertIsInstance(bbox, tuple)
        self.assertEqual(len(bbox), 4)
        for v in bbox:
            self.assertIsInstance(v, int)
        # A character that is not displayed has no bounding box.
        self.assertIsNone(text.bbox('end'))
        self.assertRaises(TclError, text.bbox, 'invalid')
        self.assertRaises(TypeError, text.bbox)
        self.assertRaises(TypeError, text.bbox, '1.0', '2.0')

    def test_dlineinfo(self):
        text = self.text
        text.insert('1.0', 'hello\nworld')
        text.update()
        info = text.dlineinfo('1.0')
        self.assertIsInstance(info, tuple)
        self.assertEqual(len(info), 5)
        for v in info:
            self.assertIsInstance(v, int)
        self.assertRaises(TclError, text.dlineinfo, 'invalid')
        self.assertRaises(TypeError, text.dlineinfo)
        self.assertRaises(TypeError, text.dlineinfo, '1.0', '2.0')

    def test_see(self):
        text = self.text
        text.insert('1.0', '\n'.join('line %d' % i for i in range(200)))
        text.update()
        # Initially the last line is not visible.
        self.assertIsNone(text.bbox('200.0'))
        text.see('200.0')
        text.update()
        self.assertIsNotNone(text.bbox('200.0'))
        self.assertRaises(TclError, text.see, 'invalid')
        self.assertRaises(TypeError, text.see)
        self.assertRaises(TypeError, text.see, '1.0', '2.0')

        # yview_pickplace is a deprecated way to make an index visible.
        text.yview_pickplace('1.0')
        text.update()
        self.assertIsNotNone(text.bbox('1.0'))

    def test_search(self):
        text = self.text

        # pattern and index are obligatory arguments.
        self.assertRaises(TclError, text.search, None, '1.0')
        self.assertRaises(TclError, text.search, 'a', None)
        self.assertRaises(TclError, text.search, None, None)

        # Invalid text index.
        self.assertRaises(TclError, text.search, '', 0)
        self.assertRaises(TclError, text.search, '', '')
        self.assertRaises(TclError, text.search, '', 'invalid')
        self.assertRaises(TclError, text.search, '', '1.0', 0)
        self.assertRaises(TclError, text.search, '', '1.0', '')
        self.assertRaises(TclError, text.search, '', '1.0', 'invalid')

        text.insert('1.0',
            'This is a test. This is only a test.\n'
            'Another line.\n'
            'Yet another line.\n'
            '64-bit')

        self.assertEqual(text.search('test', '1.0'), '1.10')
        self.assertEqual(text.search('test', '1.0', 'end'), '1.10')
        self.assertEqual(text.search('test', '1.0', '1.10'), '')
        self.assertEqual(text.search('test', '1.11'), '1.31')
        self.assertEqual(text.search('test', '1.32', 'end'), '')
        self.assertEqual(text.search('test', '1.32'), '1.10')

        self.assertEqual(text.search('', '1.0'), '1.0')  # empty pattern
        self.assertEqual(text.search('nonexistent', '1.0'), '')
        self.assertEqual(text.search('-bit', '1.0'), '4.2')  # starts with a hyphen

        self.assertEqual(text.search('line', '3.0'), '3.12')
        self.assertEqual(text.search('line', '3.0', forwards=True), '3.12')
        self.assertEqual(text.search('line', '3.0', backwards=True), '2.8')
        self.assertEqual(text.search('line', '3.0', forwards=True, backwards=True), '2.8')

        self.assertEqual(text.search('t.', '1.0'), '1.13')
        self.assertEqual(text.search('t.', '1.0', exact=True), '1.13')
        self.assertEqual(text.search('t.', '1.0', regexp=True), '1.10')
        self.assertEqual(text.search('t.', '1.0', exact=True, regexp=True), '1.10')

        self.assertEqual(text.search('TEST', '1.0'), '')
        self.assertEqual(text.search('TEST', '1.0', nocase=True), '1.10')

        self.assertEqual(text.search('.*line', '1.0', regexp=True), '2.0')
        self.assertEqual(text.search('.*line', '1.0', regexp=True, nolinestop=True), '1.0')

        self.assertEqual(text.search('test', '1.0', '1.13'), '1.10')
        self.assertEqual(text.search('test', '1.0', '1.13', strictlimits=True), '')
        self.assertEqual(text.search('test', '1.0', '1.14', strictlimits=True), '1.10')

        var = tkinter.Variable(self.root)
        self.assertEqual(text.search('test', '1.0', count=var), '1.10')
        self.assertEqual(var.get(), 4 if self.wantobjects else '4')

        # TODO: Add test for elide=True

    def test_search_all(self):
        text = self.text

        # pattern and index are obligatory arguments.
        self.assertRaises(TclError, text.search_all, None, '1.0')
        self.assertRaises(TclError, text.search_all, 'a', None)
        self.assertRaises(TclError, text.search_all, None, None)

        # Keyword-only arguments
        self.assertRaises(TypeError, text.search_all, 'a', '1.0', 'end', None)

        # Invalid text index.
        self.assertRaises(TclError, text.search_all, '', 0)
        self.assertRaises(TclError, text.search_all, '', '')
        self.assertRaises(TclError, text.search_all, '', 'invalid')
        self.assertRaises(TclError, text.search_all, '', '1.0', 0)
        self.assertRaises(TclError, text.search_all, '', '1.0', '')
        self.assertRaises(TclError, text.search_all, '', '1.0', 'invalid')

        def eq(res, expected):
            self.assertIsInstance(res, tuple)
            self.assertEqual([str(i) for i in res], expected)

        text.insert('1.0', 'ababa\naba\n64-bit')

        eq(text.search_all('aba', '1.0'), ['1.0', '2.0'])
        eq(text.search_all('aba', '1.0', 'end'), ['1.0', '2.0'])
        eq(text.search_all('aba', '1.1', 'end'), ['1.2', '2.0'])
        eq(text.search_all('aba', '1.1'), ['1.2', '2.0', '1.0'])

        res = text.search_all('', '1.0')  # empty pattern
        eq(res[:5], ['1.0', '1.1', '1.2', '1.3', '1.4'])
        eq(res[-5:], ['3.2', '3.3', '3.4', '3.5', '3.6'])
        eq(text.search_all('nonexistent', '1.0'), [])
        eq(text.search_all('-bit', '1.0'), ['3.2'])  # starts with a hyphen

        eq(text.search_all('aba', '1.0', 'end', forwards=True), ['1.0', '2.0'])
        eq(text.search_all('aba', 'end', '1.0', backwards=True), ['2.0', '1.2'])

        eq(text.search_all('aba', '1.0', overlap=True), ['1.0', '1.2', '2.0'])
        eq(text.search_all('aba', 'end', '1.0', overlap=True, backwards=True), ['2.0', '1.2', '1.0'])

        eq(text.search_all('aba', '1.0', exact=True), ['1.0', '2.0'])
        eq(text.search_all('a.a', '1.0', exact=True), [])
        eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])

        eq(text.search_all('ABA', '1.0'), [])
        eq(text.search_all('ABA', '1.0', nocase=True), ['1.0', '2.0'])

        eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])
        eq(text.search_all('a.a', '1.0', regexp=True, nolinestop=True), ['1.0', '1.4'])

        eq(text.search_all('aba', '1.0', '2.2'), ['1.0', '2.0'])
        eq(text.search_all('aba', '1.0', '2.2', strictlimits=True), ['1.0'])
        eq(text.search_all('aba', '1.0', '2.3', strictlimits=True), ['1.0', '2.0'])

        var = tkinter.Variable(self.root)
        eq(text.search_all('aba', '1.0', count=var), ['1.0', '2.0'])
        self.assertEqual(var.get(), (3, 3) if self.wantobjects else '3 3')

        # TODO: Add test for elide=True

    def test_count(self):
        text = self.text
        text.insert('1.0',
            'Lorem ipsum dolor sit amet,\n'
            'consectetur adipiscing elit,\n'
            'sed do eiusmod tempor incididunt\n'
            'ut labore et dolore magna aliqua.')

        options = ('chars', 'indices', 'lines',
                   'displaychars', 'displayindices', 'displaylines',
                   'xpixels', 'ypixels')
        self.assertEqual(len(text.count('1.0', 'end', *options, return_ints=True)), 8)
        self.assertEqual(len(text.count('1.0', 'end', *options)), 8)
        self.assertEqual(text.count('1.0', 'end', 'chars', 'lines', return_ints=True),
                         (124, 4))
        self.assertEqual(text.count('1.3', '4.5', 'chars', 'lines'), (92, 3))
        self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines', return_ints=True),
                         (-92, -3))
        self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines'), (-92, -3))
        self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines', return_ints=True),
                         (0, 0))
        self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines'), (0, 0))
        self.assertEqual(text.count('1.0', 'end', 'lines', return_ints=True), 4)
        self.assertEqual(text.count('1.0', 'end', 'lines'), (4,))
        self.assertEqual(text.count('end', '1.0', 'lines', return_ints=True), -4)
        self.assertEqual(text.count('end', '1.0', 'lines'), (-4,))
        self.assertEqual(text.count('1.3', '1.5', 'lines', return_ints=True), 0)
        self.assertEqual(text.count('1.3', '1.5', 'lines'), None)
        self.assertEqual(text.count('1.3', '1.3', 'lines', return_ints=True), 0)
        self.assertEqual(text.count('1.3', '1.3', 'lines'), None)
        # Count 'indices' by default.
        self.assertEqual(text.count('1.0', 'end', return_ints=True), 124)
        self.assertEqual(text.count('1.0', 'end'), (124,))
        self.assertEqual(text.count('1.0', 'end', 'indices', return_ints=True), 124)
        self.assertEqual(text.count('1.0', 'end', 'indices'), (124,))
        self.assertRaises(TclError, text.count, '1.0', 'end', 'spam')
        self.assertRaises(TclError, text.count, '1.0', 'end', '-lines')

        self.assertIsInstance(text.count('1.3', '1.5', 'ypixels', return_ints=True), int)
        self.assertIsInstance(text.count('1.3', '1.5', 'ypixels'), tuple)
        self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels', return_ints=True), int)
        self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels'), int)
        self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels', return_ints=True), 0)
        self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels'), None)
        self.assertEqual(text.count('1.3', '1.5', 'update', 'indices', return_ints=True), 2)
        self.assertEqual(text.count('1.3', '1.5', 'update', 'indices'), 2)
        self.assertEqual(text.count('1.3', '1.3', 'update', 'indices', return_ints=True), 0)
        self.assertEqual(text.count('1.3', '1.3', 'update', 'indices'), None)
        self.assertEqual(text.count('1.3', '1.5', 'update', return_ints=True), 2)
        self.assertEqual(text.count('1.3', '1.5', 'update'), (2,))
        self.assertEqual(text.count('1.3', '1.3', 'update', return_ints=True), 0)
        self.assertEqual(text.count('1.3', '1.3', 'update'), None)


if __name__ == "__main__":
    unittest.main()
