# 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 LICENSES directory
# or go to <https://spdx.org/licenses/GPL-3.0-only.html>.
"""Tests about exporting complete nodes as HTML

Dev note (2025-12, buhtz): Very mixed and unsharp. Should be reorganized and
split.
"""
# pylint: disable=too-many-lines
import unittest
from unittest import mock
import pathlib
import inspect
from collections import defaultdict
import locale
import datetime
import pyfakefs.fake_filesystem_unittest as pyfakefs_ut
from tests.helper import unify_html
from tests.helper import extract_html_hyperorg_content
from hyperorg.exporter import Exporter
from hyperorg.content import Node
from hyperorg.reader import Reader
from hyperorg import textruns
from hyperorg import elements
from hyperorg import exporter


org_raw_content = [
    # A-d197: node linking to another node
    '''# inline comment
    :PROPERTIES:
    :ID:       d197c651-f6c1-4e00-8d8e-ef13adac16e2
    :END:
    #+title: hyperorg_unittest_1
    #+date: [2022-04-21 Do 16:55]
    #+filetags: MyTag Zwei "Drei 3"
    Test file for unittest

    [[id:b0fc62f1-2926-4eec-a654-eab8fcea46d9][hyperorg_unittest_2]]''',

    # B-b0fc: the linked node
    '''#+begin_comment
    block
    comment
    #+end_comment
    :PROPERTIES:
    :ID:       b0fc62f1-2926-4eec-a654-eab8fcea46d9
    :END:
    #+title: hyperorg_unittest_2
    #+date: [2022-04-21 Do 16:55]
    Second hyperorg test file''',

    # C-4a2f: node with child node (heading with own Org-ID)
    ''':PROPERTIES:
    :ID:       4a2f8510-f57e-468e-9832-8ddecd3335ac
    :END:
    #+title: killme
    #+date: [2022-04-03 So 22:55]
    #+filetags: Zwei
    Governments of the Industrial World,
    * Heading
    you weary giants of flesh and steel,
    * Child
    :PROPERTIES:
    :ID:       95c99140-f044-450d-b631-6a5ff7608b2b
    :END:
    I come from Cyberspace, the new home of Mind,
    * One more
    On behalf of the future, I ask you of
    the past to leave us alone.''',

    # node with alias titles incl. subheadings
    ''':PROPERTIES:
    :ID:       7a2f8517-f57e-868e-9832-8ddecd3335ac
    :ROAM_ALIASES: aliasnode "node with alias"
    :END:
    #+title: Aliases node
    #+date: [2022-04-03 So 22:55]
    #+filetags: "Drei 3"
    * Foo
    bar
    * Bar
    :PROPERTIES:
    :ID:       da6759cb-ba23-4042-9486-abc0c76895ad
    :ROAM_ALIASES: "ein bar alias" again
    :END:
    foo''',

    # node with alias titles incl. subheadings
    ''':PROPERTIES:
    :ID:       1a2f3456-f57e-868e-9832-8ddecd3335xy
    :END:
    #+title: Email
    #+filetags: MyTag
    Angle brackets: Entry <entry@mail.com>

    * &<>"'

    - &<>"'

    &<>"'

    [[id:7a2f8517-f57e-868e-9832-8ddecd3335ac][&<>"']]

    [[id:7a2f8517-f57e-868e-9832-8ddecd3335ac][TITLE]]

    Bold *&<>"'*

    Italic /&<>"'/

    Underline _&<>"'_

    Verbatim =&<>"'=

    Code ~&<>"'~
    ''',
    # 5: a math file with inline math
    ''':PROPERTIES:
    :ID:       1a2f3456-f57e-868e-9832-8mathinline
    :END:
    #+title: Inline math

    one paragraph $e = mc^2$ with formula.

    second paragraph \\(e = mc^2\\) with formula.

    $e = mc^2$

    \\(e = mc^2\\)

    ''',
    # 6: math file with display math
    ''':PROPERTIES:
    :ID:       1a2f3456-f57e-868e-9832-8mathdisplay
    :END:
    #+title: Display math

    first paragraph $$e = mc^2$$ second one

    first paragraph \\[e = mc^2\\] second one

    $$e = mc^2$$

    \\[e = mc^2\\]
    ''',
    # 7: file with mixed math
    ''':PROPERTIES:
    :ID:       1a2f3456-f57e-868e-9832-8mathmixed
    :END:
    #+title: Mixed math

    one paragraph $e = mc^2$ with formula.

    second paragraph \\(e = mc^2\\) with formula.

    $e = mc^2$

    \\(e = mc^2\\)

    first paragraph $$e = mc^2$$ second one

    first paragraph \\[e = mc^2\\] second one

    $$e = mc^2$$

    \\[e = mc^2\\]
    ''',
]


class General(unittest.TestCase):
    """Basic tests for Exporter class."""

    # pylint: disable=protected-access

    def setUp(self):
        Node.nodes = {}
        Node.heading_ids = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

    def test_init(self):
        """ctor"""
        with mock.patch('pathlib.Path.mkdir') as mock_mkdir:
            Exporter(pathlib.Path('foobar'))

            # mkdir() called
            mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)

        # output_dir set?
        self.assertEqual(Exporter.output_dir, pathlib.Path('foobar'))

    def test_as_html_filename(self):
        """Convert a org filename to its html version."""
        self.assertEqual(
            Exporter._as_html_filename('foobar.org'),
            pathlib.Path('foobar.html')
        )

        # does not return a string
        self.assertNotEqual(
            Exporter._as_html_filename('foobar.org'),
            'foobar.html'
        )

        # include sub-folders
        self.assertEqual(
            Exporter._as_html_filename('subfolder/foobar.org'),
            pathlib.Path('subfolder/foobar.html')
        )

    def test_as_index_filename(self):
        """Index HTML filenames."""
        self.assertEqual(
            Exporter._as_index_filename('foobar', 'filetag'),
            pathlib.Path('index__filetag_foobar.html')
        )

        self.assertEqual(
            Exporter._as_index_filename('rofl', 'moon'),
            pathlib.Path('index__moon_rofl.html')
        )

    @mock.patch('pathlib.Path.mkdir')
    def test_footer(self, _):
        """Footer."""
        exp = Exporter(pathlib.Path())

        expected_start = 'Generated with ' \
            '<a href="https://codeberg.org/buhtz/hyperorg">hyperorg'
        expected_end = ' on Fri Aug  6 18:23:45 1982'

        mock_dt = mock.Mock(wraps=datetime.datetime)
        mock_dt.now.return_value = datetime.datetime(1982, 8, 6, 18, 23, 45)
        with mock.patch('datetime.datetime', new=mock_dt):
            the_footer = exp._get_footer()

        self.assertTrue(the_footer.startswith(expected_start))
        self.assertTrue(the_footer.endswith(expected_end))

        # footer string reused instead of re-generated
        self.assertEqual(id(the_footer), id(exp._get_footer()))

    @mock.patch('hyperorg.meta', {'Project-URL': {'homepage': 'https://w.x'}})
    @mock.patch('hyperorg.__name__', 'FOOBAR')
    @mock.patch('hyperorg.__version__', '123456')
    def test_meta_generator(self):
        """Generator items in HTML output"""
        result = Exporter._get_html_meta_info_generator(0)

        expect = '\n'.join([
            '<meta name="generator" content="FOOBAR" />',
            '<meta name="generator_version" content="123456" />',
            '<meta name="generator_website" content="https://w.x" />'
        ])

        self.assertEqual(result, expect)

    @mock.patch('hyperorg.exporter.Exporter._get_html_template_node')
    @mock.patch('hyperorg.exporter.Exporter._get_mathjax_code')
    @mock.patch('hyperorg.exporter.Exporter._elements_as_html')
    @mock.patch('hyperorg.exporter.Exporter._get_footer')
    def test_as_html(self,
                     mock_footer,
                     mock_elements,
                     mock_mathjax,
                     mock_template_node):
        """Not sure what the intention is here.
        I assume it is about the three placeholders"""

        self.maxDiff = None
        mock_template_node.return_value = inspect.cleandoc('''
            {{ mathjax }}{{ title }}
            {{ body }}
            {{ footer }}
        ''')

        mock_elements.return_value = 'The Elements'
        mock_footer.return_value = 'A Footer'
        mock_mathjax.return_value = ''

        expected = inspect.cleandoc('''
            Test Title
            The Elements
            A Footer
        ''')

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path('out'))

        node = Node(['#+title: Test Title'], 'file.org')

        self.assertEqual(expected, exp._as_html(node, []))

    @mock.patch('hyperorg.exporter.Exporter._backlinks_to_html')
    @mock.patch('hyperorg.exporter.Exporter._paragraph_to_html')
    @mock.patch('hyperorg.exporter.Exporter._heading_to_html')
    @mock.patch('hyperorg.exporter.Exporter._list_to_html')
    @mock.patch('hyperorg.exporter.Exporter._subtree_to_html')
    # pylint: disable-next=too-many-arguments,too-many-positional-arguments
    def test_elements_as_html(self,  # noqa: PLR0913
                              mock_subtree,
                              mock_list,
                              mock_heading,
                              mock_paragraph,
                              mock_backlinks):
        """Calling order.

        Credits: https://stackoverflow.com/a/22677452/4865723
        """
        mock_subtree.return_value = ''
        mock_list.return_value = ''
        mock_heading.return_value = ''
        mock_paragraph.return_value = ''
        mock_backlinks.return_value = ''

        manager = mock.Mock()
        manager.attach_mock(mock_subtree, 'subtree')
        manager.attach_mock(mock_list, 'alist')
        manager.attach_mock(mock_heading, 'heading')
        manager.attach_mock(mock_paragraph, 'paragraph')
        manager.attach_mock(mock_backlinks, 'backlinks')

        node = Node([], '')
        node._elements.append(elements.ListElement('- fake', False))
        node._elements.append(elements.Paragraph(''))
        node._elements.append(Node([], ''))
        node._elements.append(elements.Heading('* Fake'))
        node._elements.append(elements.Backlinks())
        node._elements[-1].runs = ['- fake']

        expected_calls = [
            mock.call.alist(node._elements[0]),
            mock.call.paragraph(node._elements[1]),
            mock.call.subtree(node._elements[2]),
            mock.call.heading(node._elements[3]),
            mock.call.backlinks(node._elements[4])
        ]

        Exporter._elements_as_html(node)

        self.assertEqual(manager.mock_calls, expected_calls)

    def test_compute_indexes(self):
        """Items for navigation bar."""

        Node.index_data = {
            Node.IDX_ALL: {
                'B': [('Title', 0, True) for _ in range(7)],
                'R': [('Title', 0, True) for _ in range(19)],
                'T': [('Title', 0, True) for _ in range(4)],
                'W': [('Title', 0, True) for _ in range(2)],
                'G': [('Title', 0, True) for _ in range(10)],
                'A': [('Title', 0, True) for _ in range(5)],
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'B': [('Title', 0, True) for _ in range(7)],
                    'W': [('Title', 0, True) for _ in range(2)],
                    'T': [('Title', 0, True) for _ in range(4)],
                },
                'Tag': {
                    'G': [('Title', 0, True) for _ in range(10)],
                    'A': [('Title', 0, True) for _ in range(5)],
                },
                'Foo and Bar': {
                    'A': [('Title', 0, True) for _ in range(5)],
                    'R': [('Title', 0, True) for _ in range(19)],
                    'G': [('Title', 0, True) for _ in range(10)],
                },
                'Alice': {
                    'A': [('Title', 0, True) for _ in range(5)],
                },

            }
        }

        # "Index" as first entry.
        # The rest sorted by title(entry) count
        expect = [
            ('Index', 'index', 47),
            ('Foo and Bar', 'Foo_and_Bar', 34),
            ('Tag', 'Tag', 15),
            ('(no tags)', '_no_tags_', 13),
            ('Alice', 'Alice', 5)
        ]
        result = Exporter._compute_indexes()

        self.assertEqual(result, expect)

    def test_compute_indexes_duplicate_filename(self):
        """Duplicates in index filenames."""

        Node.index_data = {
            Node.IDX_ALL: {
                'B': [('Title', 0, True) for _ in range(7)],
                'G': [('Title', 0, True) for _ in range(10)],
                'A': [('Title', 0, True) for _ in range(5)],
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'B': [('Title', 0, True) for _ in range(7)],
                },
                'Tag<': {
                    'G': [('Title', 0, True) for _ in range(10)],
                },
                'Tag:': {
                    'A': [('Title', 0, True) for _ in range(5)],
                },

            }
        }

        # "Index" as first entry.
        # The rest sorted by title(entry) count
        expect = [
            ('Index', 'index', 22),
            ('Tag<', 'Tag_1', 10),
            ('(no tags)', '_no_tags_', 7),
            ('Tag:', 'Tag_2', 5),
        ]
        result = Exporter._compute_indexes()

        self.assertEqual(result, expect)

    def test_compute_indexes_order(self):
        """Correct order of items for navigation bar."""

        Node.index_data = {
            Node.IDX_ALL: {
                'B': [('Title', 0, True) for _ in range(24)],
                'A': [('Title', 0, True) for _ in range(5)],
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'B': [('Title', 0, True) for _ in range(24)],
                },
                'Tag': {
                    'A': [('Title', 0, True) for _ in range(5)],
                },
                'Alice': {
                    'A': [('Title', 0, True) for _ in range(5)],
                },
                'Xerxes': {
                    'A': [('Title', 0, True) for _ in range(5)],
                },
            }
        }

        # "Index" as first entry.
        # The rest sorted by title(entry) count
        expect = [
            ('Index', 'index', 29),
            ('(no tags)', '_no_tags_', 24),
            ('Alice', 'Alice', 5),
            ('Tag', 'Tag', 5),
            ('Xerxes', 'Xerxes', 5),
        ]
        result = Exporter._compute_indexes()

        self.assertEqual(result, expect)

    def test_url_with_slash_at_end(self):
        """If there is a slash at the end of an URL was interpreted as
        emphasized inline markup. See #64.
        """
        node = Node(['[[https://my.world/]]'], '')

        expected = '<p><a target="_blank" rel="noopener noreferrer" ' \
                   'href="https://my.world/">' \
                   'https://my.world/</a></p>'

        result = Exporter._elements_as_html(node)

        # We keep this test case and invert its result when #63 is closed.
        self.assertEqual(result, expected)


class GenerateHRef(unittest.TestCase):
    """Test function generate_href_value()"""

    def setUp(self):
        Node.nodes = {}
        Node.heading_ids = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

    def test_same_root(self):
        """Target and start node in same root folder."""
        Node.nodes = {
            'root': Node([], 'root.org'),
            'gin': Node([], 'gin.org'),
        }

        # gin links to root
        sut = Exporter.generate_href_value('root', 'gin')
        self.assertEqual(sut, 'root.html')

    def test_sub_to_root(self):
        """Target is root and start in subfolder"""
        Node.nodes = {
            'root': Node([], 'root.org'),
            'gin': Node(
                [], str(pathlib.Path('subfolder') / 'deep' / 'gin.org')),
        }

        # gin links to root
        sut = Exporter.generate_href_value('root', 'gin')
        self.assertEqual(sut, '../../root.html')

    def test_root_to_sub(self):
        """Target is in subfolder and root is start"""
        Node.nodes = {
            'root': Node([], 'root.org'),
            'gin': Node(
                [], str(pathlib.Path('subfolder') / 'deep' / 'gin.org')),
        }

        # root links to gin
        sut = Exporter.generate_href_value('gin', 'root')
        self.assertEqual(sut, 'subfolder/deep/gin.html')

    def test_neighbors_in_subs_one_level(self):
        """Target and start in different subfolders on same level"""
        Node.nodes = {
            'tonic': Node(
                [], str(pathlib.Path('subFoo') / 'tonic.org')),
            'gin': Node(
                [], str(pathlib.Path('subBar') / 'gin.org')),
        }

        # tonic links to gin
        sut = Exporter.generate_href_value('gin', 'tonic')
        self.assertEqual(sut, '../subBar/gin.html')

    def test_neighbors_in_subs_three_level(self):
        """Target and start in different subfolders on same level"""
        Node.nodes = {
            'tonic': Node([], str(pathlib.Path('subFoo') /
                                  'cocktail' / 'bar' / 'tonic.org')),
            'gin': Node([], str(pathlib.Path('subBar') /
                                'snow' / 'white' / 'gin.org')),
        }

        # tonic links to gin
        sut = Exporter.generate_href_value('gin', 'tonic')
        self.assertEqual(sut, '../../../subBar/snow/white/gin.html')

    def test_no_start(self):
        """Target but no start node.

        This is the case when using the function while generating index.html.
        """
        Node.nodes = {
            'root': Node([], 'root.org'),
            'gin': Node([], 'sub/folder/gin.org'),
        }

        sut = Exporter.generate_href_value('root', None)
        self.assertEqual(sut, 'root.html')

        sut = Exporter.generate_href_value('gin', None)
        self.assertEqual(sut, 'sub/folder/gin.html')


class ParsedElements(pyfakefs_ut.TestCase):
    """Test resulting elements list in Node instance.

    Some related errors occurred in the past. The process is more complex then
    it looks in the first place. A node with subheadings contain multiple
    orgparse objects. Gluing them together to one list of elements brings
    the complexity into the process that need to get tested.

    Dev note (2025-12, buhtz): More related to Nodes class (content.py).
    """

    # pylint: disable=protected-access

    def test_simple(self):
        """Node with two paragraphs and one heading"""
        self.maxDiff = None

        sut = Node.nodes['1234']._elements  # simple.org

        self.assertEqual(sut[0], elements.Paragraph(['erster absatz']))
        self.assertEqual(sut[1], elements.Heading('* Heading A'))
        self.assertEqual(sut[2], elements.Paragraph(['zweiter absatz']))

    def test_complex(self):
        """Node with an IDed heading"""
        self.maxDiff = None

        sut = Node.nodes['9876']._elements  # complex.org

        self.assertEqual(sut[0], elements.Paragraph(['absatz eins']))
        self.assertEqual(sut[1], elements.Heading('* A Heading'))
        self.assertEqual(sut[2], elements.Paragraph(['absatz zwei']))

        self.assertIsInstance(sut[3], Node)
        self.assertEqual(sut[3][0].runs, ['Heading B mit ID'])
        self.assertEqual(sut[3][0].level, 2)
        self.assertIsNotNone(sut[3][0].anchor)
        self.assertEqual(sut[3][1], elements.Paragraph(['absatz drei']))

        self.assertEqual(sut[4], elements.Heading('* Heading C'))
        self.assertEqual(sut[5], elements.Paragraph(['absatz vier']))

        self.assertEqual(len(sut), 6)
        self.assertEqual(len(sut[3]), 2)

    @classmethod
    def setUpClass(cls):
        """Create a folder with three org files."""
        Node.nodes = {}
        Node.heading_ids = {}
        Node.backlinks = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

        # pylint: disable-next=no-member
        cls.setUpClassPyfakefs(allow_root_user=False)
        cls.input_dir = pathlib.Path('/org')
        cls.input_dir.mkdir()
        cls._create_files()
        cls.reader = Reader(cls.input_dir)
        cls.reader.read_org_files()

    @classmethod
    def _create_files(cls):
        with (cls.input_dir / 'simple.org').open('w', 'utf-8') as handle:
            content = [
                ':PROPERTIES:',
                ':ID: 1234',
                ':END:',
                '#+title: simple',
                'erster absatz',
                '* Heading A',
                'zweiter absatz',
            ]

            handle.write('\n'.join(content))

        with (cls.input_dir / 'complex.org').open('w', 'utf-8') as handle:
            content = [
                ':PROPERTIES:',
                ':ID:       9876',
                ':END:',
                '#+title: complex',
                '#+date: [2022-04-03 So 22:55]',
                '#+filetags: Zwei',
                'absatz eins',
                '* A Heading',
                'absatz zwei',
                '* Heading B mit ID',
                ':PROPERTIES:',
                ':ID:       1357',
                ':END:',
                'absatz drei',
                '* Heading C',
                'absatz vier'
            ]

            handle.write('\n'.join(content))


class RealFiles(pyfakefs_ut.TestCase):
    """Create real org files in a virtual file system."""

    # pylint: disable=protected-access

    def setUp(self):
        """Create a folder with three org files."""
        Node.nodes = {}
        Node.heading_ids = {}
        Node.backlinks = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

        self.setUpPyfakefs(allow_root_user=False)
        self.input_dir = pathlib.Path('/org')
        self.input_dir.mkdir()

    def _create_files(self, fn_idx=None):
        """Create files in the (virtual) file system"""

        if not fn_idx:
            self._create_files([
                ('test1.org', 0),
                ('test2.org', 1),
                ('killme.org', 2),
                ('titles.org', 3),
                ('email.org', 4)
            ])

            return

        for fn, idx in fn_idx:
            with (self.input_dir / fn).open('w', 'utf-8') as handle:
                handle.write(inspect.cleandoc(org_raw_content[idx]))

    def test_setup(self):
        """Setup pyfakefs."""

        self._create_files()
        reader = Reader(self.input_dir)
        reader.read_org_files()

        # 3 nodes
        expect = {
            'd197c651-f6c1-4e00-8d8e-ef13adac16e2': 'test1.org',
            'b0fc62f1-2926-4eec-a654-eab8fcea46d9': 'test2.org',
            '4a2f8510-f57e-468e-9832-8ddecd3335ac': 'killme.org',
            '7a2f8517-f57e-868e-9832-8ddecd3335ac': 'titles.org',
            '1a2f3456-f57e-868e-9832-8ddecd3335xy': 'email.org'
        }
        self.assertEqual(len(Node.nodes), len(expect))

        for orgid, expected_node in expect.items():
            self.assertEqual(
                Node.nodes[orgid].filename,
                expected_node
            )

    @mock.patch('uuid.uuid4')
    def test_no_mathjax(self, mock_uuid):
        """No MathJax if no math class is in HTML"""
        self._create_files([('titles.org', 3)])
        mock_uuid.return_value = '091827465'

        reader = Reader(self.input_dir)
        reader.read_org_files()

        exp = Exporter(pathlib.Path.cwd() / 'out')

        html = exp._as_html(list(Node.nodes.values())[0], [])

        self.assertFalse('MathJax' in html)

    @mock.patch('uuid.uuid4')
    def test_mathjax_inline(self, mock_uuid):
        """MathJax because math class is in HTML"""
        self._create_files([('inline.org', 5)])
        mock_uuid.return_value = '091827465'

        reader = Reader(self.input_dir)
        reader.read_org_files()

        exp = Exporter(pathlib.Path.cwd() / 'out')

        html = exp._as_html(list(Node.nodes.values())[0], [])

        self.assertTrue('MathJax' in html)

    @mock.patch('uuid.uuid4')
    def test_mathjax_display(self, mock_uuid):
        """MathJax because math class is in HTML"""
        self._create_files([('display.org', 6)])
        mock_uuid.return_value = '091827465'

        reader = Reader(self.input_dir)
        reader.read_org_files()

        exp = Exporter(pathlib.Path.cwd() / 'out')

        html = exp._as_html(list(Node.nodes.values())[0], [])

        self.assertTrue('MathJax' in html)

    @mock.patch('uuid.uuid4')
    def test_mathjax_mixed(self, mock_uuid):
        """MathJax because math class is in HTML"""
        self._create_files([('mixedmath.org', 7)])
        mock_uuid.return_value = '091827465'

        reader = Reader(self.input_dir)
        reader.read_org_files()

        exp = Exporter(pathlib.Path.cwd() / 'out')

        html = exp._as_html(list(Node.nodes.values())[0], [])

        self.assertTrue('MathJax' in html)

    def test_inline_comment(self):
        """Inline comment"""
        self.maxDiff = None
        self._create_files([('test1.org', 0)])

        reader = Reader(self.input_dir)
        reader.read_org_files()

        self.assertEqual(len(Node.nodes), 1)
        node = list(Node.nodes.values())[0]

        self.assertEqual(node.title_org, 'hyperorg_unittest_1')
        self.assertEqual(len(node), 2)

    def test_block_comment(self):
        """Block comment"""
        # ('test2.org', 1),
        self._create_files([('test2.org', 1)])
        self.maxDiff = None
        reader = Reader(self.input_dir)
        reader.read_org_files()

        self.assertEqual(len(Node.nodes), 1)
        node = list(Node.nodes.values())[0]

        self.assertEqual(node.title_org, 'hyperorg_unittest_2')
        self.assertEqual(len(node), 1)

    @mock.patch('uuid.uuid4')
    def test_subtree_html_output(self, mock_uid):
        """Node with a subtree in it.

        Nodes with a subtree/heading having it's own OrgId resulting in
        a Content instance having a Content instances as child element.
        """
        self._create_files([('killme.org', 2)])
        self.maxDiff = None

        mock_uid.return_value = 'x73902y'

        reader = Reader(self.input_dir)
        reader.read_org_files()

        expected_html = '\n'.join([
            '<header>',
            '<div id="hyperorg-ctime">',
            '<span class="label">date:</span>',
            '&nbsp;<time>Sun Apr  3 22:55:00 2022</time>',
            '</div>',
            '<br />',
            '<div><span class="label">filetags:</span>',
            '&nbsp;Zwei</div>',
            '</header>',
            '<p>Governments of the Industrial World,</p>',
            '<h2>Heading</h2>',
            '<p>you weary giants of flesh and steel,</p>',
            '<h2 id="x73902y">Child</h2>',
            '<p>I come from Cyberspace, the new home of Mind,</p>',
            '<h2>One more</h2>',
            '<p>On behalf of the future, I ask you of '
            'the past to leave us alone.</p>',
        ])

        node = Node.nodes['4a2f8510-f57e-468e-9832-8ddecd3335ac']
        exp = Exporter(pathlib.Path('/out'))
        html = exp._as_html(node, [])

        self.assertEqual(
            unify_html(extract_html_hyperorg_content(html)),
            unify_html(expected_html)
        )

    def test_subtree_meta_fields_inherit(self):
        """Parent nodes meta data inherits to child nodes.

        The meta data in child nodes not used in the visual (HTML) output of
        the node. But it is used to but this child node on the right index
        (e.g. index.html).
        """

        self._create_files([('killme.org', 2)])
        self.maxDiff = None

        reader = Reader(self.input_dir)
        reader.read_org_files()

        self.assertEqual(len(Node.nodes), 1)
        self.assertEqual(len(Node.heading_ids), 1)

        # parent node
        node = list(Node.nodes.values())[0]

        # child node
        child = list(Node.heading_ids.values())[0][1]
        child = node[child]

        self.assertEqual(node._meta, child._meta)

    def test_href_value_node(self):
        """a-tag href-attribute value for a simple node
        """

        self._create_files([('test1.org', 0), ('test2.org', 1)])
        reader = Reader(self.input_dir)
        reader.read_org_files()

        orgid = 'b0fc62f1-2926-4eec-a654-eab8fcea46d9'

        self.assertEqual(
            Exporter.generate_href_value(
                orgid, 'd197c651-f6c1-4e00-8d8e-ef13adac16e2'),
            'test2.html')

    @mock.patch('uuid.uuid4')
    def test_href_value_subheading(self, mock_uuid):
        """href-attribute value for a nodes subheading

        See `Content` for more details. A heading with its own OrgId results
        in a ``Content`` instance as a child element of a usual ``Content``.
        """
        self._create_files([('killme.org', 2)])
        mock_uuid.return_value = '091827465'
        reader = Reader(self.input_dir)
        reader.read_org_files()

        subheading_id = '95c99140-f044-450d-b631-6a5ff7608b2b'

        # link to subheading
        self.assertEqual(
            Exporter.generate_href_value(subheading_id, None),
            'killme.html#091827465')

    @mock.patch('uuid.uuid4')
    def test_alias_title_meta(self, mock_uuid):
        """Alias titles as meta fields.
        """
        self.maxDiff = None
        self._create_files([('titles.org', 3)])
        mock_uuid.return_value = '091827465'

        reader = Reader(self.input_dir)
        reader.read_org_files()
        exp = Exporter(pathlib.Path.cwd() / 'out')

        orgid = '7a2f8517-f57e-868e-9832-8ddecd3335ac'

        expected = '''
            <h1>Aliases node</h1>
                <section>
                    <header>
                        <div id="hyperorg-ctime">
                            <span class="label">date:</span>
                            &nbsp;<time>Sun Apr  3 22:55:00 2022</time>
                        </div>
                        <br />
                        <div>
                            <span class="label">Alias:</span>
                            &nbsp;aliasnode
                        </div>
                        <div>
                            <span class="label">Alias:</span>
                            &nbsp;node with alias
                        </div>
                        <div><span class="label">filetags:</span>
                            &nbsp;"Drei 3"</div>
                    </header>
                    <h2>Foo</h2>
                    <p>bar</p>
                    <h2 id="091827465">Bar</h2>
                    <header>
                        <div>
                            <span class="label">Alias:</span>
                            &nbsp;ein bar alias
                        </div>
                        <div>
                            <span class="label">Alias:</span>
                            &nbsp;again
                        </div>
                    </header>
                    <p>foo</p>
                </section>
        '''

        html = exp._as_html(Node.nodes[orgid], [])
        html = extract_html_hyperorg_content(html, 'main')

        self.assertEqual(unify_html(html), unify_html(expected))

    @mock.patch('uuid.uuid4')
    @mock.patch('hyperorg.exporter.secrets.token_hex')
    def test_index_all_title_types(self, mock_token, mock_uuid):
        """Index with all types of titles.

        Node titles, alias titles and the same for subheadings.
        """

        self.maxDiff = None
        self._create_files()

        mock_uuid.side_effect = ['uuid001', 'uuid002']
        mock_token.side_effect = [
            f'index{letter}' for letter in list('ABCEHKNXXXXXXXXXXXXX')
        ]  # "X" because of file-tag based indexes

        reader = Reader(self.input_dir)
        reader.read_org_files()

        exp = Exporter(pathlib.Path.cwd() / 'out')
        exp.export_to_html()

        fp = pathlib.Path.cwd() / 'out' / 'index.html'

        # index file created
        self.assertTrue(fp.exists())

        # get file content
        with fp.open('r', encoding='utf-8') as handle:
            html = handle.read()

        html = extract_html_hyperorg_content(
            html, ('main id="hyperorg-index"', 'main'))

        expected = '''
        <h1>Node index</h1>
            <div class="hyperorg-index__letter-nav">
                <a href="#indexA">A</a>
                <a href="#indexB">B</a>
                <a href="#indexC">C</a>
                <a href="#indexE">E</a>
                <a href="#indexH">H</a>
                <a href="#indexK">K</a>
                <a href="#indexN">N</a>
            </div>
            <hr />
            <article class="hyperorg-index" id="indexA">
                <h2>A</h2>
                <div><a class="hyperorg-index__toplink" href="#top">Back
                to Top</a>
                <ul>
                    <li><a href="titles.html">Aliases node</a></li>
                    <li><a href="titles.html#uuid002"><em>again</em>
                        </a></li>
                    <li><a href="titles.html"><em>aliasnode</em></a></li>
                </ul>
                </div>
            </article>
            <hr />
            <article class="hyperorg-index" id="indexB">
                <h2>B</h2>
                <div><a class="hyperorg-index__toplink" href="#top">Back
                to Top</a>
                <ul>
                    <li><a href="titles.html#uuid002">Bar</a></li>
                </ul>
                </div>
            </article>
            <hr />
            <article class="hyperorg-index" id="indexC">
                <h2>C</h2>
                <div><a class="hyperorg-index__toplink" href="#top">Back
                to Top</a>
                <ul>
                    <li><a href="killme.html#uuid001">Child</a></li>
                </ul>
                </div>
            </article>
            <hr />
            <article class="hyperorg-index" id="indexE">
                <h2>E</h2>
                <div><a class="hyperorg-index__toplink" href="#top">Back
                to Top</a>
                <ul>
                    <li><a href="email.html">Email</a></li>
                    <li><a href="titles.html#uuid002"><em>ein bar
                        alias</em></a></li>
                </ul>
                </div>
            </article>
            <hr />
            <article class="hyperorg-index" id="indexH">
                <h2>H</h2>
                <div><a class="hyperorg-index__toplink" href="#top">Back
                to Top</a>
                <ul>
                    <li><a href="test1.html">hyperorg_unittest_1</a></li>
                    <li><a href="test2.html">hyperorg_unittest_2</a></li>
                </ul>
                </div>
            </article>
            <hr />
            <article class="hyperorg-index" id="indexK">
                <h2>K</h2>
                <div><a class="hyperorg-index__toplink" href="#top">Back
                to Top</a>
                <ul>
                    <li><a href="killme.html">killme</a></li>
                </ul>
                </div>
            </article>
            <hr />
            <article class="hyperorg-index" id="indexN">
                <h2>N</h2>
                <div><a class="hyperorg-index__toplink" href="#top">Back
                to Top</a>
                <ul>
                    <li><a href="titles.html"><em>node with alias</em></a></li>
                </ul>
                </div>
            </article>
        '''

        self.assertEqual(
            unify_html(html),
            unify_html(expected)
        )

    @mock.patch('uuid.uuid4')
    def test_html_entities(self, mock_uuid):
        """Converting HTML entities (e.g. angle bracktes).

        Characters like '<' need to converted into '&lt;' in HTML output.
        """
        self.maxDiff = None
        self._create_files([('titles.org', 3), ('email.org', 4)])
        mock_uuid.return_value = '091827465'

        escaped_html = '&amp;&lt;&gt;&quot;&#x27;'
        expected = f'''
                <header><div>
                    <span class="label">filetags:</span>&nbsp;MyTag</div>
                </header>
                <p>Angle brackets: Entry &lt;entry@mail.com&gt;</p>
                <h2>{escaped_html}</h2>
                <ul>
                    <li>{escaped_html}</li>
                </ul>
                <p>{escaped_html}</p>
                <p><a href="titles.html">{escaped_html}</a></p>
                <p><a href="titles.html">TITLE</a></p>
                <p>Bold <strong>{escaped_html}</strong></p>
                <p>Italic <em>{escaped_html}</em></p>
                <p>Underline <u>{escaped_html}</u></p>
                <p>Verbatim <code class="verbatim">{escaped_html}</code></p>
                <p>Code <code>{escaped_html}</code></p>
        '''

        reader = Reader(self.input_dir)
        reader.read_org_files()

        exp = Exporter(pathlib.Path.cwd() / 'out')

        html = exp._as_html(
            Node.nodes['1a2f3456-f57e-868e-9832-8ddecd3335xy'], [])
        html = extract_html_hyperorg_content(html, 'section')

        self.assertEqual(unify_html(html), unify_html(expected))

    def test_node_nav(self):
        """Navigation bar in nodes."""
        self.maxDiff = None

        self._create_files([('killme.org', 0)])

        expected = '''
            <ul>
                <li><a href="index.html">Index</a> (1)</li>
                <li><a href="index__filetag_Drei_3.html">Drei 3</a> (1)</li>
                <li><a href="index__filetag_MyTag.html">MyTag</a> (1)</li>
                <li><a href="index__filetag_Zwei.html">Zwei</a> (1)</li>
            </ul>'''

        reader = Reader(self.input_dir)
        reader.read_org_files()

        exp = Exporter(pathlib.Path.cwd() / 'out')

        index_items = exp._compute_indexes()

        html = exp._as_html(list(Node.nodes.values())[0], index_items)
        html = extract_html_hyperorg_content(html, 'nav')

        self.assertEqual(unify_html(html), unify_html(expected))


class Subfolders(pyfakefs_ut.TestCase):
    """Org files in sub foldres"""
    # pylint: disable=protected-access

    def setUp(self):
        Node.nodes = {}
        Node.heading_ids = {}
        Node.backlinks = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

        self.setUpPyfakefs(allow_root_user=False)
        self.input_dir = pathlib.Path('/org')
        self.input_dir.mkdir()

        fp = self.input_dir / 'root.org'
        with fp.open('w', encoding='utf-8') as handle:
            handle.write(':PROPERTIES:\n')
            handle.write(':ID: 01root\n')
            handle.write(':END:\n')
            handle.write('#+title: root\n')
            handle.write('paragraph [[id:02apple][Apfel]]\n')
            handle.write('[[id:03banana][Ban]]\n')
            handle.write('[[id:04gin][gin]]\n')

        fp = self.input_dir / 'subfolder' / 'apple.org'
        fp.parent.mkdir()
        with fp.open('w', encoding='utf-8') as handle:
            handle.write(':PROPERTIES:\n')
            handle.write(':ID: 02apple\n')
            handle.write(':END:\n')
            handle.write('#+title: apple\n')
            handle.write('paragraph')

        fp = self.input_dir / 'subfolder' / 'banana.org'
        with fp.open('w', encoding='utf-8') as handle:
            handle.write(':PROPERTIES:\n')
            handle.write(':ID: 03banana\n')
            handle.write(':END:\n')
            handle.write('#+title: banana\n')
            handle.write('paragraph')

        fp = self.input_dir / 'subfolder' / 'deep' / 'gin.org'
        fp.parent.mkdir()
        with fp.open('w', encoding='utf-8') as handle:
            handle.write(':PROPERTIES:\n')
            handle.write(':ID: 04gin\n')
            handle.write(':END:\n')
            handle.write('#+title: gin\n')
            handle.write('paragraph [[id:01root][root]]')

    def test_file_folder_structure(self):
        """Created HTML in correct folder structure."""
        reader = Reader(self.input_dir)
        reader.read_org_files()

        out_dir = self.input_dir.parent / 'out'
        exp = Exporter(out_dir)
        exp.export_to_html()

        expect = [
            '/out/subfolder',
            '/out/subfolder/deep',
            '/out/root.html',
            '/out/subfolder/apple.html',
            '/out/subfolder/banana.html',
            '/out/subfolder/deep/gin.html',
        ]

        sut = list(out_dir.glob('**/*'))

        for path in expect:
            self.assertIn(pathlib.Path(path), sut)

    def test_root(self):
        """Created HTML root node."""
        reader = Reader(self.input_dir)
        reader.read_org_files()

        out_dir = self.input_dir.parent / 'out'
        exp = Exporter(out_dir)
        exp.export_to_html()

        # Root node in root folder
        sut = (out_dir / 'root.html').read_text()
        sut = sut[:sut.index('<h2>Backlinks</h2>')]

        expect = [
            'href="subfolder/apple.html"',
            'href="subfolder/banana.html"',
            'href="subfolder/deep/gin.html"',
        ]

        for exp in expect:
            self.assertIn(exp, sut)


class Index(unittest.TestCase):
    """Generation of index.html"""

    # pylint: disable=protected-access

    def setUp(self):
        Node.nodes = {}
        Node.heading_ids = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

        try:
            # Patching meta data for <head> tag won't  has an effect
            # because the template is generated only once (in the first test)
            # and then reused.
            # This force re-generating every time and prevent reusing.
            del Exporter._template_index
        except AttributeError:
            pass

    @mock.patch('hyperorg.exporter.secrets.token_hex')
    def test_node_index_to_html_body(self, mock_token):
        """Node index dict as HTML. """

        mock_token.side_effect = [
            f'index{letter}' for letter in list('BG')
        ]

        self.maxDiff = None
        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        Node.nodes['a'] = Node(['#+title: Goofy'], '01goofy.org')
        Node.nodes['b'] = Node(['#+title: Gant'], '02gant.org', ['Gant'])
        Node.nodes['c'] = Node(['#+title: Bar'], '9bar.org')

        nav, body = exp._node_index_to_html_body({
            'G': [
                ('Goofy', 'a', False),
                ('Gant', 'b', True)
            ],
            'B': [
                ('Bar', 'c', False)
            ]
        })

        # navigation (sorted)
        self.assertEqual(
            unify_html(nav),
            '<a href="#indexB">B</a><a href="#indexG">G</a>')

        # body (sorted titles)!
        expected_body = \
            '<hr /><article class="hyperorg-index" id="indexB">' \
            '<h2>B</h2>' \
            '<div>' \
            '<a class="hyperorg-index__toplink" href="#top">' \
            'Back to Top</a>' \
            '<ul><li><a href="9bar.html">Bar</a></li></ul>' \
            '</div></article><hr />' \
            '<article class="hyperorg-index" id="indexG">' \
            '<h2>G</h2>' \
            '<div>' \
            '<a class="hyperorg-index__toplink" href="#top">' \
            'Back to Top</a>' \
            '<ul>' \
            '<li><a href="02gant.html"><em>Gant</em></a></li>' \
            '<li><a href="01goofy.html">Goofy</a></li>' \
            '</ul>' \
            '</div>' \
            '</article>'
        self.assertEqual(unify_html(body), expected_body)

    @mock.patch('hyperorg.meta', {'Project-URL': {'homepage': 'https://R.i'}})
    @mock.patch('hyperorg.__name__', 'BarFoo')
    @mock.patch('hyperorg.__version__', '1984')
    def test_html_template_index(self):
        """HTML template for index."""
        self.maxDiff = None
        expected = '''
            <!DOCTYPE html>
            <html lang="{{ language }}">
                <head>
                    {{ meta_generator }}
                    <meta charset="UTF-8" />
                    <link rel="stylesheet" href="style.css" />
                    <title>Node index</title>
                </head>
                <body id="top">
                    <nav>{{ node nav }}</nav>
                    <main id="hyperorg-index">
                        <h1>Node index</h1>
                        <div class="hyperorg-index__letter-nav">
            {{ index letter nav }}
                        </div>
            {{ body }}
                    </main>
                    <footer>
            {{ footer }}
                    </footer>
                </body>
            </html>
        '''
        expected = inspect.cleandoc(expected)
        expected = expected.replace(
            '{{ language }}',
            locale.getlocale()[0].split('_')[0]
        )
        expected = expected.replace(
            '{{ meta_generator }}',
            '<meta name="generator" content="BarFoo" />\n'
            '        <meta name="generator_version" content="1984" />\n'
            '        <meta name="generator_website" content="https://R.i" />'
        )

        html = Exporter._get_html_template_index()

        self.assertEqual(expected, html)

    @mock.patch('hyperorg.exporter.secrets.token_hex')
    def test_index_html(self, mock_token):  # noqa: PLR0915
        """Test index.html

        Dev note: Separate it into multiple tests.
        """

        # pylint: disable=too-many-statements

        mock_token.side_effect = [
            f'index{letter}' for letter in list('BRTWAGAGRABT23456')
        ]

        Node.index_data = {
            Node.IDX_ALL: {
                'B': [('Title', 0, True) for _ in range(4)],
                'R': [('Title', 0, True) for _ in range(3)],
                'T': [('Title', 0, True) for _ in range(1)],
                'W': [('Title', 0, True) for _ in range(2)],
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'B': [('Title', 0, True) for _ in range(2)],
                    'T': [('Title', 0, True) for _ in range(4)],
                },
                'Tag': {
                    'G': [('Title', 0, True) for _ in range(10)],
                    'A': [('Title', 0, True) for _ in range(5)],
                },
                'Foo and Bar': {
                    'A': [('Title', 0, True) for _ in range(5)],
                    'R': [('Title', 0, True) for _ in range(3)],
                    'G': [('Title', 0, True) for _ in range(1)],
                },
                'Alice': {
                    'A': [('Title', 0, True) for _ in range(4)],
                },

            }
        }

        index_items = [
            ('Index', 'index', 8),
            ('Alice', 'Alice', 4),
            ('Tag', 'Tag', 2),
            ('(no tags)', '_no_tags_', 2),
            ('Foo and Bar', 'Foo_and_Bar', 1)
        ]

        expected_nav = '''
        <ul>
        <li><a href="index.html">Index</a> (8)</li>
        <li><a href="index__filetag_Alice.html">Alice</a> (4)</li>
        <li><a href="index__filetag_Tag.html">Tag</a> (2)</li>
        <li><a href="index__filetag__no_tags_.html">(no tags)</a> (2)</li>
        <li><a href="index__filetag_Foo_and_Bar.html">Foo and Bar</a> (1)</li>
        </ul>'''

        out_path = pathlib.Path('out')

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(out_path)

        # ------------------
        # --- index.html ---
        # ------------------
        html_code = exp._node_index_to_html(
            Node.index_data[Node.IDX_ALL],
            index_items)

        main = unify_html(extract_html_hyperorg_content(html_code, 'main'))

        # navigation bar
        self.assertEqual(
            unify_html(expected_nav),
            unify_html(extract_html_hyperorg_content(html_code, 'nav'))
        )

        # letter navigation
        expect_letter_nav = [
            '<div class="hyperorg-index__letter-nav">',
            '    <a href="#indexB">B</a>',
            '    <a href="#indexR">R</a>',
            '    <a href="#indexT">T</a>',  # codespell-ignore
            '    <a href="#indexW">W</a>',
            '</div>'
        ]
        expect_letter_nav = '\n'.join(expect_letter_nav)

        # codespell-ignore
        self.assertIn(unify_html(expect_letter_nav), main)

        # section B
        expect = '''
            <article class="hyperorg-index" id="indexB">
                <h2>B</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # section R
        expect = '''
            <article class="hyperorg-index" id="indexR">
                <h2>R</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # section T
        expect = [
            '<article class="hyperorg-index" id="indexT">',  # codespell-ignore
            '    <h2>T</h2>',
            '    <div>',
            '    <a class="hyperorg-index__toplink" '
            'href="#top">Back to Top</a>',
            '        <ul>',
            '            <li><a href="None"><em>Title</em></a></li>',
            '        </ul>',
            '    </div>',
            '</article>'
        ]
        expect = '\n'.join(expect)
        self.assertIn(unify_html(expect), main)

        # section W
        expect = '''
            <article class="hyperorg-index" id="indexW">
                <h2>W</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # ------------------
        # --- Tag ---
        # ------------------
        html_code = exp._node_index_to_html(
            Node.index_data[Node.IDX_FILETAG]['Tag'],
            index_items)

        main = unify_html(extract_html_hyperorg_content(html_code, 'main'))

        # navigation bar
        self.assertEqual(
            unify_html(expected_nav),
            unify_html(extract_html_hyperorg_content(html_code, 'nav'))
        )

        # letter navigation
        expect_letter_nav = '''
            <div class="hyperorg-index__letter-nav">
                <a href="#indexA">A</a>
                <a href="#indexG">G</a>
            </div>
        '''

        self.assertIn(unify_html(expect_letter_nav), main)

        # section A
        expect = '''
            <article class="hyperorg-index" id="indexA">
                <h2>A</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # section G
        expect = '''
            <article class="hyperorg-index" id="indexG">
                <h2>G</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # ------------------
        # --- Foo and Bar ---
        # ------------------
        html_code = exp._node_index_to_html(
            Node.index_data[Node.IDX_FILETAG]['Foo and Bar'],
            index_items)

        main = unify_html(extract_html_hyperorg_content(html_code, 'main'))

        # navigation bar
        self.assertEqual(
            unify_html(expected_nav),
            unify_html(extract_html_hyperorg_content(html_code, 'nav'))
        )

        # letter navigation
        expect_letter_nav = '''
            <div class="hyperorg-index__letter-nav">
                <a href="#indexA">A</a>
                <a href="#indexG">G</a>
                <a href="#indexR">R</a>
            </div>
        '''
        self.assertIn(unify_html(expect_letter_nav), main)

        # section A
        expect = '''
            <article class="hyperorg-index" id="indexA">
                <h2>A</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # section G
        expect = '''
            <article class="hyperorg-index" id="indexG">
                <h2>G</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # section R
        expect = '''
            <article class="hyperorg-index" id="indexR">
                <h2>R</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # ------------------
        # --- Alice ---
        # ------------------
        html_code = exp._node_index_to_html(
            Node.index_data[Node.IDX_FILETAG]['Alice'],
            index_items)

        main = unify_html(extract_html_hyperorg_content(html_code, 'main'))

        # navigation bar
        self.assertEqual(
            unify_html(expected_nav),
            unify_html(extract_html_hyperorg_content(html_code, 'nav'))
        )

        # letter navigation
        expect_letter_nav = '''
            <div class="hyperorg-index__letter-nav">
                <a href="#indexA">A</a>
            </div>
        '''
        self.assertIn(unify_html(expect_letter_nav), main)

        # section A
        expect = '''
            <article class="hyperorg-index" id="indexA">
                <h2>A</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # ------------------
        # --- no tags ---
        # ------------------
        html_code = exp._node_index_to_html(
            Node.index_data[Node.IDX_FILETAG][Node.IDX_NO_TAG],
            index_items)

        main = unify_html(extract_html_hyperorg_content(html_code, 'main'))

        # navigation bar
        self.assertEqual(
            unify_html(expected_nav),
            unify_html(extract_html_hyperorg_content(html_code, 'nav'))
        )

        # letter navigation
        expect_letter_nav = [
            '<div class="hyperorg-index__letter-nav">',
            '    <a href="#indexB">B</a>',
            '    <a href="#indexT">T</a>',  # codespell-ignore
            '</div>'
        ]
        expect_letter_nav = '\n'.join(expect_letter_nav)

        self.assertIn(unify_html(expect_letter_nav), main)

        # section B
        expect = '''
            <article class="hyperorg-index" id="indexB">
                <h2>B</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None"><em>Title</em></a></li>
                        <li><a href="None"><em>Title</em></a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)

        # section T
        expect = [
            '<article class="hyperorg-index" id="indexT">',  # codespell-ignore
            '    <h2>T</h2>',
            '    <div>',
            '    <a class="hyperorg-index__toplink" '
            'href="#top">Back to Top</a>',
            '        <ul>',
            '            <li><a href="None"><em>Title</em></a></li>',
            '            <li><a href="None"><em>Title</em></a></li>',
            '            <li><a href="None"><em>Title</em></a></li>',
            '            <li><a href="None"><em>Title</em></a></li>',
            '        </ul>',
            '    </div>',
            '</article>'
        ]
        expect = '\n'.join(expect)

        self.assertIn(unify_html(expect), main)

    @mock.patch('hyperorg.exporter.secrets.token_hex')
    def test_index_with_markup_entry(self, mock_token):
        """Index file where one of its entries has a title with markup in it.
        """

        mock_token.side_effect = [
            f'index{letter}' for letter in list('F')
        ]

        entry = (
            [
                'Foo ',
                textruns.Markup('bar', textruns.Markup.Type.VERBATIM),
            ],
            0,
            True
        )
        Node.index_data = {
            Node.IDX_ALL: {
                'F': [entry],
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'F': [entry],
                },
            }
        }

        index_items = [
            ('Index', 'index', 1),
            ('(no tags)', '_no_tags_', 1),
        ]

        out_path = pathlib.Path('out')

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(out_path)

        # ------------------
        # --- index.html ---
        # ------------------
        html_code = exp._node_index_to_html(
            Node.index_data[Node.IDX_ALL],
            index_items)

        main = unify_html(extract_html_hyperorg_content(html_code, 'main'))

        # section B
        expect = '''
            <article class="hyperorg-index" id="indexF">
                <h2>F</h2>
                <div>
                <a class="hyperorg-index__toplink" href="#top">Back to Top</a>
                    <ul>
                        <li><a href="None">
                            <em>Foo <code class="verbatim">bar</code></em>
                        </a></li>
                    </ul>
                </div>
            </article>
        '''
        self.assertIn(unify_html(expect), main)


class MetaFields(unittest.TestCase):
    """Test export of meta fields into HTML"""

    # pylint: disable=protected-access

    def test_empty(self):
        """No meta data."""
        node = Node(['#+title: One Two'], '')
        self.assertEqual(Exporter._meta_to_html(node), '')

    def test_inline_comment_before(self):
        """Inline comment before properties"""
        self.maxDiff = None

        node = Node([
            '# inline comment',
            '#+title: Titel',
            'body'], '')

        self.assertEqual(len(node), 1)
        self.assertEqual(node.title_org, 'Titel')
        self.assertEqual(node[0][0], 'body')

    def test_block_comment_before(self):
        """Block comment before properties"""
        self.maxDiff = None

        node = Node([
            '#+begin_comment',
            'block comment',
            '#+end_comment',
            '#+title: Titel',
            'body'], '')
        self.assertEqual(len(node), 1)
        self.assertEqual(node.title_org, 'Titel')
        self.assertEqual(node[0][0], 'body')

    def test_exclude_tags(self):
        """Exclude tags."""
        self.maxDiff = None

        node = Node([
            '#+filetags: One "Two zwei" Three',
            '#+foo: Bar bar',
            '#+in: Blue',
            '#+out: Red'], '')

        self.assertEqual(
            unify_html(Exporter._meta_to_html(node,
                                              exclude_fields=['out', 'foo'])),
            unify_html('''
                <div>
                    <span class="label">filetags:</span>
                    &nbsp;One "Two zwei" Three</div>
                <div>
                    <span class="label">in:</span>
                    &nbsp;Blue</div>'''))

    def test_exclude_tags_not_present(self):
        """Exclude tag that is not present."""
        self.maxDiff = None

        node = Node([
            '#+in: Blue',
            '#+out: Red'], '')

        self.assertEqual(
            unify_html(Exporter._meta_to_html(node,
                                              exclude_fields=['out', 'foo'])),
            unify_html('''
                <div>
                    <span class="label">in:</span>
                    &nbsp;Blue</div>'''))

    def test_filetags(self):
        """filetags and others"""
        self.maxDiff = None
        node = Node([
            '#+filetags: One "Two zwei" Three',
            '#+foo: Bar bar'], '')
        self.assertEqual(
            unify_html(Exporter._meta_to_html(node)),
            unify_html('''
                <div>
                    <span class="label">filetags:</span>
                    &nbsp;One "Two zwei" Three</div>
                <div>
                    <span class="label">foo:</span>
                    &nbsp;Bar bar</div>'''))

    def test_locale_conform_datestring(self):
        """Datestring"""

        # datetime object as value
        val_in = datetime.datetime(2021, 11, 28, 16, 20)
        val_out = Exporter._as_locale_conform_date_string(val_in)
        self.assertEqual(val_out, 'Sun Nov 28 16:20:00 2021')

        # This timestamp as a string exists because it wasn't
        # parseable by `dateutils.parser.parse()`.
        val_in = '2021-11-28 So 16:20'
        val_out = Exporter._as_locale_conform_date_string(val_in)
        self.assertEqual(val_out, val_out)

        # Now with brackets like %U generates them
        val_in = '[2021-11-28 So 16:20]'
        val_out = Exporter._as_locale_conform_date_string(val_in)
        self.assertEqual(val_out, val_out)

    def test_one_date_only(self):
        """Only on date in meta section.

        This date is treated as creation date and placed their.
        """
        node = Node([], '')
        node._meta['Foobar'] = datetime.datetime(2021, 11, 28, 16, 20)
        html = unify_html(Exporter._meta_to_html(node))

        self.assertEqual(
            html,
            unify_html('''
                <div id="hyperorg-ctime">
                    <span class="label">Foobar:</span>&nbsp;
                    <time>Sun Nov 28 16:20:00 2021</time>
                </div>
                <br />
            '''))

    def test_m_and_c_date(self):
        """Creation and modification date in meta section."""
        self.maxDiff = None
        node = Node([], '')
        node._meta['mtime'] = datetime.datetime(2021, 11, 28, 16, 20)
        node._meta['ctime'] = datetime.datetime(2021, 11, 28, 16, 25)

        self.assertEqual(
            unify_html(Exporter._meta_to_html(node)),
            unify_html('''
                <div id="hyperorg-mtime">
                    <span class="label">Modified:</span>&nbsp;
                    <time>Sun Nov 28 16:20:00 2021</time>
                </div>
                <div id="hyperorg-ctime">
                    <span class="label">Created:</span>&nbsp;
                    <time>Sun Nov 28 16:25:00 2021</time>
                </div>
                <br />
            '''))

    def test_m_or_c_and_extra_date(self):
        """Unknown dates."""

        self.maxDiff = None

        # cdate, mdate and extra date
        node = Node([], '')
        node._meta['mtime'] = datetime.datetime(2021, 11, 28, 16, 20)
        node._meta['ctime'] = datetime.datetime(2021, 11, 28, 17, 20)
        node._meta['foo'] = datetime.datetime(2021, 11, 28, 18, 20)

        self.assertEqual(
            unify_html(Exporter._meta_to_html(node)),
            unify_html('''
                <div id="hyperorg-mtime">
                    <span class="label">Modified:</span>&nbsp;
                    <time>Sun Nov 28 16:20:00 2021</time>
                </div>
                <div id="hyperorg-ctime">
                    <span class="label">Created:</span>&nbsp;
                    <time>Sun Nov 28 17:20:00 2021</time>
                </div>
                <br />
                <div>
                    <span class="label">foo:</span>&nbsp;
                    <time>Sun Nov 28 18:20:00 2021</time>
                </div>
            '''))

        # cdate and extra date
        node = Node([], '')
        node._meta['ctime'] = datetime.datetime(2021, 11, 28, 17, 20)
        node._meta['foo'] = datetime.datetime(2021, 11, 28, 18, 20)

        self.assertEqual(
            unify_html(Exporter._meta_to_html(node)),
            unify_html('''
                <div id="hyperorg-ctime">
                    <span class="label">Created:</span>&nbsp;
                    <time>Sun Nov 28 17:20:00 2021</time>
                </div>
                <br />
                <div>
                    <span class="label">foo:</span>&nbsp;
                    <time>Sun Nov 28 18:20:00 2021</time>
                </div>
            '''))

        # mdate and extra date
        node = Node([], '')
        node._meta['mtime'] = datetime.datetime(2021, 11, 28, 16, 20)
        node._meta['foo'] = datetime.datetime(2021, 11, 28, 18, 20)

        self.assertEqual(
            unify_html(Exporter._meta_to_html(node)),
            unify_html('''
                <div id="hyperorg-mtime">
                    <span class="label">Modified:</span>&nbsp;
                    <time>Sun Nov 28 16:20:00 2021</time>
                </div>
                <br />
                <div>
                    <span class="label">foo:</span>&nbsp;
                    <time>Sun Nov 28 18:20:00 2021</time>
                </div>
            '''))

    def test_aliases(self):
        """Alias titles."""

        node = Node([], '', ['Title one', 'A second title'])

        self.assertEqual(
            unify_html(Exporter._meta_to_html(node)),
            unify_html('''
                <div>
                    <span class="label">Alias:</span>&nbsp;Title one
                </div>
                <div>
                    <span class="label">Alias:</span>&nbsp;A second title
                </div>
            ''')
        )


class FileOperations(pyfakefs_ut.TestCase):
    """Test file operations related to exporting into HTML"""

    # pylint: disable=protected-access

    def setUp(self):
        self.setUpPyfakefs(allow_root_user=False)
        Node.nodes = {}
        Node.heading_ids = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

    def test_create_output_dir(self):
        """Create output folder on demand."""
        out_dir = pathlib.Path('output')

        # folder doesn't exists
        self.assertFalse(out_dir.exists())

        exp = Exporter(pathlib.Path('output'))
        exp.export_to_html()  # creates the folder

        # folder exists now
        self.assertTrue(out_dir.exists())

    def test_create_html_node_files(self):
        """Spielwiese."""
        Node.nodes = {
            '1': Node(['eins'], 'eins.org'),
            '2': Node(['zwei'], 'zwei.org'),
        }

        out_dir = pathlib.Path('output')
        exp = Exporter(out_dir)
        exp.export_to_html()

        for fn in ['eins.html', 'zwei.html']:
            self.assertTrue((out_dir / fn).exists())

    def test_create_html_index_files(self):
        """Spielwiese."""
        Node.nodes = {
            '1': Node(['eins'], 'eins.org'),
            '2': Node(['#+filetags: zwei', 'zwei'], 'zwei.org'),
            '3': Node(['#+filetags: zwei', 'drei'], 'drei.org'),
            '4': Node(['#+filetags: blau', 'vier'], 'vier.org'),
        }

        for orgid, node in Node.nodes.items():
            Reader._add_to_index_data(node, orgid)

        out_dir = pathlib.Path('output')
        exp = Exporter(out_dir)
        exp.export_to_html()

        for fn in ['index.html',
                   'index__filetag__no_tags_.html',
                   'index__filetag_zwei.html',
                   'index__filetag_blau.html']:
            self.assertTrue((out_dir / fn).exists(), fn)

    def test_delete_old_files(self):
        """Delete out-folder before creation."""
        out_dir = pathlib.Path('output')

        # create "old" files
        out_dir.mkdir()
        (out_dir / 'foo.bar').touch()
        (out_dir / 'ada.html').touch()
        att_dir = out_dir / 'attachments'
        att_dir.mkdir()
        (att_dir / 'foo.png').touch()
        (att_dir / 'bar.pdf').touch()

        Node.nodes = {
            '1': Node(['eins'], 'eins.org'),
            '2': Node(['zwei'], 'zwei.org'),
        }

        exp = Exporter(out_dir)
        exp.export_to_html()

        fs_elements = [str(i) for i in out_dir.rglob('**/*')]
        fs_elements.append(str(out_dir))

        self.assertCountEqual(
            fs_elements,
            [
                'output',
                'output/pygments.css',
                'output/style.css',
                'output/eins.html',
                'output/zwei.html',
                'output/index.html',
                # '/output/index__no_tags_.html'
            ]
        )

    def test_dont_delete_org_files(self):
        """Don't delete org files."""
        out_dir = pathlib.Path('output')

        # create files in output dir
        out_dir.mkdir()
        (out_dir / 'foo.bar').touch()
        (out_dir / 'bar.org').touch()  # shouldn't be there nor deleted!
        (out_dir / 'ada.html').touch()

        with self.assertRaises(FileExistsError):
            exp = Exporter(out_dir)
            exp.export_to_html()

    def test_dont_delete_Org_files(self):  # pylint: disable=invalid-name
        """Don't delete Org files."""
        out_dir = pathlib.Path('output')

        # create files in output dir
        out_dir.mkdir()
        (out_dir / 'bar.Org').touch()  # shouldn't be there nor deleted!

        with self.assertRaises(FileExistsError):
            exp = Exporter(out_dir)
            exp.export_to_html()

    def test_dont_delete_oRg_files(self):  # pylint: disable=invalid-name
        """Don't delete oRg files."""
        out_dir = pathlib.Path('output')

        # create files in output dir
        out_dir.mkdir()
        (out_dir / 'bar.oRg').touch()  # shouldn't be there nor deleted!

        with self.assertRaises(FileExistsError):
            exp = Exporter(out_dir)
            exp.export_to_html()

    def test_dont_delete_orG_files(self):  # pylint: disable=invalid-name
        """Don't delete orG files."""
        out_dir = pathlib.Path('output')

        # create files in output dir
        out_dir.mkdir()
        (out_dir / 'bar.orG').touch()  # shouldn't be there nor deleted!

        with self.assertRaises(FileExistsError):
            exp = Exporter(out_dir)
            exp.export_to_html()

    def test_dont_delete_ORG_files(self):  # pylint: disable=invalid-name
        """Don't delete ORG files."""
        out_dir = pathlib.Path('output')

        # create files in output dir
        out_dir.mkdir()
        (out_dir / 'bar.ORG').touch()  # shouldn't be there nor deleted!

        with self.assertRaises(FileExistsError):
            exp = Exporter(out_dir)
            exp.export_to_html()


class Attachments(pyfakefs_ut.TestCase):
    """Test handling of attachments"""

    # pylint: disable=protected-access,missing-function-docstring

    def setUp(self):
        self.setUpPyfakefs(allow_root_user=False)
        Node.nodes = {}
        Node.heading_ids = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

    def test_symlink_in_subfolder(self):
        indir = pathlib.Path.home() / 'org'
        outdir = pathlib.Path.home() / 'html'
        target = pathlib.Path('attachments/images/fancy.png')
        target_file = indir / target

        indir.mkdir(parents=True)
        outdir.mkdir(parents=True)
        target_file.parent.mkdir(parents=True)
        target_file.touch()

        self.assertTrue(indir.exists())
        self.assertTrue(outdir.exists())
        self.assertTrue(target_file.exists())

        exporter._establish_file_link(
            target=target,
            target_is_in=indir,
            link_is_in=outdir,
            use_hardlinks=False
        )

        link_file = outdir / target

        self.assertTrue(link_file.exists())
        self.assertTrue(link_file.is_symlink())

    def test_symlink_in_root_folder(self):
        indir = pathlib.Path.home() / 'org'
        outdir = pathlib.Path.home() / 'html'
        target = pathlib.Path('logo.png')
        target_file = indir / target

        indir.mkdir(parents=True)
        outdir.mkdir(parents=True)
        target_file.parent.mkdir(parents=True, exist_ok=True)
        target_file.touch()

        self.assertTrue(indir.exists())
        self.assertTrue(outdir.exists())
        self.assertTrue(target_file.exists())

        exporter._establish_file_link(
            target=target,
            target_is_in=indir,
            link_is_in=outdir,
            use_hardlinks=False
        )

        link_file = outdir / target

        self.assertTrue(link_file.is_symlink())

    def test_paragraph_with_image_and_link(self):
        indir = pathlib.Path.home() / 'org'
        outdir = pathlib.Path.home() / 'html'
        pic = pathlib.Path('attachments/images/pic.png')
        pdf = pathlib.Path('attachments/documents/bug.pdf')
        target_pic = indir / pic
        target_pdf = indir / pdf

        indir.mkdir(parents=True)
        outdir.mkdir(parents=True)
        target_pic.parent.mkdir(parents=True)
        target_pic.touch()
        target_pdf.parent.mkdir(parents=True)
        target_pdf.touch()

        Node.input_dir = indir
        Exporter.output_dir = outdir
        Exporter.use_hardlinks = False

        sut = elements.Paragraph([
            'lore ipsum [[attachments/images/pic.png]] and',
            ' [[attachments/documents/bug.pdf]] link'
        ])

        # pre-test
        self.assertFalse((outdir / pic).exists())
        self.assertFalse((outdir / pdf).exists())

        Exporter._paragraph_to_html(sut)

        self.assertTrue((outdir / pic).exists())
        self.assertTrue((outdir / pdf).exists())

    def test_ignore_absolute_pathes(self):
        """Files existing outside from the input directory are ignored."""
        indir = pathlib.Path.home() / 'org'
        outdir = pathlib.Path.home() / 'html'
        target_file_a = pathlib.Path.home() / 'extern' / 'A_donot.touch'

        # This can't be tested yet because pyfakefs doesn't resolve '~'
        # target_file_B = pathlib.Path('~') / 'extern' / 'B_donot.touch'

        indir.mkdir(parents=True)
        outdir.mkdir(parents=True)
        target_file_a.parent.mkdir(parents=True, exist_ok=True)
        target_file_a.touch()
        # target_file_B.resolve().touch()

        self.assertTrue(indir.exists())
        self.assertTrue(outdir.exists())
        self.assertTrue(target_file_a.exists())

        self.assertEqual(len(list(outdir.iterdir())), 0)

        for fp in (target_file_a, ):
            exporter._establish_file_link(
                target=fp,
                target_is_in=indir,
                link_is_in=outdir,
                use_hardlinks=False
            )

        self.assertEqual(len(list(outdir.iterdir())), 0)

    def test_remove_parent_folder_if_target_not_exists(self):
        indir = pathlib.Path.home() / 'org'
        outdir = pathlib.Path.home() / 'html'
        target_file = pathlib.Path('attachment') / 'image' / 'not.exist'

        indir.mkdir(parents=True)
        outdir.mkdir(parents=True)
        target_file.parent.mkdir(parents=True, exist_ok=True)

        self.assertTrue(indir.exists())
        self.assertTrue(outdir.exists())

        self.assertFalse(target_file.exists())

        exporter._establish_file_link(
            target=target_file,
            target_is_in=indir,
            link_is_in=outdir,
            use_hardlinks=True
        )

        self.assertFalse((outdir / target_file.parent).exists())
        self.assertFalse((outdir / target_file.parent.parent).exists())


class FigureImages(unittest.TestCase):
    """Test exporting Images and Figures into HTML"""

    # pylint: disable=protected-access,missing-function-docstring
    @classmethod
    def setUpClass(cls):
        def _mock_me(*args, **kwargs):  # pylint: disable=unused-argument
            pass
        cls._establish_file_link = exporter._establish_file_link
        exporter._establish_file_link = _mock_me

    @classmethod
    def tearDownClass(cls):
        exporter._establish_file_link = cls._establish_file_link

    def setUp(self):
        Node.nodes = {}
        Node.input_dir = '/'

    def test_link_to_image_file_not_figure(self):
        """The image file's filename is used as a link label not the image
        itself, because "file:" isn't used as a prefix.
        """

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node(['[[link.png][label.jpeg]]'], '')

        expect = ('<p><a target="_blank" rel="noopener noreferrer" '
                  'href="link.png">label.jpeg</a></p>')

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_linked(self):

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node(['[[link.png][file:label.jpeg]]'], '')

        expect = '\n'.join([
            '<figure>',
            '    <a target="_blank" rel="noopener noreferrer" '
            'href="link.png">',
            '        <img src="label.jpeg" alt="label.jpeg" />',
            '    </a>',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_without_caption(self):

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node(['[[image.png]]'], '')

        expect = '\n'.join([
            '<figure>',
            '    <img src="image.png" alt="image.png" />',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_with_caption(self):

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node([
            'foobar',
            '#+CAPTION: foobar',
            '[[image.png]]'
        ], '')

        expect = '\n'.join([
            '<p>foobar</p>',
            '<figure>',
            '    <img src="image.png" alt="image.png" />',
            '    <figcaption>foobar</figcaption>',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_with_attributes(self):

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node([
            'foobar',
            '#+ATTR_HTML: :width 300 :bar foo',
            '[[image.png]]'
        ], '')

        expect = '\n'.join([
            '<p>foobar</p>',
            '<figure title="{\'bar\': \'foo\'}">',
            '    <img src="image.png" '
            'alt="image.png" width="300" />',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_linked_image_with_attributes_and_downloaded(self):
        """Figure with linked image.

        The attributes are lost when it is a linked image inside the figure.
        """

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node([
            'foobar',
            '#+ATTR_HTML: :width 300 :bar foo',
            '#+DOWNLOADED: https://foo.bar',
            '[[file:link.png][file:image.png]]'
        ], '')

        expect = '\n'.join([
            '<p>foobar</p>',
            '<figure title="#+DOWNLOADED: '
            'https://foo.bar {\'bar\': \'foo\'}">',
            '    <a target="_blank" rel="noopener noreferrer" '
            'href="link.png"><img src="image.png" '
            'alt="image.png" width="300" /></a>',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_with_caption_and_multiple_attributes(self):

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node([
            'foobar',
            '#+ATTR_HTML: :width 123',
            '#+caption: FooBar',
            '#+ATTR_HTML: :style border:4px solid pink',
            '#+ATTR_HTML: :alt another alt',
            '[[image.png]]'
        ], '')

        expect = '\n'.join([
            '<p>foobar</p>',
            '<figure>',
            '    <img src="image.png" alt="another alt" width="123" '
            'style="border:4px solid pink" />',
            '    <figcaption>FooBar</figcaption>',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_unknown_attributes_to_tooltip(self):

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node([
            'foobar',
            '#+UnkNown: pink',
            '#+fooBAR: elephant',
            '[[image.png]]'
        ], '')

        expect = '\n'.join([
            '<p>foobar</p>',
            '<figure title="#+UNKNOWN: pink\n#+FOOBAR: elephant">',
            '    <img src="image.png" alt="image.png" />',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))

    def test_with_title_attribute(self):

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        sut = Node([
            'foobar',
            ('#+ATTR_HTML: :title NASA/Apollo 17 crew; '
                'taken by either Harrison Schmitt or Ron Evans'),
            '[[image.png]]'
        ], '')

        expect = '\n'.join([
            '<p>foobar</p>',
            '<figure>',
            '    <img src="image.png" alt="image.png" ',
            'title="NASA/Apollo 17 crew; taken by '
            'either Harrison Schmitt or Ron Evans" />',
            '</figure>'
        ])

        result = exp._as_html(sut, [])
        result = extract_html_hyperorg_content(result)

        self.assertEqual(unify_html(result), unify_html(expect))


class Headings(unittest.TestCase):
    """Test exporting Heading elements into HTML"""

    # pylint: disable=protected-access

    def setUp(self):
        Node.nodes = {}
        Node.heading_ids = {}

    @mock.patch('uuid.uuid4')
    def test_with_anchor(self, mock_uuid):
        """Heading with anchor."""
        mock_uuid.return_value = '123456'

        node = Node(['* eins', '* zwei'], '')
        node[1].generate_anchor()

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        generated_html = exp._as_html(node, [])
        generated_html = extract_html_hyperorg_content(generated_html)
        generated_html = generated_html.strip()

        self.assertEqual(
            generated_html,
            '<h2>eins</h2>\n<h2 id="123456">zwei</h2>'
        )

    def test_two_headings(self):
        """Two headings."""
        test_data = [
            (['* eins', '* zwei'],
             '<h2>eins</h2>\n<h2>zwei</h2>'),
               ]

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        for content, target_html in test_data:
            node = Node(content, '')
            html = exp._as_html(node, [])
            html = extract_html_hyperorg_content(html)
            html = html.strip()

            self.assertEqual(html, target_html)

    def test_title_for_page_and_h1(self):
        """Title as h1-tag and page title."""

        with mock.patch('pathlib.Path.mkdir'):
            exp = Exporter(pathlib.Path())

        # first (one and only) node
        node = Node(['Lorem ipsum'], 'filename')

        with mock.patch('hyperorg.exporter.Exporter._get_footer') as m_footer:
            m_footer.return_value = ''
            self.maxDiff = None
            html = unify_html(exp._as_html(node, []))

            self.assertTrue('<title>[no title]</title>' in html)
            self.assertTrue('<h1>[no title]</h1>' in html)


class NodeWithMathToHtml(unittest.TestCase):
    """A node with several display and inline math
    converted to HTML.

    Dev note (buhtz, 2025-12): In this test suite the goal is not to parse org
    content. The Node is constructed and than converted into HTML.
    """
    # pylint: disable=protected-access

    def test_display_simple(self):
        """One display math"""
        sut = Node([], '')
        sut._elements.append(elements.DisplayMath('a=b'))

        self.assertEqual(
            Exporter._body_and_meta(sut),
            r'<div class="math">\[a=b\]</div>'
        )

    def test_display_multiple(self):
        """Multiple display math"""
        sut = Node([], '')
        sut._elements.append(elements.DisplayMath('a=b'))
        sut._elements.append(elements.DisplayMath('c=d'))
        sut._elements.append(elements.DisplayMath('e=f'))

        self.assertEqual(
            Exporter._body_and_meta(sut),
            '<div class="math">\\[a=b\\]</div>\n'
            '<div class="math">\\[c=d\\]</div>\n'
            '<div class="math">\\[e=f\\]</div>'
        )

    def test_inline_simple(self):
        """Single inline math"""
        sut = Node([], '')

        formula = textruns.InlineMath('a=b')
        par = elements.Paragraph()
        par.runs.append(formula)
        sut._elements.append(par)

        self.assertEqual(
            Exporter._body_and_meta(sut),
            r'<p><span class="math">\(a=b\)</span></p>'
        )

    def test_inline_multi(self):
        """Multiple inline math in one paragraph"""
        sut = Node([], '')

        formula_one = textruns.InlineMath('a=b')
        formula_two = textruns.InlineMath('c=d')
        formula_three = textruns.InlineMath('e=f')
        par = elements.Paragraph()
        par.runs.append(formula_one)
        par.runs.append(formula_two)
        par.runs.append(formula_three)

        sut._elements.append(par)

        self.assertEqual(
            Exporter._body_and_meta(sut),
            '<p>'
            '<span class="math">\\(a=b\\)</span>'
            '<span class="math">\\(c=d\\)</span>'
            '<span class="math">\\(e=f\\)</span>'
            '</p>'
        )

    def test_inline_multi_with_text(self):
        """Combined with text"""
        sut = Node([], '')

        formula_one = textruns.InlineMath('a=b')
        formula_two = textruns.InlineMath('c=d')
        formula_three = textruns.InlineMath('e=f')
        par = elements.Paragraph()
        par.runs.append('foo')
        par.runs.append(formula_one)
        par.runs.append('bar')
        par.runs.append(formula_two)
        par.runs.append('one more')
        par.runs.append(formula_three)
        par.runs.append('end')

        sut._elements.append(par)

        self.assertEqual(
            Exporter._body_and_meta(sut),
            '<p>'
            'foo'
            '<span class="math">\\(a=b\\)</span>'
            'bar'
            '<span class="math">\\(c=d\\)</span>'
            'one more'
            '<span class="math">\\(e=f\\)</span>'
            'end'
            '</p>'
        )
