"""
Tests for cached robohash implementation
"""

import time
import unittest
from PIL import Image
from io import BytesIO
from django.test import TestCase

# Import our implementations
from .robohash_cached import (
    CachedRobohash,
    create_robohash,
    get_robohash_cache_info,
    clear_robohash_cache,
)
from .robohash_optimized import OptimizedRobohash


class TestCachedRobohash(TestCase):
    """Test cached robohash functionality and performance"""

    def setUp(self):
        """Clear cache before each test"""
        clear_robohash_cache()

    def test_cache_functionality(self):
        """Test that caching works correctly"""
        # Create two identical robohashes
        digest = "test@example.com"

        robohash1 = CachedRobohash(digest)
        robohash1.assemble(sizex=300, sizey=300)

        robohash2 = CachedRobohash(digest)
        robohash2.assemble(sizex=300, sizey=300)

        # Images should be identical
        self.assertEqual(robohash1.img.size, robohash2.img.size)

        # Convert to bytes for comparison
        data1 = BytesIO()
        robohash1.img.save(data1, format="PNG")

        data2 = BytesIO()
        robohash2.img.save(data2, format="PNG")

        self.assertEqual(data1.getvalue(), data2.getvalue())

    def test_cache_stats(self):
        """Test cache statistics tracking"""
        clear_robohash_cache()

        # Initial stats should be empty
        stats = get_robohash_cache_info()
        self.assertEqual(stats["hits"], 0)
        self.assertEqual(stats["misses"], 0)

        # Generate a robohash (should create cache misses)
        digest = "cache-test@example.com"
        robohash = CachedRobohash(digest)
        robohash.assemble(sizex=300, sizey=300)

        stats_after = get_robohash_cache_info()
        self.assertGreater(stats_after["misses"], 0)

        # Generate same robohash again (should create cache hits)
        robohash2 = CachedRobohash(digest)
        robohash2.assemble(sizex=300, sizey=300)

        stats_final = get_robohash_cache_info()
        self.assertGreater(stats_final["hits"], 0)

    def test_compatibility_with_optimized(self):
        """Test that cached version produces identical results to optimized version"""
        digest = "compatibility-test@example.com"

        # Clear cache to start fresh and disable caching for this test
        clear_robohash_cache()
        original_cache_enabled = CachedRobohash._cache_enabled
        CachedRobohash._cache_enabled = False

        try:
            # Generate with optimized version
            optimized = OptimizedRobohash(digest)
            optimized.assemble_fast(sizex=300, sizey=300)

            # Generate with cached version (but caching disabled)
            cached = CachedRobohash(digest)
            cached.assemble(sizex=300, sizey=300)

            # Images should be identical
            self.assertEqual(optimized.img.size, cached.img.size)
            self.assertEqual(optimized.img.mode, cached.img.mode)

            # Convert to bytes for pixel-perfect comparison
            opt_data = BytesIO()
            optimized.img.save(opt_data, format="PNG")

            cached_data = BytesIO()
            cached.img.save(cached_data, format="PNG")

            self.assertEqual(opt_data.getvalue(), cached_data.getvalue())

        finally:
            # Restore cache setting
            CachedRobohash._cache_enabled = original_cache_enabled

    def test_different_sizes_cached_separately(self):
        """Test that different sizes are cached separately"""
        digest = "size-test@example.com"

        # Generate 300x300
        robohash_300 = CachedRobohash(digest)
        robohash_300.assemble(sizex=300, sizey=300)

        # Generate 150x150 (should use different cached parts)
        robohash_150 = CachedRobohash(digest)
        robohash_150.assemble(sizex=150, sizey=150)

        # Sizes should be different
        self.assertEqual(robohash_300.img.size, (300, 300))
        self.assertEqual(robohash_150.img.size, (150, 150))

        # But robot should look the same (just different size)
        # This is hard to test programmatically, but we can check they're both valid

    def test_cache_disabled_fallback(self):
        """Test behavior when cache is disabled"""
        # Temporarily disable cache
        original_cache_enabled = CachedRobohash._cache_enabled
        CachedRobohash._cache_enabled = False

        try:
            digest = "no-cache-test@example.com"
            robohash = CachedRobohash(digest)
            robohash.assemble(sizex=300, sizey=300)

            # Should still work, just without caching
            self.assertIsNotNone(robohash.img)
            self.assertEqual(robohash.img.size, (300, 300))

        finally:
            # Restore original setting
            CachedRobohash._cache_enabled = original_cache_enabled

    def test_create_cached_robohash_function(self):
        """Test the convenience function"""
        digest = "function-test@example.com"

        # Test the convenience function
        data = create_robohash(digest, 300)

        self.assertIsInstance(data, BytesIO)

        # Should be able to load as image
        data.seek(0)
        img = Image.open(data)
        self.assertEqual(img.size, (300, 300))

    def test_performance_improvement(self):
        """Test that caching provides performance improvement"""
        digest = "performance-test@example.com"

        # Clear cache to start fresh
        clear_robohash_cache()

        # Time first generation (cache misses)
        start_time = time.time()
        robohash1 = CachedRobohash(digest)
        robohash1.assemble(sizex=300, sizey=300)
        first_time = time.time() - start_time

        # Time second generation (cache hits)
        start_time = time.time()
        robohash2 = CachedRobohash(digest)
        robohash2.assemble(sizex=300, sizey=300)
        second_time = time.time() - start_time

        # Second generation should be faster (though this might be flaky in CI)
        # At minimum, it should not be significantly slower
        self.assertLessEqual(second_time, first_time * 1.5)  # Allow 50% variance

        # Check that we got cache hits
        stats = get_robohash_cache_info()
        self.assertGreater(stats["hits"], 0)

    def test_cache_size_limit(self):
        """Test that cache respects size limits"""
        # Set a small cache size for testing
        original_size = CachedRobohash._max_cache_size
        CachedRobohash._max_cache_size = 5

        try:
            clear_robohash_cache()

            # Generate more robohashes than cache size
            for i in range(10):
                digest = f"cache-limit-test-{i}@example.com"
                robohash = CachedRobohash(digest)
                robohash.assemble(sizex=300, sizey=300)

            # Cache size should not exceed limit
            stats = get_robohash_cache_info()
            self.assertLessEqual(stats["size"], 5)

        finally:
            # Restore original cache size
            CachedRobohash._max_cache_size = original_size

    def test_error_handling(self):
        """Test error handling in cached implementation"""
        # Test with invalid digest that might cause issues
        digest = ""  # Empty digest

        try:
            robohash = CachedRobohash(digest)
            robohash.assemble(sizex=300, sizey=300)

            # Should not crash, should produce some image
            self.assertIsNotNone(robohash.img)

        except Exception as e:
            self.fail(f"Cached robohash should handle errors gracefully: {e}")


class TestCachedRobohashPerformance(TestCase):
    """Performance comparison tests"""

    def test_performance_comparison(self):
        """Compare performance between optimized and cached versions"""
        digest = "perf-comparison@example.com"
        iterations = 5

        # Test optimized version
        optimized_times = []
        for i in range(iterations):
            start_time = time.time()
            robohash = OptimizedRobohash(digest)
            robohash.assemble_fast(sizex=300, sizey=300)
            optimized_times.append(time.time() - start_time)

        # Clear cache and test cached version
        clear_robohash_cache()
        cached_times = []
        for i in range(iterations):
            start_time = time.time()
            robohash = CachedRobohash(digest)
            robohash.assemble(sizex=300, sizey=300)
            cached_times.append(time.time() - start_time)

        avg_optimized = sum(optimized_times) / len(optimized_times)
        avg_cached = sum(cached_times) / len(cached_times)

        print("\nPerformance Comparison:")
        print(f"Optimized average: {avg_optimized * 1000:.2f}ms")
        print(f"Cached average: {avg_cached * 1000:.2f}ms")
        print(f"Improvement: {avg_optimized / avg_cached:.2f}x faster")

        # Cache stats
        stats = get_robohash_cache_info()
        print(f"Cache stats: {stats}")

        # Cached version should be at least as fast (allowing for variance)
        # In practice, it should be faster after the first few generations
        self.assertLessEqual(avg_cached, avg_optimized * 1.2)  # Allow 20% variance


if __name__ == "__main__":
    # Run tests
    unittest.main()
