from io import BytesIO

from translate.convert import prop2po
from translate.storage import po, properties

from . import test_convert


class TestProp2PO:
    @staticmethod
    def prop2po(propsource, proptemplate=None, personality="java"):
        """Helper that converts .properties source to po source without requiring files."""
        inputfile = BytesIO(propsource.encode())
        inputprop = properties.propfile(inputfile, personality=personality)
        convertor = prop2po.prop2po(personality=personality)
        if proptemplate:
            templatefile = BytesIO(proptemplate.encode())
            templateprop = properties.propfile(templatefile)
            outputpo = convertor.mergestore(templateprop, inputprop)
        else:
            outputpo = convertor.convertstore(inputprop)
        return outputpo

    @staticmethod
    def convertprop(propsource):
        """Call the convertprop, return the outputfile."""
        inputfile = BytesIO(propsource.encode())
        outputfile = BytesIO()
        templatefile = None
        assert prop2po.convertprop(inputfile, outputfile, templatefile)
        return outputfile.getvalue()

    @staticmethod
    def singleelement(pofile):
        """Checks that the pofile contains a single non-header element, and returns it."""
        assert len(pofile.units) == 2
        assert pofile.units[0].isheader()
        print(pofile)
        return pofile.units[1]

    @staticmethod
    def countelements(pofile):
        """Counts the number of non-header entries."""
        assert pofile.units[0].isheader()
        print(pofile)
        return len(pofile.units) - 1

    def test_simpleentry(self) -> None:
        """Checks that a simple properties entry converts properly to a po entry."""
        propsource = "SAVEENTRY=Save file\n"
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.source == "Save file"
        assert pounit.target == ""

    def test_convertprop(self) -> None:
        """Checks that the convertprop function is working."""
        propsource = "SAVEENTRY=Save file\n"
        posource = self.convertprop(propsource)
        pofile = po.pofile(BytesIO(posource))
        pounit = self.singleelement(pofile)
        assert pounit.source == "Save file"
        assert pounit.target == ""

    def test_no_value_entry(self) -> None:
        """Checks that a properties entry without value is converted."""
        propsource = "KEY = \n"
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.getcontext() == "KEY"
        assert pounit.source == ""
        assert pounit.target == ""

    def test_no_separator_entry(self) -> None:
        """Checks that a properties entry without separator is converted."""
        propsource = "KEY\n"
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.getcontext() == "KEY"
        assert pounit.source == ""
        assert pounit.target == ""

    def test_value_no_key_entry(self) -> None:
        """Checks that a properties entry with value but no key is converted."""
        propsource = "=value\n"
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        # Empty key should use special <empty> marker for location
        assert pounit.getlocations() == ["<empty>"]
        assert pounit.source == "value"
        assert pounit.target == ""

    def test_tab_at_end_of_string(self) -> None:
        """Check that we preserve tabs at the end of a string."""
        propsource = r"TAB_AT_END=This sentence has a tab at the end.\t"
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.source == "This sentence has a tab at the end.\t"

        propsource = (
            r"SPACE_THEN_TAB_AT_END=This sentence has a space then tab at the end. \t"
        )
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.source == "This sentence has a space then tab at the end. \t"

        propsource = (
            r"SPACE_AT_END=This sentence will keep its 4 spaces at the end.    "
        )
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.source == "This sentence will keep its 4 spaces at the end.    "

        propsource = r"SPACE_AT_END_NO_TRIM=This sentence will keep its 4 spaces at the end.\    "
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.source == "This sentence will keep its 4 spaces at the end.    "

        propsource = r"SPACE_AT_END_NO_TRIM2=This sentence will keep its 4 spaces at the end.\\    "
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.source == "This sentence will keep its 4 spaces at the end.\\    "

    def test_tab_at_start_of_value(self) -> None:
        """Check that tabs in a property are ignored where appropriate."""
        propsource = r"property	=	value"
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.getlocations()[0] == "property"
        assert pounit.source == "value"

    def test_unicode(self) -> None:
        """Checks that unicode entries convert properly."""
        unistring = r"Norsk bokm\u00E5l"
        propsource = f"nb = {unistring}\n"
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        print(repr(pofile.units[0].target))
        print(repr(pounit.source))
        assert pounit.source == "Norsk bokm\u00e5l"

    def test_multiline_escaping(self) -> None:
        """Checks that multiline entries can be parsed."""
        propsource = r"""5093=Unable to connect to your IMAP server. You may have exceeded the maximum number \
of connections to this server. If so, use the Advanced IMAP Server Settings dialog to \
reduce the number of cached connections."""
        pofile = self.prop2po(propsource)
        print(repr(pofile.units[1].target))
        assert self.countelements(pofile) == 1

    def test_comments(self) -> None:
        """Test to ensure that we take comments from .properties and place them in .po."""
        propsource = """# Comment
prefPanel-smime=Security"""
        pofile = self.prop2po(propsource)
        pounit = self.singleelement(pofile)
        assert pounit.getnotes("developer") == "Comment"

    def test_multiline_comments(self) -> None:
        """Test to ensure that we handle multiline comments well."""
        propsource = """# Comment
# commentary 2

## @name GENERIC_ERROR
## @loc none
prefPanel-smime=
"""
        pofile = self.prop2po(propsource)
        print(bytes(pofile))
        # header comments:
        assert b"#. # Comment\n#. # commentary 2" in bytes(pofile)
        pounit = self.singleelement(pofile)
        assert pounit.getnotes("developer") == "# @name GENERIC_ERROR\n# @loc none"

    def test_folding_accesskeys(self) -> None:
        """Check that we can fold various accesskeys into their associated label (bug #115)."""
        propsource = r"""cmd_addEngine.label = Add Engines...
cmd_addEngine.accesskey = A"""
        pofile = self.prop2po(propsource, personality="mozilla")
        pounit = self.singleelement(pofile)
        assert pounit.source == "&Add Engines..."

    def test_dont_translate(self) -> None:
        """Check that we know how to ignore don't translate instructions in properties files (bug #116)."""
        propsource = """# LOCALIZATION NOTE (dont): DONT_TRANSLATE.
dont=don't translate me
do=translate me
"""
        pofile = self.prop2po(propsource)
        assert self.countelements(pofile) == 1

    def test_duplicate_locations_mozilla(self) -> None:
        """
        Check that we handle duplicate locations properly in Mozilla properties files.
        This tests the fix for the Lithuanian Firefox recovery issue where duplicate
        locations could cause a traceback.

        When there are duplicate keys, locationindex will contain only the first
        occurrence from the translated file. Both template entries are processed,
        but both will map to the same translation (the first occurrence).
        """
        # Template file with duplicate keys
        templateprop = """key1=Original value 1
key1=Original value 2
key2=Another value
"""
        # Translation file with duplicate keys
        translatedprop = """key1=Translated value 1
key1=Translated value 2
key2=Translated value
"""
        # Convert using Mozilla personality - should not raise AttributeError
        pofile = self.prop2po(translatedprop, templateprop, personality="mozilla")

        # We get 3 units because both key1 entries from template are processed
        # Both map to the first translated value due to locationindex behavior
        assert self.countelements(pofile) == 3

        # First key1 entry
        pounit1 = pofile.units[1]
        assert pounit1.source == "Original value 1"
        assert pounit1.target == "Translated value 1"
        assert pounit1.getlocations() == ["key1"]

        # Second key1 entry (duplicate location)
        pounit2 = pofile.units[2]
        assert pounit2.source == "Original value 2"
        # Both key1 entries map to first translated value
        assert pounit2.target == "Translated value 1"
        assert pounit2.getlocations() == ["key1"]

        # key2 entry
        pounit3 = pofile.units[3]
        assert pounit3.source == "Another value"
        assert pounit3.target == "Translated value"
        assert pounit3.getlocations() == ["key2"]

    def test_emptyproperty(self) -> None:
        """Checks that empty property definitions survive into po file, bug 15."""
        for delimiter in ["=", ""]:
            propsource = f"# comment\ncredit{delimiter}"
            pofile = self.prop2po(propsource)
            pounit = self.singleelement(pofile)
            assert pounit.getlocations() == ["credit"]
            assert pounit.getcontext() == "credit"
            assert 'msgctxt "credit"' in str(pounit)
            assert b"#. comment" in bytes(pofile)
            assert pounit.source == ""

    def test_emptyproperty_translated(self) -> None:
        """Checks that if we translate an empty property it makes it into the PO."""
        for delimiter in ["=", ""]:
            proptemplate = f"credit{delimiter}"
            propsource = "credit=Translators Names"
            pofile = self.prop2po(propsource, proptemplate)
            pounit = self.singleelement(pofile)
            assert pounit.getlocations() == ["credit"]
            # FIXME we don't seem to get a _: comment but we should
            # assert pounit.getcontext() == "credit"
            assert pounit.source == ""
            assert pounit.target == "Translators Names"

    def test_newlines_in_value(self) -> None:
        """Check that we can carry newlines that appear in the property value into the PO."""
        propsource = """prop=\\nvalue\\n\n"""
        pofile = self.prop2po(propsource)
        unit = self.singleelement(pofile)
        assert unit.source == "\nvalue\n"

    def test_header_comments(self) -> None:
        """Check that we can handle comments not directly associated with a property."""
        propsource = """# Header comment\n\n# Comment\n\nprop=value\n"""
        pofile = self.prop2po(propsource)
        unit = self.singleelement(pofile)
        assert unit.source == "value"
        assert unit.getnotes("developer") == "Comment"

    def test_unassociated_comment_order(self) -> None:
        """Check that we can handle the order of unassociated comments."""
        propsource = """# Header comment\n\n# 1st Unassociated comment\n\n# 2nd Connected comment\nprop=value\n"""
        pofile = self.prop2po(propsource)
        unit = self.singleelement(pofile)
        assert unit.source == "value"
        assert (
            unit.getnotes("developer")
            == "1st Unassociated comment\n\n2nd Connected comment"
        )

    def test_x_header(self) -> None:
        """
        Test that we correctly create the custom header entries
        (accelerators, merge criterion).
        """
        propsource = """prop=value\n"""

        outputpo = self.prop2po(propsource, personality="mozilla")
        assert b"X-Accelerator-Marker" in bytes(outputpo)
        assert b"X-Merge-On" in bytes(outputpo)

        # Even though the gaia flavour inherits from mozilla, it should not
        # get the header
        outputpo = self.prop2po(propsource, personality="gaia")
        assert b"X-Accelerator-Marker" not in bytes(outputpo)
        assert b"X-Merge-On" not in bytes(outputpo)

    def test_gaia_plurals(self) -> None:
        """Test conversion of gaia plural units."""
        propsource = """
message-multiedit-header={[ plural(n) ]}
message-multiedit-header[zero]=Edit
message-multiedit-header[one]={{ n }} selected
message-multiedit-header[two]={{ n }} selected
message-multiedit-header[few]={{ n }} selected
message-multiedit-header[many]={{ n }} selected
message-multiedit-header[other]={{ n }} selected
"""
        outputpo = self.prop2po(propsource, personality="gaia")
        pounit = outputpo.units[-1]
        assert pounit.hasplural()
        assert pounit.getlocations() == ["message-multiedit-header"]

        print(outputpo)
        zero_unit = outputpo.units[-2]
        assert not zero_unit.hasplural()
        assert zero_unit.source == "Edit"

    def test_successive_gaia_plurals(self) -> None:
        """Test conversion of two successive gaia plural units."""
        propsource = """
message-multiedit-header={[ plural(n) ]}
message-multiedit-header[zero]=Edit
message-multiedit-header[one]={{ n }} selected
message-multiedit-header[two]={{ n }} selected
message-multiedit-header[few]={{ n }} selected
message-multiedit-header[many]={{ n }} selected
message-multiedit-header[other]={{ n }} selected

message-multiedit-header2={[ plural(n) ]}
message-multiedit-header2[zero]=Edit 2
message-multiedit-header2[one]={{ n }} selected 2
message-multiedit-header2[two]={{ n }} selected 2
message-multiedit-header2[few]={{ n }} selected 2
message-multiedit-header2[many]={{ n }} selected 2
message-multiedit-header2[other]={{ n }} selected 2
"""
        outputpo = self.prop2po(propsource, personality="gaia")
        pounit = outputpo.units[-1]
        assert pounit.hasplural()
        assert pounit.getlocations() == ["message-multiedit-header2"]

        pounit = outputpo.units[-3]
        assert pounit.hasplural()
        assert pounit.getlocations() == ["message-multiedit-header"]

        print(outputpo)
        zero_unit = outputpo.units[-2]
        assert not zero_unit.hasplural()
        assert zero_unit.source == "Edit 2"

        zero_unit = outputpo.units[-4]
        assert not zero_unit.hasplural()
        assert zero_unit.source == "Edit"

    def test_duplicate_keys(self) -> None:
        """Check that we correctly handle duplicate keys."""
        source = """
key=value
key=value
"""
        po_file = self.prop2po(source)
        assert self.countelements(po_file) == 1
        po_unit = self.singleelement(po_file)
        assert po_unit.source == "value"

        source = """
key=value
key=another value
"""
        po_file = self.prop2po(source)
        assert self.countelements(po_file) == 2
        po_unit = po_file.units[1]
        assert po_unit.source == "value"
        assert po_unit.getlocations() == ["key"]
        po_unit = po_file.units[2]
        assert po_unit.source == "another value"
        assert po_unit.getlocations() == ["key"]

        source = """
key1=value
key2=value
"""
        po_file = self.prop2po(source)
        assert self.countelements(po_file) == 2
        po_unit = po_file.units[1]
        assert po_unit.source == "value"
        assert po_unit.getlocations() == ["key1"]
        po_unit = po_file.units[2]
        assert po_unit.source == "value"
        assert po_unit.getlocations() == ["key2"]

    def test_gwt_plurals(self) -> None:
        """Test conversion of gwt plural units."""
        propsource = """
message-multiedit-header={0,number} selected
message-multiedit-header[none]=Edit
message-multiedit-header[one]={0,number} selected
message-multiedit-header[two]={0,number} selected
message-multiedit-header[few]={0,number} selected
message-multiedit-header[many]={0,number} selected
"""
        outputpo = self.prop2po(propsource, personality="gwt")
        pounit = outputpo.units[-1]
        assert pounit.getlocations() == ["message-multiedit-header"]

    def test_strings_bilingual_simple(self) -> None:
        """Test that .strings files are treated as bilingual in convertstore mode."""
        propsource = r""""Source text" = "Translated text";"""
        # .strings files use UTF-16 encoding
        inputfile = BytesIO(propsource.encode("utf-16"))
        inputprop = properties.propfile(inputfile, personality="strings")
        convertor = prop2po.prop2po(personality="strings")
        outputpo = convertor.convertstore(inputprop)
        pounit = self.singleelement(outputpo)
        # After fix, source should be left side (key) and target should be right side (value)
        assert pounit.source == "Source text", (
            f"Expected 'Source text' but got {pounit.source!r}"
        )
        assert pounit.target == "Translated text", (
            f"Expected 'Translated text' but got {pounit.target!r}"
        )

    def test_strings_bilingual_multiline(self) -> None:
        """Test multiline .strings bilingual conversion (issue from bug report)."""
        # This is based on the actual bug report
        propsource = r"""/* Overwrite of the app folder */
"A '<AppNameRemovedForPrivacy>' folder has been found in  your Music folder. Unfortunately it seems incomplete.\n\nTo restart <AppNameRemovedForPrivacy> synchronization, trash the ~/Music/<AppNameRemovedForPrivacy> folder and relaunch the application." = "An incomplete <AppNameRemovedForPrivacy> folder has been found in your Music folder.\n\nTo restart, delete the ~/Music/<AppNameRemovedForPrivacy> folder and relaunch the application.";
"""
        # .strings files use UTF-16 encoding
        inputfile = BytesIO(propsource.encode("utf-16"))
        inputprop = properties.propfile(inputfile, personality="strings")
        convertor = prop2po.prop2po(personality="strings")
        outputpo = convertor.convertstore(inputprop)
        pounit = self.singleelement(outputpo)
        # The msgid should be the source (left side), not the translation (right side)
        assert "A '<AppNameRemovedForPrivacy>' folder has been found" in pounit.source
        assert "Unfortunately it seems incomplete" in pounit.source
        # The msgstr should be the translation (right side)
        assert "An incomplete <AppNameRemovedForPrivacy> folder" in pounit.target
        assert "To restart, delete" in pounit.target
        # The location should be the key (not URL-encoded full text)
        locations = pounit.getlocations()
        assert len(locations) == 1
        # Location should be a reasonable identifier, not the full multiline text
        assert "A '<AppNameRemovedForPrivacy>' folder" in locations[0]

    def test_strings_merge_mode_uses_template_value_as_msgid(self) -> None:
        """Ensure merge mode keeps standard monolingual behavior for .strings."""
        templatesource = r""""Hello" = "Hello from template";"""
        translationsource = r""""Hello" = "Hola des de la plantilla";"""

        templatefile = BytesIO(templatesource.encode("utf-16"))
        translationfile = BytesIO(translationsource.encode("utf-16"))
        templateprop = properties.propfile(templatefile, personality="strings")
        translationprop = properties.propfile(translationfile, personality="strings")

        convertor = prop2po.prop2po(personality="strings")
        outputpo = convertor.mergestore(templateprop, translationprop)
        pounit = self.singleelement(outputpo)

        # In merge mode, msgid comes from the template value and msgstr from input value.
        assert pounit.source == "Hello from template"
        assert pounit.target == "Hola des de la plantilla"
        assert pounit.getlocations() == ["Hello"]

    def test_strings_merge_template(self) -> None:
        """Test that .strings merge mode uses template value as msgid."""
        template_source = b'"greeting" = "Hello";\n"farewell" = "Goodbye";\n'
        input_source = b'"greeting" = "Hola";\n"farewell" = "Adeu";\n'

        template_file = BytesIO(template_source)
        input_file = BytesIO(input_source)

        template_prop = properties.propfile(template_file, personality="strings-utf8")
        input_prop = properties.propfile(input_file, personality="strings-utf8")

        convertor = prop2po.prop2po(personality="strings-utf8")
        outputpo = convertor.mergestore(template_prop, input_prop)

        units = [u for u in outputpo.units if u.source]
        assert len(units) == 2
        # msgid must be the English value from the template, not the key
        assert units[0].source == "Hello"
        assert units[0].target == "Hola"
        assert units[1].source == "Goodbye"
        assert units[1].target == "Adeu"


class TestProp2POCommand(test_convert.TestConvertCommand, TestProp2PO):
    """Tests running actual prop2po commands on files."""

    convertmodule = prop2po
    defaultoptions = {"progress": "none"}
    expected_options = [
        "-P, --pot",
        "-t TEMPLATE, --template=TEMPLATE",
        "--personality=TYPE",
        "--encoding=ENCODING",
        "--duplicates=DUPLICATESTYLE",
    ]
