"""
Integration tests for the async GUI refactor.
Tests the main async workflows without requiring a full GUI.
"""
import pytest
import pandas as pd
from unittest.mock import AsyncMock, patch, Mock
from pathlib import Path
import tempfile
import sys
import shutil

from fmatch.desktop.models import (
    DuplicateResultModel,
    MatchResultModel,
    ProgressUpdate
)
from fmatch.core import DedupeConfig, MatchConfig


def mock_tkinter_completely():
    """Mock all tkinter modules before they can be imported."""

    # Create mock classes
    class MockWidget:
        def __init__(self, *args, **kwargs):
            pass

        def grid(self, **kwargs):
            pass

        def pack(self, **kwargs):
            pass

        def place(self, **kwargs):
            pass

        def config(self, **kwargs):
            pass

        def configure(self, **kwargs):
            pass

        def bind(self, *args, **kwargs):
            pass

        def unbind(self, *args, **kwargs):
            pass

        def destroy(self):
            pass

        def winfo_exists(self):
            return True

        def __getitem__(self, key):
            return None

        def __setitem__(self, key, value):
            pass

    class MockTk(MockWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.tk = self
            self._w = "."
            self.children = {}

        def title(self, *args):
            pass

        def geometry(self, *args):
            pass

        def minsize(self, *args, **kwargs):
            pass

        def columnconfigure(self, *args, **kwargs):
            pass

        def rowconfigure(self, *args, **kwargs):
            pass

        def protocol(self, *args, **kwargs):
            pass

        def after(self, *args, **kwargs):
            return "after_id"

        def after_cancel(self, *args):
            pass

        def bind_all(self, *args, **kwargs):
            pass

        def update(self):
            pass

        def update_idletasks(self):
            pass

        def mainloop(self):
            pass

        def quit(self):
            pass

        def winfo_children(self):
            return []

    class MockVar:
        def __init__(self, *args, **kwargs):
            self.value = kwargs.get('value', '')

        def get(self):
            return self.value

        def set(self, value):
            self.value = value

        def trace(self, *args, **kwargs):
            pass

        def trace_add(self, *args, **kwargs):
            pass

    # Create mock tkinter module
    mock_tk = Mock()
    mock_tk.Tk = MockTk
    mock_tk.Toplevel = MockWidget
    mock_tk.Frame = MockWidget
    mock_tk.Label = MockWidget
    mock_tk.Button = MockWidget
    mock_tk.Entry = MockWidget
    mock_tk.Text = MockWidget
    mock_tk.Canvas = MockWidget
    mock_tk.Listbox = MockWidget
    mock_tk.Menu = MockWidget
    mock_tk.Message = MockWidget
    mock_tk.StringVar = MockVar
    mock_tk.IntVar = MockVar
    mock_tk.BooleanVar = MockVar
    mock_tk.DoubleVar = MockVar
    mock_tk.END = 'end'
    mock_tk.BOTH = 'both'
    mock_tk.LEFT = 'left'
    mock_tk.RIGHT = 'right'
    mock_tk.TOP = 'top'
    mock_tk.BOTTOM = 'bottom'
    mock_tk.RIDGE = 'ridge'
    mock_tk.NORMAL = 'normal'
    mock_tk.DISABLED = 'disabled'

    # Create mock ttk
    mock_ttk = Mock()
    mock_ttk.Style = Mock
    mock_ttk.Frame = MockWidget
    mock_ttk.Label = MockWidget
    mock_ttk.Button = MockWidget
    mock_ttk.Entry = MockWidget
    mock_ttk.Combobox = MockWidget
    mock_ttk.Treeview = MockWidget
    mock_ttk.Scrollbar = MockWidget
    mock_ttk.Progressbar = MockWidget
    mock_ttk.Notebook = MockWidget
    mock_ttk.LabelFrame = MockWidget
    mock_ttk.Radiobutton = MockWidget
    mock_ttk.Checkbutton = MockWidget
    mock_ttk.Spinbox = MockWidget
    mock_ttk.OptionMenu = MockWidget
    mock_ttk.Separator = MockWidget

    # Create mock messagebox
    mock_messagebox = Mock()
    mock_messagebox.showerror = Mock()
    mock_messagebox.showinfo = Mock()
    mock_messagebox.showwarning = Mock()
    mock_messagebox.askyesno = Mock(return_value=True)
    mock_messagebox.askokcancel = Mock(return_value=True)

    # Create mock filedialog
    mock_filedialog = Mock()
    mock_filedialog.askopenfilename = Mock(return_value='test_file.csv')
    mock_filedialog.asksaveasfilename = Mock(return_value='output.csv')
    mock_filedialog.askdirectory = Mock(return_value='/test/dir')

    # Create mock simpledialog
    mock_simpledialog = Mock()
    mock_simpledialog._setup_dialog = Mock()
    mock_simpledialog._place_window = Mock()
    mock_simpledialog.Dialog = MockWidget

    # Mock functions that might be imported with *
    def mock_function(*args, **kwargs):
        pass

    mock_tk._setup_dialog = mock_function
    mock_tk._place_window = mock_function
    mock_tk._get_temp_root = mock_function
    mock_tk._destroy_temp_root = mock_function

    # Install mocks in sys.modules
    sys.modules['tkinter'] = mock_tk
    sys.modules['tkinter.ttk'] = mock_ttk
    sys.modules['tkinter.messagebox'] = mock_messagebox
    sys.modules['tkinter.filedialog'] = mock_filedialog
    sys.modules['tkinter.simpledialog'] = mock_simpledialog

    # Also set ttk as attribute
    mock_tk.ttk = mock_ttk
    mock_tk.messagebox = mock_messagebox
    mock_tk.filedialog = mock_filedialog
    mock_tk.simpledialog = mock_simpledialog

    return mock_tk, mock_ttk


class TestAsyncGUIIntegration:
    """Integration tests for async GUI operations."""

    @pytest.fixture
    def mock_app(self):
        """Create a mock app instance for testing."""
        # Save original modules
        original_modules = {}
        for mod in ['tkinter', 'tkinter.ttk', 'tkinter.messagebox',
                    'tkinter.filedialog', 'tkinter.simpledialog']:
            if mod in sys.modules:
                original_modules[mod] = sys.modules[mod]

        # Create temp directory for testing
        temp_dir = tempfile.mkdtemp()

        try:
            # Mock tkinter completely before any imports
            mock_tk, mock_ttk = mock_tkinter_completely()

            # Mock other dependencies
            with patch('multiprocessing.Manager') as mock_manager:
                mock_queue = Mock()
                mock_queue.empty = Mock(return_value=True)
                mock_queue.get_nowait = Mock(side_effect=Exception("Queue empty"))
                mock_queue.put = Mock()

                mock_manager_instance = Mock()
                mock_manager_instance.Queue = Mock(return_value=mock_queue)
                mock_manager.return_value = mock_manager_instance

                # Mock additional utilities
                with patch('webbrowser.open', Mock()):
                    with patch('requests.get', Mock()):
                        # Patch build config functions to avoid Path(None) issue
                        def mock_build_dedupe_config(**kwargs):
                            if kwargs.get('output_dir') is None:
                                kwargs['output_dir'] = temp_dir
                            return DedupeConfig(
                                job_id=kwargs.get('job_id'),
                                input_path=kwargs.get('input_path'),
                                rec_id_col=kwargs.get('rec_id_col'),
                                algorithm=kwargs.get('algorithm', 'WRatio'),
                                threshold=kwargs.get('threshold', 80),
                                maps=kwargs.get('maps', []),
                                ruleset_name=kwargs.get('ruleset_name', 'default'),
                                dup_block_limit=kwargs.get('dup_block_limit', 100),
                                block_key_length=kwargs.get('block_key_len', 3),
                                output_dir=Path(kwargs.get('output_dir', temp_dir))
                            )

                        def mock_build_match_config(**kwargs):
                            if kwargs.get('output_dir') is None:
                                kwargs['output_dir'] = temp_dir
                            return MatchConfig(
                                job_id=kwargs.get('job_id'),
                                src_path=kwargs.get('src_path'),
                                ref_path=kwargs.get('ref_path'),
                                src_id_col=kwargs.get('src_id_col'),
                                ref_id_col=kwargs.get('ref_id_col'),
                                algorithm=kwargs.get('algorithm', 'WRatio'),
                                threshold=kwargs.get('threshold', 80),
                                maps=kwargs.get('maps', []),
                                output_dir=Path(kwargs.get('output_dir', temp_dir))
                            )

                        with patch('fmatch.core.build_dedupe_config', mock_build_dedupe_config):
                            with patch('fmatch.core.build_match_config', mock_build_match_config):
                                # Now import the GUI module
                                from fmatch.desktop.gui import FuzzyMatcherApp

                                # Mock initialization methods
                                with patch.object(FuzzyMatcherApp, '_load_custom_semantic_types'):
                                    with patch.object(FuzzyMatcherApp, '_load_last_session_deferred'):
                                        with patch.object(FuzzyMatcherApp, '_check_for_updates'):
                                            with patch.object(FuzzyMatcherApp, '_ensure_pool_active'):
                                                with patch.object(FuzzyMatcherApp, '_configure_styles'):
                                                    with patch.object(FuzzyMatcherApp, 'build_ui'):
                                                        # Create the app instance
                                                        app = FuzzyMatcherApp(jobs_dict={})

                # Set up test data
                app.source_df = pd.DataFrame({
                    'id': [1, 2, 3],
                    'name': ['Apple Inc', 'Apple Inc.', 'Banana Ltd'],
                    'city': ['NYC', 'NYC', 'LA']
                })
                app.ref_df = pd.DataFrame({
                    'id': [10, 20, 30],
                    'name': ['Apple Corporation', 'Banana Limited', 'Cherry Co'],
                    'city': ['NYC', 'LA', 'SF']
                })

                app.mappings = [
                    {'source': 'name', 'ref': 'name', 'weight': 1.0},
                    {'source': 'city', 'ref': 'city', 'weight': 0.5}
                ]
                app.mappings_confirmed = True
                app.progress_queue = mock_queue

                # Create mock variables with proper get/set methods
                def create_mock_var(initial_value):
                    var = Mock()
                    var.get = Mock(return_value=initial_value)
                    var.set = Mock()
                    return var

                app.mode = create_mock_var('match')
                app.source_id_var = create_mock_var('id')
                app.ref_id_var = create_mock_var('id')
                app.threshold = create_mock_var(80)
                app.algorithm_var = create_mock_var('WRatio')
                app.rules_var = create_mock_var('default')
                app.results_mode = create_mock_var('all')
                app.block_limit = create_mock_var(100)

                # Set file paths
                app.src_path = "test_source.csv"
                app.ref_path = "test_ref.csv"

                # Mock UI elements
                app.bar = Mock()
                app.lbl_status = Mock()
                app.lbl_progress = Mock()
                app.lbl_eta = Mock()

                # Initialize processing state
                app.processing = False
                app._is_processing_cancelled = False
                app.current_operation_id = None
                app.auto_determined_locking_strategy = {}

                # Initialize other attributes that might be needed
                app.source_block_col_var = create_mock_var('')
                app.ref_block_col_var = create_mock_var('')
                app.block_key_length_var = create_mock_var(3)
                app.norm_rule_vars = {}
                app.rule_rows = []

                yield app

        finally:
            # Clean up temp directory
            shutil.rmtree(temp_dir, ignore_errors=True)

            # Restore original modules
            for mod, original in original_modules.items():
                sys.modules[mod] = original

            # Remove any mocked modules that weren't there originally
            for mod in ['tkinter', 'tkinter.ttk', 'tkinter.messagebox',
                        'tkinter.filedialog', 'tkinter.simpledialog']:
                if mod not in original_modules and mod in sys.modules:
                    del sys.modules[mod]

    @pytest.mark.asyncio
    async def test_dedupe_worker_async(self, mock_app):
        """Test the async dedupe worker."""
        # Create a test config
        config = DedupeConfig(
            job_id="test-dedupe-123",
            input_path=mock_app.source_df,
            rec_id_col='id',
            algorithm='WRatio',
            threshold=80,
            ruleset_name='default',
            block_key_length=3,
            dup_block_limit=100,
            maps=mock_app.mappings
        )

        # Mock the engine_bridge.dedupe function
        mock_result = DuplicateResultModel(
            matches=[
                {'id_1': 1, 'id_2': 2, 'confidence_score': 95.5}
            ],
            total_duration=1.5
        )

        with patch('fmatch.desktop.engine_bridge.dedupe',
                   new_callable=AsyncMock) as mock_dedupe:
            mock_dedupe.return_value = mock_result

            # Run the worker
            await mock_app._dedupe_worker_async(config)

            # Verify the results
            assert mock_app.results_df is not None
            assert len(mock_app.results_df) == 1
            assert mock_app.results_df.iloc[0]['confidence_score'] == 95.5

    @pytest.mark.asyncio
    async def test_match_worker_async(self, mock_app):
        """Test the async match worker."""
        # Create a test config
        config = MatchConfig(
            job_id="test-match-456",
            src_path=mock_app.source_df,
            ref_path=mock_app.ref_df,
            src_id_col='id',
            ref_id_col='id',
            algorithm='WRatio',
            threshold=80,
            maps=mock_app.mappings
        )

        # Mock the engine_bridge.match function
        mock_result = MatchResultModel(
            matches=[
                {'source_id': 1, 'ref_id': 10, 'confidence_score': 88.0},
                {'source_id': 2, 'ref_id': 10, 'confidence_score': 85.5}
            ],
            total_duration=2.0
        )

        with patch('fmatch.desktop.engine_bridge.match',
                   new_callable=AsyncMock) as mock_match:
            mock_match.return_value = mock_result

            # Run the worker
            await mock_app._match_worker_async(config)

            # Verify the results
            assert mock_app.results_df is not None
            assert len(mock_app.results_df) == 2
            assert mock_app.results_df.iloc[0]['confidence_score'] == 88.0

    @pytest.mark.asyncio
    async def test_progress_callback(self, mock_app):
        """Test that progress callbacks work correctly."""
        # Track progress updates
        progress_updates = []

        def capture_progress(args):
            # The queue.put is called with a single tuple argument
            msg_type, payload = args
            if msg_type == "stats":
                progress_updates.append(payload)

        mock_app.progress_queue.put = Mock(side_effect=capture_progress)

        # Create a config
        config = DedupeConfig(
            job_id="test-progress",
            input_path=mock_app.source_df,
            rec_id_col='id',
            algorithm='WRatio',
            threshold=80,
            ruleset_name='default',
            block_key_length=3,
            dup_block_limit=100,
            maps=mock_app.mappings
        )

        # Mock engine_bridge to call progress callback
        async def mock_dedupe_with_progress(cfg, progress_callback):
            # Simulate progress updates
            await progress_callback(ProgressUpdate(
                progress=0.25,
                message="Processing block 1/4",
                items_processed=25,
                total_items=100
            ))
            await progress_callback(ProgressUpdate(
                progress=0.5,
                message="Processing block 2/4",
                items_processed=50,
                total_items=100
            ))
            await progress_callback(ProgressUpdate(
                progress=1.0,
                message="Complete",
                items_processed=100,
                total_items=100
            ))

            return DuplicateResultModel(matches=[], total_duration=1.0)

        with patch('fmatch.desktop.engine_bridge.dedupe',
                   side_effect=mock_dedupe_with_progress):

            await mock_app._dedupe_worker_async(config)

            # Check progress updates were captured
            assert len(progress_updates) >= 3
            assert progress_updates[0]['progress'] == 25.0  # Converted to percentage
            assert progress_updates[1]['progress'] == 50.0
            assert progress_updates[-1]['progress'] == 100.0

    def test_build_config_methods(self, mock_app):
        """Test configuration building methods."""
        mock_app.current_operation_id = "test-op-123"

        # Test dedupe config
        mock_app.mode.get.return_value = 'duplicate'
        dedupe_config = mock_app._build_dedupe_config()

        assert isinstance(dedupe_config, DedupeConfig)
        assert dedupe_config.job_id == "test-op-123"
        assert dedupe_config.rec_id_col == 'id'
        assert dedupe_config.algorithm == 'WRatio'
        assert dedupe_config.threshold == 80

        # Test match config
        mock_app.mode.get.return_value = 'match'
        match_config = mock_app._build_match_config()

        assert isinstance(match_config, MatchConfig)
        assert match_config.job_id == "test-op-123"
        assert match_config.src_id_col == 'id'
        assert match_config.ref_id_col == 'id'
        assert match_config.algorithm == 'WRatio'
        assert match_config.threshold == 80

    def test_validate_can_start(self, mock_app):
        """Test validation logic for starting processing."""
        # Test with valid state
        assert mock_app._validate_can_start() is True

        # Test without confirmed mappings
        mock_app.mappings_confirmed = False
        assert mock_app._validate_can_start() is False

        # Test while processing
        mock_app.mappings_confirmed = True
        mock_app.processing = True
        assert mock_app._validate_can_start() is False

        # Test without source data
        mock_app.processing = False
        mock_app.source_df = None
        assert mock_app._validate_can_start() is False

        # Test match mode without ref data
        mock_app.source_df = pd.DataFrame({'id': [1]})
        mock_app.mode.get.return_value = 'match'
        mock_app.ref_df = None
        assert mock_app._validate_can_start() is False


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
