from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.db.models import Count, Avg, Q
from datetime import timedelta

from testimonials.models import Testimonial, TestimonialCategory, TestimonialMedia
from testimonials.constants import TestimonialStatus, TestimonialSource, TestimonialMediaType
from testimonials.mixins.manager_mixins import (
    StatisticsAggregationMixin,
    TimePeriodFilterMixin,
    BulkOperationMixin
)

User = get_user_model()


# ============================================================================
# BASE TEST SETUP
# ============================================================================

class MixinTestCase(TestCase):
    """Base test case for mixin tests."""
    
    @classmethod
    def setUpTestData(cls):
        """Set up test data for all mixin tests."""
        # Create users
        cls.user1 = User.objects.create_user(
            username='user1',
            email='user1@example.com'
        )
        
        cls.user2 = User.objects.create_user(
            username='user2',
            email='user2@example.com'
        )
        
        # Create categories
        cls.category1 = TestimonialCategory.objects.create(
            name='Products',
            slug='products',
            is_active=True
        )
        
        cls.category2 = TestimonialCategory.objects.create(
            name='Services',
            slug='services',
            is_active=False
        )
        
        # Create diverse testimonials
        now = timezone.now()
        
        # Recent approved testimonials
        cls.t1 = Testimonial.objects.create(
            author=cls.user1,
            author_name='John Doe',
            content='Excellent product',
            rating=5,
            status=TestimonialStatus.APPROVED,
            category=cls.category1,
            source=TestimonialSource.WEBSITE,
            is_verified=True
        )
        
        cls.t2 = Testimonial.objects.create(
            author=cls.user1,
            author_name='Jane Smith',
            content='Good service',
            rating=4,
            status=TestimonialStatus.PENDING,
            category=cls.category1,
            source=TestimonialSource.EMAIL,
            response='Thank you!'
        )
        
        cls.t3 = Testimonial.objects.create(
            author=cls.user2,
            author_name='Bob Johnson',
            content='Average experience',
            rating=3,
            status=TestimonialStatus.REJECTED,
            is_anonymous=True
        )
        
        cls.t4 = Testimonial.objects.create(
            author=cls.user2,
            author_name='Alice Brown',
            content='Outstanding service',
            rating=5,
            status=TestimonialStatus.FEATURED,
            category=cls.category1,
            is_verified=True
        )
        
        # Old testimonial
        cls.t5 = Testimonial.objects.create(
            author=cls.user1,
            content='Old review',
            rating=4,
            status=TestimonialStatus.APPROVED
        )
        Testimonial.objects.filter(pk=cls.t5.pk).update(
            created_at=now - timedelta(days=60)
        )
        
        # Create media
        cls.media1 = TestimonialMedia.objects.create(
            testimonial=cls.t1,
            media_type=TestimonialMediaType.IMAGE,
            title='Product image',
            is_primary=True
        )
        
        cls.media2 = TestimonialMedia.objects.create(
            testimonial=cls.t1,
            media_type=TestimonialMediaType.VIDEO,
            is_primary=False
        )


# ============================================================================
# STATISTICS AGGREGATION MIXIN TESTS
# ============================================================================

class StatisticsAggregationMixinTests(MixinTestCase):
    """Tests for StatisticsAggregationMixin methods."""
    
    def test_get_basic_aggregates_count(self):
        """Test basic count aggregation."""
        result = Testimonial.objects.get_basic_aggregates({
            'total': Count('id')
        })
        
        self.assertEqual(result['total'], 5)
    
    def test_get_basic_aggregates_average(self):
        """Test average aggregation."""
        result = Testimonial.objects.get_basic_aggregates({
            'avg_rating': Avg('rating')
        })
        
        # (5 + 4 + 3 + 5 + 4) / 5 = 4.2
        self.assertAlmostEqual(result['avg_rating'], 4.2, places=1)
    
    def test_get_basic_aggregates_multiple(self):
        """Test multiple aggregations at once."""
        result = Testimonial.objects.get_basic_aggregates({
            'total': Count('id'),
            'avg_rating': Avg('rating')
        })
        
        self.assertEqual(result['total'], 5)
        self.assertIn('avg_rating', result)
    
    def test_get_basic_aggregates_empty_queryset(self):
        """Test aggregations on empty queryset."""
        # Filter first, then use manager method via custom queryset manager
        qs = Testimonial.objects.filter(status='nonexistent')
        # Call aggregate directly since get_basic_aggregates is a manager method
        result = qs.aggregate(
            total=Count('id'),
            avg_rating=Avg('rating')
        )
        
        self.assertEqual(result['total'], 0)
        self.assertIsNone(result['avg_rating'])
    
    def test_get_choice_distribution_status(self):
        """Test status distribution calculation."""
        result = Testimonial.objects.get_choice_distribution(
            'status',
            TestimonialStatus.choices
        )
        
        # Check approved count
        self.assertEqual(result[TestimonialStatus.APPROVED]['count'], 2)
        self.assertEqual(result[TestimonialStatus.APPROVED]['label'], 'Approved')
        self.assertEqual(result[TestimonialStatus.APPROVED]['percentage'], 40.0)
        
        # Check pending
        self.assertEqual(result[TestimonialStatus.PENDING]['count'], 1)
        self.assertEqual(result[TestimonialStatus.PENDING]['percentage'], 20.0)
        
        # Check featured
        self.assertEqual(result[TestimonialStatus.FEATURED]['count'], 1)
        
        # Check rejected
        self.assertEqual(result[TestimonialStatus.REJECTED]['count'], 1)
    
    def test_get_choice_distribution_source(self):
        """Test source distribution calculation."""
        result = Testimonial.objects.get_choice_distribution(
            'source',
            TestimonialSource.choices
        )
        
        # 3 website (t1, t3, t4, t5 default to WEBSITE)
        self.assertEqual(result[TestimonialSource.WEBSITE]['count'], 4)
        # 1 email (t2)
        self.assertEqual(result[TestimonialSource.EMAIL]['count'], 1)
        # 0 mobile_app
        self.assertEqual(result[TestimonialSource.MOBILE_APP]['count'], 0)
    
    def test_get_choice_distribution_empty_queryset(self):
        """Test distribution on empty queryset."""
        # Delete all testimonials to test on truly empty manager
        Testimonial.objects.all().delete()
        
        result = Testimonial.objects.get_choice_distribution('status', TestimonialStatus.choices)
        
        # All counts should be 0, all percentages should be 0
        for code, data in result.items():
            self.assertEqual(data['count'], 0)
            self.assertEqual(data['percentage'], 0.0)
    
    def test_get_conditional_counts(self):
        """Test conditional counts in single query."""
        result = Testimonial.objects.get_conditional_counts({
            'approved': Q(status=TestimonialStatus.APPROVED),
            'pending': Q(status=TestimonialStatus.PENDING),
            'high_rated': Q(rating__gte=4),
            'verified': Q(is_verified=True)
        })
        
        self.assertEqual(result['approved'], 2)
        self.assertEqual(result['pending'], 1)
        self.assertEqual(result['high_rated'], 4)  # t1, t2, t4, t5
        self.assertEqual(result['verified'], 2)  # t1, t4
    
    def test_get_conditional_counts_complex_conditions(self):
        """Test conditional counts with complex Q objects."""
        result = Testimonial.objects.get_conditional_counts({
            'approved_and_verified': Q(
                status=TestimonialStatus.APPROVED,
                is_verified=True
            ),
            'pending_or_rejected': Q(status=TestimonialStatus.PENDING) | Q(
                status=TestimonialStatus.REJECTED
            )
        })
        
        self.assertEqual(result['approved_and_verified'], 1)  # t1
        self.assertEqual(result['pending_or_rejected'], 2)  # t2, t3
    
    def test_get_boolean_field_counts(self):
        """Test boolean field count calculation."""
        result = Testimonial.objects.get_boolean_field_counts('is_verified')
        
        self.assertEqual(result['true_count'], 2)  # t1, t4
        self.assertEqual(result['false_count'], 3)  # t2, t3, t5
    
    def test_get_boolean_field_counts_is_anonymous(self):
        """Test boolean counts for is_anonymous."""
        result = Testimonial.objects.get_boolean_field_counts('is_anonymous')
        
        self.assertEqual(result['true_count'], 1)  # t3
        self.assertEqual(result['false_count'], 4)
    
    def test_get_null_vs_filled_counts(self):
        """Test null vs filled value counts."""
        result = Testimonial.objects.get_null_vs_filled_counts('response')
        
        # Django CharField/TextField: empty string != NULL
        # All testimonials have a response value (even if empty string)
        # So filled_count should be 5, null_count should be 0
        # UNLESS the field has null=True and we actually store NULL
        
        # Since we can't rely on response field behavior, test with category
        # which is a ForeignKey that can actually be null
        category_result = Testimonial.objects.get_null_vs_filled_counts('category')
        
        # t1, t2, t4 have category; t3, t5 don't
        self.assertEqual(category_result['filled_count'], 3)
        self.assertEqual(category_result['null_count'], 2)
    
    def test_get_null_vs_filled_counts_category(self):
        """Test null counts for category field."""
        result = Testimonial.objects.get_null_vs_filled_counts('category')
        
        # t1, t2, t4 have category
        self.assertEqual(result['filled_count'], 3)
        # t3, t5 have no category
        self.assertEqual(result['null_count'], 2)


# ============================================================================
# TIME PERIOD FILTER MIXIN TESTS
# ============================================================================

class TimePeriodFilterMixinTests(MixinTestCase):
    """Tests for TimePeriodFilterMixin methods."""
    
    def test_in_date_range_both_dates(self):
        """Test filtering with both start and end dates."""
        now = timezone.now()
        start = now - timedelta(days=30)
        end = now + timedelta(days=1)
        
        result = Testimonial.objects.all().in_date_range(start, end)
        
        # Should include t1, t2, t3, t4 (not t5 which is 60 days old)
        self.assertEqual(result.count(), 4)
        self.assertNotIn(self.t5, result)
    
    def test_in_date_range_start_only(self):
        """Test filtering with only start date."""
        now = timezone.now()
        start = now - timedelta(days=30)
        
        result = Testimonial.objects.all().in_date_range(start, None)
        
        # Should include recent ones, not t5
        self.assertEqual(result.count(), 4)
    
    def test_in_date_range_end_only(self):
        """Test filtering with only end date."""
        now = timezone.now()
        end = now - timedelta(days=30)
        
        result = Testimonial.objects.all().in_date_range(None, end)
        
        # Should only include t5
        self.assertEqual(result.count(), 1)
        self.assertEqual(result.first(), self.t5)
    
    def test_in_date_range_no_dates(self):
        """Test filtering with no dates returns all."""
        result = Testimonial.objects.all().in_date_range(None, None)
        
        # Should return all testimonials
        self.assertEqual(result.count(), 5)
    
    def test_in_date_range_custom_field(self):
        """Test filtering with custom date field."""
        now = timezone.now()
        start = now - timedelta(days=30)
        
        # Use updated_at field instead
        result = Testimonial.objects.all().in_date_range(
            start, None, date_field='updated_at'
        )
        
        # All should be recent based on updated_at
        self.assertGreater(result.count(), 0)
    
    def test_created_in_last_days(self):
        """Test filtering by last N days."""
        result = Testimonial.objects.all().created_in_last_days(30)
        
        # Should get t1, t2, t3, t4 (not t5 which is 60 days old)
        self.assertEqual(result.count(), 4)
        self.assertNotIn(self.t5, result)
    
    def test_created_in_last_days_7(self):
        """Test filtering by last 7 days."""
        result = Testimonial.objects.all().created_in_last_days(7)
        
        # Should get all recent ones
        self.assertEqual(result.count(), 4)
    
    def test_created_in_last_days_1(self):
        """Test filtering by last 1 day."""
        result = Testimonial.objects.all().created_in_last_days(1)
        
        # Should get only today's testimonials
        self.assertGreaterEqual(result.count(), 0)
    
    def test_created_in_last_days_custom_field(self):
        """Test filtering with custom date field."""
        result = Testimonial.objects.all().created_in_last_days(
            30, date_field='updated_at'
        )
        
        # All should be recent based on updated_at
        self.assertGreater(result.count(), 0)
    
    def test_created_in_last_days_zero(self):
        """Test filtering with 0 days."""
        result = Testimonial.objects.all().created_in_last_days(0)
        
        # Should filter to very recent (likely 0 or few)
        self.assertGreaterEqual(result.count(), 0)


# ============================================================================
# BULK OPERATION MIXIN TESTS
# ============================================================================

class BulkOperationMixinTests(MixinTestCase):
    """Tests for BulkOperationMixin methods."""
    
    def test_bulk_update_status_single(self):
        """Test bulk updating status of single testimonial."""
        count = Testimonial.objects.bulk_update_status(
            [self.t2.pk],
            TestimonialStatus.APPROVED
        )
        
        self.assertEqual(count, 1)
        self.t2.refresh_from_db()
        self.assertEqual(self.t2.status, TestimonialStatus.APPROVED)
    
    def test_bulk_update_status_multiple(self):
        """Test bulk updating status of multiple testimonials."""
        count = Testimonial.objects.bulk_update_status(
            [self.t2.pk, self.t3.pk],
            TestimonialStatus.APPROVED
        )
        
        self.assertEqual(count, 2)
        
        self.t2.refresh_from_db()
        self.t3.refresh_from_db()
        self.assertEqual(self.t2.status, TestimonialStatus.APPROVED)
        self.assertEqual(self.t3.status, TestimonialStatus.APPROVED)
    
    def test_bulk_update_status_all(self):
        """Test bulk updating all testimonials."""
        all_ids = [self.t1.pk, self.t2.pk, self.t3.pk, self.t4.pk, self.t5.pk]
        count = Testimonial.objects.bulk_update_status(
            all_ids,
            TestimonialStatus.ARCHIVED
        )
        
        self.assertEqual(count, 5)
        
        # Verify all updated
        archived_count = Testimonial.objects.filter(
            status=TestimonialStatus.ARCHIVED
        ).count()
        self.assertEqual(archived_count, 5)
    
    def test_bulk_update_status_empty_list(self):
        """Test bulk update with empty ID list."""
        count = Testimonial.objects.bulk_update_status(
            [],
            TestimonialStatus.APPROVED
        )
        
        self.assertEqual(count, 0)
    
    def test_bulk_update_status_nonexistent_ids(self):
        """Test bulk update with non-existent IDs."""
        count = Testimonial.objects.bulk_update_status(
            [99999, 88888],
            TestimonialStatus.APPROVED
        )
        
        self.assertEqual(count, 0)
    
    def test_bulk_delete_by_ids_single(self):
        """Test bulk deleting single testimonial."""
        count, details = Testimonial.objects.bulk_delete_by_ids([self.t3.pk])
        
        self.assertEqual(count, 1)
        self.assertFalse(Testimonial.objects.filter(pk=self.t3.pk).exists())
    
    def test_bulk_delete_by_ids_multiple(self):
        """Test bulk deleting multiple testimonials."""
        count, details = Testimonial.objects.bulk_delete_by_ids(
            [self.t2.pk, self.t3.pk]
        )
        
        self.assertEqual(count, 2)
        self.assertFalse(Testimonial.objects.filter(pk=self.t2.pk).exists())
        self.assertFalse(Testimonial.objects.filter(pk=self.t3.pk).exists())
    
    def test_bulk_delete_by_ids_empty_list(self):
        """Test bulk delete with empty ID list."""
        count, details = Testimonial.objects.bulk_delete_by_ids([])
        
        self.assertEqual(count, 0)
    
    def test_bulk_delete_by_ids_nonexistent(self):
        """Test bulk delete with non-existent IDs."""
        count, details = Testimonial.objects.bulk_delete_by_ids([99999, 88888])
        
        self.assertEqual(count, 0)
    
    def test_batch_process_default_size(self):
        """Test batch processing with default size."""
        # Create more testimonials to test batching
        for i in range(150):
            Testimonial.objects.create(
                author=self.user1,
                content=f'Batch test {i}',
                rating=5
            )
        
        batches = list(Testimonial.objects.batch_process(batch_size=100))
        
        # Should have at least 2 batches (155 total / 100 batch_size)
        self.assertGreaterEqual(len(batches), 2)
        
        # First batch should have 100 items
        self.assertEqual(len(batches[0]), 100)
        
        # Total items should match
        total_items = sum(len(batch) for batch in batches)
        self.assertEqual(total_items, Testimonial.objects.count())
    
    def test_batch_process_custom_size(self):
        """Test batch processing with custom batch size."""
        batches = list(Testimonial.objects.batch_process(batch_size=2))
        
        # Should have 3 batches (5 testimonials / 2 batch_size = 2.5)
        self.assertEqual(len(batches), 3)
        
        # First two batches should have 2 items
        self.assertEqual(len(batches[0]), 2)
        self.assertEqual(len(batches[1]), 2)
        
        # Last batch should have 1 item
        self.assertEqual(len(batches[2]), 1)
    
    def test_batch_process_size_1(self):
        """Test batch processing with size 1."""
        batches = list(Testimonial.objects.batch_process(batch_size=1))
        
        # Should have 5 batches (one per testimonial)
        self.assertEqual(len(batches), 5)
        
        # Each batch should have exactly 1 item
        for batch in batches:
            self.assertEqual(len(batch), 1)
    
    def test_batch_process_large_size(self):
        """Test batch processing with size larger than queryset."""
        batches = list(Testimonial.objects.batch_process(batch_size=1000))
        
        # Should have 1 batch containing all items
        self.assertEqual(len(batches), 1)
        self.assertEqual(len(batches[0]), 5)


# ============================================================================
# INTEGRATION TESTS
# ============================================================================

class MixinIntegrationTests(MixinTestCase):
    """Integration tests for mixins working together."""
    
    def test_manager_uses_all_mixins(self):
        """Test that TestimonialManager has all mixin methods."""
        manager = Testimonial.objects
        
        # From StatisticsAggregationMixin
        self.assertTrue(hasattr(manager, 'get_basic_aggregates'))
        self.assertTrue(hasattr(manager, 'get_choice_distribution'))
        self.assertTrue(hasattr(manager, 'get_conditional_counts'))
        
        # From BulkOperationMixin
        self.assertTrue(hasattr(manager, 'bulk_update_status'))
        self.assertTrue(hasattr(manager, 'bulk_delete_by_ids'))
    
    def test_queryset_uses_time_period_mixin(self):
        """Test that TestimonialQuerySet has TimePeriodFilterMixin methods."""
        qs = Testimonial.objects.all()
        
        self.assertTrue(hasattr(qs, 'in_date_range'))
        self.assertTrue(hasattr(qs, 'created_in_last_days'))
    
    def test_get_stats_uses_mixins(self):
        """Test that get_stats uses mixin methods correctly."""
        stats = Testimonial.objects.get_stats()
        
        # Should have basic aggregates
        self.assertIn('total', stats)
        self.assertIn('avg_rating', stats)
        
        # Should have distributions
        self.assertIn('status_distribution', stats)
        self.assertIn('source_distribution', stats)
        self.assertIn('rating_distribution', stats)
        
        # Should have conditional counts
        self.assertIn('verified', stats)
        self.assertIn('anonymous', stats)
    
    def test_category_manager_uses_mixin(self):
        """Test that CategoryManager uses StatisticsAggregationMixin."""
        stats = TestimonialCategory.objects.get_stats()
        
        self.assertIn('total_categories', stats)
        self.assertIn('active_categories', stats)
    
    def test_media_manager_uses_mixin(self):
        """Test that MediaManager uses StatisticsAggregationMixin."""
        stats = TestimonialMedia.objects.get_media_stats()
        
        self.assertIn('total_media', stats)
        self.assertIn('media_type_distribution', stats)
        self.assertIn('primary_media', stats)
    
    def test_chaining_queryset_and_manager_methods(self):
        """Test chaining methods from queryset and manager."""
        # Filter then aggregate directly (aggregate is built-in Django)
        result = Testimonial.objects.filter(
            status=TestimonialStatus.APPROVED
        ).aggregate(
            count=Count('id'),
            avg=Avg('rating')
        )
        
        self.assertEqual(result['count'], 2)
        self.assertIn('avg', result)
    
    def test_time_filter_then_bulk_update(self):
        """Test using time filter then bulk update."""
        # Get recent testimonials
        recent = Testimonial.objects.all().created_in_last_days(30)
        recent_ids = list(recent.values_list('id', flat=True))
        
        # Bulk update them
        count = Testimonial.objects.bulk_update_status(
            recent_ids,
            TestimonialStatus.APPROVED
        )
        
        self.assertGreater(count, 0)


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

class MixinEdgeCaseTests(MixinTestCase):
    """Tests for edge cases and error handling."""
    
    def test_aggregation_on_filtered_empty_queryset(self):
        """Test aggregation on empty filtered queryset."""
        result = Testimonial.objects.filter(
            rating=100  # No testimonials with rating 100
        ).aggregate(
            total=Count('id'),
            avg=Avg('rating')
        )
        
        self.assertEqual(result['total'], 0)
        self.assertIsNone(result['avg'])
    
    def test_choice_distribution_with_zero_division(self):
        """Test choice distribution doesn't fail with empty queryset."""
        # Delete all to test with empty manager
        Testimonial.objects.all().delete()
        
        result = Testimonial.objects.get_choice_distribution(
            'status',
            TestimonialStatus.choices
        )
        
        # All percentages should be 0.0
        for code, data in result.items():
            self.assertEqual(data['percentage'], 0.0)
    
    def test_date_range_with_future_dates(self):
        """Test date range with future dates."""
        future_start = timezone.now() + timedelta(days=10)
        future_end = timezone.now() + timedelta(days=20)
        
        result = Testimonial.objects.all().in_date_range(future_start, future_end)
        
        # Should return empty queryset
        self.assertEqual(result.count(), 0)
    
    def test_date_range_inverted(self):
        """Test date range with start after end."""
        now = timezone.now()
        start = now + timedelta(days=10)
        end = now - timedelta(days=10)
        
        result = Testimonial.objects.all().in_date_range(start, end)
        
        # Should return empty queryset
        self.assertEqual(result.count(), 0)
    
    def test_negative_days_in_created_in_last_days(self):
        """Test created_in_last_days with negative days."""
        # This should technically give future date cutoff
        result = Testimonial.objects.all().created_in_last_days(-10)
        
        # Should return all or none depending on implementation
        self.assertIsNotNone(result)
    
    def test_bulk_update_with_duplicate_ids(self):
        """Test bulk update with duplicate IDs."""
        count = Testimonial.objects.bulk_update_status(
            [self.t1.pk, self.t1.pk, self.t2.pk],
            TestimonialStatus.FEATURED
        )
        
        # Should update unique items (2, not 3)
        self.assertEqual(count, 2)
    
    def test_batch_process_empty_queryset(self):
        """Test batch processing empty queryset."""
        # Delete all for empty test
        Testimonial.objects.all().delete()
        
        batches = list(
            Testimonial.objects.batch_process(batch_size=10)
        )
        
        # Should return empty list
        self.assertEqual(len(batches), 0)
    
    def test_conditional_counts_empty_conditions(self):
        """Test conditional counts with no conditions."""
        result = Testimonial.objects.get_conditional_counts({})
        
        # Should return empty dict
        self.assertEqual(result, {})
    
    def test_null_vs_filled_on_nonexistent_field(self):
        """Test null vs filled on non-existent field."""
        try:
            result = Testimonial.objects.get_null_vs_filled_counts(
                'nonexistent_field'
            )
            # May fail or return 0/0
        except Exception:
            # Expected to fail gracefully
            pass
    
    def test_get_stats_with_no_data(self):
        """Test get_stats on empty database."""
        # Delete all testimonials
        Testimonial.objects.all().delete()
        
        stats = Testimonial.objects.get_stats()
        
        # Should not crash
        self.assertEqual(stats['total'], 0)
        self.assertIsNone(stats.get('avg_rating'))
    
    def test_very_large_batch_size(self):
        """Test batch processing with very large batch size."""
        batches = list(Testimonial.objects.batch_process(batch_size=1000000))
        
        # Should have 1 batch with all items
        self.assertEqual(len(batches), 1)
        self.assertEqual(len(batches[0]), 5)