"""Tests for converting WiX Localization (.wxl) files to/from Gettext PO."""

from io import BytesIO

from translate.convert import po2wxl, wxl2po
from translate.misc.xml_helpers import parse_xml
from translate.storage import wxl

from . import test_convert

WXL_V4_TEMPLATE = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" Codepage="65001">
  <String Id="WixUIBack" Overridable="yes" Value="Back" />
  <UI Id="WixUI_Mondo" />
  <String Id="WixUICancel" Overridable="yes" Value="Cancel" />
  <String Id="WixUINext" Overridable="yes" Value="Next" />
</WixLocalization>
"""

WXL_V3_TEMPLATE = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://schemas.microsoft.com/wix/2006/localization" Culture="cs-cz">
  <String Id="WixUIBack">Back</String>
  <String Id="WixUICancel">Cancel</String>
</WixLocalization>
"""

PO_CONTENT = """
#: WixUIBack
msgid "WixUIBack"
msgstr "Zurueck"

#: WixUICancel
msgid "WixUICancel"
msgstr "Abbrechen"
"""


class TestWxl2PO:
    WXL_V4 = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" Codepage="1252">
  <String Id="WixUIBack" Overridable="yes" Value="Zurueck" />
  <String Id="WixUICancel" Overridable="yes" Value="Abbrechen" />
</WixLocalization>
"""

    WXL_V3 = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://schemas.microsoft.com/wix/2006/localization" Culture="pt-br" Codepage="1252">
  <String Id="WixUIBack">Voltar</String>
  <String Id="WixUICancel">Cancelar</String>
</WixLocalization>
"""

    WXL_EN = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us">
  <String Id="WixUIBack" Value="Back" />
  <String Id="WixUICancel" Value="Cancel" />
  <String Id="WixUINext" Value="Next" />
</WixLocalization>
"""

    @staticmethod
    def _convert(input_bytes):
        inputfile = BytesIO(input_bytes)
        outputfile = BytesIO()
        converter = wxl2po.wxl2po(inputfile, outputfile)
        result = converter.run()
        return result, converter.target_store, outputfile

    @staticmethod
    def _convert_with_template(input_bytes, template_bytes):
        inputfile = BytesIO(input_bytes)
        outputfile = BytesIO()
        templatefile = BytesIO(template_bytes)
        converter = wxl2po.wxl2po(inputfile, outputfile, templatefile)
        result = converter.run()
        return result, converter.target_store, outputfile

    def test_convert_v4_value_attribute(self) -> None:
        """WiX v4 Value-attribute strings are converted to PO units."""
        result, store, _ = self._convert(self.WXL_V4)
        assert result == 1
        # skip header
        units = [u for u in store.units if not u.isheader()]
        assert len(units) == 2
        assert store.findunit("WixUIBack") is not None
        assert store.findunit("WixUIBack").target == "Zurueck"

    def test_convert_v3_text_content(self) -> None:
        """WiX v3 text-content strings are converted to PO units."""
        result, store, _ = self._convert(self.WXL_V3)
        assert result == 1
        units = [u for u in store.units if not u.isheader()]
        assert len(units) == 2
        assert store.findunit("WixUIBack").target == "Voltar"
        assert store.findunit("WixUICancel").target == "Cancelar"

    def test_empty_file_returns_zero(self) -> None:
        """An empty WXL file yields a return code of 0."""
        content = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" />
"""
        result, _, _ = self._convert(content)
        assert result == 0

    def test_output_contains_po_header(self) -> None:
        """Converted PO output contains the standard PO header."""
        _, _, outputfile = self._convert(self.WXL_V4)
        output = outputfile.getvalue().decode("utf-8")
        assert "Project-Id-Version" in output
        assert 'msgid "WixUIBack"' in output

    def test_merge_with_template(self) -> None:
        """With a template, msgid comes from template and msgstr from input."""
        wxl_de = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de">
  <String Id="WixUIBack" Value="Zurueck" />
  <String Id="WixUICancel" Value="Abbrechen" />
</WixLocalization>
"""
        result, store, _ = self._convert_with_template(wxl_de, self.WXL_EN)
        assert result == 1
        units = [u for u in store.units if not u.isheader()]
        # All template keys appear in output
        assert len(units) == 3
        back_unit = store.findunit("WixUIBack")
        assert back_unit is not None
        assert back_unit.target == "Zurueck"
        cancel_unit = store.findunit("WixUICancel")
        assert cancel_unit is not None
        assert cancel_unit.target == "Abbrechen"

    def test_merge_template_missing_translation(self) -> None:
        """Keys in template but absent in input appear with empty msgstr."""
        wxl_de = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de">
  <String Id="WixUIBack" Value="Zurueck" />
</WixLocalization>
"""
        result, store, _ = self._convert_with_template(wxl_de, self.WXL_EN)
        assert result == 1
        units = [u for u in store.units if not u.isheader()]
        # All three template keys appear
        assert len(units) == 3
        # WixUINext has no translation in input → empty msgstr
        next_unit = store.findunit("WixUINext")
        assert next_unit is not None
        assert next_unit.target == ""

    def test_merge_non_translatable_ui_excluded(self) -> None:
        """Non-translatable UI elements (no Text attr) are excluded from PO output."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us">
  <String Id="WixUIBack" Value="Back" />
  <UI Id="WixUI_Mondo" />
  <String Id="WixUINext" Value="Next" />
</WixLocalization>
"""
        wxl_de = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de">
  <String Id="WixUIBack" Value="Zurueck" />
  <String Id="WixUINext" Value="Weiter" />
</WixLocalization>
"""
        result, store, outputfile = self._convert_with_template(wxl_de, template)
        assert result == 1
        units = [u for u in store.units if not u.isheader()]
        # WixUI_Mondo (non-translatable) must not appear as a PO unit
        assert len(units) == 2
        po_text = outputfile.getvalue().decode("utf-8")
        assert "WixUI_Mondo" not in po_text
        assert 'msgid "WixUIBack"' in po_text
        assert 'msgstr "Zurueck"' in po_text

    def test_merge_xml_comments_excluded(self) -> None:
        """XML comments in the WXL template are excluded from PO output."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us">
  <!-- Buttons -->
  <String Id="WixUIBack" Value="Back" />
  <!-- Navigation -->
  <String Id="WixUINext" Value="Next" />
</WixLocalization>
"""
        wxl_de = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de">
  <String Id="WixUIBack" Value="Zurueck" />
  <String Id="WixUINext" Value="Weiter" />
</WixLocalization>
"""
        result, _store, outputfile = self._convert_with_template(wxl_de, template)
        assert result == 1
        po_text = outputfile.getvalue().decode("utf-8")
        assert "Buttons" not in po_text
        assert "Navigation" not in po_text
        assert 'msgid "WixUIBack"' in po_text
        assert 'msgstr "Zurueck"' in po_text
        assert 'msgid "WixUINext"' in po_text
        assert 'msgstr "Weiter"' in po_text

    def test_merge_translatable_ui_with_text_included(self) -> None:
        """UI elements with a Text attribute are treated as translatable."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us">
  <UI Id="WixUIInstallTitle" Text="Installation" />
  <UI Id="WixUI_Mondo" />
</WixLocalization>
"""
        wxl_de = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de">
  <UI Id="WixUIInstallTitle" Text="Installation DE" />
</WixLocalization>
"""
        result, store, outputfile = self._convert_with_template(wxl_de, template)
        assert result == 1
        units = [u for u in store.units if not u.isheader()]
        # WixUI_Mondo has no Text → not translatable, so only 1 unit
        assert len(units) == 1
        po_text = outputfile.getvalue().decode("utf-8")
        assert 'msgid "WixUIInstallTitle"' in po_text
        assert 'msgstr "Installation DE"' in po_text
        assert "WixUI_Mondo" not in po_text

    def test_no_template_non_translatable_ui_excluded(self) -> None:
        """Without a template, non-translatable UI elements are excluded from PO."""
        content = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de">
  <!-- A comment -->
  <String Id="WixUIBack" Value="Zurueck" />
  <UI Id="WixUI_Mondo" />
  <UI Id="WixUIInstallTitle" Text="Installation DE" />
</WixLocalization>
"""
        result, store, outputfile = self._convert(content)
        assert result == 1
        units = [u for u in store.units if not u.isheader()]
        assert len(units) == 2
        po_text = outputfile.getvalue().decode("utf-8")
        assert "WixUI_Mondo" not in po_text
        assert "A comment" not in po_text
        assert 'msgid "WixUIBack"' in po_text
        assert 'msgid "WixUIInstallTitle"' in po_text

    def test_merge_v3_template(self) -> None:
        """Template handling works with WiX v3 text-content format."""
        wxl_en_v3 = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://schemas.microsoft.com/wix/2006/localization" Culture="en-us">
  <String Id="WixUIBack">Back</String>
  <String Id="WixUICancel">Cancel</String>
</WixLocalization>
"""
        result, store, _ = self._convert_with_template(self.WXL_V3, wxl_en_v3)
        assert result == 1
        back_unit = store.findunit("WixUIBack")
        assert back_unit is not None
        assert back_unit.target == "Voltar"
        cancel_unit = store.findunit("WixUICancel")
        assert cancel_unit is not None
        assert cancel_unit.target == "Cancelar"


class TestPO2WXL:
    PO_SOURCE = b"""
msgid "WixUIBack"
msgstr "Voltar"

msgid "WixUICancel"
msgstr "Cancelar"
"""

    @staticmethod
    def _convert(po_bytes, template_bytes=None):
        inputfile = BytesIO(po_bytes)
        outputfile = BytesIO()
        templatefile = BytesIO(template_bytes) if template_bytes else None
        converter = po2wxl.po2wxl(inputfile, outputfile, templatefile)
        result = converter.run()
        return result, converter.target_store, outputfile

    def _convert_to_string(self, po_bytes, template_bytes=None):
        _, _, outputfile = self._convert(po_bytes, template_bytes)
        return outputfile.getvalue().decode("utf-8")

    def test_basic_conversion(self) -> None:
        """PO units are written as WXL String elements."""
        output = self._convert_to_string(self.PO_SOURCE)
        assert "WixUIBack" in output
        assert "Voltar" in output
        assert "WixUICancel" in output
        assert "Cancelar" in output

    def test_output_is_valid_xml(self) -> None:
        """po2wxl output is valid XML."""
        output_bytes = BytesIO(self.PO_SOURCE)
        out = BytesIO()
        po2wxl.po2wxl(output_bytes, out).run()
        # Should not raise
        parse_xml(out.getvalue())

    def test_with_template(self) -> None:
        """When a template is provided, extra attributes are preserved."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" Codepage="1252">
  <String Id="WixUIBack" Overridable="yes" Value="" />
  <String Id="WixUICancel" Overridable="yes" Value="" />
</WixLocalization>
"""
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"

msgid "WixUICancel"
msgstr "Abbrechen"
"""
        output = self._convert_to_string(po, template)
        assert 'Overridable="yes"' in output
        assert "Zurueck" in output
        assert "Abbrechen" in output

    def test_culture_from_template(self) -> None:
        """Culture attribute is preserved from the template."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="cs-cz">
  <String Id="Key" Value="" />
</WixLocalization>
"""
        po = b"""
msgid "Key"
msgstr "Hodnota"
"""
        output = self._convert_to_string(po, template)
        assert 'Culture="cs-cz"' in output

    def test_template_preserves_xml_comments(self) -> None:
        """XML comments in the template are preserved in po2wxl WXL output."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us">
  <!-- Navigation buttons -->
  <String Id="WixUIBack" Value="Back" />
  <!-- Footer -->
  <String Id="WixUINext" Value="Next" />
</WixLocalization>
"""
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"

msgid "WixUINext"
msgstr "Weiter"
"""
        output = self._convert_to_string(po, template)
        assert "<!-- Navigation buttons -->" in output
        assert "<!-- Footer -->" in output
        assert "Zurueck" in output
        assert "Weiter" in output

    def test_template_preserves_non_translatable_ui(self) -> None:
        """Non-translatable UI elements (no Text attr) are preserved in po2wxl WXL output."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us">
  <String Id="WixUIBack" Value="Back" />
  <UI Id="WixUI_Mondo" />
  <String Id="WixUINext" Value="Next" />
</WixLocalization>
"""
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"

msgid "WixUINext"
msgstr "Weiter"
"""
        output = self._convert_to_string(po, template)
        assert 'Id="WixUI_Mondo"' in output
        assert "Zurueck" in output
        assert "Weiter" in output

    def test_template_output_matches_structure(self) -> None:
        """po2wxl output with template is structurally identical to template with changed values."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us">
  <!-- Buttons -->
  <String Id="WixUIBack" Value="Back" />
  <UI Id="WixUI_Mondo" />
</WixLocalization>
"""
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"
"""
        output = self._convert_to_string(po, template)
        # Comment preserved
        assert "<!-- Buttons -->" in output
        # Non-translatable UI preserved
        assert 'Id="WixUI_Mondo"' in output
        # Translation applied
        assert 'Value="Zurueck"' in output
        # Source value replaced (not present)
        assert 'Value="Back"' not in output

    def test_empty_po_returns_zero(self) -> None:
        """An empty PO file yields a return code of 0."""
        result, _, _ = self._convert(b"")
        assert result == 0

    def test_comments_preserved(self) -> None:
        """XML comments in the template are preserved in the output."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" Codepage="65001">
  <!-- Back button label -->
  <String Id="WixUIBack" Value="Back" />
  <!-- Cancel button label -->
  <String Id="WixUICancel" Value="Cancel" />
</WixLocalization>
"""
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"

msgid "WixUICancel"
msgstr "Abbrechen"
"""
        output = self._convert_to_string(po, template)
        assert "<!-- Back button label -->" in output
        assert "<!-- Cancel button label -->" in output
        assert "Zurueck" in output
        assert "Abbrechen" in output

    def test_nontranslatable_ui_preserved(self) -> None:
        """Non-translatable UI elements in the template are preserved."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" Codepage="65001">
  <String Id="WixUIBack" Value="Back" />
  <UI Id="WixUI_Advanced" />
  <UI Id="WixUI_Minimal" ControlType="Button" />
</WixLocalization>
"""
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"
"""
        output = self._convert_to_string(po, template)
        assert 'Id="WixUI_Advanced"' in output
        assert 'Id="WixUI_Minimal"' in output
        assert 'ControlType="Button"' in output

    def test_untranslated_keeps_template_value(self) -> None:
        """Strings without a translation keep the value from the template."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" Codepage="65001">
  <String Id="WixUIBack" Value="Back" />
  <String Id="WixUICancel" Value="Cancel" />
</WixLocalization>
"""
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"
"""
        _, store, _ = self._convert(po, template)
        assert store.findid("WixUIBack").target == "Zurueck"
        assert store.findid("WixUICancel").target == "Cancel"

    def test_untranslated_no_template(self) -> None:
        """Without a template, untranslated units produce an empty string."""
        po = b"""
msgid "WixUIBack"
msgstr ""
"""
        _, store, _ = self._convert(po)
        unit = store.findid("WixUIBack")
        assert unit is not None
        assert unit.target == ""

    def test_element_order_preserved(self) -> None:
        """Element ordering from the template is preserved in the output."""
        template = b"""<?xml version="1.0" encoding="utf-8"?>
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="de-de" Codepage="65001">
  <String Id="ZZZ" Value="Last" />
  <String Id="AAA" Value="First" />
</WixLocalization>
"""
        po = b"""
msgid "ZZZ"
msgstr "ZZZ-translated"

msgid "AAA"
msgstr "AAA-translated"
"""
        output = self._convert_to_string(po, template)
        assert output.index("ZZZ") < output.index("AAA")


class TestPO2WXLCommand(test_convert.TestConvertCommand):
    """Tests running actual po2wxl commands on files."""

    convertmodule = po2wxl
    defaultoptions = {"progress": "none"}
    expected_options = [
        "-t TEMPLATE, --template=TEMPLATE",
    ]

    def test_convert(self) -> None:
        """Tests the conversion to a WXL file using a template."""
        self.create_testfile("template.wxl", WXL_V4_TEMPLATE)
        self.create_testfile("input.po", PO_CONTENT)
        self.run_command(template="template.wxl", i="input.po", o="output.wxl")
        with self.open_testfile("output.wxl") as handle:
            content = handle.read()
        wxl_result = wxl.WxlFile(BytesIO(content))
        assert wxl_result.findid("WixUIBack").target == "Zurueck"
        assert wxl_result.findid("WixUICancel").target == "Abbrechen"
        assert 'Id="WixUI_Mondo"' in content.decode("utf-8")

    def test_preserve_comments(self) -> None:
        """XML comments in the template are preserved in the output."""
        template_with_comments = (
            b'<?xml version="1.0" encoding="utf-8"?>\n'
            b'<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl"'
            b' Culture="de-de" Codepage="65001">\n'
            b"  <!-- Back button label -->\n"
            b'  <String Id="WixUIBack" Value="Back" />\n'
            b"  <!-- Cancel button label -->\n"
            b'  <String Id="WixUICancel" Value="Cancel" />\n'
            b"</WixLocalization>\n"
        )
        self.create_testfile("template.wxl", template_with_comments)
        self.create_testfile("input.po", PO_CONTENT)
        self.run_command(template="template.wxl", i="input.po", o="output.wxl")
        with self.open_testfile("output.wxl") as handle:
            content = handle.read().decode("utf-8")
        assert "<!-- Back button label -->" in content
        assert "<!-- Cancel button label -->" in content
        assert "Zurueck" in content
        assert "Abbrechen" in content

    def test_preserve_nontranslatable_ui(self) -> None:
        """Non-translatable UI elements in the template are preserved."""
        template_with_ui = (
            b'<?xml version="1.0" encoding="utf-8"?>\n'
            b'<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl"'
            b' Culture="de-de" Codepage="65001">\n'
            b'  <String Id="WixUIBack" Value="Back" />\n'
            b'  <UI Id="WixUI_Advanced" />\n'
            b'  <UI Id="WixUI_Minimal" ControlType="Button" />\n'
            b"</WixLocalization>\n"
        )
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"
"""
        self.create_testfile("template.wxl", template_with_ui)
        self.create_testfile("input.po", po)
        self.run_command(template="template.wxl", i="input.po", o="output.wxl")
        with self.open_testfile("output.wxl") as handle:
            content = handle.read().decode("utf-8")
        assert 'Id="WixUI_Advanced"' in content
        assert 'Id="WixUI_Minimal"' in content
        assert 'ControlType="Button"' in content

    def test_untranslated_kept_from_template(self) -> None:
        """Untranslated strings use the value from the template."""
        self.create_testfile("template.wxl", WXL_V4_TEMPLATE)
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"
"""
        self.create_testfile("input.po", po)
        self.run_command(template="template.wxl", i="input.po", o="output.wxl")
        with self.open_testfile("output.wxl") as handle:
            wxl_result = wxl.WxlFile(handle)
        assert wxl_result.findid("WixUIBack").target == "Zurueck"
        assert wxl_result.findid("WixUINext").target == "Next"

    def test_convert_v3(self) -> None:
        """V3 text-content format is handled correctly with a template."""
        self.create_testfile("template.wxl", WXL_V3_TEMPLATE)
        po = b"""
msgid "WixUIBack"
msgstr "Zurueck"

msgid "WixUICancel"
msgstr "Abbrechen"
"""
        self.create_testfile("input.po", po)
        self.run_command(template="template.wxl", i="input.po", o="output.wxl")
        with self.open_testfile("output.wxl") as handle:
            wxl_result = wxl.WxlFile(handle)
        assert wxl_result.findid("WixUIBack").target == "Zurueck"
        assert wxl_result.findid("WixUICancel").target == "Abbrechen"
