# 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 reader.py. The Reader class using orgparser package to
read multiple org files."""
# pylint: disable=too-many-lines
import pathlib
import inspect
import logging
from collections import defaultdict
import unittest
from unittest import mock
from pyfakefs import fake_filesystem_unittest as pyfakefs_ut
from hyperorg.reader import Reader
from hyperorg.content import Node
from hyperorg import elements
from hyperorg import textruns


def _reset_node_class():
    Node.nodes = {}
    Node.heading_ids = {}
    Node.index_data = {
        'all': defaultdict(list),
        'filetag': defaultdict(lambda: defaultdict(list))
    }


org_raw_content = [
    # node linking to another node
    ''':PROPERTIES:
    :ID:       d197c651-f6c1-4e00-8d8e-ef13adac16e2
    :END:
    #+title: hyperorg_unittest_1
    #+date: [2022-04-21 Do 16:55]
    Test file for unittest

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

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

    # 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]
    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:       5a2f8510-f57e-468e-9832-8ddecd3335ac
    :ROAM_ALIASES: aliasnode "node with alias"
    :END:
    #+title: killme
    #+date: [2022-04-03 So 22:55]
    * Foo
    bar
    * Bar
    :PROPERTIES:
    :ID:       da6759cb-ba23-4042-9486-abc0c76895ad
    :ROAM_ALIASES: again "ein bar alias"
    :END:
    foo'''
]

backlinks_raw_content = [

    # node that is linked to
    ''':PROPERTIES:
    :ID:       node-A
    :END:
    #+title: Node-A
    Governments of the Industrial World,
    * Heading Sub-A
    :PROPERTIES:
    :ID:       sub-A
    :END:
    you weary giants of flesh and steel,
    I come from Cyberspace, the new home of Mind,''',

    # B
    ''':PROPERTIES:
    :ID:       node-B
    :END:
    #+title: Node-B
    * Foo
    [[id:node-A][NodeA]]''',

    # C
    ''':PROPERTIES:
    :ID:       node-C
    :END:
    #+title: Node-C
    * Heading Sub-C
    :PROPERTIES:
    :ID:       sub-C
    :END:
    [[id:node-A][A]]''',

    # D
    ''':PROPERTIES:
    :ID:       node-D
    :END:
    #+title: Node-D
    [[id:node-A][A]]
    * Heading Sub-D
    :PROPERTIES:
    :ID:       sub-D
    :END:
    [[id:node-A][a]]''',

    # E
    ''':PROPERTIES:
    :ID:       node-E
    :END:
    #+title: Node-E
    [[id:node-A][a]]

    [[id:node-A][a]]

    [[id:node-A][a]]''',

    # F
    ''':PROPERTIES:
    :ID:       node-F
    :END:
    #+title: Node-F
    [[id:sub-A][sA]]''',

    # G
    ''':PROPERTIES:
    :ID:       node-G
    :END:
    #+title: Node-G
    * Heading Sub-Ga
    :PROPERTIES:
    :ID:       sub-Ga
    :END:
    [[id:node-A][a]]
    * Heading Sub-Gb
    :PROPERTIES:
    :ID:       sub-Gb
    :END:
    [[id:node-A][a]]''',
]


index_data_raw_content = [
    # title and subheading title
    ''':PROPERTIES:
    :ID:       node-A
    :END:
    #+title: Node-A
    Governments of the Industrial World,
    * Heading Sub-A
    :PROPERTIES:
    :ID:       sub-A
    :END:
    you weary giants of flesh and steel,
    I come from Cyberspace, the new home of Mind,''',

    # title and alias
    ''':PROPERTIES:
    :ID:       node-B
    :ROAM_ALIASES: Frühstücksflocken
    :END:
    #+title: Cerial
    Foo''',

    # subheading alias
    ''':PROPERTIES:
    :ID:       node-C
    :END:
    #+title: Node-C
    * Heading Sub-C
    :PROPERTIES:
    :ID:       sub-C
    :ROAM_ALIASES: "Dr. Doom"
    :END:
    Jimmy''',

    # Two Filetag
    ''':PROPERTIES:
    :ID:       node-D
    :END:
    #+title: Node-D
    #+filetags: Wiki Person
    Foo''',

    # wiki
    ''':PROPERTIES:
    :ID:       node-E
    :END:
    #+title: Node-E
    #+filetags: Wiki
    foobar''',

    # person
    ''':PROPERTIES:
    :ID:       node-F
    :END:
    #+title: Node-F
    #+filetags: Person
    Ricky''',
]


class General(unittest.TestCase):
    """General tests about the Reader"""

    def setUp(self):
        _reset_node_class()

    def test_input_dir_exists(self):
        """Input dir exists."""
        with mock.patch('pathlib.Path') as mocked_path:
            mocked_path.exists.return_value = True
            with self.assertLogs() as cm_log:
                Reader(mocked_path)
                # one log message only
                self.assertEqual(len(cm_log.output), 1)
                # content of log message
                self.assertRegex(
                    cm_log.output[0],
                    r'^INFO.*reader:Input from.*'
                )
                # exists called one only
                mocked_path.exists.assert_called_once()

    def test_input_dir_not_exists(self):
        """Input dir not exists."""
        with mock.patch('pathlib.Path') as mocked_path:
            mocked_path.exists.return_value = False
            with self.assertLogs() as cm_log:
                # exit()
                with self.assertRaises(SystemExit):
                    Reader(mocked_path)
                # exists() called
                mocked_path.exists.assert_called_once()

                # one log message only
                self.assertEqual(len(cm_log.output), 2)
                # content of log message
                self.assertRegex(
                    cm_log.output[1],
                    r'^ERROR.*reader:Input directory.*does not exists.$'
                )

    def test_input_dir_store(self):
        """Storing input dir parameter."""
        with mock.patch('pathlib.Path.exists', return_value=True):
            with mock.patch('pathlib.Path.is_dir', return_value=True):
                fn = 'foobar'
                Reader(pathlib.Path(fn))
                self.assertEqual(str(Node.input_dir), fn)

    def test_title_index_letter(self):
        """Extract index letter from titles."""
        # pylint: disable=protected-access
        test_data = [
            ('foo', 'F'),
            ('Bar', 'B'),
            ('Ähm', 'A'),
            ('äm', 'A'),
            ('öhm', 'O'),
            ('Öhm', 'O'),
            ('ühm', 'U'),
            ('Ühm', 'U'),
            ('=jup', '='),
            ('ßjup', 'S'),
            ('foo =bar=', 'F'),
            # This is not implemented yet.
            # ('=foo= bar', 'F'),
        ]

        for title, letter in test_data:
            self.assertEqual(
                Reader._title_to_index_letter(title),
                letter,
                (title, letter))

    def test_add_to_index_data(self):
        """Correct index data based on a new node"""

        # pylint: disable=protected-access
        self.maxDiff = None

        # empty index data
        self.assertEqual(
            list(Node.index_data.keys()),
            ['all', 'filetag']
        )

        self.assertEqual(Node.index_data['all'], {})
        self.assertEqual(Node.index_data['filetag'], {})

        # create content
        node = Node(
            body_lines=[
                '#+title: Titel',
                '#+filetags: My Tag',
                'lore ipsum'],
            filename='',
            title_aliases=['Peter', 'Lustig']
        )

        # add it to index data
        Reader._add_to_index_data(node, '1234')

        expect = {
            'all': {
                'L': [(['Lustig'], '1234', True)],
                'P': [(['Peter'], '1234', True)],
                'T': [(['Titel'], '1234', False)]
            },
            'filetag': {
                'My': {
                    'L': [(['Lustig'], '1234', True)],
                    'P': [(['Peter'], '1234', True)],
                    'T': [(['Titel'], '1234', False)]
                },
                'Tag': {
                    'L': [(['Lustig'], '1234', True)],
                    'P': [(['Peter'], '1234', True)],
                    'T': [(['Titel'], '1234', False)]
                }
            }
        }

        self.assertEqual(Node.index_data, expect)


class RealFiles(pyfakefs_ut.TestCase):
    """Test with real files on a fake filesystem"""

    # pylint: disable=protected-access

    def setUp(self):
        """Create a folder with three org files."""

        Node.nodes = {}
        Node.heading_ids = {}
        Node.backlinks = {}

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

    def _create_files(self, fn_idx=None):
        """
        """

        if not fn_idx:
            self._create_files([
                ('test1.org', 0),
                ('test2.org', 1),
                ('killme.org', 2),
                ('alias.org', 3)])
        else:
            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]))

        # create additional file that should be ignored
        with (self.input_dir / 'ignore.me').open('wb') as handle:
            handle.write(b'foobar')

    def test_dotfiles(self):
        """Ignore dotfiles.

        Such files can appear as hidden files from text editors.
        """

        self._create_files()

        # make on into a hidden dotfile
        path = self.input_dir / 'test1.org'
        path.rename(path.parent / f'.{path.name}')

        file_count = len(list(self.input_dir.glob('*.org')))

        # Five files do exist. One is the dotfile.
        self.assertEqual(file_count, 4)

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

        # But only four should have been read.
        self.assertEqual(len(Node.nodes), 3)

    def test_alias_titles(self):
        """Alias titles for node and subheadings.
        """

        self._create_files([('alias.org', 3)])

        # read the files
        reader = Reader(self.input_dir)
        reader.read_org_files()

        nodeid = '5a2f8510-f57e-468e-9832-8ddecd3335ac'
        # subid = 'da6759cb-ba23-4042-9486-abc0c76895ad'
        node = Node.nodes[nodeid]

        self.assertEqual(
            node._meta['aliases_org'],
            ['aliasnode', 'node with alias']
        )
        self.assertEqual(
            node._elements[2]._meta['aliases_org'],
            ['again', 'ein bar alias']
        )

    def test_read_org(self):
        """Read org-files via orgparse."""

        # Prepare the filesystem (only two files)
        self._create_files([('test1.org', 0), ('test2.org', 1)])

        self.assertEqual(
            [p.name for p in self.input_dir.glob('*')],
            ['test1.org', 'test2.org', 'ignore.me']
        )

        # read the files
        reader = Reader(self.input_dir)

        reader.read_org_files()

        # two nodes
        self.assertEqual(len(Node.nodes), 2)

        # check first node's content
        key = 'd197c651-f6c1-4e00-8d8e-ef13adac16e2'
        self.assertEqual(len(Node.nodes[key]._elements), 2)
        self.assertEqual(list(Node.nodes[key]._meta.keys()), ['date'])
        self.assertEqual(Node.nodes[key].title_org, 'hyperorg_unittest_1')
        self.assertEqual(Node.nodes[key][0][0], 'Test file for unittest')

        link_run = Node.nodes[key][1][0]
        self.assertEqual(
            link_run.target, 'b0fc62f1-2926-4eec-a654-eab8fcea46d9')
        self.assertEqual(link_run[0], 'hyperorg_unittest_2')

        # check second node's content
        key = 'b0fc62f1-2926-4eec-a654-eab8fcea46d9'
        node = Node.nodes[key]
        self.assertEqual(len(node), 2)  # incl. backlinks
        self.assertEqual(list(node._meta.keys()), ['date'])
        self.assertEqual(node.title_org, 'hyperorg_unittest_2')
        self.assertEqual(node[0][0], 'Second hyperorg test file')

        # 2nd nodes backlink
        self.assertIsInstance(node._elements[1], elements.Backlinks)
        link_run = node[1][0][0]
        self.assertEqual(
            link_run.target, 'd197c651-f6c1-4e00-8d8e-ef13adac16e2')
        self.assertEqual(link_run[0], 'hyperorg_unittest_1')

    @mock.patch('uuid.uuid4')
    def test_read_org_with_child_node(self, mock_uid):
        """Org-file with own ID for a heading/subtree."""

        mock_uid.return_value = '987654'

        self._create_files([('killme.org', 2)])
        self.assertEqual(
            [p.name for p in self.input_dir.glob('*.org')],
            ['killme.org']
        )

        # read the files
        reader = Reader(self.input_dir)
        reader.read_org_files()

        node_ids = [
            # OrgID of the node
            '4a2f8510-f57e-468e-9832-8ddecd3335ac',
            # OrgID of the subheading
            '95c99140-f044-450d-b631-6a5ff7608b2b']

        # The headings with their own OrgId (also known as subtree)
        # are created as an element of type hyperorg.Content.
        # That instances are not listed in Content.nodes but in
        # Content.heading_ids

        # only one subheading orgid
        self.assertEqual(len(Node.heading_ids), 1)
        self.assertEqual(list(Node.heading_ids)[0], node_ids[1])
        # its value is a tuple of the parents OrgId and its own element
        # index in that parents element list
        self.assertEqual(Node.heading_ids[node_ids[1]], (node_ids[0], 3))
        # the parent node
        self.assertEqual(len(Node.nodes), 1)
        self.assertEqual(list(Node.nodes)[0], node_ids[0])
        self.assertIsInstance(Node.nodes[node_ids[0]], Node)

        # one content/node
        self.assertEqual(len(Node.nodes), 1)
        # ...and its OrgID
        node = Node.nodes[node_ids[0]]

        # one heading/subtree with OrgID
        self.assertEqual(len(Node.heading_ids), 1)
        # ...and its OrgID
        heading_orgid, heading_idx = Node.heading_ids[node_ids[1]]

        # heading points to the real node
        self.assertEqual(heading_orgid, node_ids[0])

        # ...and to the corresponding content element
        self.assertEqual(heading_idx, 3)  # 4nd element
        self.assertIsInstance(node[heading_idx], Node)

        # check each element
        self.assertEqual(len(node), 6)
        self.assertIsInstance(node[0], elements.Paragraph)
        self.assertIsInstance(node[1], elements.Heading)
        self.assertIsInstance(node[2], elements.Paragraph)
        self.assertIsInstance(node[3], Node)
        self.assertIsInstance(node[4], elements.Heading)
        self.assertIsInstance(node[5], elements.Paragraph)

        # check the sub-Content element
        subtree = node[heading_idx]
        self.assertEqual(len(subtree), 2)
        self.assertIsInstance(subtree[0], elements.Heading)
        self.assertIsInstance(subtree[1], elements.Paragraph)

        # anchor for first heading generated?
        self.assertEqual(subtree[0][0], 'Child')
        self.assertEqual(subtree[0].anchor, '987654')

    def test_read_org_sub_heading(self):
        """Org-file with own ID for a heading/subtree."""

        self._create_files([('killme.org', 2)])
        self.assertEqual(
            [p.name for p in self.input_dir.glob('*.org')],
            ['killme.org']
        )

        # read the files
        reader = Reader(self.input_dir)
        reader.read_org_files()

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

        node = list(Node.nodes.values())[0]
        self.assertEqual(node.title_org, 'killme')

        self.assertEqual(node[3].title_org, 'Child')

    def test_node_with_subheading(self):
        """Node with a subheading (result in a child node)"""

        # create file
        fn = 'foo.org'
        with (self.input_dir / fn).open('w', 'utf-8') as handle:
            body_lines = [
                ':PROPERTIES:',
                ':ID:       f7c0d0d1-3d4c-4bcc-96c0-6fa19036f21e',
                ':END:',
                '#+title: Issue93sub',
                '* Heading',
                'Foobar',
                '* Next Heading',
                ':PROPERTIES:',
                ':ID:       c07df761-b328-4cde-8433-9343cee055c6',
                ':END:',
                '- Barfoo'
            ]
            handle.write('\n'.join(body_lines))

        # read the files
        reader = Reader(self.input_dir)
        reader.read_org_files()

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

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

        node = list(Node.nodes.values())[0]
        self.assertEqual(node.title_org, 'Issue93sub')
        self.assertEqual(node[2].title_org, 'Next Heading')

    def test_invalid_unicode_char(self):
        """Handle orgparse exception about invalid unicode characters"""
        # create file
        fn = 'invalid_char.org'
        with (self.input_dir / fn).open('wb') as handle:
            handle.write(b':PROPERTIES:\n')
            handle.write(b':ID:  f7c0d0d1\n')
            handle.write(b':END:\n')
            handle.write(b'nicht-UTF-8-kodierten Zeichen: \xfc')
            handle.write(b' ende')

        # read the files
        with self.assertLogs(level=logging.ERROR) as cm_log:
            reader = Reader(self.input_dir)
            reader.read_org_files()

            # one error
            self.assertEqual(len(cm_log.output), 1)

            self.assertRegex(
                 cm_log.output[0],
                 r'.*invalid start byte.*UnicodeDecodeError.*$'
            )


class Globbing(pyfakefs_ut.TestCase):
    """Test globbing of files"""

    @classmethod
    def setUpClass(cls):
        cls.setUpClassPyfakefs()  # pylint: disable=no-member

        cls.input_dir = pathlib.Path('/') / 'alpha' / 'quadrant'

        folders = [
            'bajor',
            '.gamma'
        ]

        files = [
            'picard.org',
            '.quark.org',
            'grund.img',
            'bajor/kira.oRg',
            'bajor/.opaka.ORG',
            'bajor/kaivin.png',
            '.gamma/wayon.orG',
            '.gamma/far.org'
        ]

        # create folders
        for fn in folders:
            fp = cls.input_dir / fn
            fp.mkdir(parents=True)

        # create files
        for fn in files:
            fp = cls.input_dir / fn
            fp.touch()

    def test_globbing(self):
        """Test finding files"""
        # pylint: disable=protected-access
        reader = Reader(self.input_dir)

        expected = [
            self.input_dir / 'picard.org',
            self.input_dir / 'bajor' / 'kira.oRg'
        ]

        result = reader._glob_from_input_dir()

        self.assertEqual(sorted(expected), sorted(result))


class Backlinks(pyfakefs_ut.TestCase):
    """Test handling of backlinks"""

    def setUp(self):
        """Create a folder with three org files."""

        # reset Content
        Node.nodes = {}
        Node.heading_ids = {}

        # fake filesystem
        self.setUpPyfakefs(allow_root_user=False)
        self.input_dir = pathlib.Path('/test_reader_Backlinks-input_dir')
        self.input_dir.mkdir()

        # create org files
        for idx, base in enumerate('ABCDEFG'):
            fp = self.input_dir / f'node_{base}.org'
            with fp.open('w', 'utf-8') as handle:
                handle.write(inspect.cleandoc(backlinks_raw_content[idx]))

    def _delete_nodes(self, node_orgids):
        """
        """
        for orgid in node_orgids:
            fp = self.input_dir / f'node_{orgid}.org'
            fp.unlink()

    def test_simple(self):
        """Simple node link from A to B."""

        # pylint: disable=protected-access
        # keep A and B
        self._delete_nodes(list('CDEFG'))

        # A and B are read now.
        reader = Reader(self.input_dir)
        reader.read_org_files()

        self.assertEqual(Node.nodes['node-A']._orgids_linking, [])
        self.assertEqual(Node.nodes['node-B']._orgids_linking, ['node-A'])

        # B does link to A.
        # Because of that A need to have a backlink to B.
        backlink_element = Node.nodes['node-A'][2]

        self.assertIsInstance(backlink_element, elements.Backlinks)

        link_run = backlink_element[0][0]
        self.assertEqual(link_run.target, 'node-B')
        self.assertEqual(link_run[0], 'Node-B')
        self.assertEqual(link_run.link_type, textruns.Link.Type.ORGID)

    def test_link_from_subheading(self):
        """Sub-heading links to a node.

        The subheading "sub-C" does link to "A" which is just one link but
        results in two backlinks including the parent of the subheading:
          - Node-C
            - Headinb Sub-C
        """

        # keep A and C
        self._delete_nodes(list('BDEFG'))

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

        ble = Node.nodes['node-A'][2]

        # the parent link
        self.assertEqual(ble[0][0].target, 'node-C')

        # the subheading link in a sub-list
        self.assertEqual(ble[1][0][0].target, 'sub-C')

    def test_link_from_node_and_subheading(self):
        """Node and its Sub-heading linking a node."""

        # keep A and D
        self._delete_nodes(list('BCEFG'))

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

        # Backlinks of Node-A
        ble = Node.nodes['node-A'][-1]

        # Count of 1st level entries: parent + subheading
        self.assertEqual(len(ble), 2)
        # Count of 2nd level entries: the subheading only
        self.assertEqual(len(ble[1]), 1)

        self.assertEqual(ble[0][0].target, 'node-D')
        self.assertEqual(ble[1][0][0].target, 'sub-D')

    def test_three_duplicated_links(self):
        """Three links from one node."""

        # keep A and E
        self._delete_nodes(list('BCDFG'))

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

        ble = Node.nodes['node-A'][2]

        # "node-E" liks three times to "A", but duplicates not allowed
        self.assertEqual(len(ble), 1)
        self.assertEqual(ble[0][0].target, 'node-E')

    def test_two_backlinks(self):
        """Node A with two backlinks.

        ListElement(
            ordered=False,
            _indention=0,
            runs=[
                [
                    Link(runs=['Enno Swart'],
                         kind=<Kind.ORGID: 'id'>,
                         target='8988edc3-725c-4037-91d6-0b693a94c374')
                ],
                [
                    Link(runs=['Zettelkasten'],
                         kind=<Kind.ORGID: 'id'>,
                         target='c107fe6b-cd42-4b52-b764-5dec62809cc8')
                ]
            ]
        )

        """
        # keep A, B, C
        self._delete_nodes(list('DEFG'))

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

        self.assertEqual(len(Node.nodes), 3)

        node = Node.nodes['node-A']
        back = node[2]

        self.assertIsInstance(back, elements.Backlinks)
        self.assertIsInstance(back[0][0], textruns.Link)
        self.assertIsInstance(back[1][0], textruns.Link)
        self.assertIsInstance(back[2], elements.ListElement)
        self.assertIsInstance(back[2][0][0], textruns.Link)

        # 1st link
        self.assertEqual(back[0][0].target, 'node-B')
        self.assertEqual(back[0][0].runs, ['Node-B'])

        # 2nd link
        self.assertEqual(back[1][0].target, 'node-C')
        self.assertEqual(back[1][0].runs, ['Node-C'])

        # 3rd link (to a subheading of node-c)
        self.assertEqual(back[2][0][0].target, 'sub-C')
        self.assertEqual(back[2][0][0].runs, ['Heading Sub-C'])

    def test_link_to_subheading(self):
        """Node links to a subheading."""

        # keep A and F
        self._delete_nodes(list('BCDEG'))

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

        ble = Node.nodes['node-A'][2]

        # "node-F" links to A's subheading "sub-A"
        self.assertEqual(ble[0][0].target, 'node-F')

    def test_link_from_two_subheadings(self):
        """Node links from two subheadings."""

        # keep A and G
        self._delete_nodes(list('BCDEF'))

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

        ble = Node.nodes['node-A'][2]

        # "sub-Ga" and "sub-Gb" linking to "node-A"
        self.assertEqual(ble[0][0].target, 'node-G')
        self.assertEqual(ble[1][0][0].target, 'sub-Ga')
        self.assertEqual(ble[1][1][0].target, 'sub-Gb')

    def test_dead_link(self):
        """Link to unexisting node.

        When a node links to a node that doesn't exists a backlink can not be
        created at the backlinked node because it doesn't exist.
        E.g. A links to X but X was deleted before.
        """


class IndexData(pyfakefs_ut.TestCase):
    """Extracting index data from the nodes.

    Titles, alias titles, filetags, index letters, etc

    TODO
      - test_add_to_index_data() (also for sub-heaings!)
    """

    def setUp(self):
        """Create a folder with three org files."""

        # reset Content
        Node.nodes = {}
        Node.heading_ids = {}
        Node.index_data = {
            Node.IDX_ALL: defaultdict(list),
            Node.IDX_FILETAG: defaultdict(lambda: defaultdict(list))
        }

        # fake filesystem
        self.setUpPyfakefs(allow_root_user=False)
        self.input_dir = pathlib.Path('/test_reader_IndexData-input_dir')
        self.input_dir.mkdir()

        # create org files
        for idx, base in enumerate('ABCDEF'):
            fp = self.input_dir / f'node_{base}.org'
            with fp.open('w', 'utf-8') as handle:
                handle.write(inspect.cleandoc(index_data_raw_content[idx]))

    def _delete_nodes(self, node_orgids):
        """
        """
        for orgid in node_orgids:
            fp = self.input_dir / f'node_{orgid}.org'
            fp.unlink()

    def test_simple(self):
        """One node with sub-heading"""
        self.maxDiff = None

        # keep A
        self._delete_nodes(list('BCDEF'))

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

        expected = {
            Node.IDX_ALL: {
                'H': [
                    (['Heading Sub-A'], 'sub-A', False)
                ],
                'N': [
                    (['Node-A'], 'node-A', False)
                ]
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'H': [
                        (['Heading Sub-A'], 'sub-A', False)
                    ],
                    'N': [
                        (['Node-A'], 'node-A', False)
                    ]
                }
            }
        }

        result = Node.index_data
        self.assertDictEqual(result, expected)

    def test_aliases(self):
        """One node with alias title"""
        self.maxDiff = None
        # keep A
        self._delete_nodes(list('ACDEF'))

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

        expected = {
            Node.IDX_ALL: {
                'C': [
                    (['Cerial'], 'node-B', False)
                ],
                'F': [
                    (['Frühstücksflocken'], 'node-B', True)
                ]
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'C': [
                        (['Cerial'], 'node-B', False)
                    ],
                    'F': [
                        (['Frühstücksflocken'], 'node-B', True)
                    ]
                }
            }
        }

        result = Node.index_data

        self.assertEqual(result, expected)

    def test_complex(self):
        """Multiple nodes"""

        self.maxDiff = None

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

        expected = {
            Node.IDX_ALL: {
                'C': [
                    (['Cerial'], 'node-B', False)
                ],
                'D': [
                    (['Dr. Doom'], 'sub-C', True)
                ],
                'F': [
                    (['Frühstücksflocken'], 'node-B', True)
                ],
                'H': [
                    (['Heading Sub-A'], 'sub-A', False),
                    (['Heading Sub-C'], 'sub-C', False),
                ],
                'N': [
                    (['Node-A'], 'node-A', False),
                    (['Node-C'], 'node-C', False),
                    (['Node-D'], 'node-D', False),
                    (['Node-E'], 'node-E', False),
                    (['Node-F'], 'node-F', False),
                ]
            },
            Node.IDX_FILETAG: {
                Node.IDX_NO_TAG: {
                    'C': [
                        (['Cerial'], 'node-B', False)
                    ],
                    'D': [
                        (['Dr. Doom'], 'sub-C', True)
                    ],
                    'F': [
                        (['Frühstücksflocken'], 'node-B', True)
                    ],
                    'H': [
                        (['Heading Sub-A'], 'sub-A', False),
                        (['Heading Sub-C'], 'sub-C', False),
                    ],
                    'N': [
                        (['Node-A'], 'node-A', False),
                        (['Node-C'], 'node-C', False),
                    ]
                },
                'Wiki': {
                    'N': [
                        (['Node-D'], 'node-D', False),
                        (['Node-E'], 'node-E', False),
                    ]
                },
                'Person': {
                    'N': [
                        (['Node-D'], 'node-D', False),
                        (['Node-F'], 'node-F', False),
                    ]
                },
            }
        }

        result = Node.index_data

        self.assertEqual(result, expected)
