<?php

declare(strict_types=1);

use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Compression\NoCompression;
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\GeneralCache;
use App\Framework\Cache\MultiLevelCache;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Serializer\Php\PhpSerializer;

describe('MultiLevelCache', function () {
    beforeEach(function () {
        // Fast cache: In-memory for quick access
        $fastDriver = new InMemoryCache();
        $serializer = new PhpSerializer();
        $compression = new NoCompression();
        $this->fastCache = new GeneralCache($fastDriver, $serializer, $compression);

        // Slow cache: Another in-memory cache to simulate slower storage
        $slowDriver = new InMemoryCache();
        $this->slowCache = new GeneralCache($slowDriver, $serializer, $compression);

        // Multi-level cache combining both
        $this->cache = new MultiLevelCache($this->fastCache, $this->slowCache);
    });

    describe('Basic Operations', function () {
        it('sets items in both fast and slow cache for small values', function () {
            $key = CacheKey::fromString('small-value');
            $value = 'Small text';

            $success = $this->cache->set(CacheItem::forSet($key, $value));

            expect($success)->toBeTrue();

            // Verify in fast cache
            $fastResult = $this->fastCache->get($key);
            expect($fastResult->isHit)->toBeTrue();
            expect($fastResult->value)->toBe('Small text');

            // Verify in slow cache
            $slowResult = $this->slowCache->get($key);
            expect($slowResult->isHit)->toBeTrue();
            expect($slowResult->value)->toBe('Small text');
        });

        it('sets large values only in slow cache', function () {
            $key = CacheKey::fromString('large-value');
            // Create a large value (> 1KB)
            $value = str_repeat('Large data content ', 100); // > 1KB

            $success = $this->cache->set(CacheItem::forSet($key, $value));

            expect($success)->toBeTrue();

            // Should NOT be in fast cache (too large)
            $fastResult = $this->fastCache->get($key);
            expect($fastResult->isHit)->toBeFalse();

            // Should be in slow cache
            $slowResult = $this->slowCache->get($key);
            expect($slowResult->isHit)->toBeTrue();
        });

        it('retrieves from fast cache when available', function () {
            $key = CacheKey::fromString('fast-key');

            // Set directly in fast cache
            $this->fastCache->set(CacheItem::forSet($key, 'Fast value'));

            $result = $this->cache->get($key);

            expect($result->isHit)->toBeTrue();
            expect($result->value)->toBe('Fast value');
        });

        it('falls back to slow cache on fast cache miss', function () {
            $key = CacheKey::fromString('slow-key');

            // Set only in slow cache
            $this->slowCache->set(CacheItem::forSet($key, 'Slow value'));

            $result = $this->cache->get($key);

            expect($result->isHit)->toBeTrue();
            expect($result->value)->toBe('Slow value');
        });

        it('warms up fast cache when fetching from slow cache', function () {
            $key = CacheKey::fromString('warmup-key');
            $value = 'Small value for warmup';

            // Set only in slow cache
            $this->slowCache->set(CacheItem::forSet($key, $value));

            // Fast cache should be empty initially
            expect($this->fastCache->get($key)->isHit)->toBeFalse();

            // Get from multi-level cache (will warm up fast cache)
            $result = $this->cache->get($key);
            expect($result->isHit)->toBeTrue();

            // Now fast cache should have it
            $fastResult = $this->fastCache->get($key);
            expect($fastResult->isHit)->toBeTrue();
            expect($fastResult->value)->toBe($value);
        });
    });

    describe('Multi-Key Operations', function () {
        it('gets multiple keys from mixed sources', function () {
            $key1 = CacheKey::fromString('fast-1');
            $key2 = CacheKey::fromString('slow-1');
            $key3 = CacheKey::fromString('missing-1');

            // key1 in fast cache
            $this->fastCache->set(CacheItem::forSet($key1, 'Value from fast'));

            // key2 only in slow cache
            $this->slowCache->set(CacheItem::forSet($key2, 'Value from slow'));

            // key3 nowhere

            $result = $this->cache->get($key1, $key2, $key3);

            expect($result->count())->toBe(3);
            expect($result->getValue($key1))->toBe('Value from fast');
            expect($result->getValue($key2))->toBe('Value from slow');
            expect($result->getItem($key3)->isHit)->toBeFalse();
        });

        it('sets multiple items with appropriate cache levels', function () {
            $item1 = CacheItem::forSet(CacheKey::fromString('multi-1'), 'Small');
            $item2 = CacheItem::forSet(CacheKey::fromString('multi-2'), 'Another small');

            $success = $this->cache->set($item1, $item2);

            expect($success)->toBeTrue();

            // Both should be in both caches (small values)
            expect($this->fastCache->get(CacheKey::fromString('multi-1'))->isHit)->toBeTrue();
            expect($this->fastCache->get(CacheKey::fromString('multi-2'))->isHit)->toBeTrue();
            expect($this->slowCache->get(CacheKey::fromString('multi-1'))->isHit)->toBeTrue();
            expect($this->slowCache->get(CacheKey::fromString('multi-2'))->isHit)->toBeTrue();
        });
    });

    describe('Forget Operations', function () {
        it('forgets items from both caches', function () {
            $key = CacheKey::fromString('forget-me');

            // Set in multi-level cache
            $this->cache->set(CacheItem::forSet($key, 'Temporary value'));

            // Verify it exists
            expect($this->cache->get($key)->isHit)->toBeTrue();

            // Forget
            $success = $this->cache->forget($key);

            expect($success)->toBeTrue();
            expect($this->fastCache->get($key)->isHit)->toBeFalse();
            expect($this->slowCache->get($key)->isHit)->toBeFalse();
            expect($this->cache->get($key)->isHit)->toBeFalse();
        });

        it('forgets multiple keys from both caches', function () {
            $key1 = CacheKey::fromString('forget-1');
            $key2 = CacheKey::fromString('forget-2');

            $this->cache->set(
                CacheItem::forSet($key1, 'Value 1'),
                CacheItem::forSet($key2, 'Value 2')
            );

            $this->cache->forget($key1, $key2);

            expect($this->cache->get($key1)->isHit)->toBeFalse();
            expect($this->cache->get($key2)->isHit)->toBeFalse();
        });
    });

    describe('Clear Operations', function () {
        it('clears both fast and slow caches', function () {
            $this->cache->set(
                CacheItem::forSet(CacheKey::fromString('clear-1'), 'Data 1'),
                CacheItem::forSet(CacheKey::fromString('clear-2'), 'Data 2')
            );

            $success = $this->cache->clear();

            expect($success)->toBeTrue();
            expect($this->cache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse();
            expect($this->cache->get(CacheKey::fromString('clear-2'))->isHit)->toBeFalse();
            expect($this->fastCache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse();
            expect($this->slowCache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse();
        });
    });

    describe('Has Operations', function () {
        it('checks existence in fast cache first', function () {
            $key = CacheKey::fromString('has-fast');
            $this->fastCache->set(CacheItem::forSet($key, 'Fast data'));

            $results = $this->cache->has($key);

            expect($results['has-fast'])->toBeTrue();
        });

        it('checks slow cache on fast cache miss', function () {
            $key = CacheKey::fromString('has-slow');
            $this->slowCache->set(CacheItem::forSet($key, 'Slow data'));

            $results = $this->cache->has($key);

            expect($results['has-slow'])->toBeTrue();
        });

        it('warms up fast cache during has() check', function () {
            $key = CacheKey::fromString('has-warmup');
            $value = 'Small value';
            $this->slowCache->set(CacheItem::forSet($key, $value));

            // Fast cache empty initially
            expect($this->fastCache->get($key)->isHit)->toBeFalse();

            // Check existence (triggers warmup)
            $results = $this->cache->has($key);
            expect($results['has-warmup'])->toBeTrue();

            // Fast cache should now have it
            expect($this->fastCache->get($key)->isHit)->toBeTrue();
        });

        it('returns false for non-existent keys', function () {
            $results = $this->cache->has(CacheKey::fromString('not-exists'));

            expect($results['not-exists'])->toBeFalse();
        });
    });

    describe('Remember Pattern', function () {
        it('executes callback and caches result on miss', function () {
            $key = CacheKey::fromString('remember-key');
            $callbackExecuted = false;

            $callback = function () use (&$callbackExecuted) {
                $callbackExecuted = true;
                return 'Computed value';
            };

            $result = $this->cache->remember($key, $callback);

            expect($callbackExecuted)->toBeTrue();
            expect($result->value)->toBe('Computed value');

            // Verify it's cached
            $cachedResult = $this->cache->get($key);
            expect($cachedResult->isHit)->toBeTrue();
            expect($cachedResult->value)->toBe('Computed value');
        });

        it('returns cached value without executing callback on hit', function () {
            $key = CacheKey::fromString('remember-cached');

            // Pre-populate cache
            $this->cache->set(CacheItem::forSet($key, 'Cached value'));

            $callbackExecuted = false;
            $callback = function () use (&$callbackExecuted) {
                $callbackExecuted = true;
                return 'Should not execute';
            };

            $result = $this->cache->remember($key, $callback);

            expect($callbackExecuted)->toBeFalse();
            expect($result->value)->toBe('Cached value');
        });
    });

    describe('Size-Based Cache Distribution', function () {
        it('caches primitives in both layers', function () {
            $key = CacheKey::fromString('primitive');
            $this->cache->set(CacheItem::forSet($key, 42));

            expect($this->fastCache->get($key)->isHit)->toBeTrue();
            expect($this->slowCache->get($key)->isHit)->toBeTrue();
        });

        it('does not cache objects in fast layer', function () {
            $key = CacheKey::fromString('object');
            $object = new stdClass();
            $object->data = 'test';

            $this->cache->set(CacheItem::forSet($key, $object));

            // Object should only be in slow cache
            expect($this->fastCache->get($key)->isHit)->toBeFalse();
            expect($this->slowCache->get($key)->isHit)->toBeTrue();
        });

        it('caches small arrays in both layers', function () {
            $key = CacheKey::fromString('small-array');
            $value = ['a' => 1, 'b' => 2, 'c' => 3];

            $this->cache->set(CacheItem::forSet($key, $value));

            expect($this->fastCache->get($key)->isHit)->toBeTrue();
            expect($this->slowCache->get($key)->isHit)->toBeTrue();
        });

        it('caches large arrays only in slow layer', function () {
            $key = CacheKey::fromString('large-array');
            // Create large array (> 1KB estimated)
            $value = array_fill(0, 100, str_repeat('data', 10));

            $this->cache->set(CacheItem::forSet($key, $value));

            expect($this->fastCache->get($key)->isHit)->toBeFalse();
            expect($this->slowCache->get($key)->isHit)->toBeTrue();
        });
    });

    describe('TTL Management', function () {
        it('respects TTL in slow cache', function () {
            $key = CacheKey::fromString('ttl-test');
            $item = CacheItem::forSet($key, 'TTL value', Duration::fromMinutes(10));

            $this->cache->set($item);

            // Should be cached
            expect($this->slowCache->get($key)->isHit)->toBeTrue();
        });

        it('uses shorter TTL for fast cache', function () {
            $key = CacheKey::fromString('fast-ttl');
            // Set with 10 minute TTL
            $item = CacheItem::forSet($key, 'Short value', Duration::fromMinutes(10));

            $this->cache->set($item);

            // Fast cache should have shorter TTL (5 minutes max by default)
            // We can't directly verify TTL, but we verify it's cached
            expect($this->fastCache->get($key)->isHit)->toBeTrue();
        });
    });

    describe('Edge Cases', function () {
        it('handles empty operations gracefully', function () {
            expect($this->cache->set())->toBeTrue();
            expect($this->cache->forget())->toBeTrue();
            expect($this->cache->has())->toBe([]);
            expect($this->cache->get())->toBeInstanceOf(CacheResult::class);
        });

        it('handles null values correctly', function () {
            $key = CacheKey::fromString('null-value');
            $this->cache->set(CacheItem::forSet($key, null));

            $result = $this->cache->get($key);
            expect($result->isHit)->toBeTrue();
            expect($result->value)->toBeNull();
        });

        it('handles false values correctly', function () {
            $key = CacheKey::fromString('false-value');
            $this->cache->set(CacheItem::forSet($key, false));

            $result = $this->cache->get($key);
            expect($result->isHit)->toBeTrue();
            expect($result->value)->toBeFalse();
        });
    });

    describe('Cache Layer Access', function () {
        it('provides access to fast cache layer', function () {
            $fastCache = $this->cache->getFastCache();
            expect($fastCache)->toBeInstanceOf(GeneralCache::class);
        });

        it('provides access to slow cache layer', function () {
            $slowCache = $this->cache->getSlowCache();
            expect($slowCache)->toBeInstanceOf(GeneralCache::class);
        });

        it('provides driver access through slow cache', function () {
            $driver = $this->cache->getDriver();
            expect($driver)->toBeInstanceOf(InMemoryCache::class);
        });
    });
});
