# 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 parser.py

Org content parsed into runs and elements
"""
import unittest
from hyperorg import textruns
from hyperorg import elements
from hyperorg import parser


class Paragraphs(unittest.TestCase):
    """Parse paragraphs"""

    def test_single(self):
        """One paragraph with three lines."""

        content = [
            'lorem ipsum dolor',
            'sit amet,',
            'consetetur sadipscing elitr'
        ]

        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)
        self.assertIsInstance(sut[0], elements.Paragraph)
        self.assertEqual(
            sut[0][:],
            ['lorem ipsum dolor sit amet, consetetur sadipscing elitr']
        )

    def test_three(self):
        """Three paragraphs."""

        content = [
            'paragraph 1 lorem ipsum',
            'lorem ipsum dolor',
            '',
            'paragraph 2 dolor sit amet, consetetur',
            'sadipscing elitr, sed',
            'diam nonumy eirmod tempor',
            '',
            'paragraph 3 invidunt',
            'aliquyam erat,'
        ]

        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 3)

        for idx in range(2):
            self.assertIsInstance(sut[idx], elements.Paragraph)

        expected_paragraphs = [
           # paragraph 1
           'paragraph 1 lorem ipsum lorem ipsum dolor',
           # paragraph 2
           'paragraph 2 dolor sit amet, consetetur sadipscing elitr, sed '
           'diam nonumy eirmod tempor',
           # paragraph 3
           'paragraph 3 invidunt aliquyam erat,'
        ]

        for idx in range(0):
            self.assertEqual(sut[idx][:], expected_paragraphs[idx])

    def test_mixed_with_blocks(self):
        """Blocks and paragraphs combined"""
        content = [
            'Foo',
            '#+begin_src python',
            'import eins',
            'print("eins")',
            '#+end_src',
            'Bar',
            '#+begin_src Cpp',
            'import zwei',
            'print("zwei")',
            '#+end_src',
            'Letzte Zeile'
        ]
        _, sut = parser.OrgContentParser(content).result()

        # five elements
        self.assertEqual(len(sut), 5)

        # the paragraphs
        self.assertEqual(sut[0], elements.Paragraph(['Foo']))
        self.assertEqual(sut[2], elements.Paragraph(['Bar']))
        self.assertEqual(sut[4], elements.Paragraph(['Letzte Zeile']))

        self.assertEqual(sut[1], elements.Block(
            'src', ['import eins', 'print("eins")'], 'python'))

        self.assertEqual(sut[3], elements.Block(
            'src', ['import zwei', 'print("zwei")'], 'Cpp'))


class Headings(unittest.TestCase):
    """Test parsing Headings"""

    def test_simple(self):
        """One heading"""
        content = ['* Heading']
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)
        self.assertIsInstance(sut[0], elements.Heading)
        self.assertEqual(sut[0][0], 'Heading')

    def test_multiple(self):
        """Three headings"""
        content = [
            '* Heading 1',
            '* Heading 2',
            '* Heading 3'
        ]
        _, sut = parser.OrgContentParser(content).result()
        self.assertEqual(len(sut), 3)
        for idx in (1, 2, 3):
            self.assertEqual(sut[idx-1].runs, [f'Heading {idx}'])

    def test_heading_levels(self):
        """Headings on multiple levels."""

        content = [
            '* Heading 1',
            '** Heading 2',
            '*** Heading 3',
            '**** Heading 4',
            '***** Heading 5',
            '****** Heading 6',
            '******* Heading 7',
            '******** Heading 8',
            '********* Heading 9'
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 9)

        for idx in range(9):
            self.assertTrue(isinstance(sut[idx], elements.Heading))
            self.assertEqual(sut[idx].runs, [f'Heading {idx+1}'])

    def test_mix_with_paragraphs(self):
        """Mixing headings and paragraphs."""

        content = [
            '* Heading 1',
            'lore ipsum',
            'and more',
            '** Heading 2',
            'one',
            '',
            'two',
            '*Heading 3'
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(sut[0], elements.Heading('* Heading 1'))
        self.assertEqual(sut[1][:], ['lore ipsum and more'])
        self.assertEqual(sut[2], elements.Heading('** Heading 2'))
        self.assertEqual(sut[3][:], ['one'])
        self.assertEqual(sut[4][:], ['two *Heading 3'])
        self.assertEqual(len(sut), 5)

    def test_paragraph_begin_with_markup(self):
        """The paragraph start with markup"""
        content = [
            '* Heading',
            '*bold*: Foo.'
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        self.assertEqual(sut[0], elements.Heading('* Heading'))
        self.assertEqual(len(sut[1]), 2)
        self.assertEqual(
            sut[1][0],
            textruns.Markup('bold', textruns.Markup.Type.IMPORTANT))
        self.assertEqual(sut[1][1], ': Foo.')


class Blocks(unittest.TestCase):
    """Parse (code) blocks"""

    def test_one_liner(self):
        """Codeblock with one line only"""
        content = [
            '#+begin_src python',
            'import hyperorg',
            '#+end_src'
        ]

        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)

        block = sut[0]
        self.assertIsInstance(block, elements.Block)

        # test block content
        self.assertEqual(block.name, 'src')
        self.assertEqual(block.language, 'python')
        self.assertEqual(block[0], 'import hyperorg')

    def test_multiple_lines(self):
        """Block contain multiple lines"""
        content = [
            '#+begin_src python',
            'import hyperorg',
            '',
            '',
            '',
            'if __name__ == "__main__":',
            '    print("Hello World!")',
            '',
            '    sys.exit(1)',
            '#+end_src'
        ]
        _, sut = parser.OrgContentParser(content).result()

        # expected
        codeblock = elements.Block(
            name='src',
            content_lines=[
                'import hyperorg',
                '',
                '',
                '',
                'if __name__ == "__main__":',
                '    print("Hello World!")',
                '',
                '    sys.exit(1)'],
            language='python')

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

    def test_two_blocks(self):
        """One codeblock follows the other"""
        content = [
            '#+begin_src python',
            'import eins',
            'print("eins")',
            '#+end_src',
            '#+begin_src Cpp',
            'import zwei',
            'print("zwei")',
            'foo bar',
            '#+end_src',
        ]

        pars = parser.OrgContentParser(content)
        _, sut = pars.result()

        self.assertEqual(len(sut), 2)

        python = sut[0]
        cpp = sut[1]

        # test block content
        self.assertEqual(python.language, 'python')
        self.assertEqual(len(python), 2)

        self.assertEqual(cpp.language, 'Cpp')
        self.assertEqual(len(cpp), 3)

    def test_case_insensitive(self):
        """The property code is case insensitive"""
        content = [
            '#+bEgIn_Src python',
            'import hyperorg',
            '#+enD_sRc'
        ]
        _, sut = parser.OrgContentParser(content).result()

        codeblock = elements.Block(
            name='Src',
            content_lines=['import hyperorg'],
            language='python')

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

    def test_no_language(self):
        """Missing language specifier"""
        content = [
            '#+begin_foobar',
            'import hyperorg',
            '#+end_foobar'
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(sut[0].language, '')


class TableBlocks(unittest.TestCase):
    """Test cases for quick n dirty solution to detect tables but render them
    as code blocks instead of HTML

    Syntax reference: https://orgmode.org/worg/org-syntax.html#Tables

    The pygments "text" lexer is used instead of "org" because the org lexer
    is known for not supporting tables.
    See: https://github.com/pygments/pygments/discussions/2688
    """

    def test_org_table(self):
        """Org table"""
        content = [
            '| Name  | Phone | Age |',
            '|-------+-------+-----|',
            '| Peter |  1234 |  24 |',
            '| Anna  |  4321 |  25 |'
        ]

        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)

        block = sut[0]
        self.assertIsInstance(block, elements.TableBlock)

        # test block content
        self.assertEqual(block.name, 'src')
        self.assertEqual(block.language, 'text')
        for idx, content_line in enumerate(content):
            self.assertEqual(block[idx], content_line)

    def test_etable_table(self):
        """Org table"""
        content = [
            '+-------+-------+-----+',
            '| Name  | Phone | Age |',
            '+-------+-------+-----+',
            '| Peter |  1234 |  24 |',
            '+-------+-------+-----+',
            '| Anna  |  4321 |  25 |',
            '+-------+-------+-----+'
        ]

        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)

        block = sut[0]
        self.assertIsInstance(block, elements.TableBlock)

        # test block content
        self.assertEqual(block.name, 'src')
        self.assertEqual(block.language, 'text')
        for idx, content_line in enumerate(content):
            self.assertEqual(block[idx], content_line)


class Links(unittest.TestCase):
    """Parse links"""

    def test_paragraph(self):
        """Link contained in a paragraph"""
        content = ['ada [[https://foo.bar][mylink]] lovelance']
        _, sut = parser.OrgContentParser(content).result()

        # one paragraph
        self.assertEqual(len(sut), 1)
        # with 3 runs
        self.assertEqual(len(sut[0]), 3)

        self.assertEqual(sut[0][0], 'ada ')
        self.assertEqual(sut[0][1].runs, ['mylink'])
        self.assertEqual(sut[0][1].target, 'foo.bar')
        self.assertEqual(sut[0][2], ' lovelance')

    def test_label_with_linebreak(self):
        """Link contained in a paragraph"""
        content = [
            'ada [[https://foo.bar][my',
            'link]] lovelance'
        ]
        _, sut = parser.OrgContentParser(content).result()

        # one paragraph
        self.assertEqual(len(sut), 1)
        # with 3 runs
        self.assertEqual(len(sut[0]), 3)

        self.assertEqual(sut[0][0], 'ada ')
        self.assertEqual(sut[0][1].runs, ['my link'])
        self.assertEqual(sut[0][1].target, 'foo.bar')
        self.assertEqual(sut[0][2], ' lovelance')


class Figures(unittest.TestCase):
    """Parse figures"""

    def test_simple(self):
        """Simple figure."""
        content = [
            '',
            '[[figure.bmp]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)

        figure = sut[0]
        self.assertEqual(figure.attributes, {})
        self.assertEqual(figure.caption, None)
        self.assertEqual(figure.image.target, 'figure.bmp')
        self.assertEqual(figure.link, None)

    def test_linked_to_locale_file(self):
        """Figure with locale file linked image."""
        content = ['[[link.bmp][file:figure.png]]']
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)
        figure = sut[0]
        self.assertEqual(figure.attributes, {})
        self.assertEqual(figure.caption, None)

        self.assertEqual(figure.link.target, 'link.bmp')
        self.assertEqual(figure.image.target, 'figure.png')

    def test_figure_linked_to_https_file(self):
        """Figure with https file linked image."""
        content = ['[[link.bmp][https://world.out/figure.png]]']
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)
        figure = sut[0]
        self.assertEqual(figure.attributes, {})
        self.assertEqual(figure.caption, None)
        self.assertEqual(figure.link.target, 'link.bmp')
        self.assertEqual(figure.image.target, 'https://world.out/figure.png')

    def test_not_a_figure(self):
        """Missing "file:" prefix result in a paragraph with link"""
        content = ['[[link.bmp][figure.png]]']
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 1)
        self.assertIsInstance(sut[0], elements.Paragraph)
        self.assertEqual(sut[0][0].target, 'link.bmp')
        self.assertEqual(sut[0][0][0], 'figure.png')

    def test_two_figures(self):
        """Two figures"""
        content = [
            'foobar',
            '',
            '[[figure_one.png]]',
            '',
            '[[figure_two.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 3)
        self.assertEqual(sut[1].image.target, 'figure_one.png')
        self.assertEqual(sut[2].image.target, 'figure_two.png')

    def test_attr_width(self):
        """ATTR_HTML :width"""
        content = [
            'foobar',
            '#+ATTR_HTML: :width 123',
            '[[figure_one.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        sut = sut[1]
        self.assertEqual(sut.image.target, 'figure_one.png')
        self.assertEqual(sut.image.width, '123')

    def test_attr_alt(self):
        """ATTR_HTML :alt"""
        content = [
            'foobar',
            '#+ATTR_HTML: :alt foo bar',
            '[[figure_one.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        sut = sut[1]
        self.assertEqual(sut.attributes, {})
        self.assertEqual(sut.image.target, 'figure_one.png')
        self.assertEqual(sut.image.alternative, 'foo bar')

    def test_caption(self):
        """CAPTION"""
        content = [
            'foobar',
            '#+CAPTION: ätsch',
            '[[figure.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        sut = sut[1]
        self.assertEqual(sut.attributes, {})
        self.assertEqual(sut.caption, ['ätsch'])
        self.assertEqual(sut.image.target, 'figure.png')

    def test_attr_title(self):
        """ATTR_HTML :title"""
        content = [
            'foobar',
            '#+ATTR_HTML: :title This is mars',
            '[[figure_one.png]]'
        ]

        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        sut = sut[1]
        self.assertEqual(sut.attributes, {})
        self.assertEqual(sut.image.title, 'This is mars')

    def test_unknown_property(self):
        """Unknown property"""
        content = [
            'foobar',
            '#+fooBar: pink elephant',
            '[[figure_one.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        sut = sut[1]
        self.assertEqual(sut.unknown, ['#+FOOBAR: pink elephant'])

    def test_multiple_properties(self):
        """Figure with multiple propierties"""
        content = [
            'foobar',
            '#+ATTR_HTML: :width 15%',
            '#+DOWNLOADED: https://foobar.world',
            '[[foobarfig.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        sut = sut[1]
        self.assertEqual(sut.image.target, 'foobarfig.png')
        self.assertEqual(sut.image.width, '15%')
        self.assertEqual(sut.unknown,
                         ['#+DOWNLOADED: https://foobar.world'])

    def test_multiple_attr_html(self):
        """Figure with multiple ATTR_HTML lines"""
        content = [
            'foobar',
            '#+ATTR_HTML: :alt foo bar',
            '#+ATTR_HTML: :width 15%',
            '#+ATTR_HTML: :height 72%',
            '#+ATTR_HTML: :möp X',
            '#+ATTR_HTML: :title FOOBAR CITY',
            '[[foobarfig.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        # Figures Image
        sut = sut[1]
        self.assertEqual(sut.image.target, 'foobarfig.png')
        self.assertEqual(sut.image.alternative, 'foo bar')
        self.assertEqual(sut.image.width, '15%')
        self.assertEqual(sut.image.height, '72%')
        self.assertEqual(sut.image.title, 'FOOBAR CITY')
        # Figure
        self.assertEqual(sut.attributes, {'möp': 'X'})

    def test_duplicate_attr_html(self):
        """Width overwrites the other."""
        content = [
            'foobar',
            '#+ATTR_HTML: :width 15%',
            '#+ATTR_HTML: :width 39%',
            '[[foobarfig.png]]',
        ]
        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 2)
        sut = sut[1]
        self.assertEqual(sut.image.target, 'foobarfig.png')
        self.assertEqual(sut.image.width, '39%')


class Math(unittest.TestCase):
    """Parse inline and display/block (latex) math"""

    def test_display(self):
        """Display (block) math marked by dollar sign or square brackets"""

        test_content = [
            '$$e = mc^2$$',
            r'\[e = mc^2\]',
        ]

        for content in test_content:
            _, sut = parser.OrgContentParser([content]).result()

            self.assertEqual(len(sut), 1)
            self.assertIsInstance(sut[0], elements.DisplayMath)
            self.assertEqual(sut[0][0], 'e = mc^2')

    def test_display_text_surrounded(self):
        """Display (block) math with text as pre- and suffix"""

        test_content = [
            'first paragraph $$e = mc^2$$ second one',
            r'first paragraph \[e = mc^2\] second one',
        ]

        for content in test_content:
            _, sut = parser.OrgContentParser([content, '']).result()
            self.assertEqual(len(sut), 3)
            self.assertIsInstance(sut[1], elements.DisplayMath)
            self.assertEqual(sut[1][0], 'e = mc^2')
            self.assertIsInstance(sut[0], elements.Paragraph)
            self.assertIsInstance(sut[2], elements.Paragraph)

    def test_inline(self):
        """Inline math marked marked by single dollar sign or round brackets
        """

        test_content = [
            '$e = mc^2$',
            r'\(e = mc^2\)',
        ]

        for content in test_content:
            _, sut = parser.OrgContentParser([content]).result()

            # one element only
            self.assertEqual(len(sut), 1)
            # a paragraph
            self.assertIsInstance(sut[0], elements.Paragraph)
            # contain one run only
            self.assertEqual(len(sut[0]), 1)
            # a inlinemath
            self.assertIsInstance(sut[0][0], textruns.InlineMath)
            self.assertEqual(sut[0][0][0], 'e = mc^2')

    def test_inline_text_surrounded(self):
        """Inline math marked with text as pre- and suffix"""

        test_content = [
            'one paragraph $e = mc^2$ with formula.',
            r'one paragraph \(e = mc^2\) with formula.',
        ]

        for content in test_content:
            _, sut = parser.OrgContentParser([content, '']).result()

            # one paragraph
            self.assertEqual(len(sut), 1)
            # with 3 runs
            self.assertEqual(len(sut[0]), 3)
            # second is inline math
            self.assertIsInstance(sut[0][1], textruns.InlineMath)
            self.assertEqual(sut[0][1][0], 'e = mc^2')
            # first and third are just strings
            self.assertEqual(sut[0][0], 'one paragraph ')
            self.assertEqual(sut[0][2], ' with formula.')

    def test_full(self):
        """All math variants as one"""

        content = [
            'first paragraph $$e = mc^2$$ second one',
            '',
            r'first paragraph \[e = mc^2\] second one',
            '',
            '$$e = mc^2$$',
            '',
            r'\[e = mc^2\]',
            '',
            'one paragraph $e = mc^2$ with formula.',
            '',
            r'one paragraph \(e = mc^2\) with formula.',
            '',
            '$e = mc^2$',
            '',
            r'\(e = mc^2\)',
        ]

        _, sut = parser.OrgContentParser(content).result()

        # 12 elements
        self.assertEqual(len(sut), 12)
        # some Paragraphs
        for idx in [0, 2, 3, 5, 8, 9, 10, 11]:
            self.assertIsInstance(sut[idx], elements.Paragraph, f'{idx=}')
        # some DisplayMath
        for idx in [1, 4, 6, 7]:
            self.assertIsInstance(sut[idx], elements.DisplayMath, f'{idx=}')
        # 9th has inline math
        self.assertEqual(len(sut[8]), 3)
        self.assertIsInstance(sut[8][1], textruns.InlineMath)
        # 10th has inline math
        self.assertEqual(len(sut[9]), 3)
        self.assertIsInstance(sut[9][1], textruns.InlineMath)
        # 11th has inline math
        self.assertEqual(len(sut[10]), 1)
        self.assertIsInstance(sut[10][0], textruns.InlineMath)
        # 12th (last) has inline math
        self.assertEqual(len(sut[11]), 1)
        self.assertIsInstance(sut[11][0], textruns.InlineMath)

    def test_multiple_display_in_one_line(self):
        """Special case of multiple display math in one line"""

        # One line with 5 display math of dollar and bracket kind
        content = [
            r'first para $$A = mc^2$$ second pAra \[B = mc^2\] third Para '
            r'$$c = mc^2$$ forth \[D = mc^2\] five \[e = mc^2\]',
        ]

        _, sut = parser.OrgContentParser(content).result()

        self.assertEqual(len(sut), 10)
        # some Paragraphs
        for idx in [0, 2, 4, 6, 8]:
            self.assertIsInstance(sut[idx], elements.Paragraph, f'{idx=}')
        # some DisplayMath
        for idx in [1, 3, 5, 7, 9]:
            self.assertIsInstance(sut[idx], elements.DisplayMath, f'{idx=}')

    def test_display_with_newline(self):
        """Display math with newline in it"""
        content = ['$$foo=\nbar$$']

        _, sut = parser.OrgContentParser(content).result()
        self.assertEqual(len(sut), 1)
        self.assertIsInstance(sut[0], elements.DisplayMath)
        self.assertEqual(sut[0][0], 'foo=\nbar')
