"""
Integration Tests — Failure Scenarios & Resilience.

تست‌های سناریوهای خطا، Circuit Breaker، Saga Compensation و بازیابی.
"""
import time
import pytest
from unittest.mock import patch, MagicMock

from shared.utils.circuit_breaker import (
    CircuitBreaker,
    CircuitOpenError,
    CircuitState,
    circuit_breaker,
)


# =============================================================================
# Circuit Breaker Tests
# =============================================================================


class TestCircuitBreaker:
    """تست الگوی Circuit Breaker."""

    def setup_method(self):
        """Reset all breakers between tests."""
        CircuitBreaker._registry.clear()

    def test_closed_state_allows_calls(self):
        """Circuit بسته — درخواست‌ها عبور می‌کنند."""
        breaker = CircuitBreaker(
            name='test-closed',
            failure_threshold=3,
        )

        result = breaker.call(lambda: 'success')
        assert result == 'success'
        assert breaker.state == CircuitState.CLOSED
        assert breaker.metrics.successful_calls == 1

    def test_opens_after_failure_threshold(self):
        """بعد از رسیدن به حد آستانه خطا، Circuit باز می‌شود."""
        breaker = CircuitBreaker(
            name='test-open',
            failure_threshold=3,
            recovery_timeout=60,
        )

        def failing_call():
            raise ConnectionError('Service unavailable')

        for _ in range(3):
            with pytest.raises(ConnectionError):
                breaker.call(failing_call)

        assert breaker.state == CircuitState.OPEN
        assert breaker.metrics.consecutive_failures == 0  # reset on transition
        assert breaker.metrics.failed_calls == 3

    def test_open_state_rejects_calls(self):
        """Circuit باز — درخواست‌ها بلافاصله رد می‌شوند."""
        breaker = CircuitBreaker(
            name='test-reject',
            failure_threshold=2,
            recovery_timeout=60,
        )

        def failing():
            raise RuntimeError('fail')

        # Trip the breaker
        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(failing)

        # Now should reject
        with pytest.raises(CircuitOpenError) as exc_info:
            breaker.call(lambda: 'should not reach')

        assert exc_info.value.breaker_name == 'test-reject'
        assert breaker.metrics.rejected_calls == 1

    def test_fallback_when_open(self):
        """وقتی Circuit باز است، fallback اجرا می‌شود."""
        fallback_result = {'fallback': True}
        breaker = CircuitBreaker(
            name='test-fallback',
            failure_threshold=2,
            recovery_timeout=60,
            fallback=lambda: fallback_result,
        )

        def failing():
            raise RuntimeError('fail')

        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(failing)

        result = breaker.call(lambda: 'normal')
        assert result == fallback_result

    def test_half_open_transition(self):
        """بعد از timeout بازیابی، Circuit نیمه‌باز می‌شود."""
        breaker = CircuitBreaker(
            name='test-half-open',
            failure_threshold=2,
            recovery_timeout=0.1,  # Very short for testing
        )

        def failing():
            raise RuntimeError('fail')

        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(failing)

        assert breaker._state == CircuitState.OPEN

        # Wait for recovery timeout
        time.sleep(0.15)

        # Should transition to HALF_OPEN
        assert breaker.state == CircuitState.HALF_OPEN

    def test_half_open_to_closed_on_success(self):
        """موفقیت متوالی در نیمه‌باز → Circuit بسته می‌شود."""
        breaker = CircuitBreaker(
            name='test-recovery',
            failure_threshold=2,
            recovery_timeout=0.1,
            success_threshold=2,
        )

        def failing():
            raise RuntimeError('fail')

        # Open the breaker
        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(failing)

        time.sleep(0.15)
        assert breaker.state == CircuitState.HALF_OPEN

        # Successful calls in half-open
        result = breaker.call(lambda: 'ok')
        assert result == 'ok'

        result = breaker.call(lambda: 'ok2')
        assert result == 'ok2'

        assert breaker.state == CircuitState.CLOSED

    def test_half_open_to_open_on_failure(self):
        """خطا در نیمه‌باز → Circuit مجدداً باز می‌شود."""
        breaker = CircuitBreaker(
            name='test-re-open',
            failure_threshold=2,
            recovery_timeout=0.1,
        )

        def failing():
            raise RuntimeError('fail')

        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(failing)

        time.sleep(0.15)
        assert breaker.state == CircuitState.HALF_OPEN

        with pytest.raises(RuntimeError):
            breaker.call(failing)

        assert breaker.state == CircuitState.OPEN

    def test_excluded_exceptions_dont_trip(self):
        """Exception‌های مستثنا‌شده باعث باز شدن Circuit نمی‌شوند."""
        breaker = CircuitBreaker(
            name='test-exclude',
            failure_threshold=2,
            excluded_exceptions=(ValueError,),
        )

        def raises_value_error():
            raise ValueError('expected error')

        for _ in range(5):
            with pytest.raises(ValueError):
                breaker.call(raises_value_error)

        # Should still be closed
        assert breaker.state == CircuitState.CLOSED
        assert breaker.metrics.successful_calls == 5

    def test_manual_reset(self):
        """ریست دستی Circuit Breaker."""
        breaker = CircuitBreaker(
            name='test-reset',
            failure_threshold=2,
        )

        def failing():
            raise RuntimeError('fail')

        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(failing)

        assert breaker.state == CircuitState.OPEN

        breaker.reset()
        assert breaker.state == CircuitState.CLOSED
        assert breaker.metrics.total_calls == 0

    def test_decorator_usage(self):
        """تست استفاده از decorator."""
        call_count = 0

        @circuit_breaker(name='test-decorator', failure_threshold=3)
        def my_function(x):
            nonlocal call_count
            call_count += 1
            return x * 2

        result = my_function(5)
        assert result == 10
        assert call_count == 1

    def test_get_all_statuses(self):
        """تست گزارش وضعیت همه breaker‌ها."""
        CircuitBreaker(name='breaker-1', failure_threshold=3)
        CircuitBreaker(name='breaker-2', failure_threshold=5)

        statuses = CircuitBreaker.get_all_statuses()
        assert len(statuses) == 2
        names = {s['name'] for s in statuses}
        assert names == {'breaker-1', 'breaker-2'}

    def test_callbacks_on_state_change(self):
        """تست اجرای callback‌ها هنگام تغییر وضعیت."""
        callbacks = []

        breaker = CircuitBreaker(
            name='test-callbacks',
            failure_threshold=2,
            recovery_timeout=0.1,
            on_open=lambda b: callbacks.append('opened'),
            on_half_open=lambda b: callbacks.append('half_opened'),
            on_close=lambda b: callbacks.append('closed'),
        )

        def failing():
            raise RuntimeError('fail')

        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(failing)

        assert 'opened' in callbacks

        time.sleep(0.15)
        _ = breaker.state  # Trigger transition
        assert 'half_opened' in callbacks

    def test_metrics_tracking(self):
        """تست ردیابی متریک‌ها."""
        breaker = CircuitBreaker(name='test-metrics', failure_threshold=5)

        # Successful calls
        for _ in range(3):
            breaker.call(lambda: 'ok')

        # Failed calls
        for _ in range(2):
            with pytest.raises(RuntimeError):
                breaker.call(lambda: (_ for _ in ()).throw(RuntimeError('err')))

        metrics = breaker.metrics
        assert metrics.total_calls == 5
        assert metrics.successful_calls == 3
        assert metrics.failed_calls == 2
        assert 0.3 < metrics.failure_rate < 0.5


# =============================================================================
# Saga Failure & Compensation Tests
# =============================================================================


@pytest.mark.django_db(transaction=True)
class TestSagaCompensation:
    """تست سناریوهای خطا و جبران در Saga."""

    def test_saga_model_creation(self, tenant):
        """ایجاد مدل‌های Saga و بررسی روابط."""
        from apps.services.workflow.saga.models import (
            SagaExecution,
            SagaStep,
            SagaLog,
        )

        saga = SagaExecution.objects.create(
            tenant=tenant,
            name='test-saga',
            status='started',
            context={'order_id': '123'},
            idempotency_key='saga-test-001',
        )

        step1 = SagaStep.objects.create(
            tenant=tenant,
            saga=saga,
            name='create-order',
            order=1,
            action_type='orders.actions.create_order',
            compensation_type='orders.actions.cancel_order',
            status='pending',
        )

        step2 = SagaStep.objects.create(
            tenant=tenant,
            saga=saga,
            name='reserve-inventory',
            order=2,
            action_type='inventory.actions.reserve',
            compensation_type='inventory.actions.release',
            status='pending',
        )

        log = SagaLog.objects.create(
            tenant=tenant,
            saga=saga,
            step=step1,
            event_type='step_started',
            message='شروع مرحله ایجاد سفارش',
        )

        assert saga.steps.count() == 2
        assert saga.logs.count() == 1
        assert step1.saga == saga
        assert log.saga == saga

    def test_saga_idempotency_key_unique(self, tenant):
        """کلید idempotency باید یکتا باشد."""
        from apps.services.workflow.saga.models import SagaExecution
        from django.db import IntegrityError

        SagaExecution.objects.create(
            tenant=tenant,
            name='saga-1',
            status='started',
            idempotency_key='unique-key-001',
        )

        with pytest.raises(IntegrityError):
            SagaExecution.objects.create(
                tenant=tenant,
                name='saga-2',
                status='started',
                idempotency_key='unique-key-001',
            )

    def test_saga_step_ordering(self, tenant):
        """مراحل Saga بر اساس ترتیب مرتب می‌شوند."""
        from apps.services.workflow.saga.models import SagaExecution, SagaStep

        saga = SagaExecution.objects.create(
            tenant=tenant,
            name='ordered-saga',
            status='started',
            idempotency_key='order-test-001',
        )

        SagaStep.objects.create(
            tenant=tenant, saga=saga, name='step-3', order=3,
            action_type='a.b.c', status='pending',
        )
        SagaStep.objects.create(
            tenant=tenant, saga=saga, name='step-1', order=1,
            action_type='a.b.c', status='pending',
        )
        SagaStep.objects.create(
            tenant=tenant, saga=saga, name='step-2', order=2,
            action_type='a.b.c', status='pending',
        )

        steps = list(saga.steps.order_by('order').values_list('name', flat=True))
        assert steps == ['step-1', 'step-2', 'step-3']


# =============================================================================
# Workflow State Persistence Tests
# =============================================================================


@pytest.mark.django_db(transaction=True)
class TestWorkflowStatePersistence:
    """تست ذخیره‌سازی و بازیابی وضعیت فرآیند."""

    def test_execution_log_sequence(self, tenant, process_definition, user_factory):
        """لاگ‌های اجرا با sequence_number مرتب هستند."""
        from apps.services.workflow.models import ProcessInstance
        from apps.services.workflow.state.models import WorkflowExecutionLog

        user = user_factory(email='state@test.com')

        instance = ProcessInstance.objects.create(
            tenant=tenant,
            definition=process_definition,
            started_by=user,
            status='active',
        )

        for i, event_type in enumerate([
            'instance_started',
            'task_created',
            'task_assigned',
            'task_completed',
        ], start=1):
            WorkflowExecutionLog.objects.create(
                tenant=tenant,
                execution=instance,
                event_type=event_type,
                sequence_number=i,
                payload={'step': i},
            )

        logs = WorkflowExecutionLog.objects.filter(
            execution=instance,
        ).order_by('sequence_number')

        assert logs.count() == 4
        assert list(logs.values_list('event_type', flat=True)) == [
            'instance_started',
            'task_created',
            'task_assigned',
            'task_completed',
        ]

    def test_snapshot_creation(self, tenant, process_definition, user_factory):
        """ایجاد و بازیابی Snapshot."""
        from apps.services.workflow.models import ProcessInstance
        from apps.services.workflow.state.models import WorkflowSnapshot

        user = user_factory(email='snapshot@test.com')

        instance = ProcessInstance.objects.create(
            tenant=tenant,
            definition=process_definition,
            started_by=user,
            status='active',
        )

        snapshot = WorkflowSnapshot.objects.create(
            tenant=tenant,
            execution=instance,
            state_data={'status': 'active', 'current_step': 'step-2'},
            variables_data={'approved': False, 'amount': 5000},
            tasks_data=[
                {'id': 'task-1', 'status': 'completed'},
                {'id': 'task-2', 'status': 'in_progress'},
            ],
            snapshot_reason='manual',
            last_event_sequence=10,
            created_by=user,
        )

        assert snapshot.state_data['current_step'] == 'step-2'
        assert snapshot.variables_data['amount'] == 5000
        assert len(snapshot.tasks_data) == 2

        # Verify we can query the latest snapshot
        latest = WorkflowSnapshot.objects.filter(
            execution=instance,
        ).order_by('-created_at').first()

        assert latest.id == snapshot.id


# =============================================================================
# Search Queue Tests
# =============================================================================


@pytest.mark.django_db(transaction=True)
class TestSearchIndexQueue:
    """تست صف ایندکس جستجو و Dead Letter Queue."""

    def test_queue_item_lifecycle(self, tenant):
        """چرخه عمر آیتم صف: pending → processing → completed."""
        from apps.services.search.models import SearchIndexQueue

        item = SearchIndexQueue.objects.create(
            tenant=tenant,
            entity_type='worktable_record',
            entity_id='record-123',
            operation='create',
            payload={'title': 'تست'},
            status='pending',
        )

        assert item.status == 'pending'
        assert item.retry_count == 0

        # Simulate processing
        item.status = 'processing'
        item.save()

        item.status = 'completed'
        item.save()

        item.refresh_from_db()
        assert item.status == 'completed'

    def test_dead_letter_queue(self, tenant):
        """آیتم‌های ناموفق به DLQ منتقل می‌شوند."""
        from apps.services.search.models import (
            SearchIndexQueue,
            SearchIndexDeadLetter,
        )

        # Create a failed item
        item = SearchIndexQueue.objects.create(
            tenant=tenant,
            entity_type='worktable_record',
            entity_id='failed-record',
            operation='update',
            status='failed',
            retry_count=5,
        )

        # Move to DLQ
        dlq_item = SearchIndexDeadLetter.objects.create(
            tenant=tenant,
            entity_type=item.entity_type,
            entity_id=item.entity_id,
            operation=item.operation,
            retry_count=item.retry_count,
            stack_trace='ConnectionError: Elasticsearch unreachable',
            original_created_at=item.created_at,
        )

        item.delete()

        assert SearchIndexQueue.objects.filter(
            entity_id='failed-record',
        ).count() == 0
        assert SearchIndexDeadLetter.objects.filter(
            entity_id='failed-record',
        ).count() == 1
        assert dlq_item.resolved is False


# =============================================================================
# CQRS Read Model Tests
# =============================================================================


@pytest.mark.django_db(transaction=True)
class TestCQRSReadModel:
    """تست مدل خواندنی CQRS و آرشیو."""

    def test_task_inbox_view_creation(self, tenant, user_factory):
        """ایجاد TaskInboxView (مدل خواندنی denormalized)."""
        from apps.services.worktable.cqrs.models import TaskInboxView

        user = user_factory(email='cqrs@test.com')

        view = TaskInboxView.objects.create(
            tenant=tenant,
            record_id='rec-001',
            title='بررسی درخواست مرخصی',
            description='درخواست ۳ روز مرخصی',
            table_id='table-001',
            table_name='درخواست‌ها',
            table_slug='requests',
            assigned_to_id=str(user.id),
            assigned_to_name=user.get_full_name(),
            module_name='hr',
            status='pending',
            priority=50,
        )

        assert view.title == 'بررسی درخواست مرخصی'
        assert view.status == 'pending'

        # Query by assignment
        my_tasks = TaskInboxView.objects.filter(
            assigned_to_id=str(user.id),
            tenant=tenant,
        )
        assert my_tasks.count() == 1

    def test_task_archive(self, tenant):
        """آرشیو رکوردهای تکمیل‌شده."""
        from apps.services.worktable.cqrs.models import TaskArchive

        archive = TaskArchive.objects.create(
            tenant=tenant,
            original_record_id='rec-completed-001',
            table_id='table-001',
            data={'title': 'کار تمام‌شده', 'result': 'approved'},
            status='completed',
            workflow_state='ended',
            archived_reason='auto_completed',
        )

        assert archive.status == 'completed'
        assert archive.data['result'] == 'approved'

        # Verify archived items are queryable
        completed_archives = TaskArchive.objects.filter(
            tenant=tenant,
            status='completed',
        )
        assert completed_archives.count() >= 1
