# SPDX-FileCopyrightText: © 2023 Christian BUHTZ <c.buhtz@posteo.jp>
# SPDX-FileCopyrightText: © 2025 Michael Olie <aolie1794@gmail.com>
#
# SPDX-License-Identifier: GPL-3.0-only
#
# This file is part of the program "Hyperorg" which is released under GNU
# General Public License v3 (GPLv3).
# See folder LICENSES or go to <https://www.gnu.org/licenses/#GPL>.
"""Tests related to elements.py

Element classes in the meaning of vertical runs, like Paragraph, Heading.
"""
import unittest
from unittest import mock
from hyperorg import elements
from hyperorg import textruns


class Paragraph(unittest.TestCase):
    """Test Paragraph element"""

    # pylint: disable=missing-function-docstring

    def test_empty(self):
        sut = elements.Paragraph()

        self.assertEqual(len(sut), 0)

    def test_two_lines(self):
        sut = elements.Paragraph(['foo', 'bar'])

        self.assertEqual(len(sut), 1)
        self.assertEqual(sut[0], 'foo bar')

    def test_with_link(self):
        sut = elements.Paragraph(['one [[https://bar.foo][Foobar]] two'])

        self.assertEqual(len(sut), 3)

        self.assertEqual(sut[0], 'one ')
        self.assertEqual(sut[1].target, 'bar.foo')
        self.assertEqual(sut[1][0], 'Foobar')
        self.assertEqual(sut[1].link_type, textruns.Link.Type.HTTPS)
        self.assertEqual(sut[2], ' two')

    def test_with_link_linebreak(self):
        sut = elements.Paragraph(
            [
                'one [[https://bar.foo][Foo',
                'Bar]] two'
            ]
        )

        self.assertEqual(sut[0], 'one ')
        self.assertEqual(sut[1].target, 'bar.foo')
        self.assertEqual(sut[1][0], 'Foo Bar')
        self.assertEqual(sut[1].link_type, textruns.Link.Type.HTTPS)
        self.assertEqual(sut[2], ' two')
        self.assertEqual(len(sut), 3)


class Heading(unittest.TestCase):
    """Test Heading elements"""

    # pylint: disable=missing-function-docstring

    def test_init(self):
        sut = elements.Heading('* label')

        self.assertEqual(len(sut), 1)
        self.assertEqual(sut[0], 'label')

        self.assertEqual(sut.level, 2)

    @mock.patch('uuid.uuid4')
    def test_anchor(self, mock_uuid):
        """Test heading with anchor."""

        mock_uuid.return_value = '123456'
        sut = elements.Heading('* Hea & sieben ding')

        # no anchor by default
        self.assertEqual(sut.anchor, None)

        sut.generate_anchor()

        self.assertEqual(sut.anchor, '123456')

    def test_multiple_levels(self):
        for lvl in range(1, 41):
            with self.subTest(lvl):
                # pylint: disable-next=consider-using-f-string
                sut = elements.Heading('{} Heading'.format('*' * lvl))

                self.assertEqual(sut[0], 'Heading')
                self.assertEqual(sut.level, lvl + 1)

    def test_equal(self):
        sut_a = elements.Heading('* foo')
        sut_b = elements.Heading('* foo')

        self.assertTrue(sut_a == sut_b)

        sut_c = elements.Heading('* foo')
        sut_c.level = 5
        self.assertFalse(sut_a == sut_c)

        sut_d = elements.Heading('* foo')
        sut_d.anchor = '123'
        self.assertFalse(sut_a == sut_d)

    def test_verbatim(self):
        sut = elements.Heading('* =--foo=')
        self.assertEqual(
            sut[0],
            textruns.Markup('--foo', textruns.Markup.Type.VERBATIM))


class ListElement(unittest.TestCase):
    """Plain lists.

    See https://orgmode.org/worg/dev/org-syntax.html#Plain_Lists for syntax
    reference.

    Attention: There is an inconsistency and annoying complexitiy with the
    real org-buffer behavior and description about Plain Lists and items.
    That is the current situation where.
     - List item with whitespace before and after bullet
     + List item with whitespace before and after bullet
     * List item with whitespace before and after bullet

    - List item with whitespace only after bullet
    + List item with whitespace only after bullet
    * A heading because there is no whitespace before the bullet
    """

    # pylint: disable=missing-function-docstring,too-many-public-methods

    def test_init(self):
        sut = elements.ListElement('- item', False)

        self.assertEqual(sut.ordered, False)
        self.assertEqual(len(sut), 1)
        self.assertEqual(sut[0], ['item'])

    def test_init_with_item_none(self):
        """Empty list is valid"""
        sut = elements.ListElement(None, False)

        self.assertEqual(sut.ordered, False)
        self.assertEqual(len(sut), 0)

    def test_init_with_item_something_else(self):
        with self.assertRaises(AttributeError):
            elements.ListElement(7, False)

    def test_init_with_list_of_strings(self):
        sut = elements.ListElement(['- item', 'foo'], False)

        self.assertEqual(sut.ordered, False)
        self.assertEqual(len(sut), 1)
        self.assertEqual(sut[0], ['item', ' foo'])

    def test_init_with_code_block(self):
        code = elements.Block(['import sys', 'print(foo)'], 'src', 'Cpp')
        sut = elements.ListElement(['- item', code, 'foo'], False)

        self.assertEqual(sut.ordered, False)
        self.assertEqual(len(sut), 1)
        self.assertEqual(len(sut[0]), 3)
        self.assertEqual(sut[0][0], 'item')
        self.assertEqual(sut[0][2], 'foo')
        self.assertIsInstance(sut[0][1], elements.Block)

    def test_equal(self):
        sut_a = elements.ListElement(' * foo', False)
        sut_b = elements.ListElement(' * foo', False)

        self.assertTrue(sut_a == sut_b)

        sut_c = elements.ListElement(' * foo', True)
        self.assertFalse(sut_a == sut_c)

        sut_d = elements.ListElement(
            item=' * foo', ordered=False, indentation=8)
        self.assertFalse(sut_a == sut_d)

    def test_add_item(self):
        sut = elements.ListElement('- item', False)

        sut.add_item('- foobar')

        self.assertEqual(len(sut), 2)

        self.assertEqual(sut[0], ['item'])
        self.assertEqual(sut[1], ['foobar'])

    def test_belongs_to_me(self):
        sut = elements.ListElement('- item', False)

        self.assertTrue(sut.belongs_to_me('  foobar'))
        self.assertFalse(sut.belongs_to_me(' foobar'))
        self.assertFalse(sut.belongs_to_me('   foobar'))

    def test_add_string_to_last_item(self):
        sut = elements.ListElement('- item', False)

        sut.append_to_last_item('foobar')

        self.assertEqual(len(sut), 1)
        self.assertEqual(sut[0], ['item', ' foobar'])

    def test_add_markup_to_last_item(self):
        sut = elements.ListElement('- item', False)

        sut.append_to_last_item('*foobar*')

        self.assertEqual(len(sut), 1)
        self.assertEqual(
            sut[0],
            [
                'item',
                ' ',
                textruns.Markup('foobar', textruns.Markup.Type.IMPORTANT)
            ]
        )

    def test_append_to_last_item_nested(self):
        sut = elements.ListElement('- one', False)
        sut.add_item('- two')
        sut.add_item('  - three')
        sut.append_to_last_item('    four')

        # three items in first level
        self.assertEqual(len(sut), 3)

        # 3rd item is a sub list
        self.assertIsInstance(sut[2], elements.ListElement)

        # one item in sub list
        self.assertEqual(len(sut[2]), 1)

        self.assertEqual(
            sut[2][0],
            [
                'three',
                ' four'
            ]
        )

    def test_bullets_asterix(self):
        """Special situation with asterix as list item bullet.

        An asterix as a list item bullet is allowed only if there is minimal
        one whitespace in front of it. If not it's not a list item but a
        heading.
        """

        # whitespace before the bullet
        sut = elements.ListElement(' * one', False)
        sut.add_item(' * two')

        self.assertEqual(len(sut), 2)
        self.assertEqual(sut[0], ['one'])
        self.assertEqual(sut[1], ['two'])

    def test_bullets_plus_minus(self):
        """Unordered list items with + or - as bullet."""

        # allowed bullets
        for bullet in ['+', '-', ' +', ' -']:
            with self.subTest(bullet):
                sut = elements.ListElement(f'{bullet} one', False)
                sut.add_item(f'{bullet} two')

                self.assertEqual(len(sut), 2)
                self.assertEqual(sut[0], ['one'])
                self.assertEqual(sut[1], ['two'])

    def test_bullets_counters(self):
        """Allowed counters for ordered list items.

        The syntax docu says that letters from a-z are allowed also. But an
        org-buffer does not react on it."""

        for bullet_a, bullet_b in [('1.', '2.'),
                                   ('1.', '1.'),
                                   ('1)', '2)')]:
            with self.subTest((bullet_a, bullet_b)):
                sut = elements.ListElement(f'{bullet_a} one', True)
                sut.add_item(f'{bullet_b} two')

                self.assertEqual(len(sut), 2)
                self.assertEqual(sut[0], ['one'])
                self.assertEqual(sut[1], ['two'])

    def test_empty_entries_single(self):
        """List with empty entries."""

        sut = elements.ListElement('- ', False)
        self.assertEqual(sut[0], [''])

    def test_empty_entries_multiple(self):
        """List with empty entries."""

        sut = elements.ListElement('- one', False)
        sut.add_item('- ')
        sut.add_item('- two')
        sut.add_item('-')
        sut.add_item('- three')

        for idx, exp in enumerate(['one', '', 'two', '', 'three']):
            self.assertEqual(sut[idx], [exp])

    def test_nested_list_simple(self):
        """Simple nested lists."""

        sut = elements.ListElement('- one', False)
        sut.add_item('- two')
        sut.add_item('  - three')

        self.assertEqual(len(sut), 3)

        self.assertEqual(sut[0], ['one'])
        self.assertEqual(sut[1], ['two'])
        self.assertEqual(sut[2][0], ['three'])

    def test_nested_list_three_levels_one_step_back(self):
        """Nested lists with three levels and one step back."""

        sut = elements.ListElement('- one', False)
        for item_string in ['- eins',
                            '  - two',
                            '  - zwei',
                            '    - three',
                            '  - foo']:
            sut.add_item(item_string)

        # list items (1st level)
        self.assertEqual(len(sut), 3)  # 2 entries + 1 sub-lists
        self.assertEqual(sut[0], ['one'])
        self.assertEqual(sut[1], ['eins'])

        # list items (2nd level)
        self.assertEqual(len(sut[2]), 4)  # 3 items + 1 sub-list
        self.assertEqual(sut[2][0], ['two'])
        self.assertEqual(sut[2][1], ['zwei'])

        # list items (3rd level)
        self.assertEqual(len(sut[2][2]), 1)
        self.assertEqual(sut[2][2][0], ['three'])

        self.assertEqual(sut[2][3], ['foo'])

    def test_nested_list_four_levels_two_steps_back(self):
        """Nested lists with four levels and two steps back."""

        sut = elements.ListElement('- one', False)
        for item_string in ['  - two',
                            '    - three',
                            '      - four',
                            '  - foo']:
            sut.add_item(item_string)

        # first level items
        first = sut
        self.assertEqual(len(first), 2)
        self.assertEqual(first[0], ['one'])

        # second level items
        second = first[1]
        self.assertEqual(len(second), 3)
        self.assertEqual(second[0], ['two'])
        self.assertEqual(second[2], ['foo'])

        # third level items
        third = second[1]
        self.assertEqual(len(third), 2)
        self.assertEqual(third[0], ['three'])

        # forth level
        forth = third[1]
        self.assertEqual(len(forth), 1)
        self.assertEqual(forth[0], ['four'])

    def test_markup_entries(self):
        """List entries with multiple runs."""
        sut = elements.ListElement('- one *big* foo', False)

        self.assertEqual(
            sut[0],
            [
                'one ',
                textruns.Markup('big', textruns.Markup.Type.IMPORTANT),
                ' foo'
            ]
        )

    def test_items_linebreak(self):
        sut = elements.ListElement('- one\ntwo', False)
        sut.add_item('- three')

        self.assertEqual(sut[0], ['one\ntwo'])
        self.assertEqual(sut[1], ['three'])


class ListElementFromRuns(unittest.TestCase):
    """Basic functionality used by `class Backlinks`.

    This is exceptional behavior. The list can be created from previously
    parsed runs instead of strings in org syntax.
    """

    def test_nested_list(self):
        """Simple nested lists."""

        # empty list
        sut = elements.ListElement(None, False)

        # 1st & 2nd item
        sut.runs.append(['one'])
        sut.runs.append(['two'])

        # 3rd item is a sub list
        three = elements.ListElement(None, False, 2)
        three.runs.append(['three'])
        sut.runs.append(three)

        self.assertEqual(len(sut), 3)

        self.assertEqual(sut[0], ['one'])
        self.assertEqual(sut[1], ['two'])
        self.assertEqual(sut[2][0], ['three'])


class Backlinks(unittest.TestCase):
    """Backling element"""
    def test_simple(self):
        """Simple"""
        sut = elements.Backlinks()
        self.assertEqual(len(sut), 0)
        self.assertFalse(sut.ordered)

    def test_add_link(self):
        """Multiple backlinks in a row"""

        sut = elements.Backlinks()

        # runs, target
        test_data = [
            (['one'], 'IDone'),
            (['two'], 'IDtwo'),
            (['sub-two'], 'sub-IDtwo'),
            (['sub-twomore'], 'sub-IDtwomore'),
            (['three'], 'IDthree'),
        ]
        for runs, target in test_data:
            sut.add_link(textruns.Link(
                target=target,
                link_type=textruns.Link.Type.ORGID,
                label_runs=runs
            ))

        self.assertEqual(len(sut), len(test_data))
        for idx, (runs, target) in enumerate(test_data):
            self.assertEqual(len(sut[idx]), 1)
            self.assertEqual(sut[idx][0].runs, runs)
            self.assertEqual(sut[idx][0].target, target)
            self.assertEqual(sut[idx][0].link_type, textruns.Link.Type.ORGID)


class Block(unittest.TestCase):
    """Test Block element"""
    # pylint: disable=missing-function-docstring

    def test_mandatory_arguments(self):
        with self.assertRaises(TypeError):
            sut = elements.Block()

        # no exception
        sut = elements.Block('src', [])

        self.assertEqual(len(sut), 0)
        self.assertEqual(sut.name, 'src')
        self.assertEqual(sut.language, '')

    def test_equal(self):
        sut_a = elements.Block('src', ['import'], 'Python')
        sut_b = elements.Block('src', ['import'], 'Python')

        self.assertEqual(sut_a, sut_b)

    def test_content_not_equal(self):
        sut_a = elements.Block('src', ['import'], 'Python')
        sut_b = elements.Block('src', ['foobar'], 'Python')

        self.assertNotEqual(sut_a, sut_b)

    def test_name_not_equal(self):
        sut_a = elements.Block('foobar', ['import'], 'Python')
        sut_b = elements.Block('src', ['import'], 'Python')

        self.assertNotEqual(sut_a, sut_b)

    def test_language_not_equal(self):
        sut_a = elements.Block('src', ['import'], 'Python')
        sut_b = elements.Block('src', ['import'], 'kauderwelsch')

        self.assertNotEqual(sut_a, sut_b)


class Figure(unittest.TestCase):
    """Test Figure element"""

    def test_properties_to_dict(self):
        """Convert property lines to dict"""
        sut = [
            '#+Attr_HTML: :width 300 :style float:left',
            '#+caption: Foo bar',
            '#+ATTR_HTML:  :alt free',
            '#+DOWNLOADED: https://x.net/img.png @ 2022-08-03 15:12:48'
        ]

        expect = {
            'attr': {
                'width': '300',
                'style': 'float:left',
                'alt': 'free'
            },
            'caption': 'Foo bar',
            'unknown': [
                '#+DOWNLOADED: https://x.net/img.png @ 2022-08-03 15:12:48'
            ]
        }

        result = elements.Figure.to_properties_dict(sut)

        self.assertEqual(result, expect)

    def test_empty_lines_arent_properties(self):
        """Empty property lines raise an error"""
        sut = [
            '#+caption: Foo bar',
            '',
        ]

        with self.assertRaises(ValueError):
            elements.Figure.to_properties_dict(sut)

    def test_init(self):
        """Instantiate a figure"""
        img = textruns.Image.to_image('image.png')
        props = {'caption': 'foobar', 'attr': {'width': '200'}}
        sut = elements.Figure(img, props)

        self.assertEqual(len(sut), 2)
        # Image
        self.assertEqual(sut[0].target, 'image.png')
        self.assertEqual(sut[0].width, '200')
        self.assertEqual(sut[0].height, None)
        self.assertEqual(sut[0], sut.image)
        # "alternate"
        self.assertEqual(sut[1], ['foobar'])
        self.assertEqual(sut[1], sut.caption)
        # Figure
        self.assertEqual(sut.attributes, {})
        self.assertEqual(sut.unknown, None)

    def test_unknown_properties(self):
        """Images with unknown property fields"""
        img = textruns.Image.to_image('image.png')
        props = {'unknown': ['foobar']}
        sut = elements.Figure(img, props)

        self.assertEqual(len(sut), 1)
        self.assertEqual(sut.unknown, ['foobar'])

    def test_caption_runs(self):
        """Image captions with markup"""
        img = textruns.Image.to_image('image.png')
        props = {'caption': 'foo *drei* bar', 'attr': {}}
        sut = elements.Figure(img, props)

        self.assertEqual(
            sut.caption,
            [
                'foo ',
                textruns.Markup('drei', textruns.Markup.Type.IMPORTANT),
                ' bar'
            ]
        )


class DisplayMath(unittest.TestCase):
    """DisplayMathelement"""

    def test_empty(self):
        """Simple init with empty content"""
        sut = elements.DisplayMath('')
        self.assertEqual(sut.runs, [''])

    def test_einstein(self):
        """Simple init with empty content"""
        sut = elements.DisplayMath('e = mc^{2}')
        self.assertEqual(sut.runs, ['e = mc^{2}'])
