"""
Unit tests for project serialization (save/load to ZIP)
"""

import pytest
import os
import json
import zipfile
import tempfile
import shutil
from pathlib import Path
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.models import ImageData, TextBoxData
from pyPhotoAlbum.project_serializer import save_to_zip, load_from_zip, get_project_info


@pytest.fixture
def temp_dir():
    """Create a temporary directory for testing"""
    temp_path = tempfile.mkdtemp()
    yield temp_path
    # Cleanup
    if os.path.exists(temp_path):
        shutil.rmtree(temp_path)


@pytest.fixture
def sample_project(temp_dir):
    """Create a sample project for testing"""
    project = Project(name="Test Project", folder_path=os.path.join(temp_dir, "test_project"))
    project.page_size_mm = (210, 297)
    project.working_dpi = 300
    project.export_dpi = 300
    return project


@pytest.fixture
def sample_image(temp_dir):
    """Create a sample image file for testing"""
    from PIL import Image

    # Create a simple test image
    img = Image.new("RGB", (100, 100), color="red")
    image_path = os.path.join(temp_dir, "test_image.jpg")
    img.save(image_path)
    return image_path


class TestBasicSerialization:
    """Tests for basic save/load functionality"""

    def test_save_empty_project(self, sample_project, temp_dir):
        """Test saving an empty project to ZIP"""
        zip_path = os.path.join(temp_dir, "empty_project.ppz")

        success, error = save_to_zip(sample_project, zip_path)

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

    def test_save_adds_ppz_extension(self, sample_project, temp_dir):
        """Test that .ppz extension is added automatically"""
        zip_path = os.path.join(temp_dir, "project")

        success, error = save_to_zip(sample_project, zip_path)

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

    def test_load_empty_project(self, sample_project, temp_dir):
        """Test loading an empty project from ZIP"""
        zip_path = os.path.join(temp_dir, "empty_project.ppz")
        save_to_zip(sample_project, zip_path)

        loaded_project = load_from_zip(zip_path)

        assert loaded_project is not None
        assert loaded_project.name == "Test Project"
        assert loaded_project.page_size_mm == (210, 297)
        assert loaded_project.working_dpi == 300
        assert len(loaded_project.pages) == 0

    def test_load_nonexistent_file(self, temp_dir):
        """Test loading from a non-existent file"""
        zip_path = os.path.join(temp_dir, "nonexistent.ppz")

        try:
            loaded_project = load_from_zip(zip_path)
            assert False, "Should have raised an exception"
        except Exception as error:
            assert error is not None
            assert "not found" in str(error).lower()

    def test_save_project_with_pages(self, sample_project, temp_dir):
        """Test saving a project with multiple pages"""
        # Add pages
        for i in range(3):
            layout = PageLayout()
            page = Page(layout=layout, page_number=i + 1)
            sample_project.add_page(page)

        zip_path = os.path.join(temp_dir, "project_with_pages.ppz")
        success, error = save_to_zip(sample_project, zip_path)

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

    def test_load_project_with_pages(self, sample_project, temp_dir):
        """Test loading a project with multiple pages"""
        # Add pages
        for i in range(3):
            layout = PageLayout()
            page = Page(layout=layout, page_number=i + 1)
            sample_project.add_page(page)

        # Save and load
        zip_path = os.path.join(temp_dir, "project_with_pages.ppz")
        save_to_zip(sample_project, zip_path)
        loaded_project = load_from_zip(zip_path)

        assert loaded_project is not None
        assert len(loaded_project.pages) == 3
        assert loaded_project.pages[0].page_number == 1
        assert loaded_project.pages[2].page_number == 3


class TestZipStructure:
    """Tests for ZIP file structure and content"""

    def test_zip_contains_project_json(self, sample_project, temp_dir):
        """Test that ZIP contains project.json"""
        zip_path = os.path.join(temp_dir, "test.ppz")
        save_to_zip(sample_project, zip_path)

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

    def test_project_json_is_valid(self, sample_project, temp_dir):
        """Test that project.json contains valid JSON"""
        zip_path = os.path.join(temp_dir, "test.ppz")
        save_to_zip(sample_project, zip_path)

        with zipfile.ZipFile(zip_path, "r") as zipf:
            project_json = zipf.read("project.json").decode("utf-8")
            data = json.loads(project_json)

            assert "name" in data
            assert "serialization_version" in data
            assert data["name"] == "Test Project"

    def test_version_in_serialized_data(self, sample_project, temp_dir):
        """Test that version information is included"""
        zip_path = os.path.join(temp_dir, "test.ppz")
        save_to_zip(sample_project, zip_path)

        with zipfile.ZipFile(zip_path, "r") as zipf:
            project_json = zipf.read("project.json").decode("utf-8")
            data = json.loads(project_json)

            assert "serialization_version" in data
            assert data["serialization_version"] == "3.0"


class TestAssetManagement:
    """Tests for asset bundling and management"""

    def test_save_project_with_image(self, sample_project, sample_image, temp_dir):
        """Test saving a project with an image"""
        # Import image to project
        imported_path = sample_project.asset_manager.import_asset(sample_image)

        # Create page with image
        layout = PageLayout()
        img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
        layout.add_element(img_data)
        page = Page(layout=layout, page_number=1)
        sample_project.add_page(page)

        # Save
        zip_path = os.path.join(temp_dir, "project_with_image.ppz")
        success, error = save_to_zip(sample_project, zip_path)

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

    def test_assets_folder_in_zip(self, sample_project, sample_image, temp_dir):
        """Test that assets folder is included in ZIP"""
        # Import image
        imported_path = sample_project.asset_manager.import_asset(sample_image)

        # Create page with image
        layout = PageLayout()
        img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
        layout.add_element(img_data)
        page = Page(layout=layout, page_number=1)
        sample_project.add_page(page)

        # Save
        zip_path = os.path.join(temp_dir, "project_with_image.ppz")
        save_to_zip(sample_project, zip_path)

        # Check ZIP contents
        with zipfile.ZipFile(zip_path, "r") as zipf:
            names = zipf.namelist()
            # Should contain assets folder
            asset_files = [n for n in names if n.startswith("assets/")]
            assert len(asset_files) > 0

    def test_load_project_with_image(self, sample_project, sample_image, temp_dir):
        """Test loading a project with images"""
        # Import image
        imported_path = sample_project.asset_manager.import_asset(sample_image)

        # Create page with image
        layout = PageLayout()
        img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
        layout.add_element(img_data)
        page = Page(layout=layout, page_number=1)
        sample_project.add_page(page)

        # Save and load
        zip_path = os.path.join(temp_dir, "project_with_image.ppz")
        save_to_zip(sample_project, zip_path)
        loaded_project = load_from_zip(zip_path)

        assert loaded_project is not None
        assert len(loaded_project.pages) == 1
        assert len(loaded_project.pages[0].layout.elements) == 1

        # Verify image element
        img_element = loaded_project.pages[0].layout.elements[0]
        assert isinstance(img_element, ImageData)
        assert img_element.image_path != ""

    def test_asset_reference_counts_preserved(self, sample_project, sample_image, temp_dir):
        """Test that asset reference counts are preserved"""
        # Import image
        imported_path = sample_project.asset_manager.import_asset(sample_image)

        # Use image twice
        layout1 = PageLayout()
        img1 = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
        layout1.add_element(img1)
        page1 = Page(layout=layout1, page_number=1)
        sample_project.add_page(page1)

        layout2 = PageLayout()
        img2 = ImageData(image_path=imported_path, x=20, y=20, width=100, height=100)
        layout2.add_element(img2)
        page2 = Page(layout=layout2, page_number=2)
        sample_project.add_page(page2)

        # Get relative path for reference count check
        rel_path = os.path.relpath(imported_path, sample_project.folder_path)
        original_ref_count = sample_project.asset_manager.get_reference_count(rel_path)

        # Save and load
        zip_path = os.path.join(temp_dir, "project_refs.ppz")
        save_to_zip(sample_project, zip_path)
        loaded_project = load_from_zip(zip_path)

        assert loaded_project is not None
        # Reference counts should be preserved
        # Note: The actual reference counting behavior depends on deserialize implementation


class TestPortability:
    """Tests for project portability across different locations"""

    def test_load_to_different_directory(self, sample_project, sample_image, temp_dir):
        """Test loading project to a different directory"""
        # Import image and create page
        imported_path = sample_project.asset_manager.import_asset(sample_image)
        layout = PageLayout()
        img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
        layout.add_element(img_data)
        page = Page(layout=layout, page_number=1)
        sample_project.add_page(page)

        # Save
        zip_path = os.path.join(temp_dir, "portable_project.ppz")
        save_to_zip(sample_project, zip_path)

        # Load to a different location
        new_location = os.path.join(temp_dir, "different_location")
        loaded_project = load_from_zip(zip_path, extract_to=new_location)

        assert loaded_project is not None
        assert loaded_project.folder_path == new_location
        assert os.path.exists(new_location)

        # Verify assets were extracted
        assets_folder = os.path.join(new_location, "assets")
        assert os.path.exists(assets_folder)

    def test_relative_paths_work_after_move(self, sample_project, sample_image, temp_dir):
        """Test that relative paths still work after loading to different location"""
        # Import image
        imported_path = sample_project.asset_manager.import_asset(sample_image)
        layout = PageLayout()
        img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100)
        layout.add_element(img_data)
        page = Page(layout=layout, page_number=1)
        sample_project.add_page(page)

        # Save
        zip_path = os.path.join(temp_dir, "portable_project.ppz")
        save_to_zip(sample_project, zip_path)

        # Load to different location
        new_location = os.path.join(temp_dir, "new_location")
        loaded_project = load_from_zip(zip_path, extract_to=new_location)

        # Verify image path is accessible from new location
        img_element = loaded_project.pages[0].layout.elements[0]
        image_path = img_element.image_path

        # Image path should exist
        # Note: May be absolute or relative depending on implementation
        if not os.path.isabs(image_path):
            full_path = os.path.join(loaded_project.folder_path, image_path)
            assert os.path.exists(full_path)
        else:
            assert os.path.exists(image_path)


class TestProjectInfo:
    """Tests for get_project_info utility function"""

    def test_get_project_info(self, sample_project, temp_dir):
        """Test getting project info without loading"""
        # Add some pages
        for i in range(5):
            layout = PageLayout()
            page = Page(layout=layout, page_number=i + 1)
            sample_project.add_page(page)

        # Save
        zip_path = os.path.join(temp_dir, "info_test.ppz")
        save_to_zip(sample_project, zip_path)

        # Get info
        info = get_project_info(zip_path)

        assert info is not None
        assert info["name"] == "Test Project"
        assert info["page_count"] == 5
        assert info["version"] == "3.0"
        assert info["working_dpi"] == 300

    def test_get_info_invalid_zip(self, temp_dir):
        """Test getting info from invalid ZIP"""
        zip_path = os.path.join(temp_dir, "invalid.ppz")

        info = get_project_info(zip_path)

        assert info is None


class TestEdgeCases:
    """Tests for edge cases and error handling"""

    def test_save_to_invalid_path(self, sample_project):
        """Test saving to an invalid path"""
        invalid_path = "/nonexistent/directory/project.ppz"

        success, error = save_to_zip(sample_project, invalid_path)

        assert success is False
        assert error is not None

    def test_load_corrupted_zip(self, temp_dir):
        """Test loading a corrupted ZIP file"""
        # Create a fake corrupted file
        corrupted_path = os.path.join(temp_dir, "corrupted.ppz")
        with open(corrupted_path, "w") as f:
            f.write("This is not a ZIP file")

        try:

            loaded_project = load_from_zip(corrupted_path)

            assert False, "Should have raised an exception"

        except Exception as error:

            assert error is not None

    def test_load_zip_without_project_json(self, temp_dir):
        """Test loading a ZIP without project.json"""
        zip_path = os.path.join(temp_dir, "no_json.ppz")

        # Create ZIP without project.json
        with zipfile.ZipFile(zip_path, "w") as zipf:
            zipf.writestr("dummy.txt", "dummy content")

        try:
            loaded_project = load_from_zip(zip_path)
            assert False, "Should have raised an exception"
        except Exception as error:
            assert error is not None
            assert "project.json not found" in str(error)

    def test_project_with_text_elements(self, sample_project, temp_dir):
        """Test saving/loading project with text elements"""
        # Create page with text
        layout = PageLayout()
        text = TextBoxData(text_content="Hello World", x=10, y=10, width=200, height=50)
        layout.add_element(text)
        page = Page(layout=layout, page_number=1)
        sample_project.add_page(page)

        # Save and load
        zip_path = os.path.join(temp_dir, "with_text.ppz")
        save_to_zip(sample_project, zip_path)
        loaded_project = load_from_zip(zip_path)

        assert loaded_project is not None
        assert len(loaded_project.pages) == 1

        text_element = loaded_project.pages[0].layout.elements[0]
        assert isinstance(text_element, TextBoxData)
        assert text_element.text_content == "Hello World"
