from unittest.mock import patch, MagicMock, PropertyMock
from threading import Thread, Timer
from django.test import TestCase, override_settings
from testimonials.services.task_executor import TaskExecutor, execute_task


# ============================================================================
# ASYNC AVAILABILITY TESTS
# ============================================================================

class TaskExecutorAsyncAvailabilityTests(TestCase):
    """Test background task availability detection."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_is_async_enabled_when_enabled(self):
        """Test is_async_enabled returns True when enabled in settings."""
        result = TaskExecutor.is_async_enabled()
        self.assertTrue(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_is_async_enabled_when_disabled(self):
        """Test is_async_enabled returns False when disabled in settings."""
        result = TaskExecutor.is_async_enabled()
        self.assertFalse(result)


# ============================================================================
# SYNCHRONOUS EXECUTION TESTS
# ============================================================================

class TaskExecutorSyncTests(TestCase):
    """Test synchronous task execution."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_sync_basic_function(self):
        """Test synchronous execution of basic function."""
        def simple_task(x, y):
            return x + y

        result = TaskExecutor.execute(simple_task, 5, 10)
        self.assertEqual(result, 15)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_sync_with_kwargs(self):
        """Test synchronous execution with keyword arguments."""
        def task_with_kwargs(a, b=10, c=20):
            return a + b + c

        result = TaskExecutor.execute(task_with_kwargs, 5, b=15, c=25)
        self.assertEqual(result, 45)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_sync_returns_none_on_exception(self):
        """Test sync execution returns None on exception."""
        def failing_task():
            raise ValueError("Task failed")

        result = TaskExecutor.execute(failing_task)
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_sync_with_complex_return_value(self):
        """Test sync execution with complex return value."""
        def complex_task():
            return {
                'status': 'success',
                'data': [1, 2, 3],
                'meta': {'count': 3}
            }

        result = TaskExecutor.execute(complex_task)
        self.assertIsInstance(result, dict)
        self.assertEqual(result['status'], 'success')
        self.assertEqual(result['data'], [1, 2, 3])

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_sync_removes_async_specific_kwargs(self):
        """Test sync execution removes async-specific kwargs."""
        def my_task(x):
            return x * 2

        # These async-specific kwargs should be removed
        result = TaskExecutor.execute(
            my_task,
            10,
            countdown=60,
            eta='2024-01-01',
            expires=3600
        )
        self.assertEqual(result, 20)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_sync_with_no_arguments(self):
        """Test sync execution with no arguments."""
        def no_args_task():
            return "executed"

        result = TaskExecutor.execute(no_args_task)
        self.assertEqual(result, "executed")

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_sync_with_side_effects(self):
        """Test sync execution with side effects."""
        call_log = []

        def task_with_side_effects(value):
            call_log.append(value)
            return len(call_log)

        result = TaskExecutor.execute(task_with_side_effects, 'test')
        self.assertEqual(result, 1)
        self.assertEqual(call_log, ['test'])


# ============================================================================
# ASYNCHRONOUS EXECUTION TESTS
# ============================================================================

class TaskExecutorAsyncTests(TestCase):
    """Test asynchronous task execution using threads."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_async_returns_thread(self):
        """Test async execution returns a Thread object."""
        def simple_task():
            pass

        result = TaskExecutor.execute(simple_task, use_async=True)
        self.assertIsInstance(result, Thread)
        # Wait for thread to finish
        result.join(timeout=2)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_async_runs_task_in_background(self):
        """Test async execution actually runs the task."""
        call_log = []

        def logging_task(value):
            call_log.append(value)

        result = TaskExecutor.execute(logging_task, 'test_value', use_async=True)
        self.assertIsInstance(result, Thread)
        result.join(timeout=2)

        self.assertEqual(call_log, ['test_value'])

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_async_with_regular_function(self):
        """Test async execution with a regular function."""
        executed = []

        def regular_function(x):
            executed.append(x)

        result = TaskExecutor.execute(regular_function, 42, use_async=True)
        self.assertIsInstance(result, Thread)
        result.join(timeout=2)

        self.assertEqual(executed, [42])

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_async_falls_back_to_sync_on_thread_error(self):
        """Test async execution falls back to sync when thread creation fails."""
        def simple_task():
            return 'sync_result'

        with patch('testimonials.services.task_executor.Thread', side_effect=Exception("Thread error")):
            # With fallback_to_sync=True (default), should fall back to sync
            result = TaskExecutor.execute(simple_task, use_async=True)
            self.assertEqual(result, 'sync_result')

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_async_no_fallback_returns_none(self):
        """Test async execution without fallback returns None on error."""
        def regular_function():
            return 'value'

        with patch('testimonials.services.task_executor.Thread', side_effect=Exception("Thread error")):
            result = TaskExecutor.execute(
                regular_function,
                use_async=True,
                fallback_to_sync=False
            )
            self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_respects_use_async_parameter(self):
        """Test execute respects explicit use_async parameter."""
        def simple_task():
            return 'sync_result'

        # Force sync even though background tasks are enabled
        result = TaskExecutor.execute(simple_task, use_async=False)

        # Should return the actual result (not a Thread)
        self.assertEqual(result, 'sync_result')


# ============================================================================
# DELAYED EXECUTION TESTS
# ============================================================================

class TaskExecutorDelayedTests(TestCase):
    """Test delayed task execution."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_delayed_returns_timer(self):
        """Test delayed execution returns a Timer object."""
        def delayed_task():
            pass

        result = TaskExecutor.execute_delayed(delayed_task, 1)
        self.assertIsInstance(result, Timer)
        result.cancel()  # Cancel to clean up

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_delayed_falls_back_when_async_unavailable(self):
        """Test delayed execution falls back to immediate sync when async unavailable."""
        def my_task(x):
            return x * 2

        result = TaskExecutor.execute_delayed(my_task, 60, 10)

        # Should execute immediately and return result
        self.assertEqual(result, 20)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_delayed_falls_back_on_exception(self):
        """Test delayed execution falls back to sync on exception."""
        def simple_task():
            return 'immediate_result'

        with patch('testimonials.services.task_executor.Timer', side_effect=Exception("Timer failed")):
            result = TaskExecutor.execute_delayed(simple_task, 60)

            # Should fall back to immediate sync
            self.assertEqual(result, 'immediate_result')

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_delayed_with_zero_delay(self):
        """Test delayed execution with zero delay."""
        executed = []

        def task():
            executed.append(True)

        result = TaskExecutor.execute_delayed(task, 0)
        self.assertIsInstance(result, Timer)
        # Wait for it to fire
        result.join(timeout=2)
        self.assertEqual(executed, [True])

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_delayed_with_large_delay(self):
        """Test delayed execution with large delay value."""
        def task():
            pass

        # 24 hours delay - should create timer without error
        result = TaskExecutor.execute_delayed(task, 86400)
        self.assertIsInstance(result, Timer)
        result.cancel()  # Cancel to clean up


# ============================================================================
# BATCH PROCESSING TESTS
# ============================================================================

class TaskExecutorBatchTests(TestCase):
    """Test batch task execution."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_basic(self):
        """Test basic batch processing."""
        processed = []

        def process_batch(batch):
            processed.extend(batch)
            return len(batch)

        items = list(range(10))
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=3
        )

        # Should create 4 batches: [0,1,2], [3,4,5], [6,7,8], [9]
        self.assertEqual(len(results), 4)
        self.assertEqual(processed, items)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_exact_batch_size(self):
        """Test batch processing when items exactly match batch size."""
        def process_batch(batch):
            return sum(batch)

        items = list(range(10))  # 10 items
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=10
        )

        # Should create 1 batch
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0], 45)  # sum of 0-9

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_with_empty_list(self):
        """Test batch processing with empty list."""
        def process_batch(batch):
            return len(batch)

        results = TaskExecutor.execute_batch(process_batch, [], batch_size=5)

        # Should return empty results
        self.assertEqual(len(results), 0)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_with_single_item(self):
        """Test batch processing with single item."""
        def process_batch(batch):
            return batch[0] * 2

        results = TaskExecutor.execute_batch(
            process_batch,
            [42],
            batch_size=10
        )

        self.assertEqual(len(results), 1)
        self.assertEqual(results[0], 84)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_with_large_batch_size(self):
        """Test batch processing with batch size larger than items."""
        def process_batch(batch):
            return len(batch)

        items = list(range(5))
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=100
        )

        # Should create 1 batch
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0], 5)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_with_batch_size_one(self):
        """Test batch processing with batch size of 1."""
        def process_batch(batch):
            return batch[0]

        items = [10, 20, 30]
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=1
        )

        # Should create 3 batches
        self.assertEqual(len(results), 3)
        self.assertEqual(results, [10, 20, 30])

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_batch_async(self):
        """Test batch processing with async execution."""
        executed_batches = []

        def process_batch(batch):
            executed_batches.append(batch)
            return len(batch)

        items = list(range(10))
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=4,
            use_async=True
        )

        # Should create 3 batches and return Thread objects
        self.assertEqual(len(results), 3)
        # Wait for all threads to complete
        for r in results:
            if isinstance(r, Thread):
                r.join(timeout=2)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_preserves_order(self):
        """Test batch processing preserves order."""
        def process_batch(batch):
            return [x * 2 for x in batch]

        items = [1, 2, 3, 4, 5]
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=2
        )

        # Flatten results
        flattened = []
        for result in results:
            flattened.extend(result)

        self.assertEqual(flattened, [2, 4, 6, 8, 10])

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_batch_with_complex_items(self):
        """Test batch processing with complex items."""
        def process_batch(batch):
            return sum(item['value'] for item in batch)

        items = [{'id': i, 'value': i * 10} for i in range(10)]
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=3
        )

        # Should sum values in each batch
        self.assertEqual(len(results), 4)
        # First batch: 0 + 10 + 20 = 30
        self.assertEqual(results[0], 30)


# ============================================================================
# EXECUTE TASK CONVENIENCE FUNCTION TESTS
# ============================================================================

class ExecuteTaskFunctionTests(TestCase):
    """Test execute_task convenience function."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_delegates_to_executor(self):
        """Test execute_task delegates to TaskExecutor.execute."""
        def my_task(x, y):
            return x + y

        result = execute_task(my_task, 5, 10)
        self.assertEqual(result, 15)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_with_kwargs(self):
        """Test execute_task with keyword arguments."""
        def my_task(a, b=10):
            return a + b

        result = execute_task(my_task, 5, b=20)
        self.assertEqual(result, 25)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    @patch('testimonials.services.task_executor.TaskExecutor.execute')
    def test_execute_task_passes_through_all_arguments(self, mock_execute):
        """Test execute_task passes all arguments to TaskExecutor."""
        mock_execute.return_value = 'result'

        def my_task():
            pass

        result = execute_task(
            my_task,
            'arg1',
            'arg2',
            use_async=True,
            kwarg1='value1'
        )

        mock_execute.assert_called_once_with(
            my_task,
            'arg1',
            'arg2',
            use_async=True,
            kwarg1='value1'
        )
        self.assertEqual(result, 'result')


# ============================================================================
# ERROR HANDLING TESTS
# ============================================================================

class TaskExecutorErrorHandlingTests(TestCase):
    """Test error handling in task execution."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_sync_execution_handles_value_error(self):
        """Test sync execution handles ValueError."""
        def failing_task():
            raise ValueError("Invalid value")

        result = TaskExecutor.execute(failing_task)
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_sync_execution_handles_key_error(self):
        """Test sync execution handles KeyError."""
        def failing_task():
            data = {}
            return data['nonexistent']

        result = TaskExecutor.execute(failing_task)
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_sync_execution_handles_attribute_error(self):
        """Test sync execution handles AttributeError."""
        def failing_task():
            obj = None
            return obj.some_method()

        result = TaskExecutor.execute(failing_task)
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_sync_execution_handles_type_error(self):
        """Test sync execution handles TypeError."""
        def failing_task(required_arg):
            return required_arg

        # Call without required argument
        result = TaskExecutor._execute_sync(failing_task)
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_sync_execution_handles_index_error(self):
        """Test sync execution handles IndexError."""
        def failing_task():
            items = [1, 2, 3]
            return items[10]

        result = TaskExecutor.execute(failing_task)
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_async_execution_handles_thread_creation_error(self):
        """Test async execution handles exception in thread creation."""
        def simple_task():
            pass

        with patch('testimonials.services.task_executor.Thread', side_effect=Exception("Thread error")):
            result = TaskExecutor._execute_async(simple_task)
            self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_delayed_execution_handles_exception(self):
        """Test delayed execution handles exception gracefully."""
        def simple_task():
            return 'fallback'

        with patch('testimonials.services.task_executor.Timer', side_effect=Exception("Timer failed")):
            result = TaskExecutor.execute_delayed(simple_task, 60)

            # Should fall back to sync
            self.assertEqual(result, 'fallback')

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_batch_execution_handles_task_exception(self):
        """Test batch execution handles exception in task."""
        def failing_task(batch):
            raise RuntimeError("Batch processing failed")

        items = list(range(10))
        results = TaskExecutor.execute_batch(failing_task, items, batch_size=5)

        # Should still complete but with None results
        self.assertEqual(len(results), 2)
        self.assertIsNone(results[0])
        self.assertIsNone(results[1])


# ============================================================================
# EDGE CASE TESTS
# ============================================================================

class TaskExecutorEdgeCaseTests(TestCase):
    """Test edge cases and boundary conditions."""

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_with_none_task(self):
        """Test execute with None as task handles gracefully."""
        # Calling with None should return None after error handling
        result = TaskExecutor.execute(None)
        # The implementation catches exceptions and returns None
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_returns_none(self):
        """Test execute when task returns None."""
        def none_task():
            return None

        result = TaskExecutor.execute(none_task)
        self.assertIsNone(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_returns_false(self):
        """Test execute when task returns False."""
        def false_task():
            return False

        result = TaskExecutor.execute(false_task)
        self.assertFalse(result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_returns_zero(self):
        """Test execute when task returns 0."""
        def zero_task():
            return 0

        result = TaskExecutor.execute(zero_task)
        self.assertEqual(result, 0)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_returns_empty_string(self):
        """Test execute when task returns empty string."""
        def empty_task():
            return ''

        result = TaskExecutor.execute(empty_task)
        self.assertEqual(result, '')

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_returns_empty_list(self):
        """Test execute when task returns empty list."""
        def empty_list_task():
            return []

        result = TaskExecutor.execute(empty_list_task)
        self.assertEqual(result, [])

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_task_returns_empty_dict(self):
        """Test execute when task returns empty dict."""
        def empty_dict_task():
            return {}

        result = TaskExecutor.execute(empty_dict_task)
        self.assertEqual(result, {})

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_with_lambda(self):
        """Test execute with lambda function."""
        result = TaskExecutor.execute(lambda x: x * 2, 5, use_async=False)
        self.assertEqual(result, 10)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_with_nested_function(self):
        """Test execute with nested function."""
        def outer():
            def inner(x):
                return x ** 2
            return inner(5)

        result = TaskExecutor.execute(outer)
        self.assertEqual(result, 25)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_with_class_method(self):
        """Test execute with class method."""
        class MyClass:
            @staticmethod
            def my_method(x):
                return x * 3

        result = TaskExecutor.execute(MyClass.my_method, 7)
        self.assertEqual(result, 21)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_batch_with_heterogeneous_items(self):
        """Test batch processing with mixed data types."""
        def process_batch(batch):
            return [str(item) for item in batch]

        items = [1, 'two', 3.0, None, True]
        results = TaskExecutor.execute_batch(
            process_batch,
            items,
            batch_size=2
        )

        # Should handle mixed types
        self.assertEqual(len(results), 3)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_with_very_large_args(self):
        """Test execute with very large arguments."""
        def sum_list(items):
            return sum(items)

        large_list = list(range(10000))
        result = TaskExecutor.execute(sum_list, large_list)

        self.assertEqual(result, sum(range(10000)))

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_toggles_between_sync_and_async(self):
        """Test execute can toggle between sync and async."""
        def simple_task():
            return 'result'

        # Force sync
        result1 = TaskExecutor.execute(simple_task, use_async=False)
        self.assertEqual(result1, 'result')

        # Force async
        result2 = TaskExecutor.execute(simple_task, use_async=True)
        self.assertIsInstance(result2, Thread)
        result2.join(timeout=2)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_batch_processing_stops_on_empty_batches(self):
        """Test batch processing handles empty batches correctly."""
        call_count = 0

        def process_batch(batch):
            nonlocal call_count
            call_count += 1
            return len(batch)

        # Empty list should not call process at all
        results = TaskExecutor.execute_batch(process_batch, [], batch_size=5)

        self.assertEqual(len(results), 0)
        self.assertEqual(call_count, 0)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=True)
    def test_execute_async_with_callable_object(self):
        """Test async execute with callable object."""
        class CallableTask:
            __name__ = 'CallableTask'

            def __call__(self):
                return 'result'

        task = CallableTask()
        result = TaskExecutor.execute(task, use_async=True)
        self.assertIsInstance(result, Thread)
        result.join(timeout=2)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_with_unicode_arguments(self):
        """Test execute with unicode arguments."""
        def echo_task(text):
            return f"Processed: {text}"

        result = TaskExecutor.execute(echo_task, "Hello 你好 مرحبا 🎉")
        self.assertIn("Hello", result)
        self.assertIn("你好", result)

    @override_settings(TESTIMONIALS_USE_BACKGROUND_TASKS=False)
    def test_execute_removes_only_async_kwargs(self):
        """Test execute only removes async-specific kwargs, not others."""
        received_kwargs = {}

        def task_with_kwargs(**kwargs):
            received_kwargs.update(kwargs)
            return True

        TaskExecutor.execute(
            task_with_kwargs,
            my_arg='value',
            countdown=60,  # Should be removed
            expires=3600,  # Should be removed
            eta='2024-01-01',  # Should be removed
            other_arg='keep'  # Should be kept
        )

        self.assertIn('my_arg', received_kwargs)
        self.assertIn('other_arg', received_kwargs)
        self.assertNotIn('countdown', received_kwargs)
        self.assertNotIn('expires', received_kwargs)
        self.assertNotIn('eta', received_kwargs)
