import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest import mock

import pptx_image_compress as pic


class TestPptxImageCompress(unittest.TestCase):
    def test_discover_images_filters_extensions(self):
        with tempfile.TemporaryDirectory() as td:
            media_dir = Path(td)
            (media_dir / "a.jpg").write_bytes(b"1")
            (media_dir / "b.png").write_bytes(b"1")
            (media_dir / "c.txt").write_bytes(b"1")
            (media_dir / "d.GIF").write_bytes(b"1")
            (media_dir / "e.svg").write_bytes(b"<svg/>")
            images = pic.discover_images(media_dir)
            self.assertEqual([p.name for p in images], ["a.jpg", "b.png", "d.GIF", "e.svg"])

    def test_image_result_to_log_line(self):
        image_result = pic.ImageProcessResult(
            image_name="image1.png",
            orig_size=1000,
            chosen_size=800,
            slide_nr="[1, 2]",
            image_type_changed="",
        )
        line = pic.image_result_to_log_line(image_result)
        self.assertIn("image1.png", line)
        self.assertIn("[1, 2]", line)
        self.assertIn("20.0", line)
        self.assertTrue(line.endswith(";\n"))

    def test_process_image_file_replaces_when_smaller(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            img = root / "image1.png"
            img.write_bytes(b"A" * 100)
            scratch = root / "scratch"

            def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str):
                out_dir.mkdir(parents=True, exist_ok=True)
                out = out_dir / original.name
                out.write_bytes(b"B" * 40)
                return out

            result = pic.process_image_file(
                idx=1,
                img_path=img,
                scratch_dir=scratch,
                image_to_slides={"image1.png": [1]},
                caesium_threads=1,
                quality=90,
                min_savings="2%",
                compressor=fake_compressor,
            )

            self.assertEqual(result.chosen_size, 40)
            self.assertEqual(img.stat().st_size, 40)
            self.assertEqual(result.slide_nr, "[1]")

    def test_process_image_file_converts_large_png_to_jpg(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            img = root / "image1.png"
            img.write_bytes(b"A" * 800000)
            scratch = root / "scratch"

            def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str, output_format: str = "original"):
                out_dir.mkdir(parents=True, exist_ok=True)
                if output_format == "jpeg":
                    out = out_dir / "image1.jpg"
                    out.write_bytes(b"C" * 200000)
                    return out
                out = out_dir / original.name
                out.write_bytes(b"B" * 700000)
                return out

            result = pic.process_image_file(
                idx=1,
                img_path=img,
                scratch_dir=scratch,
                image_to_slides={"image1.png": [2]},
                caesium_threads=1,
                quality=90,
                min_savings="2%",
                compressor=fake_compressor,
            )

            self.assertEqual(result.image_name, "image1.jpg")
            self.assertEqual(result.chosen_size, 200000)
            self.assertEqual(result.image_type_changed, "png_jpg")
            self.assertFalse(img.exists())
            self.assertTrue((root / "image1.jpg").exists())

    def test_process_image_file_keeps_original_when_bigger(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            img = root / "image1.png"
            img.write_bytes(b"A" * 100)
            scratch = root / "scratch"

            def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str):
                out_dir.mkdir(parents=True, exist_ok=True)
                out = out_dir / original.name
                out.write_bytes(b"B" * 120)
                return out

            result = pic.process_image_file(
                idx=1,
                img_path=img,
                scratch_dir=scratch,
                image_to_slides={},
                caesium_threads=1,
                quality=90,
                min_savings="2%",
                compressor=fake_compressor,
            )

            self.assertEqual(result.chosen_size, 100)
            self.assertEqual(img.stat().st_size, 100)
            self.assertEqual(result.slide_nr, "NOT_USED")
            self.assertEqual(result.image_type_changed, "")

    def test_process_single_deck_with_injected_compressor(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            input_pptx = root / "input.pptx"
            output_pptx = root / "output.pptx"
            source_tree = root / "src"
            rels_dir = source_tree / "ppt" / "slides" / "_rels"
            media_dir = source_tree / "ppt" / "media"
            rels_dir.mkdir(parents=True, exist_ok=True)
            media_dir.mkdir(parents=True, exist_ok=True)

            rels_xml = (
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
                "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
                "<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"../media/image1.png\"/>"
                "</Relationships>"
            )
            (rels_dir / "slide1.xml.rels").write_text(rels_xml, encoding="utf-8")
            (media_dir / "image1.png").write_bytes(b"A" * 100)

            with zipfile.ZipFile(input_pptx, "w", compression=zipfile.ZIP_DEFLATED) as z:
                for p in source_tree.rglob("*"):
                    if p.is_file():
                        z.write(p, arcname=str(p.relative_to(source_tree)))

            def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str):
                out_dir.mkdir(parents=True, exist_ok=True)
                out = out_dir / original.name
                out.write_bytes(b"B" * 50)
                return out

            result = pic.process_single_deck(
                input_pptx=input_pptx,
                output_pptx=output_pptx,
                threads=2,
                quality=90,
                min_savings="2%",
                compressor=fake_compressor,
            )

            self.assertTrue(result.ok)
            self.assertEqual(result.error, None)
            self.assertTrue(output_pptx.exists())
            self.assertIsNotNone(result.log_file)
            with zipfile.ZipFile(output_pptx, "r") as z:
                out_image = z.read("ppt/media/image1.png")
            self.assertEqual(len(out_image), 50)
            log_file = result.log_file
            if log_file is None:
                self.fail("log_file should not be None")
            log_text = Path(log_file).read_text(encoding="utf-8")
            self.assertIn("image_name;size_before(kb);size_after(kb);saving(kb);saving_percent(%);in_slide_number;image_type_changed", log_text)
            self.assertIn("image1.png", log_text)
            self.assertIn("[1]", log_text)
    def test_process_single_deck_updates_rels_on_png_to_jpg(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            input_pptx = root / "input_convert.pptx"
            output_pptx = root / "output_convert.pptx"
            source_tree = root / "src_convert"
            rels_dir = source_tree / "ppt" / "slides" / "_rels"
            media_dir = source_tree / "ppt" / "media"
            rels_dir.mkdir(parents=True, exist_ok=True)
            media_dir.mkdir(parents=True, exist_ok=True)

            rels_xml = (
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
                "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
                "<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"../media/image1.png\"/>"
                "</Relationships>"
            )
            content_types_xml = (
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
                "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"
                "<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>"
                "<Default Extension=\"xml\" ContentType=\"application/xml\"/>"
                "<Default Extension=\"png\" ContentType=\"image/png\"/>"
                "</Types>"
            )
            (source_tree / "[Content_Types].xml").write_text(content_types_xml, encoding="utf-8")
            (rels_dir / "slide1.xml.rels").write_text(rels_xml, encoding="utf-8")
            (media_dir / "image1.png").write_bytes(b"A" * 800000)

            with zipfile.ZipFile(input_pptx, "w", compression=zipfile.ZIP_DEFLATED) as z:
                for p in source_tree.rglob("*"):
                    if p.is_file():
                        z.write(p, arcname=str(p.relative_to(source_tree)))

            def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str, output_format: str = "original"):
                out_dir.mkdir(parents=True, exist_ok=True)
                if output_format == "jpeg":
                    out = out_dir / "image1.jpg"
                    out.write_bytes(b"C" * 200000)
                    return out
                out = out_dir / original.name
                out.write_bytes(b"B" * 700000)
                return out

            result = pic.process_single_deck(
                input_pptx=input_pptx,
                output_pptx=output_pptx,
                threads=1,
                quality=90,
                min_savings="2%",
                compressor=fake_compressor,
            )

            self.assertTrue(result.ok)
            with zipfile.ZipFile(output_pptx, "r") as z:
                self.assertIn("ppt/media/image1.jpg", z.namelist())
                self.assertNotIn("ppt/media/image1.png", z.namelist())
                rels_out = z.read("ppt/slides/_rels/slide1.xml.rels").decode("utf-8")
                content_types_out = z.read("[Content_Types].xml").decode("utf-8")
            self.assertIn("image1.jpg", rels_out)
            self.assertIn("Extension=\"jpg\"", content_types_out)
            log_file = result.log_file
            if log_file is None:
                self.fail("log_file should not be None")
            log_text = Path(log_file).read_text(encoding="utf-8")
            self.assertIn("png_jpg", log_text)


    def test_compress_image_with_routing_delegates_to_raster(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            original = root / "image1.png"
            original.write_bytes(b"A" * 100)
            out_dir = root / "out"

            def fake_compressor(original_path: Path, out_subdir: Path, caesium_threads: int | None, quality: int, min_savings: str):
                out_subdir.mkdir(parents=True, exist_ok=True)
                out = out_subdir / original_path.name
                out.write_bytes(b"B" * 80)
                return out

            out = pic.compress_image_with_routing(
                compressor=fake_compressor,
                original=original,
                out_dir=out_dir,
                caesium_threads=1,
                quality=90,
                min_savings="2%",
            )

            self.assertIsNotNone(out)
            if out is None:
                self.fail("Output should not be None")
            self.assertEqual(out.name, "image1.png")
            self.assertEqual(out.stat().st_size, 80)
    def test_compress_svg_with_svgo_returns_none_when_npx_missing(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            svg = root / "vector.svg"
            svg.write_text("<svg></svg>", encoding="utf-8")
            out_dir = root / "out"

            with mock.patch("pptx_image_compress.which", return_value=None):
                out = pic.compress_svg_with_svgo(svg, out_dir)

            self.assertEqual(out, None)

    def test_compress_image_with_routing_uses_svg_backend(self):
        with tempfile.TemporaryDirectory() as td:
            root = Path(td)
            original = root / "vector.svg"
            original.write_text("<svg></svg>", encoding="utf-8")
            out_dir = root / "out"
            out_dir.mkdir(parents=True, exist_ok=True)
            vector_out = out_dir / "vector.svg"
            vector_out.write_text("<svg/>", encoding="utf-8")

            def fake_compressor(original_path: Path, out_subdir: Path, caesium_threads: int | None, quality: int, min_savings: str):
                raise AssertionError("Raster compressor should not be called for svg")

            with mock.patch("pptx_image_compress.compress_vector_image", return_value=vector_out):
                out = pic.compress_image_with_routing(
                    compressor=fake_compressor,
                    original=original,
                    out_dir=out_dir,
                    caesium_threads=1,
                    quality=90,
                    min_savings="2%",
                )

            self.assertEqual(out, vector_out)


if __name__ == "__main__":
    unittest.main()
