# Copyright (C) 2005, 2006, 2007, 2009, 2010, 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

"""Tests for rio serialization.

A simple, reproducible structured IO format.

rio itself works in Unicode strings.  It is typically encoded to UTF-8,
but this depends on the transport.
"""

import re
from tempfile import TemporaryFile

from .. import rio as _mod_rio
from ..osutils import IterableFile
from ..rio_patch import read_patch_stanza, to_patch_lines
from . import TestCase


def rio_file(stanzas):
    return IterableFile(_mod_rio.rio_iter(stanzas))


class TestRio(TestCase):
    def test_stanza(self):
        """Construct rio stanza in memory."""
        s = _mod_rio.Stanza(number="42", name="fred")
        self.assertIn("number", s)
        self.assertNotIn("color", s)
        self.assertNotIn("42", s)
        self.assertEqual(list(s.iter_pairs()), [("name", "fred"), ("number", "42")])
        self.assertEqual(s.get("number"), "42")
        self.assertEqual(s.get("name"), "fred")

    def test_empty_value(self):
        """Serialize stanza with empty field."""
        s = _mod_rio.Stanza(empty="")
        self.assertEqual(s.to_string(), b"empty: \n")

    def test_to_lines(self):
        """Write simple rio stanza to string."""
        s = _mod_rio.Stanza(number="42", name="fred")
        self.assertEqual(list(s.to_lines()), [b"name: fred\n", b"number: 42\n"])

    def test_as_dict(self):
        """Convert rio Stanza to dictionary."""
        s = _mod_rio.Stanza(number="42", name="fred")
        sd = s.as_dict()
        self.assertEqual(sd, {"number": "42", "name": "fred"})

    def test_to_file(self):
        """Write rio to file."""
        tmpf = TemporaryFile()
        s = _mod_rio.Stanza(
            a_thing='something with "quotes like \\"this\\""', number="42", name="fred"
        )
        s.write(tmpf)
        tmpf.seek(0)
        self.assertEqual(
            tmpf.read(),
            b"""\
a_thing: something with "quotes like \\"this\\""
name: fred
number: 42
""",
        )

    def test_multiline_string(self):
        tmpf = TemporaryFile()
        s = _mod_rio.Stanza(
            motto="war is peace\nfreedom is slavery\nignorance is strength"
        )
        s.write(tmpf)
        tmpf.seek(0)
        self.assertEqual(
            tmpf.read(),
            b"""\
motto: war is peace
\tfreedom is slavery
\tignorance is strength
""",
        )
        tmpf.seek(0)
        s2 = _mod_rio.read_stanza(tmpf)
        self.assertEqual(s, s2)

    def test_read_stanza(self):
        """Load stanza from string."""
        lines = b"""\
revision: mbp@sourcefrog.net-123-abc
timestamp: 1130653962
timezone: 36000
committer: Martin Pool <mbp@test.sourcefrog.net>
""".splitlines(True)
        s = _mod_rio.read_stanza(lines)
        self.assertIn("revision", s)
        self.assertEqual(s.get("revision"), "mbp@sourcefrog.net-123-abc")
        self.assertEqual(
            list(s.iter_pairs()),
            [
                ("revision", "mbp@sourcefrog.net-123-abc"),
                ("timestamp", "1130653962"),
                ("timezone", "36000"),
                ("committer", "Martin Pool <mbp@test.sourcefrog.net>"),
            ],
        )
        self.assertEqual(len(s), 4)

    def test_repeated_field(self):
        """Repeated field in rio."""
        s = _mod_rio.Stanza()
        for k, v in [
            ("a", "10"),
            ("b", "20"),
            ("a", "100"),
            ("b", "200"),
            ("a", "1000"),
            ("b", "2000"),
        ]:
            s.add(k, v)
        s2 = _mod_rio.read_stanza(s.to_lines())
        self.assertEqual(s, s2)
        self.assertEqual(s.get_all("a"), ["10", "100", "1000"])
        self.assertEqual(s.get_all("b"), ["20", "200", "2000"])

    def test_backslash(self):
        s = _mod_rio.Stanza(q="\\")
        t = s.to_string()
        self.assertEqual(t, b"q: \\\n")
        s2 = _mod_rio.read_stanza(s.to_lines())
        self.assertEqual(s, s2)

    def test_blank_line(self):
        s = _mod_rio.Stanza(none="", one="\n", two="\n\n")
        self.assertEqual(
            s.to_string(),
            b"""\
none:\x20
one:\x20
\t
two:\x20
\t
\t
""",
        )
        s2 = _mod_rio.read_stanza(s.to_lines())
        self.assertEqual(s, s2)

    def test_whitespace_value(self):
        s = _mod_rio.Stanza(space=" ", tabs="\t\t\t", combo="\n\t\t\n")
        self.assertEqual(
            s.to_string(),
            b"""\
combo:\x20
\t\t\t
\t
space:\x20\x20
tabs: \t\t\t
""",
        )
        s2 = _mod_rio.read_stanza(s.to_lines())
        self.assertEqual(s, s2)
        self.rio_file_stanzas([s])

    def test_quoted(self):
        """Rio quoted string cases."""
        s = _mod_rio.Stanza(
            q1='"hello"',
            q2=' "for',
            q3='\n\n"for"\n',
            q4='for\n"\nfor',
            q5="\n",
            q6='"',
            q7='""',
            q8="\\",
            q9='\\"\\"',
        )
        s2 = _mod_rio.read_stanza(s.to_lines())
        self.assertEqual(s, s2)
        # apparent bug in read_stanza
        # s3 = _mod_rio.read_stanza(self.stanzas_to_str([s]))
        # self.assertEqual(s, s3)

    def test_read_empty(self):
        """Detect end of rio file."""
        s = _mod_rio.read_stanza([])
        self.assertEqual(s, None)
        self.assertIsNone(s)

    def test_read_nul_byte(self):
        """File consisting of a nul byte causes an error."""
        self.assertRaises(ValueError, _mod_rio.read_stanza, [b"\0"])

    def test_read_nul_bytes(self):
        """File consisting of many nul bytes causes an error."""
        self.assertRaises(ValueError, _mod_rio.read_stanza, [b"\0" * 100])

    def test_read_iter(self):
        """Read several stanzas from file."""
        tmpf = TemporaryFile()
        tmpf.write(
            b"""\
version_header: 1

name: foo
val: 123

name: bar
val: 129319
"""
        )
        tmpf.seek(0)
        reader = _mod_rio.read_stanzas(tmpf)
        stuff = list(reader)
        self.assertEqual(
            stuff,
            [
                _mod_rio.Stanza(version_header="1"),
                _mod_rio.Stanza(name="foo", val="123"),
                _mod_rio.Stanza(name="bar", val="129319"),
            ],
        )

    def test_read_several(self):
        """Read several stanzas from file."""
        tmpf = TemporaryFile()
        tmpf.write(
            b"""\
version_header: 1

name: foo
val: 123

name: quoted
address:   "Willowglen"
\t  42 Wallaby Way
\t  Sydney

name: bar
val: 129319
"""
        )
        tmpf.seek(0)
        s = _mod_rio.read_stanza(tmpf)
        self.assertEqual(s, _mod_rio.Stanza(version_header="1"))
        s = _mod_rio.read_stanza(tmpf)
        self.assertEqual(s, _mod_rio.Stanza(name="foo", val="123"))
        s = _mod_rio.read_stanza(tmpf)
        self.assertEqual(s.get("name"), "quoted")
        self.assertEqual(s.get("address"), '  "Willowglen"\n  42 Wallaby Way\n  Sydney')
        s = _mod_rio.read_stanza(tmpf)
        self.assertEqual(s, _mod_rio.Stanza(name="bar", val="129319"))
        s = _mod_rio.read_stanza(tmpf)
        self.assertEqual(s, None)

    def check_rio_file(self, real_file):
        real_file.seek(0)
        read_write = rio_file(_mod_rio.RioReader(real_file)).read()
        real_file.seek(0)
        self.assertEqual(read_write, real_file.read())

    @staticmethod
    def stanzas_to_str(stanzas):
        return rio_file(stanzas).read()

    def rio_file_stanzas(self, stanzas):
        new_stanzas = list(_mod_rio.RioReader(rio_file(stanzas)))
        self.assertEqual(new_stanzas, stanzas)

    def test_tricky_quoted(self):
        tmpf = TemporaryFile()
        tmpf.write(
            b'''\
s: "one"

s:\x20
\t"one"
\t

s: "

s: ""

s: """

s:\x20
\t

s: \\

s:\x20
\t\\
\t\\\\
\t

s: word\\

s: quote"

s: backslashes\\\\\\

s: both\\\"

'''
        )
        tmpf.seek(0)
        expected_vals = [
            '"one"',
            '\n"one"\n',
            '"',
            '""',
            '"""',
            "\n",
            "\\",
            "\n\\\n\\\\\n",
            "word\\",
            'quote"',
            "backslashes\\\\\\",
            'both\\"',
        ]
        for expected in expected_vals:
            stanza = _mod_rio.read_stanza(tmpf)
            self.rio_file_stanzas([stanza])
            self.assertEqual(len(stanza), 1)
            self.assertEqual(stanza.get("s"), expected)

    def test_write_empty_stanza(self):
        """Write empty stanza."""
        l = list(_mod_rio.Stanza().to_lines())
        self.assertEqual(l, [])

    def test_rio_raises_type_error(self):
        """TypeError on adding invalid type to Stanza."""
        s = _mod_rio.Stanza()
        self.assertRaises(TypeError, s.add, "foo", {})

    def test_rio_raises_type_error_key(self):
        """TypeError on adding invalid type to Stanza."""
        s = _mod_rio.Stanza()
        self.assertRaises(TypeError, s.add, 10, {})

    def test_rio_surrogateescape(self):
        raw_bytes = b"\xcb"
        self.assertRaises(UnicodeDecodeError, raw_bytes.decode, "utf-8")
        try:
            uni_data = raw_bytes.decode("utf-8", "surrogateescape")
        except LookupError:
            self.skipTest("surrogateescape is not available on Python < 3")
        try:
            _mod_rio.Stanza(foo=uni_data)
        except TypeError:
            pass
        else:
            self.fail()

    def test_rio_unicode(self):
        uni_data = "\N{KATAKANA LETTER O}"
        s = _mod_rio.Stanza(foo=uni_data)
        self.assertEqual(s.get("foo"), uni_data)
        raw_lines = s.to_lines()
        self.assertEqual(raw_lines, [b"foo: " + uni_data.encode("utf-8") + b"\n"])
        new_s = _mod_rio.read_stanza(raw_lines)
        self.assertEqual(new_s.get("foo"), uni_data)

    def mail_munge(self, lines, dos_nl=True):
        new_lines = []
        for line in lines:
            line = re.sub(b" *\n", b"\n", line)
            if dos_nl:
                line = re.sub(b"([^\r])\n", b"\\1\r\n", line)
            new_lines.append(line)
        return new_lines

    def test_patch_rio(self):
        stanza = _mod_rio.Stanza(data="#\n\r\\r ", space=" " * 255, hash="#" * 255)
        lines = to_patch_lines(stanza)
        for line in lines:
            self.assertContainsRe(line, b"^# ")
            self.assertGreaterEqual(72, len(line))
        for line in to_patch_lines(stanza, max_width=12):
            self.assertGreaterEqual(12, len(line))
        new_stanza = read_patch_stanza(self.mail_munge(lines, dos_nl=False))
        lines = self.mail_munge(lines)
        new_stanza = read_patch_stanza(lines)
        self.assertEqual("#\n\r\\r ", new_stanza.get("data"))
        self.assertEqual(" " * 255, new_stanza.get("space"))
        self.assertEqual("#" * 255, new_stanza.get("hash"))

    def test_patch_rio_linebreaks(self):
        stanza = _mod_rio.Stanza(breaktest="linebreak -/" * 30)
        line1 = to_patch_lines(stanza, 71)[0]
        self.assertContainsRe(line1, b"linebreak\\\\\n")
        stanza = _mod_rio.Stanza(breaktest="linebreak-/" * 30)
        self.assertContainsRe(to_patch_lines(stanza, 70)[0], b"linebreak-\\\\\n")
        stanza = _mod_rio.Stanza(breaktest="linebreak/" * 30)
        self.assertContainsRe(to_patch_lines(stanza, 70)[0], b"linebreak\\\\\n")


class TestValidTag(TestCase):
    def test_ok(self):
        self.assertTrue(_mod_rio.valid_tag("foo"))

    def test_no_spaces(self):
        self.assertFalse(_mod_rio.valid_tag("foo bla"))

    def test_numeric(self):
        self.assertTrue(_mod_rio.valid_tag("3foo423"))

    def test_no_colon(self):
        self.assertFalse(_mod_rio.valid_tag("foo:bla"))

    def test_type_error(self):
        self.assertRaises(TypeError, _mod_rio.valid_tag, 423)

    def test_empty(self):
        self.assertFalse(_mod_rio.valid_tag(""))

    def test_unicode(self):
        # When str is a unicode type, it is valid for a tag
        self.assertTrue(_mod_rio.valid_tag("foo"))

    def test_non_ascii_char(self):
        self.assertFalse(_mod_rio.valid_tag("\xb5"))


class TestReadUTF8Stanza(TestCase):
    def assertReadStanza(self, result, line_iter):
        s = _mod_rio.read_stanza(line_iter)
        self.assertEqual(result, s)
        if s is not None:
            for tag, value in s.iter_pairs():
                self.assertIsInstance(tag, str)
                self.assertIsInstance(value, str)

    def assertReadStanzaRaises(self, exception, line_iter):
        self.assertRaises(exception, _mod_rio.read_stanza, line_iter)

    def test_no_string(self):
        self.assertReadStanzaRaises(TypeError, [21323])

    def test_empty(self):
        self.assertReadStanza(None, [])

    def test_none(self):
        self.assertReadStanza(None, [b""])

    def test_simple(self):
        self.assertReadStanza(_mod_rio.Stanza(foo="bar"), [b"foo: bar\n", b""])

    def test_multi_line(self):
        self.assertReadStanza(
            _mod_rio.Stanza(foo="bar\nbla"), [b"foo: bar\n", b"\tbla\n"]
        )

    def test_repeated(self):
        s = _mod_rio.Stanza()
        s.add("foo", "bar")
        s.add("foo", "foo")
        self.assertReadStanza(s, [b"foo: bar\n", b"foo: foo\n"])

    def test_invalid_early_colon(self):
        self.assertReadStanzaRaises(ValueError, [b"f:oo: bar\n"])

    def test_invalid_tag(self):
        self.assertReadStanzaRaises(ValueError, [b"f%oo: bar\n"])

    def test_continuation_too_early(self):
        self.assertReadStanzaRaises(ValueError, [b"\tbar\n"])

    def test_large(self):
        value = b"bla" * 9000
        self.assertReadStanza(
            _mod_rio.Stanza(foo=value.decode()), [b"foo: %s\n" % value]
        )

    def test_non_ascii_char(self):
        self.assertReadStanza(
            _mod_rio.Stanza(foo="n\xe5me"), ["foo: n\xe5me\n".encode()]
        )
