<?php

declare(strict_types=1);

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\SmartCache;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\EntityManager;
use App\Framework\StateManagement\Database\ComponentStateEntity;
use App\Framework\StateManagement\Database\DatabaseStateManager;
use App\Framework\StateManagement\SerializableState;

// Test State Value Object
final readonly class TestComponentState implements SerializableState
{
    public function __construct(
        public int $count = 0,
        public string $message = 'test'
    ) {}

    public function toArray(): array
    {
        return [
            'count' => $this->count,
            'message' => $this->message,
        ];
    }

    public static function fromArray(array $data): self
    {
        return new self(
            count: $data['count'] ?? 0,
            message: $data['message'] ?? 'test'
        );
    }
}

describe('DatabaseStateManager', function () {
    beforeEach(function () {
        // Mock EntityManager
        $this->entityManager = Mockery::mock(EntityManager::class);
        $this->unitOfWork = Mockery::mock(\App\Framework\Database\UnitOfWork::class);
        $this->entityManager->unitOfWork = $this->unitOfWork;

        // Real Cache for testing
        $this->cache = new SmartCache();

        // Create DatabaseStateManager
        $this->stateManager = new DatabaseStateManager(
            entityManager: $this->entityManager,
            cache: $this->cache,
            stateClass: TestComponentState::class,
            logger: null,
            cacheTtl: Duration::fromSeconds(60)
        );
    });

    afterEach(function () {
        Mockery::close();
    });

    describe('setState', function () {
        it('creates new state entity when none exists', function () {
            $key = 'test-component:1';
            $state = new TestComponentState(count: 42, message: 'hello');

            // Mock: Entity does not exist
            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn(null);

            // Expect persist and commit
            $this->unitOfWork
                ->shouldReceive('persist')
                ->once()
                ->with(Mockery::type(ComponentStateEntity::class));

            $this->unitOfWork
                ->shouldReceive('commit')
                ->once();

            // Set state
            $this->stateManager->setState($key, $state);

            // Verify cache was populated
            $cached = $this->cache->get(CacheKey::from("component_state:{$key}"));
            expect($cached->isHit)->toBeTrue();
        });

        it('updates existing state entity', function () {
            $key = 'test-component:2';
            $state = new TestComponentState(count: 100, message: 'updated');

            // Mock: Entity exists
            $existingEntity = new ComponentStateEntity(
                componentId: $key,
                stateData: json_encode(['count' => 50, 'message' => 'old']),
                stateClass: TestComponentState::class,
                componentName: 'test-component',
                userId: null,
                sessionId: null,
                version: 1,
                checksum: 'old-checksum',
                createdAt: Timestamp::now(),
                updatedAt: Timestamp::now(),
                expiresAt: null
            );

            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn($existingEntity);

            // Expect persist with updated entity
            $this->unitOfWork
                ->shouldReceive('persist')
                ->once()
                ->with(Mockery::on(function (ComponentStateEntity $entity) {
                    return $entity->version === 2;
                }));

            $this->unitOfWork
                ->shouldReceive('commit')
                ->once();

            // Set state
            $this->stateManager->setState($key, $state);
        });
    });

    describe('getState', function () {
        it('returns state from cache on hit', function () {
            $key = 'test-component:3';
            $state = new TestComponentState(count: 77, message: 'cached');

            // Populate cache
            $stateData = json_encode($state->toArray());
            $this->cache->set(
                CacheKey::from("component_state:{$key}"),
                $stateData,
                Duration::fromSeconds(60)
            );

            // Should NOT call EntityManager
            $this->entityManager->shouldNotReceive('find');

            // Get state
            $result = $this->stateManager->getState($key);

            expect($result)->toBeInstanceOf(TestComponentState::class);
            expect($result->count)->toBe(77);
            expect($result->message)->toBe('cached');
        });

        it('falls back to database on cache miss', function () {
            $key = 'test-component:4';
            $state = new TestComponentState(count: 88, message: 'database');

            // Mock: Entity exists in database
            $entity = new ComponentStateEntity(
                componentId: $key,
                stateData: json_encode($state->toArray()),
                stateClass: TestComponentState::class,
                componentName: 'test-component',
                userId: null,
                sessionId: null,
                version: 1,
                checksum: hash('sha256', json_encode($state->toArray())),
                createdAt: Timestamp::now(),
                updatedAt: Timestamp::now(),
                expiresAt: null
            );

            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn($entity);

            // Get state
            $result = $this->stateManager->getState($key);

            expect($result)->toBeInstanceOf(TestComponentState::class);
            expect($result->count)->toBe(88);
            expect($result->message)->toBe('database');

            // Cache should be populated
            $cached = $this->cache->get(CacheKey::from("component_state:{$key}"));
            expect($cached->isHit)->toBeTrue();
        });

        it('returns null when state does not exist', function () {
            $key = 'test-component:nonexistent';

            // Mock: Entity does not exist
            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn(null);

            // Get state
            $result = $this->stateManager->getState($key);

            expect($result)->toBeNull();
        });
    });

    describe('hasState', function () {
        it('returns true when state exists in cache', function () {
            $key = 'test-component:5';
            $state = new TestComponentState(count: 99, message: 'exists');

            // Populate cache
            $this->cache->set(
                CacheKey::from("component_state:{$key}"),
                json_encode($state->toArray()),
                Duration::fromSeconds(60)
            );

            // Should NOT call EntityManager
            $this->entityManager->shouldNotReceive('find');

            // Check existence
            $result = $this->stateManager->hasState($key);

            expect($result)->toBeTrue();
        });

        it('checks database when not in cache', function () {
            $key = 'test-component:6';

            // Mock: Entity exists in database
            $entity = new ComponentStateEntity(
                componentId: $key,
                stateData: json_encode(['count' => 1, 'message' => 'test']),
                stateClass: TestComponentState::class,
                componentName: 'test-component',
                userId: null,
                sessionId: null,
                version: 1,
                checksum: 'checksum',
                createdAt: Timestamp::now(),
                updatedAt: Timestamp::now(),
                expiresAt: null
            );

            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn($entity);

            // Check existence
            $result = $this->stateManager->hasState($key);

            expect($result)->toBeTrue();
        });

        it('returns false when state does not exist', function () {
            $key = 'test-component:nonexistent';

            // Mock: Entity does not exist
            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn(null);

            // Check existence
            $result = $this->stateManager->hasState($key);

            expect($result)->toBeFalse();
        });
    });

    describe('removeState', function () {
        it('removes state from database and cache', function () {
            $key = 'test-component:7';

            // Populate cache
            $this->cache->set(
                CacheKey::from("component_state:{$key}"),
                json_encode(['count' => 1, 'message' => 'test']),
                Duration::fromSeconds(60)
            );

            // Mock: Entity exists
            $entity = new ComponentStateEntity(
                componentId: $key,
                stateData: json_encode(['count' => 1, 'message' => 'test']),
                stateClass: TestComponentState::class,
                componentName: 'test-component',
                userId: null,
                sessionId: null,
                version: 1,
                checksum: 'checksum',
                createdAt: Timestamp::now(),
                updatedAt: Timestamp::now(),
                expiresAt: null
            );

            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn($entity);

            // Expect remove and commit
            $this->unitOfWork
                ->shouldReceive('remove')
                ->once()
                ->with($entity);

            $this->unitOfWork
                ->shouldReceive('commit')
                ->once();

            // Remove state
            $this->stateManager->removeState($key);

            // Cache should be cleared
            $cached = $this->cache->get(CacheKey::from("component_state:{$key}"));
            expect($cached->isHit)->toBeFalse();
        });
    });

    describe('updateState', function () {
        it('atomically updates state with updater function', function () {
            $key = 'test-component:8';
            $initialState = new TestComponentState(count: 10, message: 'initial');

            // Mock: Get current state
            $entity = new ComponentStateEntity(
                componentId: $key,
                stateData: json_encode($initialState->toArray()),
                stateClass: TestComponentState::class,
                componentName: 'test-component',
                userId: null,
                sessionId: null,
                version: 1,
                checksum: hash('sha256', json_encode($initialState->toArray())),
                createdAt: Timestamp::now(),
                updatedAt: Timestamp::now(),
                expiresAt: null
            );

            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn($entity);

            // Expect persist with updated state
            $this->entityManager
                ->shouldReceive('find')
                ->once()
                ->with(ComponentStateEntity::class, $key)
                ->andReturn($entity);

            $this->unitOfWork
                ->shouldReceive('persist')
                ->once()
                ->with(Mockery::type(ComponentStateEntity::class));

            $this->unitOfWork
                ->shouldReceive('commit')
                ->once();

            // Update state
            $result = $this->stateManager->updateState(
                $key,
                fn(TestComponentState $state) => new TestComponentState(
                    count: $state->count + 5,
                    message: 'updated'
                )
            );

            expect($result)->toBeInstanceOf(TestComponentState::class);
            expect($result->count)->toBe(15);
            expect($result->message)->toBe('updated');
        });
    });

    describe('getAllStates', function () {
        it('returns all states for state class', function () {
            // Mock: Multiple entities
            $entities = [
                new ComponentStateEntity(
                    componentId: 'test-component:1',
                    stateData: json_encode(['count' => 1, 'message' => 'first']),
                    stateClass: TestComponentState::class,
                    componentName: 'test-component',
                    userId: null,
                    sessionId: null,
                    version: 1,
                    checksum: 'checksum1',
                    createdAt: Timestamp::now(),
                    updatedAt: Timestamp::now(),
                    expiresAt: null
                ),
                new ComponentStateEntity(
                    componentId: 'test-component:2',
                    stateData: json_encode(['count' => 2, 'message' => 'second']),
                    stateClass: TestComponentState::class,
                    componentName: 'test-component',
                    userId: null,
                    sessionId: null,
                    version: 1,
                    checksum: 'checksum2',
                    createdAt: Timestamp::now(),
                    updatedAt: Timestamp::now(),
                    expiresAt: null
                ),
            ];

            $this->entityManager
                ->shouldReceive('findBy')
                ->once()
                ->with(
                    ComponentStateEntity::class,
                    ['state_class' => TestComponentState::class]
                )
                ->andReturn($entities);

            // Get all states
            $result = $this->stateManager->getAllStates();

            expect($result)->toBeArray();
            expect($result)->toHaveCount(2);
            expect($result['test-component:1'])->toBeInstanceOf(TestComponentState::class);
            expect($result['test-component:1']->count)->toBe(1);
            expect($result['test-component:2']->count)->toBe(2);
        });
    });

    describe('getStatistics', function () {
        it('returns statistics about state storage', function () {
            // Mock: Multiple entities
            $entities = [
                new ComponentStateEntity(
                    componentId: 'test-component:1',
                    stateData: json_encode(['count' => 1, 'message' => 'test']),
                    stateClass: TestComponentState::class,
                    componentName: 'test-component',
                    userId: null,
                    sessionId: null,
                    version: 1,
                    checksum: 'checksum',
                    createdAt: Timestamp::now(),
                    updatedAt: Timestamp::now(),
                    expiresAt: null
                ),
            ];

            $this->entityManager
                ->shouldReceive('findBy')
                ->once()
                ->with(
                    ComponentStateEntity::class,
                    ['state_class' => TestComponentState::class]
                )
                ->andReturn($entities);

            // Get statistics
            $stats = $this->stateManager->getStatistics();

            expect($stats->totalKeys)->toBe(1);
            expect($stats->hitCount)->toBeInt();
            expect($stats->missCount)->toBeInt();
        });
    });
});
