"""Unit tests for img2ico module."""

from __future__ import annotations

import json
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from PIL import Image

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from pytola.office.img2ico.img2ico import (
    ImageToIcoConfig,
    ImageToIcoRunner,
    is_valid_image,
    main,
    parse_sizes,
)


class TestParseSizes:
    """Tests for parse_sizes function."""

    def test_parse_single_size(self):
        """Test parsing single size."""
        result = parse_sizes("32")
        assert result == {32}

    def test_parse_multiple_sizes(self):
        """Test parsing multiple sizes."""
        result = parse_sizes("16,32,48,256")
        assert result == {16, 32, 48, 256}

    def test_parse_sizes_with_spaces(self):
        """Test parsing sizes with spaces."""
        result = parse_sizes("16, 32, 48")
        assert result == {16, 32, 48}

    def test_parse_invalid_sizes(self):
        """Test parsing invalid sizes raises ValueError."""
        with pytest.raises(ValueError, match=r"Invalid sizes format: invalid\. Expected comma-separated integers\."):
            parse_sizes("invalid")

    def test_parse_empty_sizes(self):
        """Test parsing empty sizes raises ValueError."""
        with pytest.raises(ValueError, match=r"Invalid sizes format: \. Expected comma-separated integers\."):
            parse_sizes("")


class TestIsValidImage:
    """Tests for is_valid_image function."""

    def test_valid_png_file(self, tmp_path: Path):
        """Test validation of valid PNG file."""
        img_path = tmp_path / "test.png"
        img = Image.new("RGB", (100, 100), color="red")
        img.save(img_path)

        assert is_valid_image(img_path) is True

    def test_valid_jpg_file(self, tmp_path: Path):
        """Test validation of valid JPG file."""
        img_path = tmp_path / "test.jpg"
        img = Image.new("RGB", (100, 100), color="red")
        img.save(img_path, "JPEG")

        assert is_valid_image(img_path) is True

    def test_invalid_extension(self, tmp_path: Path):
        """Test validation of file with invalid extension."""
        txt_path = tmp_path / "test.txt"
        txt_path.write_text("not an image")

        assert is_valid_image(txt_path) is False

    def test_empty_file(self, tmp_path: Path):
        """Test validation of empty file."""
        empty_path = tmp_path / "empty.png"
        empty_path.touch()

        assert is_valid_image(empty_path) is False

    def test_nonexistent_file(self, tmp_path: Path):
        """Test validation of non-existent file."""
        nonexistent = tmp_path / "nonexistent.png"

        assert is_valid_image(nonexistent) is False


class TestImageToIcoRunner:
    """Tests for ImageToIcoRunner class."""

    def test_convert_single_file(self, tmp_path: Path):
        """Test converting single image file."""
        # Create test image
        img_path = tmp_path / "test.png"
        img = Image.new("RGBA", (256, 256), color=(255, 0, 0, 255))
        img.save(img_path)

        # Convert to ICO
        output_path = tmp_path / "output.ico"
        runner = ImageToIcoRunner(
            input_path=img_path,
            output_path=output_path,
            sizes={32, 64},
        )
        runner.run()

        # Verify output
        assert output_path.exists()
        with Image.open(output_path) as ico:
            assert ico.format == "ICO"

            # Verify multiple sizes are included
            sizes_found = set()
            try:
                for i in range(10):  # Check up to 10 frames
                    try:
                        ico.seek(i)
                        sizes_found.add((ico.width, ico.height))
                    except EOFError:
                        break

                # Should contain both 32x32 and 64x64
                expected_sizes = {(32, 32), (64, 64)}
                assert expected_sizes.issubset(sizes_found), (
                    f"Missing sizes. Found: {sizes_found}, Expected subset: {expected_sizes}"
                )

            except Exception:
                # If seeking fails, at least verify the main image
                assert ico.width in [32, 64]
                assert ico.height in [32, 64]

    def test_convert_directory(self, tmp_path: Path):
        """Test converting all images in directory."""
        # Create test images
        for i in range(3):
            img_path = tmp_path / f"test{i}.png"
            img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
            img.save(img_path)

        # Convert directory
        output_dir = tmp_path / "output"
        runner = ImageToIcoRunner(
            input_path=tmp_path,
            output_path=output_dir,
            sizes={32},
        )
        runner.run()

        # Verify output
        assert output_dir.exists()
        ico_files = list(output_dir.glob("*.ico"))
        assert len(ico_files) == 3

    def test_default_output_path_file(self, tmp_path: Path):
        """Test default output path for single file."""
        img_path = tmp_path / "test.png"
        img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
        img.save(img_path)

        runner = ImageToIcoRunner(input_path=img_path, sizes={32})
        runner.run()

        expected_output = tmp_path / "test.ico"
        assert expected_output.exists()

    def test_resize_for_icon(self):
        """Test image resizing for icon."""
        runner = ImageToIcoRunner.__new__(ImageToIcoRunner)

        # Create test image (rectangular)
        img = Image.new("RGBA", (400, 200), color=(255, 0, 0, 255))

        # Resize to 64x64
        result = runner._resize_for_icon(img, 64)

        assert result.size == (64, 64)
        assert result.mode == "RGBA"

    @patch("pytola.office.img2ico.img2ico.logger")
    def test_run_with_nonexistent_path(self, mock_logger: MagicMock):
        """Test run with non-existent path logs error."""
        runner = ImageToIcoRunner(input_path=Path("/nonexistent/path"))
        runner.run()

        mock_logger.error.assert_called_once()


class TestImageToIcoConfig:
    """Tests for ImageToIcoConfig class."""

    def test_default_values(self):
        """Test default configuration values."""
        config = ImageToIcoConfig()

        # Test optimized default sizes for Windows and macOS
        expected_sizes = {16, 24, 32, 48, 64, 128, 256, 512, 1024}
        assert expected_sizes == config.DEFAULT_SIZES
        assert ".png" in config.EXTENSIONS
        assert ".jpg" in config.EXTENSIONS

    def test_custom_values(self):
        """Test custom configuration values."""
        config = ImageToIcoConfig(
            DEFAULT_SIZES={32, 64},
            EXTENSIONS={".png", ".jpg"},
        )

        assert {32, 64} == config.DEFAULT_SIZES
        assert {".png", ".jpg"} == config.EXTENSIONS


class TestMain:
    """Tests for main function."""

    @patch("pytola.office.img2ico.img2ico.ImageToIcoRunner")
    @patch("pytola.office.img2ico.img2ico.parse_args")
    def test_main_with_valid_args(self, mock_parse_args: MagicMock, mock_runner: MagicMock):
        """Test main function with valid arguments."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            img_path = Path(tmp_dir) / "test.png"
            img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
            img.save(img_path)

            mock_args = MagicMock()
            mock_args.input = str(img_path)
            mock_args.output = None
            mock_args.sizes = None
            mock_parse_args.return_value = mock_args

            main()

            mock_runner.assert_called_once()
            mock_runner.return_value.run.assert_called_once()

    @patch("pytola.office.img2ico.img2ico.parse_args")
    @patch("pytola.office.img2ico.img2ico.logger")
    def test_main_with_invalid_sizes(self, mock_logger: MagicMock, mock_parse_args: MagicMock):
        """Test main function with invalid sizes."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            img_path = Path(tmp_dir) / "test.png"
            img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
            img.save(img_path)

            mock_args = MagicMock()
            mock_args.input = str(img_path)
            mock_args.output = None
            mock_args.sizes = "invalid"
            mock_parse_args.return_value = mock_args

            main()

            mock_logger.exception.assert_called_once()

    @patch("pytola.office.img2ico.img2ico.parse_args")
    @patch("pytola.office.img2ico.img2ico.logger")
    def test_main_with_nonexistent_path(self, mock_logger: MagicMock, mock_parse_args: MagicMock):
        """Test main function with non-existent path."""
        mock_args = MagicMock()
        mock_args.input = "/nonexistent/path"
        mock_args.output = None
        mock_args.sizes = None
        mock_parse_args.return_value = mock_args

        main()

        mock_logger.error.assert_called_once()


class TestImageToIcoConfigAdvanced:
    """Advanced tests for ImageToIcoConfig class."""

    def test_config_load_from_file(self, tmp_path: Path):
        """Test loading configuration from file."""
        # Create temp config file
        config_file = tmp_path / "img2ico.json"
        config_data = {
            "DEFAULT_SIZES": [32, 64, 128],
            "EXTENSIONS": [".png", ".jpg"],
        }
        config_file.write_text(json.dumps(config_data))

        # Read config data directly and verify structure
        loaded_data = json.loads(config_file.read_text())
        assert set(loaded_data["DEFAULT_SIZES"]) == {32, 64, 128}
        assert set(loaded_data["EXTENSIONS"]) == {".png", ".jpg"}

    def test_config_load_invalid_json(self, tmp_path: Path, monkeypatch, capsys):
        """Test handling invalid JSON in config file."""
        from pytola.office.img2ico import img2ico

        config_file = tmp_path / "img2ico.json"
        config_file.write_text("invalid json")

        monkeypatch.setattr(img2ico, "CONFIG_FILE", config_file)

        config = ImageToIcoConfig()
        # Should use defaults when config is invalid
        expected_sizes = {16, 24, 32, 48, 64, 128, 256, 512, 1024}
        assert expected_sizes == config.DEFAULT_SIZES

    def test_config_save(self, tmp_path: Path, monkeypatch):
        """Test saving configuration to file."""
        from pytola.office.img2ico import img2ico

        config_file = tmp_path / "img2ico.json"
        monkeypatch.setattr(img2ico, "CONFIG_FILE", config_file)

        config = ImageToIcoConfig(
            DEFAULT_SIZES={64, 128},
            EXTENSIONS={".png"},
        )
        config.save()

        assert config_file.exists()
        saved_data = json.loads(config_file.read_text())
        assert saved_data["DEFAULT_SIZES"] == [64, 128]
        assert saved_data["EXTENSIONS"] == [".png"]


class TestIsValidImageAdvanced:
    """Advanced tests for is_valid_image function."""

    def test_valid_gif_file(self, tmp_path: Path):
        """Test validation of valid GIF file."""
        img_path = tmp_path / "test.gif"
        img = Image.new("RGB", (100, 100), color="red")
        img.save(img_path, "GIF")

        assert is_valid_image(img_path) is True

    def test_valid_bmp_file(self, tmp_path: Path):
        """Test validation of valid BMP file."""
        img_path = tmp_path / "test.bmp"
        img = Image.new("RGB", (100, 100), color="red")
        img.save(img_path, "BMP")

        assert is_valid_image(img_path) is True

    def test_valid_webp_file(self, tmp_path: Path):
        """Test validation of valid WebP file."""
        img_path = tmp_path / "test.webp"
        img = Image.new("RGB", (100, 100), color="red")
        img.save(img_path, "WEBP")

        assert is_valid_image(img_path) is True

    def test_valid_svg_file(self, tmp_path: Path):
        """Test validation of valid SVG file."""
        svg_path = tmp_path / "test.svg"
        svg_content = """<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <circle cx="50" cy="50" r="40" fill="red"/>
</svg>"""
        svg_path.write_text(svg_content, encoding="utf-8")

        assert is_valid_image(svg_path) is True

    def test_invalid_image_header(self, tmp_path: Path):
        """Test validation of file with valid extension but invalid header."""
        img_path = tmp_path / "fake.png"
        img_path.write_text("This is not a real image file")

        assert is_valid_image(img_path) is False

    def test_unreadable_file(self, tmp_path: Path):
        """Test validation of unreadable file."""
        img_path = tmp_path / "test.png"
        img = Image.new("RGB", (100, 100), color="red")
        img.save(img_path)

        # Skip on Windows as chmod doesn't work the same way
        import platform

        if platform.system() == "Windows":
            pytest.skip("Cannot test unreadable file on Windows")

        # Remove read permission
        try:
            img_path.chmod(0o000)
            result = is_valid_image(img_path)
            img_path.chmod(0o644)
            assert result is False
        except (OSError, PermissionError):
            pytest.skip("Cannot test unreadable file on this platform")


class TestImageToIcoRunnerAdvanced:
    """Advanced tests for ImageToIcoRunner class."""

    def test_convert_single_file_with_output_dir(self, tmp_path: Path):
        """Test converting single file with output directory."""
        img_path = tmp_path / "test.png"
        img = Image.new("RGBA", (256, 256), color=(255, 0, 0, 255))
        img.save(img_path)

        output_dir = tmp_path / "output"
        output_dir.mkdir()

        runner = ImageToIcoRunner(
            input_path=img_path,
            output_path=output_dir,
            sizes={32, 64},
        )
        runner.run()

        expected_output = output_dir / "test.ico"
        assert expected_output.exists()

    def test_convert_invalid_image_file(self, tmp_path: Path):
        """Test converting invalid image file logs error."""
        img_path = tmp_path / "fake.png"
        img_path.write_text("Not a real image")

        runner = ImageToIcoRunner(
            input_path=img_path,
            sizes={32},
        )
        runner.run()

        # Should complete without exception, but not create output
        assert not (tmp_path / "fake.ico").exists()

    def test_convert_empty_directory(self, tmp_path: Path):
        """Test converting empty directory."""
        runner = ImageToIcoRunner(
            input_path=tmp_path,
            sizes={32},
        )
        runner.run()

        # Should complete without exception
        assert True

    def test_convert_directory_with_invalid_files(self, tmp_path: Path):
        """Test converting directory with some invalid files."""
        # Create one valid image
        valid_img = tmp_path / "valid.png"
        img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
        img.save(valid_img)

        # Create one invalid file
        invalid_file = tmp_path / "invalid.png"
        invalid_file.write_text("Not an image")

        runner = ImageToIcoRunner(
            input_path=tmp_path,
            sizes={32},
        )
        runner.run()

        # Should create output for valid file only
        output_dir = tmp_path / "ico_output"
        assert output_dir.exists()
        ico_files = list(output_dir.glob("*.ico"))
        assert len(ico_files) == 1

    def test_resize_for_icon_square_image(self):
        """Test resizing square image."""
        runner = ImageToIcoRunner.__new__(ImageToIcoRunner)
        img = Image.new("RGBA", (256, 256), color=(255, 0, 0, 255))

        result = runner._resize_for_icon(img, 64)

        assert result.size == (64, 64)

    def test_resize_for_icon_tall_image(self):
        """Test resizing tall image."""
        runner = ImageToIcoRunner.__new__(ImageToIcoRunner)
        img = Image.new("RGBA", (100, 400), color=(255, 0, 0, 255))

        result = runner._resize_for_icon(img, 64)

        assert result.size == (64, 64)
        # Image should be centered

    def test_image_files_property(self, tmp_path: Path):
        """Test image_files cached property."""
        # Create test images
        for i in range(3):
            img_path = tmp_path / f"test{i}.png"
            img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
            img.save(img_path)

        # Create non-image file
        txt_path = tmp_path / "readme.txt"
        txt_path.write_text("Not an image")

        runner = ImageToIcoRunner(input_path=tmp_path, sizes={32})
        files = runner.image_files

        assert len(files) == 3
        assert all(f.suffix == ".png" for f in files)


class TestParseArgs:
    """Tests for parse_args function."""

    def test_parse_args_default(self):
        """Test parsing args with defaults."""
        from pytola.office.img2ico.img2ico import parse_args

        with patch("sys.argv", ["img2ico"]):
            args = parse_args()

        assert args.input == str(Path.cwd())
        assert args.output is None
        assert args.sizes is None

    def test_parse_args_with_values(self):
        """Test parsing args with custom values."""
        from pytola.office.img2ico.img2ico import parse_args

        with patch("sys.argv", ["img2ico", "input.png", "output.ico", "--sizes", "16,32"]):
            args = parse_args()

        assert args.input == "input.png"
        assert args.output == "output.ico"
        assert args.sizes == "16,32"

    def test_parse_args_short_flag(self):
        """Test parsing args with short flag."""
        from pytola.office.img2ico.img2ico import parse_args

        with patch("sys.argv", ["img2ico", "-s", "32,64"]):
            args = parse_args()

        assert args.sizes == "32,64"


class TestMainAdvanced:
    """Advanced tests for main function."""

    @patch("pytola.office.img2ico.img2ico.parse_args")
    def test_main_with_custom_sizes(self, mock_parse_args: MagicMock, tmp_path: Path):
        """Test main function with custom sizes."""
        img_path = tmp_path / "test.png"
        img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
        img.save(img_path)

        mock_args = MagicMock()
        mock_args.input = str(img_path)
        mock_args.output = None
        mock_args.sizes = "32,64,128"
        mock_parse_args.return_value = mock_args

        main()

        # Verify ICO file was created with correct sizes
        ico_path = img_path.with_suffix(".ico")
        assert ico_path.exists()

    @patch("pytola.office.img2ico.img2ico.parse_args")
    def test_main_with_directory_input(self, mock_parse_args: MagicMock, tmp_path: Path):
        """Test main function with directory input."""
        # Create test images
        for i in range(2):
            img_path = tmp_path / f"test{i}.png"
            img = Image.new("RGBA", (128, 128), color=(255, 0, 0, 255))
            img.save(img_path)

        mock_args = MagicMock()
        mock_args.input = str(tmp_path)
        mock_args.output = None
        mock_args.sizes = None
        mock_parse_args.return_value = mock_args

        main()

        # Verify output directory was created
        output_dir = tmp_path / "ico_output"
        assert output_dir.exists()
        assert len(list(output_dir.glob("*.ico"))) == 2

    def test_convert_svg_file_no_cairosvg(self, tmp_path: Path, monkeypatch, caplog):
        """Test converting SVG file when cairosvg is not available."""
        # Mock cairosvg unavailability
        monkeypatch.setattr("pytola.office.img2ico.img2ico.CAIROSVG_AVAILABLE", False)

        # Create test SVG
        svg_path = tmp_path / "test.svg"
        svg_content = """<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <circle cx="50" cy="50" r="40" fill="red"/>
</svg>"""
        svg_path.write_text(svg_content, encoding="utf-8")

        # Convert to ICO (should fail gracefully)
        output_path = tmp_path / "output.ico"
        runner = ImageToIcoRunner(
            input_path=svg_path,
            output_path=output_path,
            sizes={32},
        )
        runner.run()

        # Should not create output file
        assert not output_path.exists()
