#
# Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team
# All rights reserved.
#
# This software is licensed as described in the file LICENSE, which
# you should have received as part of this distribution. The terms
# are also available at http://babel.edgewall.org/wiki/License.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://babel.edgewall.org/log/.

import unittest
from datetime import datetime
from io import BytesIO, StringIO

import pytest

from babel.core import Locale
from babel.messages import pofile
from babel.messages.catalog import Catalog, Message
from babel.util import FixedOffsetTimezone


class ReadPoTestCase(unittest.TestCase):

    def test_preserve_locale(self):
        buf = StringIO(r'''msgid "foo"
msgstr "Voh"''')
        catalog = pofile.read_po(buf, locale='en_US')
        assert Locale('en', 'US') == catalog.locale

    def test_locale_gets_overridden_by_file(self):
        buf = StringIO(r'''
msgid ""
msgstr ""
"Language: en_US\n"''')
        catalog = pofile.read_po(buf, locale='de')
        assert Locale('en', 'US') == catalog.locale
        buf = StringIO(r'''
msgid ""
msgstr ""
"Language: ko-KR\n"''')
        catalog = pofile.read_po(buf, locale='de')
        assert Locale('ko', 'KR') == catalog.locale

    def test_preserve_domain(self):
        buf = StringIO(r'''msgid "foo"
msgstr "Voh"''')
        catalog = pofile.read_po(buf, domain='mydomain')
        assert catalog.domain == 'mydomain'

    def test_applies_specified_encoding_during_read(self):
        buf = BytesIO('''
msgid ""
msgstr ""
"Project-Id-Version:  3.15\\n"
"Report-Msgid-Bugs-To: Fliegender Zirkus <fliegender@zirkus.de>\\n"
"POT-Creation-Date: 2007-09-27 11:19+0700\\n"
"PO-Revision-Date: 2007-09-27 21:42-0700\\n"
"Last-Translator: John <cleese@bavaria.de>\\n"
"Language-Team: German Lang <de@babel.org>\\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=iso-8859-1\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Generated-By: Babel 1.0dev-r313\\n"

msgid "foo"
msgstr "bär"'''.encode('iso-8859-1'))
        catalog = pofile.read_po(buf, locale='de_DE')
        assert catalog.get('foo').string == 'bär'

    def test_encoding_header_read(self):
        buf = BytesIO(b'msgid ""\nmsgstr ""\n"Content-Type: text/plain; charset=mac_roman\\n"\n')
        catalog = pofile.read_po(buf, locale='xx_XX')
        assert catalog.charset == 'mac_roman'

    def test_plural_forms_header_parsed(self):
        buf = BytesIO(b'msgid ""\nmsgstr ""\n"Plural-Forms: nplurals=42; plural=(n % 11);\\n"\n')
        catalog = pofile.read_po(buf, locale='xx_XX')
        assert catalog.plural_expr == '(n % 11)'
        assert catalog.num_plurals == 42

    def test_read_multiline(self):
        buf = StringIO(r'''msgid ""
"Here's some text that\n"
"includesareallylongwordthatmightbutshouldnt"
" throw us into an infinite "
"loop\n"
msgstr ""''')
        catalog = pofile.read_po(buf)
        assert len(catalog) == 1
        message = list(catalog)[1]
        assert message.id == (
            "Here's some text that\nincludesareallylongwordthat"
            "mightbutshouldnt throw us into an infinite loop\n"
        )

    def test_fuzzy_header(self):
        buf = StringIO(r'''
# Translations template for AReallyReallyLongNameForAProject.
# Copyright (C) 2007 ORGANIZATION
# This file is distributed under the same license as the
# AReallyReallyLongNameForAProject project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
#
#, fuzzy
''')
        catalog = pofile.read_po(buf)
        assert len(list(catalog)) == 1
        assert list(catalog)[0].fuzzy

    def test_not_fuzzy_header(self):
        buf = StringIO(r'''
# Translations template for AReallyReallyLongNameForAProject.
# Copyright (C) 2007 ORGANIZATION
# This file is distributed under the same license as the
# AReallyReallyLongNameForAProject project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
#
''')
        catalog = pofile.read_po(buf)
        assert len(list(catalog)) == 1
        assert not list(catalog)[0].fuzzy

    def test_header_entry(self):
        buf = StringIO(r'''
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2007 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version:  3.15\n"
"Report-Msgid-Bugs-To: Fliegender Zirkus <fliegender@zirkus.de>\n"
"POT-Creation-Date: 2007-09-27 11:19+0700\n"
"PO-Revision-Date: 2007-09-27 21:42-0700\n"
"Last-Translator: John <cleese@bavaria.de>\n"
"Language: de\n"
"Language-Team: German Lang <de@babel.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=iso-8859-2\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.0dev-r313\n"
''')
        catalog = pofile.read_po(buf)
        assert len(list(catalog)) == 1
        assert catalog.version == '3.15'
        assert catalog.msgid_bugs_address == 'Fliegender Zirkus <fliegender@zirkus.de>'
        assert datetime(2007, 9, 27, 11, 19, tzinfo=FixedOffsetTimezone(7 * 60)) == catalog.creation_date
        assert catalog.last_translator == 'John <cleese@bavaria.de>'
        assert Locale('de') == catalog.locale
        assert catalog.language_team == 'German Lang <de@babel.org>'
        assert catalog.charset == 'iso-8859-2'
        assert list(catalog)[0].fuzzy

    def test_obsolete_message(self):
        buf = StringIO(r'''# This is an obsolete message
#~ msgid "foo"
#~ msgstr "Voh"

# This message is not obsolete
#: main.py:1
msgid "bar"
msgstr "Bahr"
''')
        catalog = pofile.read_po(buf)
        assert len(catalog) == 1
        assert len(catalog.obsolete) == 1
        message = catalog.obsolete['foo']
        assert message.id == 'foo'
        assert message.string == 'Voh'
        assert message.user_comments == ['This is an obsolete message']

    def test_obsolete_message_ignored(self):
        buf = StringIO(r'''# This is an obsolete message
#~ msgid "foo"
#~ msgstr "Voh"

# This message is not obsolete
#: main.py:1
msgid "bar"
msgstr "Bahr"
''')
        catalog = pofile.read_po(buf, ignore_obsolete=True)
        assert len(catalog) == 1
        assert len(catalog.obsolete) == 0

    def test_multi_line_obsolete_message(self):
        buf = StringIO(r'''# This is an obsolete message
#~ msgid ""
#~ "foo"
#~ "foo"
#~ msgstr ""
#~ "Voh"
#~ "Vooooh"

# This message is not obsolete
#: main.py:1
msgid "bar"
msgstr "Bahr"
''')
        catalog = pofile.read_po(buf)
        assert len(catalog.obsolete) == 1
        message = catalog.obsolete['foofoo']
        assert message.id == 'foofoo'
        assert message.string == 'VohVooooh'
        assert message.user_comments == ['This is an obsolete message']

    def test_unit_following_multi_line_obsolete_message(self):
        buf = StringIO(r'''# This is an obsolete message
#~ msgid ""
#~ "foo"
#~ "fooooooo"
#~ msgstr ""
#~ "Voh"
#~ "Vooooh"

# This message is not obsolete
#: main.py:1
msgid "bar"
msgstr "Bahr"
''')
        catalog = pofile.read_po(buf)
        assert len(catalog) == 1
        message = catalog['bar']
        assert message.id == 'bar'
        assert message.string == 'Bahr'
        assert message.user_comments == ['This message is not obsolete']

    def test_unit_before_obsolete_is_not_obsoleted(self):
        buf = StringIO(r'''
# This message is not obsolete
#: main.py:1
msgid "bar"
msgstr "Bahr"

# This is an obsolete message
#~ msgid ""
#~ "foo"
#~ "fooooooo"
#~ msgstr ""
#~ "Voh"
#~ "Vooooh"
''')
        catalog = pofile.read_po(buf)
        assert len(catalog) == 1
        message = catalog['bar']
        assert message.id == 'bar'
        assert message.string == 'Bahr'
        assert message.user_comments == ['This message is not obsolete']

    def test_with_context(self):
        buf = BytesIO(b'''# Some string in the menu
#: main.py:1
msgctxt "Menu"
msgid "foo"
msgstr "Voh"

# Another string in the menu
#: main.py:2
msgctxt "Menu"
msgid "bar"
msgstr "Bahr"
''')
        catalog = pofile.read_po(buf, ignore_obsolete=True)
        assert len(catalog) == 2
        message = catalog.get('foo', context='Menu')
        assert message.context == 'Menu'
        message = catalog.get('bar', context='Menu')
        assert message.context == 'Menu'

        # And verify it pass through write_po
        out_buf = BytesIO()
        pofile.write_po(out_buf, catalog, omit_header=True)
        assert out_buf.getvalue().strip() == buf.getvalue().strip()

    def test_obsolete_message_with_context(self):
        buf = StringIO('''
# This message is not obsolete
msgid "baz"
msgstr "Bazczch"

# This is an obsolete message
#~ msgctxt "other"
#~ msgid "foo"
#~ msgstr "Voh"

# This message is not obsolete
#: main.py:1
msgid "bar"
msgstr "Bahr"
''')
        catalog = pofile.read_po(buf)
        assert len(catalog) == 2
        assert len(catalog.obsolete) == 1
        message = catalog.obsolete["foo"]
        assert message.context == 'other'
        assert message.string == 'Voh'

    def test_multiline_context(self):
        buf = StringIO('''
msgctxt "a really long "
"message context "
"why?"
msgid "mid"
msgstr "mst"
        ''')
        catalog = pofile.read_po(buf)
        assert len(catalog) == 1
        message = catalog.get('mid', context="a really long message context why?")
        assert message is not None
        assert message.context == 'a really long message context why?'

    def test_with_context_two(self):
        buf = BytesIO(b'''msgctxt "Menu"
msgid "foo"
msgstr "Voh"

msgctxt "Mannu"
msgid "bar"
msgstr "Bahr"
''')
        catalog = pofile.read_po(buf, ignore_obsolete=True)
        assert len(catalog) == 2
        message = catalog.get('foo', context='Menu')
        assert message.context == 'Menu'
        message = catalog.get('bar', context='Mannu')
        assert message.context == 'Mannu'

        # And verify it pass through write_po
        out_buf = BytesIO()
        pofile.write_po(out_buf, catalog, omit_header=True)
        assert out_buf.getvalue().strip() == buf.getvalue().strip(), out_buf.getvalue()

    def test_single_plural_form(self):
        buf = StringIO(r'''msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"''')
        catalog = pofile.read_po(buf, locale='ja_JP')
        assert len(catalog) == 1
        assert catalog.num_plurals == 1
        message = catalog['foo']
        assert len(message.string) == 1

    def test_singular_plural_form(self):
        buf = StringIO(r'''msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Vohs"''')
        catalog = pofile.read_po(buf, locale='nl_NL')
        assert len(catalog) == 1
        assert catalog.num_plurals == 2
        message = catalog['foo']
        assert len(message.string) == 2

    def test_more_than_two_plural_forms(self):
        buf = StringIO(r'''msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Vohs"
msgstr[2] "Vohss"''')
        catalog = pofile.read_po(buf, locale='lv_LV')
        assert len(catalog) == 1
        assert catalog.num_plurals == 3
        message = catalog['foo']
        assert len(message.string) == 3
        assert message.string[2] == 'Vohss'

    def test_plural_with_square_brackets(self):
        buf = StringIO(r'''msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh [text]"
msgstr[1] "Vohs [text]"''')
        catalog = pofile.read_po(buf, locale='nb_NO')
        assert len(catalog) == 1
        assert catalog.num_plurals == 2
        message = catalog['foo']
        assert len(message.string) == 2

    def test_obsolete_plural_with_square_brackets(self):
        buf = StringIO('''\
#~ msgid "foo"
#~ msgid_plural "foos"
#~ msgstr[0] "Voh [text]"
#~ msgstr[1] "Vohs [text]"
''')
        catalog = pofile.read_po(buf, locale='nb_NO')
        assert len(catalog) == 0
        assert len(catalog.obsolete) == 1
        assert catalog.num_plurals == 2
        message = catalog.obsolete[('foo', 'foos')]
        assert len(message.string) == 2
        assert message.string[0] == 'Voh [text]'
        assert message.string[1] == 'Vohs [text]'

    def test_missing_plural(self):
        buf = StringIO('''\
msgid ""
msgstr ""
"Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n"

msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh [text]"
msgstr[1] "Vohs [text]"
''')
        catalog = pofile.read_po(buf, locale='nb_NO')
        assert len(catalog) == 1
        assert catalog.num_plurals == 3
        message = catalog['foo']
        assert len(message.string) == 3
        assert message.string[0] == 'Voh [text]'
        assert message.string[1] == 'Vohs [text]'
        assert message.string[2] == ''

    def test_missing_plural_in_the_middle(self):
        buf = StringIO('''\
msgid ""
msgstr ""
"Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n"

msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh [text]"
msgstr[2] "Vohs [text]"
''')
        catalog = pofile.read_po(buf, locale='nb_NO')
        assert len(catalog) == 1
        assert catalog.num_plurals == 3
        message = catalog['foo']
        assert len(message.string) == 3
        assert message.string[0] == 'Voh [text]'
        assert message.string[1] == ''
        assert message.string[2] == 'Vohs [text]'

    def test_abort_invalid_po_file(self):
        invalid_po = '''
            msgctxt ""
            "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": "
            "270005359}"
            msgid ""
            "Thank you very much for your time.\n"
            "If you have any questions regarding this survey, please contact Fulano "
            "at nadie@blah.com"
            msgstr "Merci de prendre le temps de remplir le sondage.
            Pour toute question, veuillez communiquer avec Fulano  à nadie@blah.com
            "
        '''
        invalid_po_2 = '''
            msgctxt ""
            "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": "
            "270005359}"
            msgid ""
            "Thank you very much for your time.\n"
            "If you have any questions regarding this survey, please contact Fulano "
            "at fulano@blah.com."
            msgstr "Merci de prendre le temps de remplir le sondage.
            Pour toute question, veuillez communiquer avec Fulano a fulano@blah.com
            "
            '''
        # Catalog not created, throws Unicode Error
        buf = StringIO(invalid_po)
        output = pofile.read_po(buf, locale='fr', abort_invalid=False)
        assert isinstance(output, Catalog)

        # Catalog not created, throws PoFileError
        buf = StringIO(invalid_po_2)
        with pytest.raises(pofile.PoFileError):
            pofile.read_po(buf, locale='fr', abort_invalid=True)

        # Catalog is created with warning, no abort
        buf = StringIO(invalid_po_2)
        output = pofile.read_po(buf, locale='fr', abort_invalid=False)
        assert isinstance(output, Catalog)

        # Catalog not created, aborted with PoFileError
        buf = StringIO(invalid_po_2)
        with pytest.raises(pofile.PoFileError):
            pofile.read_po(buf, locale='fr', abort_invalid=True)

    def test_invalid_pofile_with_abort_flag(self):
        parser = pofile.PoFileParser(None, abort_invalid=True)
        lineno = 10
        line = 'Algo esta mal'
        msg = 'invalid file'
        with pytest.raises(pofile.PoFileError):
            parser._invalid_pofile(line, lineno, msg)


class WritePoTestCase(unittest.TestCase):

    def test_join_locations(self):
        catalog = Catalog()
        catalog.add('foo', locations=[('main.py', 1)])
        catalog.add('foo', locations=[('utils.py', 3)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True)
        assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3
msgid "foo"
msgstr ""'''

    def test_write_po_file_with_specified_charset(self):
        catalog = Catalog(charset='iso-8859-1')
        catalog.add('foo', 'äöü', locations=[('main.py', 1)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=False)
        po_file = buf.getvalue().strip()
        assert b'"Content-Type: text/plain; charset=iso-8859-1\\n"' in po_file
        assert 'msgstr "äöü"'.encode('iso-8859-1') in po_file

    def test_duplicate_comments(self):
        catalog = Catalog()
        catalog.add('foo', auto_comments=['A comment'])
        catalog.add('foo', auto_comments=['A comment'])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True)
        assert buf.getvalue().strip() == b'''#. A comment
msgid "foo"
msgstr ""'''

    def test_wrap_long_lines(self):
        text = """Here's some text where
white space and line breaks matter, and should

not be removed

"""
        catalog = Catalog()
        catalog.add(text, locations=[('main.py', 1)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, no_location=True, omit_header=True,
                        width=42)
        assert buf.getvalue().strip() == b'''msgid ""
"Here's some text where\\n"
"white space and line breaks matter, and"
" should\\n"
"\\n"
"not be removed\\n"
"\\n"
msgstr ""'''

    def test_wrap_long_lines_with_long_word(self):
        text = """Here's some text that
includesareallylongwordthatmightbutshouldnt throw us into an infinite loop
"""
        catalog = Catalog()
        catalog.add(text, locations=[('main.py', 1)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, no_location=True, omit_header=True,
                        width=32)
        assert buf.getvalue().strip() == b'''msgid ""
"Here's some text that\\n"
"includesareallylongwordthatmightbutshouldnt"
" throw us into an infinite "
"loop\\n"
msgstr ""'''

    def test_wrap_long_lines_in_header(self):
        """
        Verify that long lines in the header comment are wrapped correctly.
        """
        catalog = Catalog(project='AReallyReallyLongNameForAProject',
                          revision_date=datetime(2007, 4, 1))
        buf = BytesIO()
        pofile.write_po(buf, catalog)
        assert b'\n'.join(buf.getvalue().splitlines()[:7]) == b'''\
# Translations template for AReallyReallyLongNameForAProject.
# Copyright (C) 2007 ORGANIZATION
# This file is distributed under the same license as the
# AReallyReallyLongNameForAProject project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
#
#, fuzzy'''

    def test_wrap_locations_with_hyphens(self):
        catalog = Catalog()
        catalog.add('foo', locations=[
            ('doupy/templates/base/navmenu.inc.html.py', 60)
        ])
        catalog.add('foo', locations=[
            ('doupy/templates/job-offers/helpers.html', 22)
        ])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True)
        assert buf.getvalue().strip() == b'''#: doupy/templates/base/navmenu.inc.html.py:60
#: doupy/templates/job-offers/helpers.html:22
msgid "foo"
msgstr ""'''

    def test_no_wrap_and_width_behaviour_on_comments(self):
        catalog = Catalog()
        catalog.add("Pretty dam long message id, which must really be big "
                    "to test this wrap behaviour, if not it won't work.",
                    locations=[("fake.py", n) for n in range(1, 30)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, width=None, omit_header=True)
        assert buf.getvalue().lower() == b"""\
#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7
#: fake.py:8 fake.py:9 fake.py:10 fake.py:11 fake.py:12 fake.py:13 fake.py:14
#: fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 fake.py:20 fake.py:21
#: fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
#: fake.py:29
msgid "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't work."
msgstr ""

"""
        buf = BytesIO()
        pofile.write_po(buf, catalog, width=100, omit_header=True)
        assert buf.getvalue().lower() == b"""\
#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 fake.py:8 fake.py:9 fake.py:10
#: fake.py:11 fake.py:12 fake.py:13 fake.py:14 fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19
#: fake.py:20 fake.py:21 fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
#: fake.py:29
msgid ""
"pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't"
" work."
msgstr ""

"""

    def test_pot_with_translator_comments(self):
        catalog = Catalog()
        catalog.add('foo', locations=[('main.py', 1)],
                    auto_comments=['Comment About `foo`'])
        catalog.add('bar', locations=[('utils.py', 3)],
                    user_comments=['Comment About `bar` with',
                                   'multiple lines.'])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True)
        assert buf.getvalue().strip() == b'''#. Comment About `foo`
#: main.py:1
msgid "foo"
msgstr ""

# Comment About `bar` with
# multiple lines.
#: utils.py:3
msgid "bar"
msgstr ""'''

    def test_po_with_obsolete_message(self):
        catalog = Catalog()
        catalog.add('foo', 'Voh', locations=[('main.py', 1)])
        catalog.obsolete['bar'] = Message('bar', 'Bahr',
                                          locations=[('utils.py', 3)],
                                          user_comments=['User comment'])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True)
        assert buf.getvalue().strip() == b'''#: main.py:1
msgid "foo"
msgstr "Voh"

# User comment
#~ msgid "bar"
#~ msgstr "Bahr"'''

    def test_po_with_multiline_obsolete_message(self):
        catalog = Catalog()
        catalog.add('foo', 'Voh', locations=[('main.py', 1)])
        msgid = r"""Here's a message that covers
multiple lines, and should still be handled
correctly.
"""
        msgstr = r"""Here's a message that covers
multiple lines, and should still be handled
correctly.
"""
        catalog.obsolete[msgid] = Message(msgid, msgstr,
                                          locations=[('utils.py', 3)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True)
        assert buf.getvalue().strip() == b'''#: main.py:1
msgid "foo"
msgstr "Voh"

#~ msgid ""
#~ "Here's a message that covers\\n"
#~ "multiple lines, and should still be handled\\n"
#~ "correctly.\\n"
#~ msgstr ""
#~ "Here's a message that covers\\n"
#~ "multiple lines, and should still be handled\\n"
#~ "correctly.\\n"'''

    def test_po_with_obsolete_message_ignored(self):
        catalog = Catalog()
        catalog.add('foo', 'Voh', locations=[('main.py', 1)])
        catalog.obsolete['bar'] = Message('bar', 'Bahr',
                                          locations=[('utils.py', 3)],
                                          user_comments=['User comment'])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True, ignore_obsolete=True)
        assert buf.getvalue().strip() == b'''#: main.py:1
msgid "foo"
msgstr "Voh"'''

    def test_po_with_previous_msgid(self):
        catalog = Catalog()
        catalog.add('foo', 'Voh', locations=[('main.py', 1)],
                    previous_id='fo')
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
        assert buf.getvalue().strip() == b'''#: main.py:1
#| msgid "fo"
msgid "foo"
msgstr "Voh"'''

    def test_po_with_previous_msgid_plural(self):
        catalog = Catalog()
        catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
                    locations=[('main.py', 1)], previous_id=('fo', 'fos'))
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
        assert buf.getvalue().strip() == b'''#: main.py:1
#| msgid "fo"
#| msgid_plural "fos"
msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Voeh"'''

    def test_sorted_po(self):
        catalog = Catalog()
        catalog.add('bar', locations=[('utils.py', 3)],
                    user_comments=['Comment About `bar` with',
                                   'multiple lines.'])
        catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
                    locations=[('main.py', 1)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, sort_output=True)
        value = buf.getvalue().strip()
        assert b'''\
# Comment About `bar` with
# multiple lines.
#: utils.py:3
msgid "bar"
msgstr ""

#: main.py:1
msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Voeh"''' in value
        assert value.find(b'msgid ""') < value.find(b'msgid "bar"') < value.find(b'msgid "foo"')

    def test_sorted_po_context(self):
        catalog = Catalog()
        catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
                    locations=[('main.py', 1)],
                    context='there')
        catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
                    locations=[('main.py', 1)])
        catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
                    locations=[('main.py', 1)],
                    context='here')
        buf = BytesIO()
        pofile.write_po(buf, catalog, sort_output=True)
        value = buf.getvalue().strip()
        # We expect the foo without ctx, followed by "here" foo and "there" foo
        assert b'''\
#: main.py:1
msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Voeh"

#: main.py:1
msgctxt "here"
msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Voeh"

#: main.py:1
msgctxt "there"
msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Voeh"''' in value

    def test_file_sorted_po(self):
        catalog = Catalog()
        catalog.add('bar', locations=[('utils.py', 3)])
        catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, sort_by_file=True)
        value = buf.getvalue().strip()
        assert value.find(b'main.py') < value.find(b'utils.py')

    def test_file_with_no_lineno(self):
        catalog = Catalog()
        catalog.add('bar', locations=[('utils.py', None)],
                    user_comments=['Comment About `bar` with',
                                   'multiple lines.'])
        buf = BytesIO()
        pofile.write_po(buf, catalog, sort_output=True)
        value = buf.getvalue().strip()
        assert b'''\
# Comment About `bar` with
# multiple lines.
#: utils.py
msgid "bar"
msgstr ""''' in value

    def test_silent_location_fallback(self):
        buf = BytesIO(b'''\
#: broken_file.py
msgid "missing line number"
msgstr ""

#: broken_file.py:broken_line_number
msgid "broken line number"
msgstr ""''')
        catalog = pofile.read_po(buf)
        assert catalog['missing line number'].locations == [('broken_file.py', None)]
        assert catalog['broken line number'].locations == []

    def test_include_lineno(self):
        catalog = Catalog()
        catalog.add('foo', locations=[('main.py', 1)])
        catalog.add('foo', locations=[('utils.py', 3)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
        assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3
msgid "foo"
msgstr ""'''

    def test_no_include_lineno(self):
        catalog = Catalog()
        catalog.add('foo', locations=[('main.py', 1)])
        catalog.add('foo', locations=[('main.py', 2)])
        catalog.add('foo', locations=[('utils.py', 3)])
        buf = BytesIO()
        pofile.write_po(buf, catalog, omit_header=True, include_lineno=False)
        assert buf.getvalue().strip() == b'''#: main.py utils.py
msgid "foo"
msgstr ""'''


class PofileFunctionsTestCase(unittest.TestCase):

    def test_unescape(self):
        escaped = '"Say:\\n  \\"hello, world!\\"\\n"'
        unescaped = 'Say:\n  "hello, world!"\n'
        assert unescaped != escaped
        assert unescaped == pofile.unescape(escaped)

    def test_unescape_of_quoted_newline(self):
        # regression test for #198
        assert pofile.unescape(r'"\\n"') == '\\n'

    def test_denormalize_on_msgstr_without_empty_first_line(self):
        # handle irregular multi-line msgstr (no "" as first line)
        # gracefully (#171)
        msgstr = '"multi-line\\n"\n" translation"'
        expected_denormalized = 'multi-line\n translation'

        assert expected_denormalized == pofile.denormalize(msgstr)
        assert expected_denormalized == pofile.denormalize(f'""\n{msgstr}')


def test_unknown_language_roundtrip():
    buf = StringIO(r'''
msgid ""
msgstr ""
"Language: sr_SP\n"''')
    catalog = pofile.read_po(buf)
    assert catalog.locale_identifier == 'sr_SP'
    assert not catalog.locale
    buf = BytesIO()
    pofile.write_po(buf, catalog)
    assert 'sr_SP' in buf.getvalue().decode()


def test_unknown_language_write():
    catalog = Catalog(locale='sr_SP')
    assert catalog.locale_identifier == 'sr_SP'
    assert not catalog.locale
    buf = BytesIO()
    pofile.write_po(buf, catalog)
    assert 'sr_SP' in buf.getvalue().decode()
