"""
Concurrency tests for payment approval
Tests race conditions and concurrent approval attempts
"""
import pytest
from django.test import TestCase, TransactionTestCase
from django.contrib.auth import get_user_model
from django.db import transaction
from concurrent.futures import ThreadPoolExecutor, as_completed
from decimal import Decimal
from igny8_core.business.billing.models import (
    Invoice, Payment, Subscription, Plan, Account
)
from igny8_core.business.billing.views import approve_payment
from unittest.mock import Mock
import threading

User = get_user_model()


class PaymentApprovalConcurrencyTest(TransactionTestCase):
    """Test concurrent payment approval scenarios"""
    
    def setUp(self):
        """Set up test data"""
        # Create admin user
        self.admin = User.objects.create_user(
            email='admin@test.com',
            password='testpass123',
            is_staff=True
        )
        
        # Create account
        self.account = Account.objects.create(
            name='Test Account',
            owner=self.admin,
            credit_balance=0
        )
        
        # Create plan
        self.plan = Plan.objects.create(
            name='Test Plan',
            slug='test-plan',
            price=Decimal('100.00'),
            currency='USD',
            billing_period='monthly',
            included_credits=1000
        )
        
        # Create subscription
        self.subscription = Subscription.objects.create(
            account=self.account,
            plan=self.plan,
            status='pending_payment'
        )
        
        # Create invoice
        self.invoice = Invoice.objects.create(
            account=self.account,
            invoice_number='INV-TEST-001',
            status='pending',
            subtotal=Decimal('100.00'),
            total_amount=Decimal('100.00'),
            currency='USD',
            invoice_type='subscription'
        )
        
        # Create payment
        self.payment = Payment.objects.create(
            account=self.account,
            invoice=self.invoice,
            amount=Decimal('100.00'),
            currency='USD',
            payment_method='bank_transfer',
            status='pending_approval',
            manual_reference='TEST-REF-001'
        )
    
    def test_concurrent_approval_attempts(self):
        """
        Test that only one concurrent approval succeeds
        Multiple admins trying to approve same payment simultaneously
        """
        num_threads = 5
        success_count = 0
        failure_count = 0
        results = []
        
        def approve_payment_thread(payment_id, admin_user):
            """Thread worker to approve payment"""
            try:
                # Simulate approval logic with transaction
                with transaction.atomic():
                    payment = Payment.objects.select_for_update().get(id=payment_id)
                    
                    # Check if already approved
                    if payment.status == 'succeeded':
                        return {'success': False, 'reason': 'already_approved'}
                    
                    # Approve payment
                    payment.status = 'succeeded'
                    payment.approved_by = admin_user
                    payment.save()
                    
                    # Update invoice
                    invoice = payment.invoice
                    invoice.status = 'paid'
                    invoice.save()
                    
                    return {'success': True}
                    
            except Exception as e:
                return {'success': False, 'error': str(e)}
        
        # Create multiple threads attempting approval
        with ThreadPoolExecutor(max_workers=num_threads) as executor:
            futures = []
            for i in range(num_threads):
                future = executor.submit(approve_payment_thread, self.payment.id, self.admin)
                futures.append(future)
            
            # Collect results
            for future in as_completed(futures):
                result = future.result()
                results.append(result)
                if result.get('success'):
                    success_count += 1
                else:
                    failure_count += 1
        
        # Verify only one approval succeeded
        self.assertEqual(success_count, 1, "Only one approval should succeed")
        self.assertEqual(failure_count, num_threads - 1, "Other attempts should fail")
        
        # Verify final state
        payment = Payment.objects.get(id=self.payment.id)
        self.assertEqual(payment.status, 'succeeded')
        
        invoice = Invoice.objects.get(id=self.invoice.id)
        self.assertEqual(invoice.status, 'paid')
    
    def test_payment_and_invoice_consistency(self):
        """
        Test that payment and invoice remain consistent under concurrent operations
        """
        def read_payment_invoice(payment_id):
            """Read payment and invoice status"""
            payment = Payment.objects.get(id=payment_id)
            invoice = Invoice.objects.get(id=payment.invoice_id)
            return {
                'payment_status': payment.status,
                'invoice_status': invoice.status,
                'consistent': (
                    (payment.status == 'succeeded' and invoice.status == 'paid') or
                    (payment.status == 'pending_approval' and invoice.status == 'pending')
                )
            }
        
        # Approve payment in one thread
        def approve():
            with transaction.atomic():
                payment = Payment.objects.select_for_update().get(id=self.payment.id)
                payment.status = 'succeeded'
                payment.save()
                
                invoice = Invoice.objects.select_for_update().get(id=self.invoice.id)
                invoice.status = 'paid'
                invoice.save()
        
        # Read state in parallel threads
        results = []
        with ThreadPoolExecutor(max_workers=10) as executor:
            # Start approval
            approval_future = executor.submit(approve)
            
            # Multiple concurrent reads
            read_futures = [
                executor.submit(read_payment_invoice, self.payment.id)
                for _ in range(20)
            ]
            
            # Wait for approval
            approval_future.result()
            
            # Collect read results
            for future in as_completed(read_futures):
                results.append(future.result())
        
        # All reads should show consistent state
        for result in results:
            self.assertTrue(
                result['consistent'],
                f"Inconsistent state: payment={result['payment_status']}, invoice={result['invoice_status']}"
            )
    
    def test_double_approval_prevention(self):
        """
        Test that payment cannot be approved twice
        """
        # First approval
        with transaction.atomic():
            payment = Payment.objects.select_for_update().get(id=self.payment.id)
            payment.status = 'succeeded'
            payment.approved_by = self.admin
            payment.save()
            
            invoice = payment.invoice
            invoice.status = 'paid'
            invoice.save()
        
        # Attempt second approval
        result = None
        try:
            with transaction.atomic():
                payment = Payment.objects.select_for_update().get(id=self.payment.id)
                
                # Should detect already approved
                if payment.status == 'succeeded':
                    result = 'already_approved'
                else:
                    payment.status = 'succeeded'
                    payment.save()
                    result = 'approved'
        except Exception as e:
            result = f'error: {str(e)}'
        
        self.assertEqual(result, 'already_approved', "Second approval should be prevented")


class CreditTransactionConcurrencyTest(TransactionTestCase):
    """Test concurrent credit additions/deductions"""
    
    def setUp(self):
        self.admin = User.objects.create_user(
            email='admin@test.com',
            password='testpass123'
        )
        self.account = Account.objects.create(
            name='Test Account',
            owner=self.admin,
            credit_balance=1000
        )
    
    def test_concurrent_credit_deductions(self):
        """
        Test that concurrent credit deductions maintain correct balance
        """
        initial_balance = self.account.credit_balance
        deduction_amount = 10
        num_operations = 20
        
        def deduct_credits(account_id, amount):
            """Deduct credits atomically"""
            from igny8_core.business.billing.models import CreditTransaction
            
            with transaction.atomic():
                account = Account.objects.select_for_update().get(id=account_id)
                
                # Check sufficient balance
                if account.credit_balance < amount:
                    return {'success': False, 'reason': 'insufficient_credits'}
                
                # Deduct credits
                account.credit_balance -= amount
                new_balance = account.credit_balance
                account.save()
                
                # Record transaction
                CreditTransaction.objects.create(
                    account=account,
                    transaction_type='deduction',
                    amount=-amount,
                    balance_after=new_balance,
                    description='Test deduction'
                )
                
                return {'success': True, 'new_balance': new_balance}
        
        # Concurrent deductions
        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [
                executor.submit(deduct_credits, self.account.id, deduction_amount)
                for _ in range(num_operations)
            ]
            
            results = [future.result() for future in as_completed(futures)]
        
        # Verify all succeeded
        success_count = sum(1 for r in results if r.get('success'))
        self.assertEqual(success_count, num_operations, "All deductions should succeed")
        
        # Verify final balance
        self.account.refresh_from_db()
        expected_balance = initial_balance - (deduction_amount * num_operations)
        self.assertEqual(
            self.account.credit_balance,
            expected_balance,
            f"Final balance should be {expected_balance}"
        )
