# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

import os
from typing import Callable
import unittest
import warnings

from gi.repository import GLib, Gtk, GtkSource
from iotas.formatter import Formatter, LinkStateInfo

warnings.filterwarnings("ignore", "version")


class Test(unittest.TestCase):

    formatter: Formatter
    buffer: GtkSource.Buffer
    awaiting_signal = False

    def test_bold(self) -> None:
        # Markup around cursor
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.bold)
        self.assertEqual(self.__get_text(), "****")
        centre = buffer.get_iter_at_offset(2)
        buffer.place_cursor(centre)
        formatter.bold()
        self.assertEqual(self.__get_text(), "")

        # Markup around selection
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "text")
        buffer.select_range(buffer.get_start_iter(), start_iter)
        self.__call_and_wait(formatter.bold)
        self.assertEqual(self.__get_text(), "**text**")
        self.__call_and_wait(formatter.bold)
        self.assertEqual(self.__get_text(), "text")
        Gtk.TextBuffer.select_range

        # Removing markup when cursor within bold
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.bold)
        self.assertEqual(self.__get_text(), "**text**")
        buffer.place_cursor(buffer.get_iter_at_offset(4))
        self.__call_and_wait(formatter.bold)
        self.assertEqual(self.__get_text(), "text")

        # Multi-line selection ignored (uses selection start as cursor instead)
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "line\nline\nline\nline")
        buffer.select_range(buffer.get_start_iter(), buffer.get_iter_at_offset(15))
        self.__call_and_wait(formatter.bold)
        self.assertEqual(self.__get_text(), "****line\nline\nline\nline")

    def test_italic(self) -> None:
        # Markup around cursor
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.italic)
        self.assertEqual(self.__get_text(), "__")
        centre = buffer.get_iter_at_offset(1)
        buffer.place_cursor(centre)
        formatter.italic()
        self.assertEqual(self.__get_text(), "")

        # Markup around selection
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "text")
        buffer.select_range(buffer.get_start_iter(), start_iter)
        self.__call_and_wait(formatter.italic)
        self.assertEqual(self.__get_text(), "_text_")
        self.__call_and_wait(formatter.italic)
        self.assertEqual(self.__get_text(), "text")
        Gtk.TextBuffer.select_range

        # Removing markup when cursor within italic
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.italic)
        self.assertEqual(self.__get_text(), "_text_")
        buffer.place_cursor(buffer.get_iter_at_offset(3))
        self.__call_and_wait(formatter.italic)
        self.assertEqual(self.__get_text(), "text")

        # Multi-line selection ignored (uses selection start as cursor instead)
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "line\nline\nline\nline")
        buffer.select_range(buffer.get_start_iter(), buffer.get_iter_at_offset(15))
        self.__call_and_wait(formatter.italic)
        self.assertEqual(self.__get_text(), "__line\nline\nline\nline")

    def test_strikethrough(self) -> None:
        # Markup around cursor
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.strikethrough)
        self.assertEqual(self.__get_text(), "~~~~")
        centre = buffer.get_iter_at_offset(2)
        buffer.place_cursor(centre)
        formatter.strikethrough()
        self.assertEqual(self.__get_text(), "")

        # Markup around selection
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "text")
        buffer.select_range(buffer.get_start_iter(), start_iter)
        self.__call_and_wait(formatter.strikethrough)
        self.assertEqual(self.__get_text(), "~~text~~")
        self.__call_and_wait(formatter.strikethrough)
        self.assertEqual(self.__get_text(), "text")
        Gtk.TextBuffer.select_range

        # Removing markup when cursor within strikethrough
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.strikethrough)
        self.assertEqual(self.__get_text(), "~~text~~")
        buffer.place_cursor(buffer.get_iter_at_offset(4))
        self.__call_and_wait(formatter.strikethrough)
        self.assertEqual(self.__get_text(), "text")

        # Multi-line selection ignored (uses selection start as cursor instead)
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "line\nline\nline\nline")
        buffer.select_range(buffer.get_start_iter(), buffer.get_iter_at_offset(15))
        self.__call_and_wait(formatter.strikethrough)
        self.assertEqual(self.__get_text(), "~~~~line\nline\nline\nline")

    def test_quote(self) -> None:
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "> ")
        formatter.quote()
        self.assertEqual(self.__get_text(), "")

        # Cursor location and selection not making a difference
        buffer.insert(buffer.get_start_iter(), "text")
        buffer.place_cursor(self.buffer.get_end_iter())
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "> text")
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "text")
        buffer.place_cursor(self.buffer.get_start_iter())
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "> text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "text")
        buffer.select_range(buffer.get_iter_at_offset(2), buffer.get_iter_at_offset(3))
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "> text")

        # Basic multi-line
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "line\nline\nline\nline")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "> line\n> line\n> line\n> line")
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "line\nline\nline\nline")

        # Mixed quote indentation multi-line
        formatter, buffer = self.__reset()
        start_iter = buffer.get_start_iter()
        buffer.insert(start_iter, "line\n> line\nline\nline")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "> line\n> > line\n> line\n> line")
        self.__call_and_wait(formatter.quote)
        self.assertEqual(self.__get_text(), "line\n> line\nline\nline")

    def test_heading(self) -> None:
        # Empty
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.heading, 1)
        self.assertEqual(self.__get_text(), "# ")
        formatter.heading(0)
        self.assertEqual(self.__get_text(), "")

        # Basic text
        buffer.insert(buffer.get_start_iter(), "text")
        self.__call_and_wait(formatter.heading, 1)
        self.assertEqual(self.__get_text(), "# text")
        self.__call_and_wait(formatter.heading, 3)
        self.assertEqual(self.__get_text(), "### text")
        formatter.heading(0)
        self.assertEqual(self.__get_text(), "text")

        # Cursor location and selection not making a difference
        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "text")
        buffer.place_cursor(self.buffer.get_end_iter())
        self.__call_and_wait(formatter.heading, 1)
        self.assertEqual(self.__get_text(), "# text")
        formatter.heading(0)
        self.assertEqual(self.__get_text(), "text")
        buffer.place_cursor(self.buffer.get_start_iter())
        self.__call_and_wait(formatter.heading, 1)
        self.assertEqual(self.__get_text(), "# text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        formatter.heading(0)
        self.assertEqual(self.__get_text(), "text")
        buffer.select_range(buffer.get_iter_at_offset(2), buffer.get_iter_at_offset(3))
        self.__call_and_wait(formatter.heading, 1)
        self.assertEqual(self.__get_text(), "# text")

    def test_get_link_state(self) -> None:
        # Empty
        formatter, buffer = self.__reset()
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "")
        self.assertEqual(state.text, "")

        # Typical inline link
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "[text](<https://linky.com>)")
        buffer.place_cursor(buffer.get_start_iter())
        state = formatter.get_link_state()
        self.assertFalse(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "https://linky.com")
        self.assertEqual(state.text, "text")
        self.assertEqual(state.source_text, "[text](<https://linky.com>)")
        self.assertEqual(state.start_offset, 0)
        self.assertEqual(state.end_offset, 27)

        # Creating inline from automatic
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "<https://linky.com>")
        buffer.place_cursor(buffer.get_start_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertTrue(state.automatic_link)
        self.assertEqual(state.link, "https://linky.com")
        self.assertEqual(state.text, "")

        # URL in plain text
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "https://linky.com")
        buffer.place_cursor(buffer.get_start_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "https://linky.com")
        self.assertEqual(state.text, "")

        # New from URL selection
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "https://linky.com")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "https://linky.com")
        self.assertEqual(state.text, "")

        # Edit selected inline
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "[text](<https://linky.com>)")
        buffer.select_range(buffer.get_iter_at_offset(3), buffer.get_iter_at_offset(10))
        state = formatter.get_link_state()
        self.assertFalse(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "https://linky.com")
        self.assertEqual(state.text, "text")
        self.assertEqual(state.source_text, "[text](<https://linky.com>)")
        self.assertEqual(state.start_offset, 0)
        self.assertEqual(state.end_offset, 27)

        # Creating inline from selected automatic
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "<https://linky.com>")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertTrue(state.automatic_link)
        self.assertEqual(state.link, "https://linky.com")
        self.assertEqual(state.text, "")

        # Selection text for title in creation
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "plain text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "")
        self.assertEqual(state.text, "plain text")

        # Avoiding creating link from selection which has markup
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "- item")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "")
        self.assertEqual(state.text, "")

        # Create when seeing only part of URI
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "plain text https://")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "")
        self.assertEqual(state.text, "")

        # Ignore selection when multi-line, create new
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "[text](<https://linky.com>)\ntext")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        state = formatter.get_link_state()
        self.assertTrue(state.creating)
        self.assertFalse(state.automatic_link)
        self.assertEqual(state.link, "")
        self.assertEqual(state.text, "")

    def test_link(self) -> None:
        # No replacement
        formatter, buffer = self.__reset()
        info = LinkStateInfo()
        info.text = "text"
        info.link = "https://linky.com"
        formatter.link(info)
        self.assertEqual(self.__get_text(), "[text](https://linky.com)")

        # Replace text used for title
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "plain text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        info = formatter.get_link_state()
        self.assertTrue(info.creating)
        self.assertFalse(info.automatic_link)
        self.assertEqual(info.link, "")
        self.assertEqual(info.text, "plain text")
        info.link = "https://linky.com"
        formatter.link(info)
        self.assertEqual(self.__get_text(), "[plain text](https://linky.com)")

        # Edit inline
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "[text](https://linky.com)")
        buffer.place_cursor(buffer.get_start_iter())
        info = formatter.get_link_state()
        self.assertFalse(info.creating)
        self.assertFalse(info.automatic_link)
        self.assertEqual(info.link, "https://linky.com")
        self.assertEqual(info.text, "text")
        self.assertEqual(info.source_text, "[text](https://linky.com)")
        self.assertEqual(info.start_offset, 0)
        self.assertEqual(info.end_offset, 25)
        info.link = "https://newlink.com"
        info.text = "new text"
        formatter.link(info)
        self.assertEqual(self.__get_text(), "[new text](https://newlink.com)")

        # Insert instead of edit if buffer is updated remotely while in edit link dialog
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "[text](https://linky.com)")
        buffer.place_cursor(buffer.get_start_iter())
        info = formatter.get_link_state()
        info.link = "https://newlink.com"
        info.text = "new text"
        self.__insert_and_wait(buffer.get_start_iter(), "modified")
        formatter.link(info)
        self.assertEqual(
            self.__get_text(), "modified[new text](https://newlink.com)[text](https://linky.com)"
        )

    def test_code(self) -> None:
        # Markup around cursor
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.code)
        self.assertEqual(self.__get_text(), "``")
        formatter.code()
        self.assertEqual(self.__get_text(), "")

        # Code block
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "line\nline\nline")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.code)
        self.assertEqual(self.__get_text(), "```\nline\nline\nline\n```\n")
        formatter.code()
        self.assertEqual(self.__get_text(), "line\nline\nline\n\n")

        # Basic span
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "code")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.code)
        self.assertEqual(self.__get_text(), "`code`")
        formatter.code()
        self.assertEqual(self.__get_text(), "code")

        # Double backtick span
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "``code``")
        buffer.place_cursor(buffer.get_iter_at_offset(3))
        formatter.code()
        self.assertEqual(self.__get_text(), "code")

    def test_unordered_list(self) -> None:
        # Empty
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.unordered_list)
        self.assertEqual(self.__get_text(), "- ")
        formatter.unordered_list()
        self.assertEqual(self.__get_text(), "")

        # Basic
        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "text")
        self.__call_and_wait(formatter.unordered_list)
        self.assertEqual(self.__get_text(), "- text")

        # Multi-line
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "text\ntext\n- text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.unordered_list)
        self.assertEqual(self.__get_text(), "- text\n- text\ntext")
        self.__call_and_wait(formatter.unordered_list)
        self.assertEqual(self.__get_text(), "text\ntext\n- text")

        # Other list items
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "- [ ] text")
        self.__call_and_wait(formatter.unordered_list)
        self.assertEqual(self.__get_text(), "- text")
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "1. text")
        self.__call_and_wait(formatter.unordered_list)
        self.assertEqual(self.__get_text(), "- text")

    def test_ordered_list(self) -> None:
        # Empty
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.ordered_list)
        self.assertEqual(self.__get_text(), "1. ")
        formatter.ordered_list()
        self.assertEqual(self.__get_text(), "")

        # Basic
        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "text")
        self.__call_and_wait(formatter.ordered_list)
        self.assertEqual(self.__get_text(), "1. text")

        # Multi-line
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "text\ntext\ntext")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.ordered_list)
        self.assertEqual(self.__get_text(), "1. text\n2. text\n3. text")
        self.__call_and_wait(formatter.ordered_list)
        self.assertEqual(self.__get_text(), "text\ntext\ntext")

        # Continuation of existing
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "1. text\n2. text\n3. text\ntext\ntext")
        success, line_iter = buffer.get_iter_at_line(3)
        self.assertTrue(success)
        buffer.select_range(line_iter, buffer.get_end_iter())
        self.__call_and_wait(formatter.ordered_list)
        self.assertEqual(self.__get_text(), "1. text\n2. text\n3. text\n4. text\n5. text")

        # Other list items
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "- [ ] text\n* text\n+ text\n- text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.ordered_list)
        self.assertEqual(self.__get_text(), "1. text\n2. text\n3. text\n4. text")
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "- [ ] text")
        self.__call_and_wait(formatter.ordered_list)
        self.assertEqual(self.__get_text(), "1. text")

    def test_checkbox(self) -> None:
        # Empty
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.checkbox)
        self.assertEqual(self.__get_text(), "- [ ] ")
        formatter.checkbox()
        self.assertEqual(self.__get_text(), "")

        # Basic
        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "text")
        self.__call_and_wait(formatter.checkbox)
        self.assertEqual(self.__get_text(), "- [ ] text")

        # Multi-line
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "text\ntext\n- [ ] text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.checkbox)
        self.assertEqual(self.__get_text(), "- [ ] text\n- [ ] text\ntext")
        self.__call_and_wait(formatter.checkbox)
        self.assertEqual(self.__get_text(), "text\ntext\n- [ ] text")

        # Other list items
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "- text\n* text\n+ text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.checkbox)
        self.assertEqual(self.__get_text(), "- [ ] text\n- [ ] text\n- [ ] text")
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "1. text")
        self.__call_and_wait(formatter.checkbox)
        self.assertEqual(self.__get_text(), "- [ ] text")

    def test_toggle_checkbox(self) -> None:
        # Empty
        formatter, buffer = self.__reset()
        formatter.toggle_checkbox()
        self.assertEqual(self.__get_text(), "")

        # Plain text
        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "text")
        formatter.toggle_checkbox()
        self.assertEqual(self.__get_text(), "text")

        # Basic
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "- [ ] text")
        self.__call_and_wait(formatter.toggle_checkbox)
        self.assertEqual(self.__get_text(), "- [x] text")
        formatter.toggle_checkbox()
        self.assertEqual(self.__get_text(), "- [ ] text")

        # Indented
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "  - [ ] text")
        self.__call_and_wait(formatter.toggle_checkbox)
        self.assertEqual(self.__get_text(), "  - [x] text")
        formatter.toggle_checkbox()
        self.assertEqual(self.__get_text(), "  - [ ] text")

        # Multi line
        formatter, buffer = self.__reset()
        self.__insert_and_wait(buffer.get_start_iter(), "- [ ] text\n- [ ] text\n- [x] text")
        buffer.select_range(buffer.get_start_iter(), buffer.get_end_iter())
        self.__call_and_wait(formatter.toggle_checkbox)
        self.assertEqual(self.__get_text(), "- [x] text\n- [x] text\n- [ ] text")
        formatter.toggle_checkbox()
        self.assertEqual(self.__get_text(), "- [ ] text\n- [ ] text\n- [x] text")

        # Other list items
        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "- text")
        formatter.toggle_checkbox()
        self.assertEqual(self.__get_text(), "- text")
        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "1. text")
        formatter.toggle_checkbox()
        self.assertEqual(self.__get_text(), "1. text")

    def test_horizontal_rule(self) -> None:
        formatter, buffer = self.__reset()
        self.__call_and_wait(formatter.horizontal_rule)
        self.assertEqual(self.__get_text(), "\n- - -\n")
        self.__call_and_wait(formatter.horizontal_rule)
        self.assertEqual(self.__get_text(), "\n\n")

        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_start_iter(), "text")
        buffer.place_cursor(buffer.get_start_iter())
        self.__call_and_wait(formatter.horizontal_rule)
        self.assertEqual(self.__get_text(), "\n- - -\n\ntext")

        formatter, buffer = self.__reset()
        buffer.insert(buffer.get_end_iter(), "text")
        buffer.place_cursor(buffer.get_start_iter())
        self.__call_and_wait(formatter.horizontal_rule)
        self.assertEqual(self.__get_text(), "\n- - -\n\ntext")

    def test_table(self) -> None:

        def add_table(columns, rows, view_width, character_width) -> None:
            self.__call_and_wait(formatter.table, columns, rows, view_width, character_width)

        def check_table(columns: int, rows: int, min_column_width: int) -> bool:
            text = self.__get_text()
            lines = text.strip().split("\n")
            if len(lines) != rows + 2:
                return False
            if lines[0].replace(" ", "") != "|" * (columns + 1):
                return False
            return True

        formatter, buffer = self.__reset()
        add_table(columns=3, rows=3, view_width=600, character_width=5)
        self.assertTrue(check_table(3, 3, 20))

        # Check GFM min column width
        formatter, buffer = self.__reset()
        add_table(columns=300, rows=3, view_width=600, character_width=5)
        self.assertTrue(check_table(300, 3, 3))

    def __get_language(self) -> tuple[GtkSource.LanguageManager, GtkSource.Language]:
        manager = GtkSource.LanguageManager()
        file_dir = os.path.dirname(__file__)
        language_path = os.path.join(
            file_dir, os.pardir, "data", "gtksourceview-5", "language-specs"
        )
        manager.prepend_search_path(language_path)
        language = manager.get_language("iotas-markdown")
        return (manager, language)

    def __reset(self) -> tuple[Formatter, GtkSource.Buffer]:
        self.formatter = Formatter()
        self.__manager, language = self.__get_language()
        self.buffer = GtkSource.Buffer.new_with_language(language)
        self.formatter.buffer = self.buffer
        return (self.formatter, self.buffer)

    def __get_text(self) -> str:
        if self.buffer is None:
            return ""
        else:
            start = self.buffer.get_start_iter()
            end = self.buffer.get_end_iter()
            return self.buffer.get_text(start, end, False)

    def __call_and_wait(self, call: Callable, *args) -> None:
        context = GLib.MainContext.default()
        self.buffer.connect("highlight-updated", self.__flag_not_waiting)
        self.awaiting_signal = True
        call(*args)
        while self.awaiting_signal:
            context.iteration(False)

    def __insert_and_wait(self, location: Gtk.TextIter, text: str) -> None:
        context = GLib.MainContext.default()
        self.buffer.connect("highlight-updated", self.__flag_not_waiting)
        self.awaiting_signal = True
        self.buffer.insert(location, text)
        while self.awaiting_signal:
            context.iteration(False)

    def __flag_not_waiting(self, *_args) -> None:
        self.awaiting_signal = False
