"""
Tests for django_cronjob_utils locks.
"""
from datetime import date
from django.test import TestCase
from django.db import transaction
from django_cronjob_utils.locks import DatabaseLock
from django_cronjob_utils.models import CronExecution
from django_cronjob_utils.exceptions import ConcurrentExecutionError


class DatabaseLockTests(TestCase):
    """Tests for DatabaseLock."""
    
    def test_lock_acquires_successfully(self):
        """Test lock acquires when no running execution exists."""
        lock = DatabaseLock('A001', date(2024, 1, 15))
        
        # Should not raise exception
        with lock:
            pass
    
    def test_lock_prevents_concurrent_execution(self):
        """Test lock prevents concurrent execution."""
        # Create a running execution
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        
        lock = DatabaseLock('A001', date(2024, 1, 15))
        
        # Should raise ConcurrentExecutionError
        with self.assertRaises(ConcurrentExecutionError) as cm:
            with lock:
                pass
        
        self.assertIn('A001', str(cm.exception))
        self.assertIn('2024-01-15', str(cm.exception))
    
    def test_lock_allows_different_task_code(self):
        """Test lock allows execution for different task code."""
        # Create running execution for A001
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        
        # Lock for A002 should work
        lock = DatabaseLock('A002', date(2024, 1, 15))
        with lock:
            pass  # Should not raise
    
    def test_lock_allows_different_execution_date(self):
        """Test lock allows execution for different execution date."""
        # Create running execution for 2024-01-15
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        
        # Lock for 2024-01-16 should work
        lock = DatabaseLock('A001', date(2024, 1, 16))
        with lock:
            pass  # Should not raise
    
    def test_lock_allows_completed_execution(self):
        """Test lock allows execution when previous execution is completed."""
        # Create completed execution
        execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=True
        )
        
        # Lock should work
        lock = DatabaseLock('A001', date(2024, 1, 15))
        with lock:
            pass  # Should not raise
    
    def test_lock_allows_successful_execution(self):
        """Test lock allows execution when previous execution succeeded."""
        execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=True
        )
        
        lock = DatabaseLock('A001', date(2024, 1, 15))
        with lock:
            pass  # Should not raise
    
    def test_lock_allows_failed_execution(self):
        """Test lock allows execution when previous execution failed."""
        execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=False
        )
        
        lock = DatabaseLock('A001', date(2024, 1, 15))
        with lock:
            pass  # Should not raise
    
    def test_lock_multiple_running_executions(self):
        """Test lock detects multiple running executions."""
        # Create multiple running executions
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        
        lock = DatabaseLock('A001', date(2024, 1, 15))
        
        with self.assertRaises(ConcurrentExecutionError):
            with lock:
                pass
    
    def test_lock_released_after_context(self):
        """Test lock is released after context manager exits."""
        lock = DatabaseLock('A001', date(2024, 1, 15))
        
        with lock:
            # Create a running execution inside lock context
            CronExecution.objects.create(
                task_code='A001',
                task_name='Test Task',
                execution_date=date(2024, 1, 15),
                completed=False
            )
        
        # After context exits, we should be able to acquire lock again
        # (if we mark the execution as completed)
        execution = CronExecution.objects.get(
            task_code='A001',
            execution_date=date(2024, 1, 15)
        )
        execution.mark_completed(success=True)
        
        # Now lock should work
        lock2 = DatabaseLock('A001', date(2024, 1, 15))
        with lock2:
            pass  # Should not raise
    
    def test_lock_within_transaction(self):
        """Test lock works within database transaction."""
        with transaction.atomic():
            lock = DatabaseLock('A001', date(2024, 1, 15))
            with lock:
                # Create execution within transaction
                CronExecution.objects.create(
                    task_code='A001',
                    task_name='Test Task',
                    execution_date=date(2024, 1, 15),
                    completed=False
                )
    
    def test_lock_select_for_update(self):
        """Test lock uses SELECT FOR UPDATE (indirect test)."""
        # Create running execution
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        
        lock = DatabaseLock('A001', date(2024, 1, 15))
        
        # Should detect running execution
        with self.assertRaises(ConcurrentExecutionError):
            with lock:
                pass
    
    def test_lock_edge_case_empty_task_code(self):
        """Test lock with empty task code (edge case)."""
        CronExecution.objects.create(
            task_code='',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        
        lock = DatabaseLock('', date(2024, 1, 15))
        
        with self.assertRaises(ConcurrentExecutionError):
            with lock:
                pass
    
    def test_lock_edge_case_future_date(self):
        """Test lock with future date (edge case)."""
        future_date = date(2030, 12, 31)
        
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=future_date,
            completed=False
        )
        
        lock = DatabaseLock('A001', future_date)
        
        with self.assertRaises(ConcurrentExecutionError):
            with lock:
                pass
    
    def test_lock_edge_case_past_date(self):
        """Test lock with past date (edge case)."""
        past_date = date(2020, 1, 1)
        
        CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=past_date,
            completed=False
        )
        
        lock = DatabaseLock('A001', past_date)
        
        with self.assertRaises(ConcurrentExecutionError):
            with lock:
                pass
