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


def _reset_node_class():
    content.Node.nodes = {}
    content.Node.heading_ids = {}


class TestNode(unittest.TestCase):
    """Test related to Node class"""
    # pylint: disable=protected-access
    def setUp(self):
        _reset_node_class()

    def test_class_attributes(self):
        """Default class attributes"""
        self.assertEqual(content.Node.nodes, {})
        self.assertEqual(content.Node.heading_ids, {})

    def test_empty(self):
        """empty by default"""
        n = content.Node([], '')
        self.assertEqual(len(n), 0)

    def test_string(self):
        """Strings allowed"""
        randstring = helper.rand_string()
        n = content.Node([randstring], '')
        self.assertEqual(len(n), 1)

    def test_number(self):
        """Numbers not allowed"""
        with self.assertRaises(TypeError):
            content.Node(1)  # pylint: disable=no-value-for-parameter

    def test_inherit_meta_fields(self):
        """Meta fields are inherited by child nodes/subheadings."""

        a = content.Node([
            '#+filetags: One "Two zwei" Three',
            '#+title: Titel',
            '#+date: [1982-08-06 18:23]',
            'foobar'],
            '',
            ['aliasA', 'B C'])

        b = content.Node([], '', ['alias Y'])

        # parent node
        self.assertEqual(
            a._meta,
            {
                'filetags': ['One', 'Two zwei', 'Three'],
                'aliases_org': ['aliasA', 'B C'],
                'aliases_runs': [['aliasA'], ['B C']],
                'date': datetime.datetime(1982, 8, 6, 18, 23),
            })

        # child node (before inheritance)
        self.assertEqual(
            b._meta,
            {
                'aliases_org': ['alias Y'],
                'aliases_runs': [['alias Y']]
            }
        )

        # Action!
        b.inherit_meta_fields_from(a)

        # child node (After inheritance)
        self.assertEqual(
            b._meta,
            {
                'aliases_org': ['alias Y'],
                'aliases_runs': [['alias Y']],
                'filetags': ['One', 'Two zwei', 'Three'],
                'date': datetime.datetime(1982, 8, 6, 18, 23),
            }
        )

    def test_org_properties(self):
        """ Buffer wide org properties."""

        n = content.Node(['#+title: Foobar', '#+foo: Bar'], '')

        self.assertEqual(len(n), 0)
        self.assertEqual(n._meta, {'foo': 'Bar'})
        self.assertEqual(n.title_runs, ['Foobar'])
        self.assertEqual(n.title_org, 'Foobar')


class Linking(unittest.TestCase):
    """Tests related to links and linking nodes and subheadings"""
    def setUp(self):
        _reset_node_class()

    def test_add_backlink(self):
        """Implicit creation of Backlink elements"""

        # create two nodes
        content.Node.nodes['A'] = content.Node(['#+title: A-title'], '')
        content.Node.nodes['X'] = content.Node(['#+title: X-title'], '')

        # they are empty
        self.assertEqual(len(content.Node.nodes['X']), 0)
        self.assertEqual(len(content.Node.nodes['A']), 0)

        # assuming A links to X
        content.Node.nodes['X'].add_backlinks({'A': []})

        # then X should contain a backlink element to A
        self.assertEqual(len(content.Node.nodes['X']), 1)

        # but not A
        self.assertEqual(len(content.Node.nodes['A']), 0)

        # There is a Backlink element
        be = content.Node.nodes['X'][0]
        self.assertIsInstance(be, elements.Backlinks)

        # its one and only item has one Link run
        link = be[0][0]
        self.assertEqual(link.target, 'A')
        self.assertEqual(link.kind, textruns.Link.Kind.ORGID)
        self.assertEqual(link[0], 'A-title')

    def test_add_two_backlinks(self):
        """Backlink element with two links"""

        # create two nodes
        content.Node.nodes['A'] = content.Node(['#+title: A-title'], '')
        content.Node.nodes['B'] = content.Node(['#+title: B-title'], '')
        content.Node.nodes['X'] = content.Node(['#+title: X-title'], '')

        # they are empty
        self.assertEqual(len(content.Node.nodes['X']), 0)
        self.assertEqual(len(content.Node.nodes['A']), 0)
        self.assertEqual(len(content.Node.nodes['B']), 0)

        # assuming A links to X
        content.Node.nodes['X'].add_backlinks({'A': [], 'B': []})

        # There are two Backlink element
        be = content.Node.nodes['X'][0]
        self.assertIsInstance(be, elements.Backlinks)

        # its one and only item has two Link runs
        self.assertEqual(len(be), 2)

    def test_get_orgid_links(self):
        """Orgids contained in one Node"""
        lines = [
            '#+title: Titel',
            'Paragraph without link',
            '',
            'Link [[id:123][with label]]',
            '',
            'Link without [[id:456]]',
        ]
        sut = content.Node(lines, '')

        result = sut.get_orgid_links()

        self.assertEqual(result, ['123', '456'])

    def test_get_orgid_links_but_without_subheadings(self):
        """A node knows its orgids but not the orgids of its
        child's (subheadings).
        """
        lines = [
            '#+title: Titel',
            'Paragraph without link',
            '',
            'Link [[id:123][with label]]',
        ]
        n = content.Node(lines, '')

        lines = [
            '* Subheading',
            ':PROPERTIES:',
            ':ID:b0572191-4c8d-4834-a240-5853b3d6a2e5',
            ':END:',
            'This [[id:456][link]]',
        ]
        subn = content.Node(lines, '')
        n.add_subheading_content(subn)

        result = n.get_orgid_links()

        self.assertEqual(result, ['123'])


class ParsingMeta(unittest.TestCase):
    """Parsing meta fields."""

    # pylint: disable=protected-access

    def test_title(self):
        """Title set via parse lines."""

        n = content.Node([
            '#+title: Foobar', '#+foo: Bar', 'one'], '')

        self.assertEqual(n.title_runs, ['Foobar'])
        self.assertEqual(n.title_org, 'Foobar')
        self.assertEqual(n._meta, {'foo': 'Bar'})
        self.assertEqual(len(n), 1)
        self.assertEqual(n[0][0], 'one')

    def test_title_with_markup(self):
        """Title with markup tokens"""

        n = content.Node(['#+title: Foo =bar='], '')

        self.assertEqual(n.title_org, 'Foo =bar=')
        self.assertEqual(
            n.title_runs,
            [
                'Foo ',
                textruns.Markup('bar', textruns.Markup.Kind.VERBATIM)
            ]
        )

    def test_alias_titles(self):
        """Alias titles"""
        n = content.Node([], 'filename', ['alias a', 'boo'])

        self.assertEqual(n._meta['aliases_org'], ['alias a', 'boo'])
        self.assertEqual(n._meta['aliases_runs'], [['alias a'], ['boo']])

    def test_no_alias_titles(self):
        """No alias titles"""
        n = content.Node([], 'filename')

        self.assertNotIn('aliases', n._meta)

    def test_meta_at_file_beginning(self):
        """Parse meta (property) lines at beginning of a file."""

        c = content.Node([], '')
        rest = c._parse_meta_lines([
            '#+title: Foobar',
            '#+date: 3022-08-06 18:23',
            '#+foo: bar',
            'one',
            'two',
            'three'])

        self.assertEqual(rest, ['one', 'two', 'three'])
        self.assertEqual(c.title_org, '[no title]')
        self.assertEqual(
            c._meta,
            {
                'title': 'Foobar',
                'date': datetime.datetime(3022, 8, 6, 18, 23),
                'foo': 'bar'
            }
        )

    def test_ignore_meta_in_file_body(self):
        """Possible meta (property) lines in the body of a file.

        The key point here is that "meta" lines in the file body are not
        parsed as meta data.
        """

        c = content.Node([], '')
        rest = c._parse_meta_lines([
            '#+title: Foobar',
            '#+date: 3022-08-06 18:23',
            '#+foo: bar',
            'one',
            'two',
            '#+DOWNLOADED: url',
            'three'])

        self.assertEqual(rest, ['one', 'two', '#+DOWNLOADED: url', 'three'])
        self.assertEqual(c.title_org, '[no title]')
        self.assertEqual(
            c._meta,
            {
                'title': 'Foobar',
                'date': datetime.datetime(3022, 8, 6, 18, 23),
                'foo': 'bar'
            }
        )

    def test_ignore_meta_missing_hash(self):
        """The '#' is missing."""

        c = content.Node([], '')
        rest = c._parse_meta_lines(['+foo: bar', 'three'])
        self.assertEqual(rest, ['+foo: bar', 'three'])
        self.assertEqual(c._meta, {})

    def test_ignore_meta_missing_blank(self):
        """Missing blank after ':'."""

        c = content.Node([], '')
        rest = c._parse_meta_lines(['#+foo:bar', 'three'])
        self.assertEqual(rest, ['#+foo:bar', 'three'])
        self.assertEqual(c._meta, {})

    def test_date_unparsable_with_short_weekdayname(self):
        """Unparsable dates because of the short weekday name."""

        # string only because unparsable
        c = content.Node([], '')

        # return value is an empty list because all parsed lines are removed
        self.assertEqual(
            c._parse_meta_lines(['#+date: [2021-11-28 So 16:20]']),
            [])
        # here we check what was parsed
        self.assertEqual(c._meta, {'date': '[2021-11-28 So 16:20]'})

        # string only because unparsable
        c = content.Node([], '')
        c._parse_meta_lines(['#+date: 2021-11-28 So 16:20'])
        self.assertEqual(c._meta, {'date': '2021-11-28 So 16:20'})

    def test_date_without_brackets(self):
        """Date without brackets."""

        c = content.Node([], '')
        c._parse_meta_lines(['#+date: 2021-11-28 16:20'])

        self.assertEqual(
            c._meta,
            {'date': datetime.datetime(2021, 11, 28, 16, 20)})

    def test_date_active(self):
        """Active dates with <>."""
        c = content.Node([], '')
        c._parse_meta_lines(['#+date: <2021-11-28 16:20>'])

        self.assertEqual(
            c._meta,
            {'date': datetime.datetime(2021, 11, 28, 16, 20)})

    def test_date_inactive(self):
        """Inactive dates with []."""
        c = content.Node([], '')
        c._parse_meta_lines(['#+date: [2021-11-28 16:20]'])

        self.assertEqual(
            c._meta,
            {'date': datetime.datetime(2021, 11, 28, 16, 20)})

    def test_filetags_complex(self):
        """Multiple filetags with multiple words"""
        c = content.Node([], '')

        self.assertEqual(
            c._parse_meta_lines(['#+filetags: One "Two zwei" Three']),
            [])

        self.assertEqual(
            c._meta,
            {'filetags': ['One', 'Two zwei', 'Three']})

    def test_filetags_simple(self):
        """One word filetag"""
        c = content.Node([], '')

        self.assertEqual(
            c._parse_meta_lines(['#+filetags: One']),
            [])

        self.assertEqual(
            c._meta,
            {'filetags': ['One']})
