"""Tests for kiln.model_metadata -- 3MF metadata extraction."""

from __future__ import annotations

import zipfile

from kiln.model_metadata import extract_3mf_metadata

_NS = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"


def _make_3mf(tmp_path, model_xml=None, extra_files=None, filename="test.3mf"):
    """Create a minimal .3mf file (ZIP) in *tmp_path* and return its path."""
    fpath = tmp_path / filename
    with zipfile.ZipFile(fpath, "w") as zf:
        if model_xml is not None:
            zf.writestr("3D/3dmodel.model", model_xml)
        if extra_files:
            for name, data in extra_files.items():
                zf.writestr(name, data)
    return str(fpath)


class TestExtract3mfMetadata:
    """Tests for extract_3mf_metadata()."""

    def test_valid_3mf_with_materials_and_colors(self, tmp_path):
        """A well-formed 3MF with basematerials returns materials and colors."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}">'
            f'<resources>'
            f'<basematerials id="1">'
            f'<base name="PLA" displaycolor="#FF0000"/>'
            f'<base name="PETG" displaycolor="#00FF00"/>'
            f'</basematerials>'
            f'</resources>'
            f'</model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert meta["materials"] == ["PLA", "PETG"]
        assert meta["colors"] == ["#FF0000", "#00FF00"]

    def test_no_basematerials_element(self, tmp_path):
        """A 3MF without <basematerials> returns empty lists."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}"><resources/></model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert meta["materials"] == []
        assert meta["colors"] == []

    def test_prusaslicer_metadata_in_config(self, tmp_path):
        """PrusaSlicer .config files under Metadata/ are NOT parsed (only .xml)."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}"><resources/></model>'
        )
        config_content = "; generated by PrusaSlicer\nlayer_height = 0.2\n"
        path = _make_3mf(
            tmp_path, model_xml=xml,
            extra_files={"Metadata/Slic3r_PE.config": config_content},
        )
        meta = extract_3mf_metadata(path)
        assert meta["print_settings"] is None

    def test_non_3mf_extension(self, tmp_path):
        """A file without .3mf extension returns default/empty result."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}"><resources/></model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml, filename="model.stl")
        meta = extract_3mf_metadata(path)
        assert meta["materials"] == []
        assert meta["colors"] == []
        assert meta["print_settings"] is None

    def test_nonexistent_file(self):
        """A path that does not exist returns default/empty result."""
        meta = extract_3mf_metadata("/nonexistent/path/model.3mf")
        assert meta["materials"] == []
        assert meta["colors"] == []
        assert meta["print_settings"] is None

    def test_corrupt_zip(self, tmp_path):
        """A corrupt (non-ZIP) file returns default/empty result."""
        path = tmp_path / "corrupt.3mf"
        path.write_bytes(b"this is not a zip file at all")
        meta = extract_3mf_metadata(str(path))
        assert meta["materials"] == []
        assert meta["colors"] == []
        assert meta["print_settings"] is None

    def test_empty_zip_no_model(self, tmp_path):
        """A valid ZIP with no 3dmodel.model returns empty materials."""
        path = tmp_path / "empty.3mf"
        with zipfile.ZipFile(path, "w") as zf:
            zf.writestr("README.txt", "no model here")
        meta = extract_3mf_metadata(str(path))
        assert meta["materials"] == []
        assert meta["colors"] == []

    def test_multiple_materials(self, tmp_path):
        """Multiple <base> elements inside basematerials are all captured."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}">'
            f'<resources>'
            f'<basematerials id="1">'
            f'<base name="PLA" displaycolor="#FF0000"/>'
            f'<base name="PETG" displaycolor="#00FF00"/>'
            f'<base name="ABS" displaycolor="#0000FF"/>'
            f'<base name="TPU" displaycolor="#FFFF00"/>'
            f'</basematerials>'
            f'</resources>'
            f'</model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert len(meta["materials"]) == 4
        assert meta["materials"] == ["PLA", "PETG", "ABS", "TPU"]
        assert meta["colors"] == ["#FF0000", "#00FF00", "#0000FF", "#FFFF00"]

    def test_colors_without_material_names(self, tmp_path):
        """A base element with empty name is skipped (name is required)."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}">'
            f'<resources>'
            f'<basematerials id="1">'
            f'<base name="" displaycolor="#FF0000"/>'
            f'<base name="PLA" displaycolor="#00FF00"/>'
            f'</basematerials>'
            f'</resources>'
            f'</model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert meta["materials"] == ["PLA"]
        assert meta["colors"] == ["#00FF00"]

    def test_namespaced_xml_elements(self, tmp_path):
        """Elements with the 3MF namespace are found correctly."""
        xml = (
            '<?xml version="1.0" encoding="UTF-8"?>'
            f'<m:model xmlns:m="{_NS}">'
            f'<m:resources>'
            f'<m:basematerials id="1">'
            f'<m:base name="Nylon" displaycolor="#AABBCC"/>'
            f'</m:basematerials>'
            f'</m:resources>'
            f'</m:model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert meta["materials"] == ["Nylon"]
        assert meta["colors"] == ["#AABBCC"]

    def test_model_level_metadata_elements(self, tmp_path):
        """<metadata> elements in the model XML are extracted as print settings."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}">'
            f'<metadata name="slicer">PrusaSlicer</metadata>'
            f'<metadata name="layer_height">0.2</metadata>'
            f'<resources/>'
            f'</model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert meta["print_settings"] is not None
        assert meta["print_settings"]["slicer"] == "PrusaSlicer"
        assert meta["print_settings"]["layer_height"] == "0.2"

    def test_metadata_xml_file(self, tmp_path):
        """XML files under Metadata/ are parsed for key-value settings."""
        model_xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}"><resources/></model>'
        )
        settings_xml = (
            '<?xml version="1.0" encoding="UTF-8"?>'
            '<settings>'
            '<setting name="infill_density" value="20"/>'
            '<setting name="support_material" value="true"/>'
            '</settings>'
        )
        path = _make_3mf(
            tmp_path, model_xml=model_xml,
            extra_files={"Metadata/print_settings.xml": settings_xml},
        )
        meta = extract_3mf_metadata(path)
        assert meta["print_settings"] is not None
        assert meta["print_settings"]["infill_density"] == "20"
        assert meta["print_settings"]["support_material"] == "true"

    def test_empty_file_path(self):
        """An empty string path returns default/empty result."""
        meta = extract_3mf_metadata("")
        assert meta["materials"] == []
        assert meta["colors"] == []
        assert meta["print_settings"] is None

    def test_material_with_missing_color(self, tmp_path):
        """A <base> with name but no displaycolor stores empty color string."""
        xml = (
            f'<?xml version="1.0" encoding="UTF-8"?>'
            f'<model xmlns="{_NS}">'
            f'<resources>'
            f'<basematerials id="1">'
            f'<base name="PLA"/>'
            f'</basematerials>'
            f'</resources>'
            f'</model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert meta["materials"] == ["PLA"]
        assert meta["colors"] == [""]

    def test_no_namespace_xml(self, tmp_path):
        """A model without any XML namespace still works."""
        xml = (
            '<?xml version="1.0" encoding="UTF-8"?>'
            '<model>'
            '<resources>'
            '<basematerials id="1">'
            '<base name="ASA" displaycolor="#112233"/>'
            '</basematerials>'
            '</resources>'
            '</model>'
        )
        path = _make_3mf(tmp_path, model_xml=xml)
        meta = extract_3mf_metadata(path)
        assert meta["materials"] == ["ASA"]
        assert meta["colors"] == ["#112233"]
