# SPDX-FileCopyrightText: © 2023 Christian BUHTZ <c.buhtz@posteo.jp>
#
# 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"""
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].kind, textruns.Link.Kind.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].kind, textruns.Link.Kind.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.Kind.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

    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_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.Kind.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 a, b in [('1.', '2.'),
                     ('1.', '1.'),
                     ('1)', '2)')]:
            with self.subTest((a, b)):
                sut = elements.ListElement(f'{a} one', True)
                sut.add_item(f'{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.Kind.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 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.Kind.IMPORTANT),
                ' bar'
            ]
        )
