# 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 runs"""
import unittest
import logging
from hyperorg import textruns


class TestRun(unittest.TestCase):
    """Runs"""

    # pylint: disable=missing-function-docstring

    class ConcreteRun(textruns.Run):
        """A concrete but simple run just for testing"""
        # pylint: disable=too-few-public-methods

        def __init__(self, count):
            runs = [f'run {i}' for i in range(count)]
            super().__init__(runs=runs)

    def test_abstract(self):
        with self.assertRaises(TypeError):
            textruns.Run()

    def test_len(self):
        sut = __class__.ConcreteRun(7)

        self.assertEqual(len(sut), 7)

    def test_get_item(self):
        sut = __class__.ConcreteRun(7)

        self.assertEqual(sut[0], 'run 0')
        self.assertEqual(sut[5], 'run 5')

    def test_get_item_out_of_index(self):
        sut = __class__.ConcreteRun(7)

        with self.assertRaises(IndexError):
            _ = sut[7]

    def test_equal(self):
        runa = __class__.ConcreteRun(2)
        runb = __class__.ConcreteRun(2)
        self.assertTrue(runa == runb)


class TestMarkup(unittest.TestCase):
    """Markup"""

    # pylint: disable=missing-function-docstring

    def test_init(self):
        sut = textruns.Markup(
            'marked',
            textruns.Markup.Type.IMPORTANT
        )

        self.assertEqual(len(sut), 1)
        self.assertEqual(sut[0], 'marked')
        self.assertEqual(sut.markup_type, textruns.Markup.Type.IMPORTANT)

    def test_nested_markup(self):
        """Markup with markup in it."""

        # This is an italic (emphasized) markup with one bold (important) word
        # in it.
        sut = textruns.Markup(
            'foo *important* bar',
            textruns.Token.EMPHASIZED
        )

        self.assertEqual(len(sut), 3)

        self.assertEqual(sut[0], 'foo ')
        self.assertEqual(sut[1][0], 'important')
        self.assertEqual(sut[1].markup_type, textruns.Markup.Type.IMPORTANT)
        self.assertEqual(sut[2], ' bar')

    def test_link_inside(self):
        """Link inside the markup"""
        sut = textruns.Markup(
            'foo [[id:123][geordie]] bar',
            textruns.Token.UNDERLINED
        )
        self.assertEqual(len(sut), 3)

        self.assertEqual(sut[0], 'foo ')
        self.assertEqual(sut[1][0], 'geordie')
        self.assertEqual(sut[1].link_type, textruns.Link.Type.ORGID)
        self.assertEqual(sut[1].target, '123')
        self.assertEqual(sut[2], ' bar')

    def test_equal(self):
        sut_a = textruns.Markup('foobar', textruns.Markup.Type.IMPORTANT)
        sut_b = textruns.Markup('foobar', textruns.Markup.Type.IMPORTANT)
        sut_c = textruns.Markup('foobar', textruns.Markup.Type.UNDERLINED)

        self.assertTrue(sut_a == sut_b)
        self.assertFalse(sut_a == sut_c)


class TestLink(unittest.TestCase):
    """Test Link class."""

    def test_from_label(self):
        """Construct from label.

        Be aware that the opening ``[[`` and closing ``]]`` are removed before
        the Link run is initialized. That re-movement happens on the layer of
        the `element` module.
        """
        link = textruns.Link.from_org_string('id:target][label')

        self.assertEqual(link[0], 'label')
        self.assertEqual(link.target, 'target')
        self.assertEqual(link.link_type, textruns.Link.Type.ORGID)

    def test_init_target_only(self):
        """No label"""
        link = textruns.Link.from_org_string('id:target')

        self.assertEqual(len(link), 0)
        self.assertEqual(link.target, 'target')
        self.assertEqual(link.link_type, textruns.Link.Type.ORGID)

    def test_init_all_types(self):
        """Sub testing all available link types"""
        test_data = {
            link_type: f'{link_type.value}:foo'
            for link_type in textruns.Link.Type
        }

        test_data[textruns.Link.Type.NONE] \
            = test_data[textruns.Link.Type.NONE].replace(':', '')
        test_data[textruns.Link.Type.HTTPS] \
            = test_data[textruns.Link.Type.HTTPS].replace(':', '://')

        for link_type, link in test_data.items():
            sut = textruns.Link.from_org_string(link)
            self.assertEqual(sut.link_type, link_type)

    def test_case_insensitive_types(self):
        """Upper or lower case doesn't matter when recognizing link type"""
        test_data = {
            textruns.Link.Type.ORGID: (
                'ID:', 'id:', 'Id:', 'iD:'),
            textruns.Link.Type.HTTPS: (
                'https://', 'HTTPS://', 'Https://', 'HTtPs://', 'httPs://'),
            textruns.Link.Type.FILE: (
                'fIle:', 'fiLE:', 'FILE:', 'file:')
        }

        # pylint: disable-next=consider-using-dict-items
        test_data = [(k, v) for k in test_data for v in test_data[k]]

        for link_type, link_prefix in test_data:
            with self.subTest((link_type, link_prefix)):

                sut = textruns.Link.from_org_string(f'{link_prefix}foobar')

                self.assertEqual(sut.link_type, link_type)

    def test_no_type_links(self):
        """Links without types are OK. """
        link = textruns.Link.from_org_string('foo')

        self.assertEqual(link.link_type, textruns.Link.Type.NONE)

    def test_equal(self):
        """Equal operator"""
        sut_a = textruns.Link.from_org_string('foo][bar')
        sut_b = textruns.Link.from_org_string('foo][bar')
        sut_c = textruns.Link.from_org_string('id:foo][bar')
        sut_d = textruns.Link.from_org_string('fxx][bar')

        self.assertTrue(sut_a == sut_b)
        self.assertFalse(sut_a == sut_c)
        self.assertFalse(sut_a == sut_d)

    def test_type_target_labels(self):
        """Recognition of type, targets and labels at once"""
        test_data = (
            ('id:foo][bar', ('id', 'foo', 'bar')),
            ('https:foo][bar', ('https', 'foo', 'bar')),
            ('foo][bar', (None, 'foo', 'bar')),
            ('id:foo', ('id', 'foo', None)),
            ('id:foo][bar:extra', ('id', 'foo', 'bar:extra')),
            ('foo][bar:extra', (None, 'foo', 'bar:extra')),
        )

        for sut, expect in test_data:
            with self.subTest(sut):
                # pylint: disable=protected-access
                self.assertEqual(
                    textruns.Link._REX_TYPE_TARGET_LABEL.match(sut).groups(),
                    expect
                )

    def test_unknown_type_with_label(self):
        """Link with in an unknown type (protocol) and a label."""
        with self.assertLogs(level=logging.WARNING) as cm_log:
            sut = textruns.Link.from_org_string(
                'unknowntype:rainbowtarget][colorfullabel'
            )

            self.assertEqual(1, len(cm_log.output))
            self.assertRegex(
                cm_log.output[0],
                r'.*Link type.*not supported.*'
            )

        self.assertEqual(sut.link_type, textruns.Link.Type.UNKNOWN)
        # First run is the original type
        self.assertEqual(sut[0], 'unknowntype')
        self.assertEqual(sut[1], 'colorfullabel')
        self.assertEqual(len(sut), 2)
        self.assertEqual(sut.target, 'rainbowtarget')

    def test_unknown_type_without_label(self):
        """Link with in an unknown type (protocol)."""
        with self.assertLogs(level=logging.WARNING) as cm_log:
            sut = textruns.Link.from_org_string('unknowntype:rainbowtarget')

            self.assertEqual(1, len(cm_log.output))
            self.assertRegex(
                cm_log.output[0],
                r'.*Link type.*not supported.*'
            )

        self.assertEqual(sut.link_type, textruns.Link.Type.UNKNOWN)
        # First run is the original type
        self.assertEqual(sut[0], 'unknowntype')
        self.assertEqual(len(sut), 1)
        self.assertEqual(sut.target, 'rainbowtarget')


class TestImage(unittest.TestCase):
    """Image"""

    # pylint: disable=missing-function-docstring

    def test_target(self):
        sut = textruns.Image('img.png')
        self.assertEqual(sut.target, 'img.png')

    def test_alternative(self):
        sut = textruns.Image('img.png')
        self.assertEqual(sut.alternative, 'img.png')

    def test_alternative_https(self):
        sut = textruns.Image('https://foo.bar/img.png')
        self.assertEqual(sut.alternative, 'img.png')

    def test_leading_folder_seperator(self):
        sut = textruns.Image('/img.png')

        self.assertEqual(sut.target, '/img.png')


class TestInlineMath(unittest.TestCase):
    """Test Math class."""

    def test_init_defaults(self):
        """Default init call."""
        sut = textruns.InlineMath('e = mc^{2}')

        self.assertEqual(len(sut), 1)
        self.assertEqual(sut[0], 'e = mc^{2}')


class DontTouchVerbatimOrCode(unittest.TestCase):
    """Test special rule that content enclosed by verbatim or code markups
    (``=`` and ``~``) should not be parsed or cut.
    """

    # pylint: disable=missing-function-docstring

    def test_markup_class(self):
        """Class `textruns.Markup` shouldn't parse verbatim/code."""

        for text in ['[[id:123][geordie]]',
                     '*don\'t parse it*']:

            for markup_type in [textruns.Markup.Type.CODE,
                                textruns.Markup.Type.VERBATIM]:

                markup = textruns.Markup(text, markup_type)

                self.assertEqual(len(markup.runs), 1)
                self.assertEqual(str(markup.runs[0]), text)

    def test_ignore_links(self):
        """Don't treat links when enclosded by verbatim or code markup."""
        for markup_type in [textruns.Markup.Type.CODE,
                            textruns.Markup.Type.VERBATIM]:
            text = 'Foo {m}very [[id:1234][important]] link{m} bar' \
                   .format(m=markup_type.value)

            result = textruns.cut_into_runs(text)

            self.assertEqual(len(result), 3)
            self.assertIsInstance(result[1], textruns.Markup)

            self.assertEqual(len(result[1].runs), 1)
            self.assertEqual(str(result[1].runs[0]),
                             'very [[id:1234][important]] link')

            self.assertEqual(result[1].markup_type, markup_type)

    def test_ignore_markup(self):
        for outer_type in [textruns.Markup.Type.CODE,
                           textruns.Markup.Type.VERBATIM]:
            for markup_type in textruns.Markup.Type:

                if markup_type == outer_type:
                    continue

                text = 'Foo {vc}with {m}in-markup{m} bar{vc} fin.'.format(
                    vc=outer_type.value, m=markup_type.value)

                result = textruns.cut_into_runs(text)

                self.assertEqual(len(result), 3)
                self.assertIsInstance(result[1], textruns.Markup)

                self.assertEqual(len(result[1].runs), 1)
                self.assertEqual(
                    str(result[1].runs[0]),
                    (f'with {markup_type.value}in-markup'
                     f'{markup_type.value} bar'))


class NestedLinkAndMarkup(unittest.TestCase):
    """Test special situation with nested markup/links."""

    # pylint: disable=missing-function-docstring

    def test_markup_in_link(self):
        text = 'Foo [[id:1234][This is *important*]] bar'
        result = textruns.cut_into_runs(text)

        self.assertEqual(len(result), 3)
        self.assertEqual(result[0], 'Foo ')
        self.assertEqual(len(result[1]), 2)
        self.assertEqual(result[1][0], 'This is ')
        self.assertEqual(result[1].link_type, textruns.Link.Type.ORGID)
        self.assertEqual(result[1][1][0], 'important')
        self.assertEqual(result[1][1].markup_type,
                         textruns.Markup.Type.IMPORTANT)
        self.assertEqual(result[2], ' bar')

    def test_link_in_markup(self):
        text = 'Foo *very [[id:1234][important]] link* bar'
        result = textruns.cut_into_runs(text)

        self.assertEqual(len(result), 3)

        self.assertEqual(result[0], 'Foo ')
        self.assertEqual(len(result[1]), 3)
        self.assertEqual(result[1][0], 'very ')
        self.assertEqual(result[1][1][0], 'important')
        self.assertEqual(result[1][1].target, '1234')
        self.assertEqual(result[1][1].link_type, textruns.Link.Type.ORGID)
        self.assertEqual(result[1][2], ' link')
        self.assertEqual(result[1].markup_type, textruns.Markup.Type.IMPORTANT)
        self.assertEqual(result[2], ' bar')


class ImagesAndLinks(unittest.TestCase):
    """Images and Links"""
    def test_inline_image(self):
        """Simple inline image."""
        sut = 'Foo [[img.png]] bar'

        result = textruns.cut_into_runs(sut)

        self.assertEqual(len(result), 3)
        self.assertIsInstance(result[1], textruns.Image)
        image = result[1]
        self.assertEqual(image.target, 'img.png')

    def test_inline_image_file_type(self):
        """Inline image with "file:" prefix"""
        sut = 'Foo [[file:img.png]] bar'

        result = textruns.cut_into_runs(sut)

        self.assertEqual(len(result), 3)
        self.assertEqual(result[1].target, 'img.png')

    def test_inline_linked_image_locale_file(self):
        """Linked inline image with "file:" prefix"""
        sut = 'Foo [[linktarget.png][file:labelimage.png]] bar'

        result = textruns.cut_into_runs(sut)
        self.assertEqual(len(result), 3)

        link = result[1]
        self.assertIsInstance(link, textruns.Link)

        self.assertEqual(link.link_type, textruns.Link.Type.NONE)
        self.assertEqual(link.target, 'linktarget.png')

        # The "label" of the link is an image
        image = link[0]
        self.assertIsInstance(image, textruns.Image)
        self.assertEqual(image.target, 'labelimage.png')

    def test_inline_linked_image_https(self):
        """Linked inline image with "https:" prefix"""
        sut = 'Foo [[linktarget.png][https://labelimage.png]] bar'

        result = textruns.cut_into_runs(sut)
        self.assertEqual(len(result), 3)

        link = result[1]
        self.assertIsInstance(link, textruns.Link)

        self.assertEqual(link.link_type, textruns.Link.Type.NONE)
        self.assertEqual(link.target, 'linktarget.png')

        # The "label" of the link is an image
        image = link[0]
        self.assertIsInstance(image, textruns.Image)
        self.assertEqual(image.target, 'https://labelimage.png')

    def test_not_a_linked_image(self):
        """Just a linked text."""

        # Images as labels are only recoginied with file: or https:// prefix.
        # Otherwise it is just a string.
        sut = 'Foo [[linktarget.png][labelimage.png]] bar'

        result = textruns.cut_into_runs(sut)

        link = result[1]

        label = link[0]
        self.assertEqual(label, 'labelimage.png')
