"""
Tests for MergeOperationsMixin
"""

import pytest
import tempfile
import os
from unittest.mock import Mock, MagicMock, patch, call
from PyQt6.QtWidgets import QMainWindow, QMessageBox, QFileDialog
from pyPhotoAlbum.mixins.base import ApplicationStateMixin
from pyPhotoAlbum.mixins.operations.merge_ops import MergeOperationsMixin
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.models import TextBoxData


class MergeOpsWindow(MergeOperationsMixin, ApplicationStateMixin, QMainWindow):
    """Test window with merge operations mixin"""

    def __init__(self):
        super().__init__()
        self._project = Project(name="Test Project")
        self._gl_widget = Mock()
        self._gl_widget.current_page_index = 0
        self._autosave_timer = Mock()
        self._status_bar = Mock()

        # Track calls
        self._save_project_called = False
        self._update_view_called = False

    @property
    def gl_widget(self):
        return self._gl_widget

    @property
    def status_bar(self):
        return self._status_bar

    def save_project(self):
        self._save_project_called = True

    def update_view(self):
        self._update_view_called = True


class TestMergeProjects:
    """Test merge_projects method"""

    def test_dirty_project_user_cancels(self, qtbot):
        """Test user cancels when prompted about unsaved changes"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Make project dirty
        window.project.mark_dirty()

        with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Cancel):
            window.merge_projects()

        # Should return early without attempting merge
        assert not window._save_project_called

    def test_dirty_project_user_saves(self, qtbot):
        """Test user chooses to save before merging"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Make project dirty
        window.project.mark_dirty()

        # Mock QMessageBox.question to return Yes, then mock file dialog to cancel
        with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Yes):
            with patch.object(QFileDialog, 'getOpenFileName', return_value=('', '')):
                window.merge_projects()

        # Should have called save_project
        assert window._save_project_called

    def test_dirty_project_user_skips_save(self, qtbot):
        """Test user chooses not to save before merging"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Make project dirty
        window.project.mark_dirty()

        # Mock QMessageBox.question to return No, then mock file dialog to cancel
        with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.No):
            with patch.object(QFileDialog, 'getOpenFileName', return_value=('', '')):
                window.merge_projects()

        # Should NOT have called save_project
        assert not window._save_project_called

    def test_user_cancels_file_selection(self, qtbot):
        """Test user cancels the file selection dialog"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Project is clean (not dirty)
        assert not window.project.is_dirty()

        with patch.object(QFileDialog, 'getOpenFileName', return_value=('', '')):
            window.merge_projects()

        # Should return early
        assert not window._update_view_called

    def test_autosave_timer_stopped_and_restarted(self, qtbot, tmp_path):
        """Test autosave timer is stopped during merge and restarted after"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Create a temporary project file
        test_project = Project("Other Project")
        test_file = tmp_path / "test.ppz"

        from pyPhotoAlbum.project_serializer import save_to_zip
        save_to_zip(test_project, str(test_file))

        # Track timer calls
        timer_stop_called = False
        timer_start_called = False

        def mock_stop():
            nonlocal timer_stop_called
            timer_stop_called = True

        def mock_start():
            nonlocal timer_start_called
            timer_start_called = True

        window._autosave_timer.stop = mock_stop
        window._autosave_timer.start = mock_start

        # Mock file dialog to return our test file
        with patch.object(QFileDialog, 'getOpenFileName', return_value=(str(test_file), '')):
            # Mock QMessageBox for the concatenation question
            with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Yes):
                # Mock the information box
                with patch.object(QMessageBox, 'information'):
                    window.merge_projects()

        # Verify timer was stopped and started
        assert timer_stop_called
        assert timer_start_called

    def test_merge_same_project_no_conflicts(self, qtbot, tmp_path):
        """Test merging same project with no conflicts"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Create base project
        base_project = Project("Base Project")
        base_project._project_id = "same-id-123"
        page = Page(page_number=1)
        text = TextBoxData(text_content="Original Text", x=10, y=10, width=100, height=50)
        page.layout.add_element(text)
        base_project.add_page(page)

        # Set window's project to have same ID
        window._project = Project("Base Project")
        window._project._project_id = "same-id-123"
        page1 = Page(page_number=1)
        window._project.add_page(page1)

        # Create temporary project file
        test_file = tmp_path / "test.ppz"
        from pyPhotoAlbum.project_serializer import save_to_zip
        save_to_zip(base_project, str(test_file))

        # Mock file dialog
        with patch.object(QFileDialog, 'getOpenFileName', return_value=(str(test_file), '')):
            # Mock QMessageBox to accept auto-merge (no conflicts)
            with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Yes):
                # Mock the completion information box
                with patch.object(QMessageBox, 'information') as mock_info:
                    window.merge_projects()

                    # Should show completion message
                    assert mock_info.called

    def test_merge_different_projects_concatenation(self, qtbot, tmp_path):
        """Test concatenating different projects"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Create another project with different ID
        other_project = Project("Other Project")
        page = Page(page_number=1)
        other_project.add_page(page)

        # Create temporary project file
        test_file = tmp_path / "test.ppz"
        from pyPhotoAlbum.project_serializer import save_to_zip
        save_to_zip(other_project, str(test_file))

        # Mock file dialog
        with patch.object(QFileDialog, 'getOpenFileName', return_value=(str(test_file), '')):
            # Mock QMessageBox to accept concatenation
            with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Yes):
                # Mock the completion information box
                with patch.object(QMessageBox, 'information') as mock_info:
                    window.merge_projects()

                    # Should show completion message
                    assert mock_info.called

    def test_merge_error_handling(self, qtbot):
        """Test error handling during merge"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Mock file dialog to return invalid file
        with patch.object(QFileDialog, 'getOpenFileName', return_value=('/invalid/path.ppz', '')):
            # Mock QMessageBox.critical to capture error
            with patch.object(QMessageBox, 'critical') as mock_critical:
                window.merge_projects()

                # Should show error message
                assert mock_critical.called
                args = mock_critical.call_args[0]
                assert "Merge Error" in args[1]

    def test_merge_timer_restarted_after_error(self, qtbot):
        """Test autosave timer is restarted even after error"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        timer_start_called = False

        def mock_start():
            nonlocal timer_start_called
            timer_start_called = True

        window._autosave_timer.start = mock_start

        # Mock file dialog to return invalid file (will cause error)
        with patch.object(QFileDialog, 'getOpenFileName', return_value=('/invalid/path.ppz', '')):
            with patch.object(QMessageBox, 'critical'):
                window.merge_projects()

        # Timer should be restarted even after error
        assert timer_start_called


class TestPerformMergeWithConflicts:
    """Test _perform_merge_with_conflicts method"""

    def test_no_conflicts_user_accepts(self, qtbot):
        """Test auto-merge when no conflicts and user accepts"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Create mock data
        our_data = {'pages': [], 'name': 'Our Project', 'project_id': 'test-123'}
        their_data = {'pages': [], 'name': 'Their Project', 'project_id': 'test-123'}

        # Mock MergeManager to return no conflicts
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.MergeManager') as MockMergeManager:
            mock_manager = MockMergeManager.return_value
            mock_manager.detect_conflicts.return_value = []
            mock_manager.apply_resolutions.return_value = {'pages': [], 'name': 'Merged'}

            # Mock QMessageBox to accept auto-merge
            with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Yes):
                with patch.object(QMessageBox, 'information') as mock_info:
                    window._perform_merge_with_conflicts(our_data, their_data)

                    # Should have called apply_resolutions
                    mock_manager.apply_resolutions.assert_called_once()
                    # Should show completion message
                    assert mock_info.called

    def test_no_conflicts_user_rejects(self, qtbot):
        """Test user rejects auto-merge when no conflicts"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        our_data = {'pages': [], 'name': 'Our Project', 'project_id': 'test-123'}
        their_data = {'pages': [], 'name': 'Their Project', 'project_id': 'test-123'}

        # Mock MergeManager to return no conflicts
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.MergeManager') as MockMergeManager:
            mock_manager = MockMergeManager.return_value
            mock_manager.detect_conflicts.return_value = []

            # Mock QMessageBox to reject auto-merge
            with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.No):
                window._perform_merge_with_conflicts(our_data, their_data)

                # Should NOT have called apply_resolutions
                mock_manager.apply_resolutions.assert_not_called()

    def test_with_conflicts_user_accepts_dialog(self, qtbot):
        """Test merge with conflicts when user accepts dialog"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        our_data = {'pages': [], 'name': 'Our Project', 'project_id': 'test-123'}
        their_data = {'pages': [], 'name': 'Their Project', 'project_id': 'test-123'}

        # Mock MergeManager to return conflicts
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.MergeManager') as MockMergeManager:
            mock_manager = MockMergeManager.return_value
            mock_manager.detect_conflicts.return_value = [Mock()]  # One conflict

            # Mock MergeDialog
            with patch('pyPhotoAlbum.mixins.operations.merge_ops.MergeDialog') as MockDialog:
                mock_dialog = MockDialog.return_value
                mock_dialog.exec.return_value = QMessageBox.DialogCode.Accepted
                mock_dialog.get_merged_project_data.return_value = {'pages': [], 'name': 'Merged'}

                with patch.object(QMessageBox, 'information'):
                    window._perform_merge_with_conflicts(our_data, their_data)

                    # Should have shown dialog
                    MockDialog.assert_called_once()
                    # Should have gotten merged data
                    mock_dialog.get_merged_project_data.assert_called_once()

    def test_with_conflicts_user_cancels_dialog(self, qtbot):
        """Test merge with conflicts when user cancels dialog"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        our_data = {'pages': [], 'name': 'Our Project', 'project_id': 'test-123'}
        their_data = {'pages': [], 'name': 'Their Project', 'project_id': 'test-123'}

        # Mock MergeManager to return conflicts
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.MergeManager') as MockMergeManager:
            mock_manager = MockMergeManager.return_value
            mock_manager.detect_conflicts.return_value = [Mock()]  # One conflict

            # Mock MergeDialog
            with patch('pyPhotoAlbum.mixins.operations.merge_ops.MergeDialog') as MockDialog:
                mock_dialog = MockDialog.return_value
                mock_dialog.exec.return_value = QMessageBox.DialogCode.Rejected

                with patch.object(QMessageBox, 'information') as mock_info:
                    window._perform_merge_with_conflicts(our_data, their_data)

                    # Should have shown cancellation message
                    assert mock_info.called
                    args = mock_info.call_args[0]
                    assert "Cancelled" in args[1]
                    # Should NOT have gotten merged data
                    mock_dialog.get_merged_project_data.assert_not_called()


class TestPerformConcatenation:
    """Test _perform_concatenation method"""

    def test_user_accepts_concatenation(self, qtbot):
        """Test concatenation when user accepts"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        our_data = {'pages': [], 'name': 'Project A', 'project_id': 'id-a'}
        their_data = {'pages': [], 'name': 'Project B', 'project_id': 'id-b'}

        # Mock concatenate_projects
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.concatenate_projects') as mock_concat:
            mock_concat.return_value = {'pages': [], 'name': 'Combined'}

            # Mock QMessageBox to accept
            with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Yes):
                with patch.object(QMessageBox, 'information') as mock_info:
                    window._perform_concatenation(our_data, their_data)

                    # Should have called concatenate_projects
                    mock_concat.assert_called_once_with(our_data, their_data)
                    # Should show completion message
                    assert mock_info.called
                    args = mock_info.call_args[0]
                    assert "Concatenation Complete" in args[1]

    def test_user_rejects_concatenation(self, qtbot):
        """Test concatenation when user rejects"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        our_data = {'pages': [], 'name': 'Project A', 'project_id': 'id-a'}
        their_data = {'pages': [], 'name': 'Project B', 'project_id': 'id-b'}

        # Mock concatenate_projects
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.concatenate_projects') as mock_concat:
            # Mock QMessageBox to reject
            with patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.No):
                window._perform_concatenation(our_data, their_data)

                # Should NOT have called concatenate_projects
                mock_concat.assert_not_called()

    def test_concatenation_shows_project_names(self, qtbot):
        """Test concatenation dialog shows both project names"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        our_data = {'pages': [], 'name': 'My Project', 'project_id': 'id-a'}
        their_data = {'pages': [], 'name': 'Their Project', 'project_id': 'id-b'}

        # Mock QMessageBox.question to capture the message
        with patch.object(QMessageBox, 'question') as mock_question:
            mock_question.return_value = QMessageBox.StandardButton.No

            window._perform_concatenation(our_data, their_data)

            # Check that the dialog message contains both project names
            args = mock_question.call_args[0]
            message = args[2]  # Third argument is the message
            assert 'My Project' in message
            assert 'Their Project' in message


class TestApplyMergedData:
    """Test _apply_merged_data method"""

    def test_apply_merged_data_updates_project(self, qtbot):
        """Test applying merged data creates new project"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Create merged data
        merged_data = {
            'pages': [],
            'name': 'Merged Project',
            'project_id': 'merged-123'
        }

        # Mock set_asset_resolution_context
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.set_asset_resolution_context'):
            window._apply_merged_data(merged_data)

        # Should have updated project
        assert window.project.name == 'Merged Project'
        # Project should be marked dirty
        assert window.project.is_dirty()

    def test_apply_merged_data_updates_gl_widget(self, qtbot):
        """Test applying merged data updates GL widget"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        merged_data = {
            'pages': [],
            'name': 'Merged Project',
            'project_id': 'merged-123'
        }

        # Mock set_asset_resolution_context
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.set_asset_resolution_context'):
            window._apply_merged_data(merged_data)

        # Should have updated gl_widget
        window.gl_widget.set_project.assert_called_once()
        window.gl_widget.update.assert_called_once()

    def test_apply_merged_data_shows_status(self, qtbot):
        """Test applying merged data shows status message"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        merged_data = {
            'pages': [],
            'name': 'Merged Project',
            'project_id': 'merged-123'
        }

        # Mock set_asset_resolution_context
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.set_asset_resolution_context'):
            window._apply_merged_data(merged_data)

        # Should have shown status message
        window.status_bar.showMessage.assert_called_once()
        args = window.status_bar.showMessage.call_args[0]
        assert "Merge completed successfully" in args[0]

    def test_apply_merged_data_sets_asset_context(self, qtbot, tmp_path):
        """Test applying merged data sets asset resolution context"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Create project with folder_path
        test_path = tmp_path / "test"
        test_path.mkdir()
        window._project.folder_path = str(test_path)

        merged_path = tmp_path / "merged"
        merged_path.mkdir()

        merged_data = {
            'pages': [],
            'name': 'Merged Project',
            'project_id': 'merged-123',
            'folder_path': str(merged_path)
        }

        # Mock set_asset_resolution_context
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.set_asset_resolution_context') as mock_set:
            window._apply_merged_data(merged_data)

            # Should have called set_asset_resolution_context with new project's folder_path
            mock_set.assert_called_once()

    def test_apply_merged_data_without_gl_widget(self, qtbot):
        """Test applying merged data when gl_widget doesn't exist"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Remove gl_widget
        delattr(window, '_gl_widget')

        merged_data = {
            'pages': [],
            'name': 'Merged Project',
            'project_id': 'merged-123'
        }

        # Should not raise error
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.set_asset_resolution_context'):
            window._apply_merged_data(merged_data)

        # Project should still be updated
        assert window.project.name == 'Merged Project'

    def test_apply_merged_data_without_status_bar(self, qtbot):
        """Test applying merged data when status_bar doesn't exist"""
        window = MergeOpsWindow()
        qtbot.addWidget(window)

        # Remove status_bar
        delattr(window, '_status_bar')

        merged_data = {
            'pages': [],
            'name': 'Merged Project',
            'project_id': 'merged-123'
        }

        # Should not raise error
        with patch('pyPhotoAlbum.mixins.operations.merge_ops.set_asset_resolution_context'):
            window._apply_merged_data(merged_data)

        # Project should still be updated
        assert window.project.name == 'Merged Project'
