"""
Tests for the Django Redis Panel key detail view using Django TestCase.

The key detail view displays detailed information about a specific Redis key
and provides CRUD operations (view, edit value, update TTL, delete) with
feature flag support.
"""
import redis
from django.urls import reverse
from .base import RedisTestCase


class TestKeyDetailView(RedisTestCase):
    """Test cases for the key detail view using Django TestCase."""
    
    def test_key_detail_requires_staff_permission(self):
        """Test that key detail requires staff permission."""
        # Use unauthenticated client
        client = self.create_unauthenticated_client()
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:string'])
        response = client.get(url)
        
        # Should redirect to login page
        self.assertEqual(response.status_code, 302)
        self.assertIn('/admin/login/', response.url)
    
    def test_key_detail_success(self):
        """Test successful key detail rendering with real Redis data."""
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:string'])
        response = self.client.get(url)
        
        # Check response
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'test:string')
        
        # Check template used
        self.assertTemplateUsed(response, 'admin/dj_redis_panel/key_detail.html')
        
        # Check context data
        self.assertIn('key_data', response.context)
        key_data = response.context['key_data']
        
        self.assertEqual(key_data['name'], 'test:string')
        self.assertEqual(key_data['type'], 'string')
        self.assertEqual(key_data['value'], 'test_value')
        self.assertTrue(key_data['exists'])
        
        # Check feature flags
        self.assertTrue(response.context['allow_key_delete'])
        self.assertTrue(response.context['allow_key_edit'])
        self.assertTrue(response.context['allow_ttl_update'])
    
    def test_key_detail_nonexistent_instance(self):
        """Test key detail with nonexistent instance raises 404."""
        url = reverse('dj_redis_panel:key_detail', args=['nonexistent', 15, 'test_key'])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)
    
    def test_key_detail_nonexistent_key(self):
        """Test key detail with nonexistent key raises 404."""
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'nonexistent_key'])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)
    
    def test_key_detail_different_key_types(self):
        """Test key detail view with different Redis key types."""
        key_tests = [
            ('test:string', 'string'),
            ('test:list', 'list'),
            ('test:set', 'set'),
            ('test:hash', 'hash'),
            ('test:zset', 'zset'),
        ]
        
        for key_name, expected_type in key_tests:
            with self.subTest(key=key_name, type=expected_type):
                url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, key_name])
                response = self.client.get(url)
                
                self.assertEqual(response.status_code, 200)
                self.assertEqual(response.context['key_data']['type'], expected_type)
                self.assertEqual(response.context['key_data']['name'], key_name)
                self.assertTrue(response.context['key_data']['exists'])
    
    def test_key_detail_feature_flags_disabled(self):
        """Test key detail with feature flags disabled."""
        # First, create the key in database 14
        conn_14 = redis.Redis(host='127.0.0.1', port=6379, db=14, decode_responses=True)
        conn_14.set('test:string', 'test_value')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis_no_features', 14, 'test:string'])
            response = self.client.get(url)
            
            self.assertEqual(response.status_code, 200)
            self.assertFalse(response.context['allow_key_delete'])
            self.assertFalse(response.context['allow_key_edit'])
            self.assertFalse(response.context['allow_ttl_update'])
        finally:
            # Cleanup
            conn_14.delete('test:string')
    
    def test_key_detail_update_value_success(self):
        """Test successful key value update with real Redis."""
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:string'])
        
        response = self.client.post(url, {
            'action': 'update_value',
            'new_value': 'updated_value'
        })
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['success_message'], "Key value updated successfully")
        self.assertEqual(response.context['key_data']['value'], 'updated_value')
        
        # Verify the value was actually updated in Redis
        actual_value = self.redis_conn.get('test:string')
        self.assertEqual(actual_value, 'updated_value')
    
    def test_key_detail_update_value_disabled(self):
        """Test key value update when editing is disabled."""
        # Create key in test database 14 (no_features instance)
        conn_14 = redis.Redis(host='127.0.0.1', port=6379, db=14, decode_responses=True)
        conn_14.set('test:string', 'original_value')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis_no_features', 14, 'test:string'])
            response = self.client.post(url, {
                'action': 'update_value',
                'new_value': 'updated_value'
            })
            
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.context['error_message'], "Key editing is disabled for this instance")
            
            # Verify the value was NOT updated in Redis
            actual_value = conn_14.get('test:string')
            self.assertEqual(actual_value, 'original_value')
        finally:
            # Cleanup
            conn_14.delete('test:string')
    
    def test_key_detail_update_value_non_string_key(self):
        """Test key value update on non-string key type."""
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:list'])
        
        response = self.client.post(url, {
            'action': 'update_value',
            'new_value': 'updated_value'
        })
        
        self.assertEqual(response.status_code, 200)
        self.assertIn("Direct editing not supported for list keys", response.context['error_message'])
    
    def test_key_detail_update_ttl_success(self):
        """Test successful TTL update with real Redis."""
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:string'])
        
        response = self.client.post(url, {
            'action': 'update_ttl',
            'new_ttl': '7200'
        })
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['success_message'], "TTL set to 7200 seconds")
        self.assertEqual(response.context['key_data']['ttl'], 7200)
        
        # Verify the TTL was actually set in Redis
        actual_ttl = self.redis_conn.ttl('test:string')
        self.assertGreater(actual_ttl, 7190)  # Allow some margin for test execution time
    
    def test_key_detail_remove_ttl_success(self):
        """Test successful TTL removal (persist) with real Redis."""
        # First set a TTL on the key
        self.redis_conn.expire('test:string', 3600)
        
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:string'])
        response = self.client.post(url, {
            'action': 'update_ttl',
            'new_ttl': ''  # Empty string removes TTL
        })
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['success_message'], "TTL removed (key will not expire)")
        self.assertIsNone(response.context['key_data']['ttl'])
        
        # Verify the TTL was actually removed in Redis
        actual_ttl = self.redis_conn.ttl('test:string')
        self.assertEqual(actual_ttl, -1)  # -1 means no expiration
    
    def test_key_detail_update_ttl_invalid_value(self):
        """Test TTL update with invalid value."""
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:string'])
        
        response = self.client.post(url, {
            'action': 'update_ttl',
            'new_ttl': 'invalid'
        })
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['error_message'], "TTL must be a valid number")
    
    def test_key_detail_update_ttl_negative_value(self):
        """Test TTL update with negative value."""
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:string'])
        
        response = self.client.post(url, {
            'action': 'update_ttl',
            'new_ttl': '-100'
        })
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['error_message'], "TTL must be a positive number")
    
    def test_key_detail_delete_key_success(self):
        """Test successful key deletion with real Redis."""
        # Create a test key specifically for deletion
        self.redis_conn.set('test:delete_me', 'to_be_deleted')
        
        url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, 'test:delete_me'])
        response = self.client.post(url, {
            'action': 'delete_key'
        })
        
        # Should redirect to key search with success message
        self.assertEqual(response.status_code, 302)
        expected_redirect = reverse('dj_redis_panel:key_search', args=['test_redis', 15]) + '?deleted=1'
        self.assertEqual(response.url, expected_redirect)
        
        # Verify the key was actually deleted from Redis
        self.assertFalse(self.redis_conn.exists('test:delete_me'))
    
    def test_key_detail_delete_key_disabled(self):
        """Test key deletion when feature is disabled."""
        # Create key in test database 14 (no_features instance)
        conn_14 = redis.Redis(host='127.0.0.1', port=6379, db=14, decode_responses=True)
        conn_14.set('test:no_delete', 'protected_value')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis_no_features', 14, 'test:no_delete'])
            response = self.client.post(url, {
                'action': 'delete_key'
            })
            
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.context['error_message'], "Key deletion is disabled for this instance")
            
            # Verify the key was NOT deleted from Redis
            self.assertTrue(conn_14.exists('test:no_delete'))
        finally:
            # Cleanup
            conn_14.delete('test:no_delete')
    
    def test_key_detail_key_with_slashes(self):
        """Test key detail with key names containing slashes."""
        key_with_slashes = 'cache/user/123/data'
        
        # Create the key in Redis
        self.redis_conn.set(key_with_slashes, 'slash_test_value')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, key_with_slashes])
            response = self.client.get(url)
            
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.context['key_data']['name'], key_with_slashes)
            self.assertEqual(response.context['key_data']['value'], 'slash_test_value')
        finally:
            # Cleanup
            self.redis_conn.delete(key_with_slashes)

    def test_key_detail_pagination_small_collection_no_pagination(self):
        """Test that small collections are not paginated."""
        # Create a small list (under pagination threshold)
        small_list_key = 'test:small_list'
        for i in range(10):
            self.redis_conn.lpush(small_list_key, f'item_{i}')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, small_list_key])
            response = self.client.get(url)
            
            self.assertEqual(response.status_code, 200)
            key_data = response.context['key_data']
            
            # Should not be paginated
            self.assertFalse(key_data.get('is_paginated', False))
            self.assertEqual(len(key_data['value']), 10)
            self.assertFalse(response.context['is_paginated'])
        finally:
            self.redis_conn.delete(small_list_key)

    def test_key_detail_pagination_large_collections_by_type(self):
        """Test page-based pagination for all large collection types."""
        # Test data: (key_suffix, key_type, create_function, total_items, per_page, expected_pages, special_validation)
        test_cases = [
            ('list', 'list', self._create_large_list, 150, 50, 3, None),
            ('set', 'set', self._create_large_set, 150, 25, 6, None),
            ('hash', 'hash', self._create_large_hash, 150, 25, 6, None),
            ('zset', 'zset', self._create_large_zset, 150, 25, 6, self._validate_zset_scores),
        ]
        
        for key_suffix, expected_type, create_func, total_items, per_page, expected_pages, validator in test_cases:
            with self.subTest(key_type=expected_type, total_items=total_items, per_page=per_page):
                key_name = f'test:large_{key_suffix}'
                
                # Create the collection
                create_func(key_name, total_items)
                
                try:
                    # Test first page
                    url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, key_name])
                    response = self.client.get(url, {'per_page': per_page})
                    
                    self.assertEqual(response.status_code, 200)
                    key_data = response.context['key_data']
                    
                    # Should be paginated
                    self.assertTrue(key_data.get('is_paginated', False))
                    self.assertTrue(response.context['is_paginated'])
                    self.assertEqual(key_data['type'], expected_type)
                    self.assertEqual(len(key_data['value']), per_page)
                    self.assertEqual(response.context['current_page'], 1)
                    self.assertEqual(response.context['total_pages'], expected_pages)
                    self.assertTrue(response.context['has_next'])
                    self.assertFalse(response.context['has_previous'])
                    
                    # Run type-specific validation if provided
                    if validator:
                        validator(key_data['value'])
                    
                    # Test second page navigation
                    response = self.client.get(url, {'per_page': per_page, 'page': 2})
                    self.assertEqual(response.status_code, 200)
                    key_data = response.context['key_data']
                    
                    self.assertEqual(len(key_data['value']), per_page)
                    self.assertEqual(response.context['current_page'], 2)
                    self.assertTrue(response.context['has_next'])
                    self.assertTrue(response.context['has_previous'])
                    
                finally:
                    self.redis_conn.delete(key_name)

    def _create_large_list(self, key_name, count):
        """Helper to create a large list."""
        for i in range(count):
            self.redis_conn.lpush(key_name, f'item_{i:03d}')

    def _create_large_set(self, key_name, count):
        """Helper to create a large set."""
        for i in range(count):
            self.redis_conn.sadd(key_name, f'member_{i:03d}')

    def _create_large_hash(self, key_name, count):
        """Helper to create a large hash."""
        for i in range(count):
            self.redis_conn.hset(key_name, f'field_{i:03d}', f'value_{i:03d}')

    def _create_large_zset(self, key_name, count):
        """Helper to create a large sorted set."""
        for i in range(count):
            self.redis_conn.zadd(key_name, {f'member_{i:03d}': i})

    def _validate_zset_scores(self, zset_value):
        """Helper to validate that sorted set items have scores."""
        self.assertTrue(len(zset_value) > 0)
        first_item = zset_value[0]
        self.assertIsInstance(first_item, tuple)  # (member, score)
        self.assertEqual(len(first_item), 2)

    def _create_large_list_cursor(self, cursor_conn, key_name, count):
        """Helper to create a large list in the cursor database."""
        for i in range(count):
            cursor_conn.lpush(key_name, f'cursor_item_{i:03d}')

    def _create_large_set_cursor(self, cursor_conn, key_name, count):
        """Helper to create a large set in the cursor database."""
        for i in range(count):
            cursor_conn.sadd(key_name, f'cursor_member_{i:03d}')

    def _create_large_hash_cursor(self, cursor_conn, key_name, count):
        """Helper to create a large hash in the cursor database."""
        for i in range(count):
            cursor_conn.hset(key_name, f'cursor_field_{i:03d}', f'cursor_value_{i:03d}')

    def _create_large_zset_cursor(self, cursor_conn, key_name, count):
        """Helper to create a large sorted set in the cursor database."""
        for i in range(count):
            cursor_conn.zadd(key_name, {f'cursor_member_{i:03d}': i})

    def test_key_detail_pagination_large_collections_cursor_based(self):
        """Test cursor-based pagination for all large collection types using test_redis_cursor instance."""
        # Create connection to the cursor instance database (db 12)
        import redis
        cursor_conn = redis.Redis(host='127.0.0.1', port=6379, db=12, decode_responses=True)
        
        # Test data: (key_suffix, key_type, create_function, total_items, per_page, special_validation)
        test_cases = [
            ('list_cursor', 'list', self._create_large_list_cursor, 150, 50, None),
            ('set_cursor', 'set', self._create_large_set_cursor, 150, 25, None),
            ('hash_cursor', 'hash', self._create_large_hash_cursor, 150, 25, None),
            ('zset_cursor', 'zset', self._create_large_zset_cursor, 150, 25, self._validate_zset_scores),
        ]
        
        for key_suffix, expected_type, create_func, total_items, per_page, validator in test_cases:
            with self.subTest(key_type=expected_type, total_items=total_items, per_page=per_page):
                key_name = f'test:large_{key_suffix}'
                
                # Create the collection in the cursor database
                create_func(cursor_conn, key_name, total_items)
                
                try:
                    # Test first cursor page (cursor=0)
                    url = reverse('dj_redis_panel:key_detail', args=['test_redis_cursor', 12, key_name])
                    response = self.client.get(url, {'per_page': per_page, 'cursor': 0})
                    
                    self.assertEqual(response.status_code, 200)
                    key_data = response.context['key_data']
                    
                    # Should be using cursor-based pagination (no fallback needed)
                    self.assertTrue(key_data.get('is_paginated', False))
                    self.assertTrue(response.context['is_paginated'])
                    self.assertTrue(response.context['use_cursor_pagination'])
                    self.assertEqual(key_data['type'], expected_type)
                    
                    # Should have cursor-specific context variables
                    self.assertEqual(response.context['current_cursor'], 0)
                    self.assertIn('next_cursor', response.context)
                    self.assertEqual(key_data.get('pagination_type'), 'cursor')
                    
                    # Should have data (amount may vary with cursor pagination)
                    self.assertGreater(len(key_data['value']), 0)
                    
                    # Run type-specific validation if provided
                    if validator:
                        validator(key_data['value'])
                    
                    # Test cursor navigation if has_more is True
                    if response.context.get('has_more', False):
                        next_cursor = response.context['next_cursor']
                        self.assertIsNotNone(next_cursor)
                        
                        # For lists and zsets, cursor should advance by the number of items
                        if expected_type in ['list', 'zset']:
                            self.assertGreater(next_cursor, 0)
                        # For sets and hashes, next_cursor is a Redis scan cursor (could be any value)
                        
                        # Test second cursor page
                        response2 = self.client.get(url, {'per_page': per_page, 'cursor': next_cursor})
                        self.assertEqual(response2.status_code, 200)
                        key_data2 = response2.context['key_data']
                        
                        # Should have different data than first page
                        self.assertGreater(len(key_data2['value']), 0)
                        self.assertEqual(response2.context['current_cursor'], next_cursor)
                        
                        # Verify range information for cursor pagination
                        if response.context.get('range_start') and response.context.get('range_end'):
                            # Lists and sorted sets should have range info
                            self.assertIn(expected_type, ['list', 'zset'])
                            self.assertGreater(response.context['range_start'], 0)
                            self.assertGreater(response.context['range_end'], response.context['range_start'])
                        else:
                            # Sets and hashes use scan cursors without exact ranges
                            self.assertIn(expected_type, ['set', 'hash'])
                    
                finally:
                    cursor_conn.delete(key_name)

    def test_key_detail_pagination_per_page_reset(self):
        """Test that changing per_page resets pagination to beginning."""
        # Create a large list
        large_list_key = 'test:pagination_reset'
        for i in range(100):
            self.redis_conn.lpush(large_list_key, f'item_{i:03d}')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, large_list_key])
            
            # Go to page 3 with 25 per page
            response = self.client.get(url, {'per_page': 25, 'page': 3})
            self.assertEqual(response.context['current_page'], 3)
            
            # Change per_page to 50 - should reset to page 1
            response = self.client.get(url, {'per_page': 50})
            self.assertEqual(response.context['current_page'], 1)
            self.assertEqual(response.context['per_page'], 50)
            
        finally:
            self.redis_conn.delete(large_list_key)

    def test_key_detail_pagination_invalid_page(self):
        """Test pagination with invalid page numbers."""
        # Create a large list
        large_list_key = 'test:invalid_page'
        for i in range(75):
            self.redis_conn.lpush(large_list_key, f'item_{i:03d}')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, large_list_key])
            
            # Test page 0 (should default to 1)
            response = self.client.get(url, {'per_page': 25, 'page': 0})
            self.assertEqual(response.context['current_page'], 1)
            
            # Test negative page (should default to 1)
            response = self.client.get(url, {'per_page': 25, 'page': -5})
            self.assertEqual(response.context['current_page'], 1)
            
            # Test page beyond total (should clamp to valid range)
            response = self.client.get(url, {'per_page': 25, 'page': 999})
            # The view should handle this gracefully
            self.assertEqual(response.status_code, 200)
            
        finally:
            self.redis_conn.delete(large_list_key)

    def test_key_detail_pagination_per_page_validation(self):
        """Test per_page parameter validation."""
        # Create a large list
        large_list_key = 'test:per_page_validation'
        for i in range(100):
            self.redis_conn.lpush(large_list_key, f'item_{i:03d}')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, large_list_key])
            
            # Test invalid per_page values should default to 50
            invalid_values = [0, -10, 999, 'invalid']
            for invalid_value in invalid_values:
                response = self.client.get(url, {'per_page': invalid_value})
                self.assertEqual(response.context['per_page'], 50)
            
            # Test valid per_page values
            valid_values = [25, 50, 100, 200]
            for valid_value in valid_values:
                response = self.client.get(url, {'per_page': valid_value})
                self.assertEqual(response.context['per_page'], valid_value)
                
        finally:
            self.redis_conn.delete(large_list_key)

    def test_key_detail_pagination_context_variables(self):
        """Test that all pagination context variables are properly set."""
        # Create a large list
        large_list_key = 'test:pagination_context'
        for i in range(130):
            self.redis_conn.lpush(large_list_key, f'item_{i:03d}')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, large_list_key])
            response = self.client.get(url, {'per_page': 50, 'page': 2})
            
            self.assertEqual(response.status_code, 200)
            
            # Check all pagination context variables
            self.assertTrue(response.context['is_paginated'])
            self.assertEqual(response.context['per_page'], 50)
            self.assertEqual(response.context['current_page'], 2)
            self.assertEqual(response.context['total_pages'], 3)  # 130 / 50 = 2.6 -> 3
            self.assertTrue(response.context['has_previous'])
            self.assertTrue(response.context['has_next'])
            self.assertEqual(response.context['previous_page'], 1)
            self.assertEqual(response.context['next_page'], 3)
            self.assertEqual(response.context['showing_count'], 50)
            
            # Check page_range is present
            self.assertIn('page_range', response.context)
            page_range = response.context['page_range']
            self.assertIn(1, page_range)
            self.assertIn(2, page_range)
            self.assertIn(3, page_range)
            
        finally:
            self.redis_conn.delete(large_list_key)

    def test_key_detail_string_not_paginated(self):
        """Test that string keys are never paginated."""
        # Create a large string value
        large_string_key = 'test:large_string'
        large_string_value = 'x' * 10000  # 10KB string
        self.redis_conn.set(large_string_key, large_string_value)
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, large_string_key])
            response = self.client.get(url, {'per_page': 25})
            
            self.assertEqual(response.status_code, 200)
            key_data = response.context['key_data']
            
            # String should never be paginated
            self.assertFalse(key_data.get('is_paginated', False))
            self.assertFalse(response.context['is_paginated'])
            self.assertEqual(key_data['value'], large_string_value)
            
        finally:
            self.redis_conn.delete(large_string_key)

    def test_key_detail_pagination_template_includes(self):
        """Test that paginated collections use the correct template includes."""
        # Create a large list (over pagination threshold)
        large_list_key = 'test:template_includes'
        for i in range(120):  # Above threshold of 100
            self.redis_conn.lpush(large_list_key, f'item_{i:03d}')
        
        try:
            url = reverse('dj_redis_panel:key_detail', args=['test_redis', 15, large_list_key])
            response = self.client.get(url, {'per_page': 25})
            
            self.assertEqual(response.status_code, 200)
            
            # Check that the collection is paginated
            self.assertTrue(response.context['is_paginated'])
            
            # Check that it uses the partitioned templates
            self.assertTemplateUsed(response, 'admin/dj_redis_panel/key_detail.html')
            
        finally:
            self.redis_conn.delete(large_list_key)
