# Copyright (C) 2011, 2016 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import re
import sys
import textwrap

from io import StringIO

from .. import (
    commands,
    export_pot,
    option,
    registry,
    tests,
    )


class TestEscape(tests.TestCase):

    def test_simple_escape(self):
        self.assertEqual(
            export_pot._escape('foobar'),
            'foobar')

        s = '''foo\nbar\r\tbaz\\"spam"'''
        e = '''foo\\nbar\\r\\tbaz\\\\\\"spam\\"'''
        self.assertEqual(export_pot._escape(s), e)

    def test_complex_escape(self):
        s = '''\\r \\\n'''
        e = '''\\\\r \\\\\\n'''
        self.assertEqual(export_pot._escape(s), e)


class TestNormalize(tests.TestCase):

    def test_single_line(self):
        s = 'foobar'
        e = '"foobar"'
        self.assertEqual(export_pot._normalize(s), e)

        s = 'foo"bar'
        e = '"foo\\"bar"'
        self.assertEqual(export_pot._normalize(s), e)

    def test_multi_lines(self):
        s = 'foo\nbar\n'
        e = '""\n"foo\\n"\n"bar\\n"'
        self.assertEqual(export_pot._normalize(s), e)

        s = '\nfoo\nbar\n'
        e = ('""\n'
             '"\\n"\n'
             '"foo\\n"\n'
             '"bar\\n"')
        self.assertEqual(export_pot._normalize(s), e)


class TestParseSource(tests.TestCase):
    """Check mappings to line numbers generated from python source"""

    def test_classes(self):
        src = '''
class Ancient:
    """Old style class"""

class Modern(object):
    """New style class"""
'''
        cls_lines, _ = export_pot._parse_source(src)
        self.assertEqual(cls_lines,
                         {"Ancient": 2, "Modern": 5})

    def test_classes_nested(self):
        src = '''
class Matroska(object):
    class Smaller(object):
        class Smallest(object):
            pass
'''
        cls_lines, _ = export_pot._parse_source(src)
        self.assertEqual(cls_lines,
                         {"Matroska": 2, "Smaller": 3, "Smallest": 4})

    def test_strings_docstrings(self):
        src = '''\
"""Module"""

def function():
    """Function"""

class Class(object):
    """Class"""

    def method(self):
        """Method"""
'''
        _, str_lines = export_pot._parse_source(src)
        self.assertEqual(str_lines,
                         {"Module": 1, "Function": 4, "Class": 7, "Method": 10})

    def test_strings_literals(self):
        src = '''\
s = "One"
t = (2, "Two")
f = dict(key="Three")
'''
        _, str_lines = export_pot._parse_source(src)
        self.assertEqual(str_lines,
                         {"One": 1, "Two": 2, "Three": 3})

    def test_strings_multiline(self):
        src = '''\
"""Start

End
"""
t = (
    "A"
    "B"
    "C"
    )
'''
        _, str_lines = export_pot._parse_source(src)
        self.assertEqual(str_lines,
                         {"Start\n\nEnd\n": 1, "ABC": 6})

    def test_strings_multiline_escapes(self):
        src = '''\
s = "Escaped\\n"
r = r"Raw\\n"
t = (
    "A\\n\\n"
    "B\\n\\n"
    "C\\n\\n"
    )
'''
        _, str_lines = export_pot._parse_source(src)
        if sys.version_info < (3, 8):
            self.expectFailure("Escaped newlines confuses the multiline handling",
                               self.assertNotEqual, str_lines,
                               {"Escaped\n": 0, "Raw\\n": 2, "A\n\nB\n\nC\n\n": -2})
        else:
            self.assertEqual(
                str_lines, {"Escaped\n": 1, "Raw\\n": 2, "A\n\nB\n\nC\n\n": 4})


class TestModuleContext(tests.TestCase):
    """Checks for source context tracking objects"""

    def check_context(self, context, path, lineno):
        self.assertEqual((context.path, context.lineno), (path, lineno))

    def test___init__(self):
        context = export_pot._ModuleContext("one.py")
        self.check_context(context, "one.py", 1)
        context = export_pot._ModuleContext("two.py", 5)
        self.check_context(context, "two.py", 5)

    def test_from_class(self):
        """New context returned with lineno updated from class"""
        path = "cls.py"

        class A(object):
            pass

        class B(object):
            pass
        cls_lines = {"A": 5, "B": 7}
        context = export_pot._ModuleContext(path, _source_info=(cls_lines, {}))
        contextA = context.from_class(A)
        self.check_context(contextA, path, 5)
        contextB1 = context.from_class(B)
        self.check_context(contextB1, path, 7)
        contextB2 = contextA.from_class(B)
        self.check_context(contextB2, path, 7)
        self.check_context(context, path, 1)
        self.assertEqual("", self.get_log())

    def test_from_class_missing(self):
        """When class has no lineno the old context details are returned"""
        path = "cls_missing.py"

        class A(object):
            pass

        class M(object):
            pass
        context = export_pot._ModuleContext(path, 3, ({"A": 15}, {}))
        contextA = context.from_class(A)
        contextM1 = context.from_class(M)
        self.check_context(contextM1, path, 3)
        contextM2 = contextA.from_class(M)
        self.check_context(contextM2, path, 15)
        self.assertContainsRe(self.get_log(), "Definition of <.*M'> not found")

    def test_from_string(self):
        """New context returned with lineno updated from string"""
        path = "str.py"
        str_lines = {"one": 14, "two": 42}
        context = export_pot._ModuleContext(path, _source_info=({}, str_lines))
        context1 = context.from_string("one")
        self.check_context(context1, path, 14)
        context2A = context.from_string("two")
        self.check_context(context2A, path, 42)
        context2B = context1.from_string("two")
        self.check_context(context2B, path, 42)
        self.check_context(context, path, 1)
        self.assertEqual("", self.get_log())

    def test_from_string_missing(self):
        """When string has no lineno the old context details are returned"""
        path = "str_missing.py"
        context = export_pot._ModuleContext(path, 4, ({}, {"line\n": 21}))
        context1 = context.from_string("line\n")
        context2A = context.from_string("not there")
        self.check_context(context2A, path, 4)
        context2B = context1.from_string("not there")
        self.check_context(context2B, path, 21)
        self.assertContainsRe(self.get_log(), "String b?'not there' not found")


class TestWriteOption(tests.TestCase):
    """Tests for writing texts extracted from options in pot format"""

    def pot_from_option(self, opt, context=None, note="test"):
        sio = StringIO()
        exporter = export_pot._PotExporter(sio)
        if context is None:
            context = export_pot._ModuleContext("nowhere", 0)
        export_pot._write_option(exporter, context, opt, note)
        return sio.getvalue()

    def test_option_without_help(self):
        opt = option.Option("helpless")
        self.assertEqual("", self.pot_from_option(opt))

    def test_option_with_help(self):
        opt = option.Option("helpful", help="Info.")
        self.assertContainsString(self.pot_from_option(opt), "\n"
                                  "# help of 'helpful' test\n"
                                  "msgid \"Info.\"\n")

    def test_option_hidden(self):
        opt = option.Option("hidden", help="Unseen.", hidden=True)
        self.assertEqual("", self.pot_from_option(opt))

    def test_option_context_missing(self):
        context = export_pot._ModuleContext("remote.py", 3)
        opt = option.Option("metaphor", help="Not a literal in the source.")
        self.assertContainsString(self.pot_from_option(opt, context),
                                  "#: remote.py:3\n"
                                  "# help of 'metaphor' test\n")

    def test_option_context_string(self):
        s = "Literally."
        context = export_pot._ModuleContext("local.py", 3, ({}, {s: 17}))
        opt = option.Option("example", help=s)
        self.assertContainsString(self.pot_from_option(opt, context),
                                  "#: local.py:17\n"
                                  "# help of 'example' test\n")

    def test_registry_option_title(self):
        opt = option.RegistryOption.from_kwargs("group", help="Pick one.",
                                                title="Choose!")
        pot = self.pot_from_option(opt)
        self.assertContainsString(pot, "\n"
                                  "# title of 'group' test\n"
                                  "msgid \"Choose!\"\n")
        self.assertContainsString(pot, "\n"
                                  "# help of 'group' test\n"
                                  "msgid \"Pick one.\"\n")

    def test_registry_option_title_context_missing(self):
        context = export_pot._ModuleContext("theory.py", 3)
        opt = option.RegistryOption.from_kwargs("abstract", title="Unfounded!")
        self.assertContainsString(self.pot_from_option(opt, context),
                                  "#: theory.py:3\n"
                                  "# title of 'abstract' test\n")

    def test_registry_option_title_context_string(self):
        s = "Grounded!"
        context = export_pot._ModuleContext("practice.py", 3, ({}, {s: 144}))
        opt = option.RegistryOption.from_kwargs("concrete", title=s)
        self.assertContainsString(self.pot_from_option(opt, context),
                                  "#: practice.py:144\n"
                                  "# title of 'concrete' test\n")

    def test_registry_option_value_switches(self):
        opt = option.RegistryOption.from_kwargs("switch", help="Flip one.",
                                                value_switches=True, enum_switch=False,
                                                red="Big.", green="Small.")
        pot = self.pot_from_option(opt)
        self.assertContainsString(pot, "\n"
                                  "# help of 'switch' test\n"
                                  "msgid \"Flip one.\"\n")
        self.assertContainsString(pot, "\n"
                                  "# help of 'switch=red' test\n"
                                  "msgid \"Big.\"\n")
        self.assertContainsString(pot, "\n"
                                  "# help of 'switch=green' test\n"
                                  "msgid \"Small.\"\n")

    def test_registry_option_value_switches_hidden(self):
        reg = registry.Registry()

        class Hider(object):
            hidden = True
        reg.register("new", 1, "Current.")
        reg.register("old", 0, "Legacy.", info=Hider())
        opt = option.RegistryOption("protocol", "Talking.", reg,
                                    value_switches=True, enum_switch=False)
        pot = self.pot_from_option(opt)
        self.assertContainsString(pot, "\n"
                                  "# help of 'protocol' test\n"
                                  "msgid \"Talking.\"\n")
        self.assertContainsString(pot, "\n"
                                  "# help of 'protocol=new' test\n"
                                  "msgid \"Current.\"\n")
        self.assertNotContainsString(pot, "'protocol=old'")


class TestPotExporter(tests.TestCase):
    """Test for logic specific to the _PotExporter class"""

    # This test duplicates test_duplicates below
    def test_duplicates(self):
        exporter = export_pot._PotExporter(StringIO())
        context = export_pot._ModuleContext("mod.py", 1)
        exporter.poentry_in_context(context, "Common line.")
        context.lineno = 3
        exporter.poentry_in_context(context, "Common line.")
        self.assertEqual(1, exporter.outf.getvalue().count("Common line."))

    def test_duplicates_included(self):
        exporter = export_pot._PotExporter(StringIO(), True)
        context = export_pot._ModuleContext("mod.py", 1)
        exporter.poentry_in_context(context, "Common line.")
        context.lineno = 3
        exporter.poentry_in_context(context, "Common line.")
        self.assertEqual(2, exporter.outf.getvalue().count("Common line."))


class PoEntryTestCase(tests.TestCase):

    def setUp(self):
        super(PoEntryTestCase, self).setUp()
        self.exporter = export_pot._PotExporter(StringIO())

    def check_output(self, expected):
        self.assertEqual(
            self.exporter.outf.getvalue(),
            textwrap.dedent(expected)
            )


class TestPoEntry(PoEntryTestCase):

    def test_simple(self):
        self.exporter.poentry('dummy', 1, "spam")
        self.exporter.poentry('dummy', 2, "ham", 'EGG')
        self.check_output('''\
                #: dummy:1
                msgid "spam"
                msgstr ""

                #: dummy:2
                # EGG
                msgid "ham"
                msgstr ""

                ''')

    def test_duplicate(self):
        self.exporter.poentry('dummy', 1, "spam")
        # This should be ignored.
        self.exporter.poentry('dummy', 2, "spam", 'EGG')

        self.check_output('''\
                #: dummy:1
                msgid "spam"
                msgstr ""\n
                ''')


class TestPoentryPerPergraph(PoEntryTestCase):

    def test_single(self):
        self.exporter.poentry_per_paragraph(
            'dummy',
            10,
            '''foo\nbar\nbaz\n'''
            )
        self.check_output('''\
                #: dummy:10
                msgid ""
                "foo\\n"
                "bar\\n"
                "baz\\n"
                msgstr ""\n
                ''')

    def test_multi(self):
        self.exporter.poentry_per_paragraph(
            'dummy',
            10,
            '''spam\nham\negg\n\nSPAM\nHAM\nEGG\n'''
            )
        self.check_output('''\
                #: dummy:10
                msgid ""
                "spam\\n"
                "ham\\n"
                "egg"
                msgstr ""

                #: dummy:14
                msgid ""
                "SPAM\\n"
                "HAM\\n"
                "EGG\\n"
                msgstr ""\n
                ''')


class TestExportCommandHelp(PoEntryTestCase):

    def test_command_help(self):

        class cmd_Demo(commands.Command):
            __doc__ = """A sample command.

            :Usage:
                bzr demo

            :Examples:
                Example 1::

                    cmd arg1

            Blah Blah Blah
            """

        export_pot._write_command_help(self.exporter, cmd_Demo())
        result = self.exporter.outf.getvalue()
        # We don't care about filename and lineno here.
        result = re.sub(r'(?m)^#: [^\n]+\n', '', result)

        self.assertEqualDiff(
            'msgid "A sample command."\n'
            'msgstr ""\n'
            '\n'                # :Usage: should not be translated.
            'msgid ""\n'
            '":Examples:\\n"\n'
            '"    Example 1::"\n'
            'msgstr ""\n'
            '\n'
            'msgid "        cmd arg1"\n'
            'msgstr ""\n'
            '\n'
            'msgid "Blah Blah Blah"\n'
            'msgstr ""\n'
            '\n',
            result
            )
