"""
Comprehensive tests for project_serializer module
"""

import pytest
import os
import json
import zipfile
import tempfile
import shutil
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock

from pyPhotoAlbum.project_serializer import (
    save_to_zip,
    load_from_zip,
    get_project_info,
    _normalize_asset_paths,
    _import_external_images,
    SERIALIZATION_VERSION,
)
from pyPhotoAlbum.project import Project
from pyPhotoAlbum.models import ImageData


class TestSaveToZip:
    """Tests for save_to_zip function"""

    def test_save_to_zip_basic(self, tmp_path):
        """Test basic project saving to zip"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="TestProject", folder_path=str(project_folder))

        zip_path = str(tmp_path / "test_project.ppz")
        success, error = save_to_zip(project, zip_path)

        assert success is True
        assert error is None
        assert os.path.exists(zip_path)

    def test_save_to_zip_adds_extension(self, tmp_path):
        """Test that .ppz extension is added if missing"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="TestProject", folder_path=str(project_folder))

        zip_path = str(tmp_path / "test_project")  # No extension
        success, error = save_to_zip(project, zip_path)

        assert success is True
        assert os.path.exists(zip_path + ".ppz")

    def test_save_to_zip_includes_project_json(self, tmp_path):
        """Test that saved zip contains project.json"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="TestProject", folder_path=str(project_folder))

        zip_path = str(tmp_path / "test_project.ppz")
        save_to_zip(project, zip_path)

        with zipfile.ZipFile(zip_path, "r") as zipf:
            assert "project.json" in zipf.namelist()

            project_data = json.loads(zipf.read("project.json"))
            assert project_data["name"] == "TestProject"
            assert "data_version" in project_data

    def test_save_to_zip_includes_assets(self, tmp_path):
        """Test that saved zip includes asset files"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        # Create a dummy asset file
        asset_file = assets_folder / "image.jpg"
        asset_file.write_bytes(b"fake image data")

        project = Project(name="TestProject", folder_path=str(project_folder))

        zip_path = str(tmp_path / "test_project.ppz")
        save_to_zip(project, zip_path)

        with zipfile.ZipFile(zip_path, "r") as zipf:
            assert "assets/image.jpg" in zipf.namelist()

    def test_save_to_zip_handles_error(self, tmp_path):
        """Test error handling during save"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="TestProject", folder_path=str(project_folder))

        # Try to save to an invalid path
        zip_path = "/nonexistent/directory/test.ppz"
        success, error = save_to_zip(project, zip_path)

        assert success is False
        assert error is not None
        assert "Error saving" in error


class TestLoadFromZip:
    """Tests for load_from_zip function"""

    def test_load_from_zip_basic(self, tmp_path):
        """Test basic project loading from zip"""
        # First create a valid project zip
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="LoadTest", folder_path=str(project_folder))
        zip_path = str(tmp_path / "test_project.ppz")
        save_to_zip(project, zip_path)

        # Now load it
        extract_to = str(tmp_path / "extracted")
        loaded_project = load_from_zip(zip_path, extract_to)

        assert loaded_project.name == "LoadTest"
        assert loaded_project.folder_path == extract_to

    def test_load_from_zip_creates_temp_dir(self, tmp_path):
        """Test that loading creates a temp directory when none specified"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="TempTest", folder_path=str(project_folder))
        zip_path = str(tmp_path / "test_project.ppz")
        save_to_zip(project, zip_path)

        # Load without specifying extraction directory
        loaded_project = load_from_zip(zip_path)

        assert loaded_project.name == "TempTest"
        assert loaded_project.folder_path is not None
        assert os.path.exists(loaded_project.folder_path)

        # Should have a _temp_dir attribute
        assert hasattr(loaded_project, "_temp_dir")

    def test_load_from_zip_file_not_found(self, tmp_path):
        """Test loading from nonexistent file"""
        with pytest.raises(FileNotFoundError):
            load_from_zip(str(tmp_path / "nonexistent.ppz"))

    def test_load_from_zip_invalid_zip(self, tmp_path):
        """Test loading from invalid zip file"""
        invalid_file = tmp_path / "invalid.ppz"
        invalid_file.write_text("not a zip file")

        with pytest.raises(Exception):
            load_from_zip(str(invalid_file))

    def test_load_from_zip_missing_project_json(self, tmp_path):
        """Test loading from zip without project.json"""
        zip_path = tmp_path / "no_project.ppz"

        # Create zip without project.json
        with zipfile.ZipFile(str(zip_path), "w") as zipf:
            zipf.writestr("other_file.txt", "some content")

        with pytest.raises(ValueError) as exc_info:
            load_from_zip(str(zip_path))

        assert "project.json not found" in str(exc_info.value)


class TestGetProjectInfo:
    """Tests for get_project_info function"""

    def test_get_project_info_basic(self, tmp_path):
        """Test getting project info from zip"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="InfoTest", folder_path=str(project_folder))
        zip_path = str(tmp_path / "test_project.ppz")
        save_to_zip(project, zip_path)

        info = get_project_info(zip_path)

        assert info is not None
        assert info["name"] == "InfoTest"
        assert "version" in info
        assert "page_count" in info
        assert "page_size_mm" in info
        assert "working_dpi" in info

    def test_get_project_info_invalid_file(self, tmp_path):
        """Test getting info from invalid file"""
        invalid_file = tmp_path / "invalid.ppz"
        invalid_file.write_text("not a zip")

        info = get_project_info(str(invalid_file))

        assert info is None

    def test_get_project_info_nonexistent_file(self, tmp_path):
        """Test getting info from nonexistent file"""
        info = get_project_info(str(tmp_path / "nonexistent.ppz"))

        assert info is None


class TestNormalizeAssetPaths:
    """Tests for _normalize_asset_paths function"""

    def test_normalize_relative_path_unchanged(self, tmp_path):
        """Test that simple relative paths are unchanged"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="Test", folder_path=str(project_folder))

        # Add a page with an image that has a simple relative path
        from pyPhotoAlbum.page_layout import PageLayout

        page_mock = Mock()
        layout = PageLayout(width=210, height=297)
        img = ImageData(image_path="assets/image.jpg")
        layout.add_element(img)
        page_mock.layout = layout
        project.pages = [page_mock]

        _normalize_asset_paths(project, str(project_folder))

        # Path should be unchanged
        assert img.image_path == "assets/image.jpg"

    def test_normalize_absolute_path(self, tmp_path):
        """Test that absolute paths are normalized"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="Test", folder_path=str(project_folder))

        from pyPhotoAlbum.page_layout import PageLayout

        page_mock = Mock()
        layout = PageLayout(width=210, height=297)
        # Use a path that contains /assets/ pattern
        abs_path = str(project_folder / "assets" / "image.jpg")
        img = ImageData(image_path=abs_path)
        layout.add_element(img)
        page_mock.layout = layout
        project.pages = [page_mock]

        _normalize_asset_paths(project, str(project_folder))

        # Path should be normalized to relative
        assert img.image_path == "assets/image.jpg"

    def test_normalize_legacy_path(self, tmp_path):
        """Test normalizing legacy project path format"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="Test", folder_path=str(project_folder))

        from pyPhotoAlbum.page_layout import PageLayout

        page_mock = Mock()
        layout = PageLayout(width=210, height=297)
        # Legacy path format
        img = ImageData(image_path="./projects/old_project/assets/image.jpg")
        layout.add_element(img)
        page_mock.layout = layout
        project.pages = [page_mock]

        _normalize_asset_paths(project, str(project_folder))

        # Should extract just the assets/filename part
        assert img.image_path == "assets/image.jpg"


class TestImportExternalImages:
    """Tests for _import_external_images function"""

    def test_import_external_images_no_external(self, tmp_path):
        """Test with no external images"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="Test", folder_path=str(project_folder))

        from pyPhotoAlbum.page_layout import PageLayout

        page_mock = Mock()
        layout = PageLayout(width=210, height=297)
        img = ImageData(image_path="assets/existing.jpg")
        layout.add_element(img)
        page_mock.layout = layout
        project.pages = [page_mock]

        # Should not raise and not change path
        _import_external_images(project)

        assert img.image_path == "assets/existing.jpg"


class TestRoundTrip:
    """Test save and load roundtrip"""

    def test_roundtrip_basic(self, tmp_path):
        """Test saving and loading a project"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        original = Project(name="RoundTrip", folder_path=str(project_folder))
        original.working_dpi = 150

        zip_path = str(tmp_path / "roundtrip.ppz")
        success, _ = save_to_zip(original, zip_path)
        assert success

        extract_to = str(tmp_path / "extracted")
        loaded = load_from_zip(zip_path, extract_to)

        assert loaded.name == original.name
        assert loaded.working_dpi == original.working_dpi

    def test_roundtrip_with_pages(self, tmp_path):
        """Test roundtrip with pages"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        original = Project(name="WithPages", folder_path=str(project_folder))
        # Project starts with 1 page, add more using create_page
        from pyPhotoAlbum.project import Page
        from pyPhotoAlbum.page_layout import PageLayout

        page2 = Page(PageLayout(width=210, height=297))
        page3 = Page(PageLayout(width=210, height=297))
        original.add_page(page2)
        original.add_page(page3)

        zip_path = str(tmp_path / "pages.ppz")
        save_to_zip(original, zip_path)

        extract_to = str(tmp_path / "extracted")
        loaded = load_from_zip(zip_path, extract_to)

        # Pages are preserved (Project might not start with a default page)
        assert len(loaded.pages) >= 2

    def test_roundtrip_with_elements(self, tmp_path, temp_image_file):
        """Test roundtrip with elements on page"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        # Copy temp image to assets
        shutil.copy(temp_image_file, assets_folder / "test.jpg")

        original = Project(name="WithElements", folder_path=str(project_folder))

        # Add element to first page (project starts with at least 1 page)
        img = ImageData(image_path="assets/test.jpg", x=50, y=50, width=100, height=100)
        # Check if there's a default page, add one if needed
        if not original.pages:
            from pyPhotoAlbum.project import Page
            from pyPhotoAlbum.page_layout import PageLayout

            original.add_page(Page(PageLayout(width=210, height=297)))
        original.pages[0].layout.add_element(img)

        zip_path = str(tmp_path / "elements.ppz")
        save_to_zip(original, zip_path)

        extract_to = str(tmp_path / "extracted")
        loaded = load_from_zip(zip_path, extract_to)

        assert len(loaded.pages) >= 1
        assert len(loaded.pages[0].layout.elements) >= 1
        loaded_elem = loaded.pages[0].layout.elements[0]
        assert loaded_elem.position == (50.0, 50.0)
        assert loaded_elem.size == (100.0, 100.0)


class TestVersionCompatibility:
    """Tests for version handling"""

    def test_version_included_in_save(self, tmp_path):
        """Test that version is included when saving"""
        project_folder = tmp_path / "project"
        project_folder.mkdir()
        assets_folder = project_folder / "assets"
        assets_folder.mkdir()

        project = Project(name="Version", folder_path=str(project_folder))
        zip_path = str(tmp_path / "version.ppz")
        save_to_zip(project, zip_path)

        with zipfile.ZipFile(zip_path, "r") as zipf:
            data = json.loads(zipf.read("project.json"))

        # Should have both legacy and new version fields
        assert "serialization_version" in data
        assert "data_version" in data
