/**
 * Optimistic UI Tests
 *
 * Tests for version-based optimistic concurrency control and conflict resolution.
 */

import { OptimisticStateManager } from '../../../../resources/js/modules/livecomponent/OptimisticStateManager.js';

describe('OptimisticStateManager', () => {
    let manager;

    beforeEach(() => {
        manager = new OptimisticStateManager();
    });

    afterEach(() => {
        manager.clear();
    });

    describe('applyOptimisticUpdate', () => {
        it('creates snapshot before applying update', () => {
            const componentId = 'test-component:1';
            const currentState = {
                data: { count: 5 },
                version: 1
            };

            const optimisticState = manager.applyOptimisticUpdate(
                componentId,
                currentState,
                (state) => ({
                    ...state,
                    data: { ...state.data, count: state.data.count + 1 }
                }),
                { action: 'increment' }
            );

            // Should create snapshot
            const snapshot = manager.getSnapshot(componentId);
            expect(snapshot).not.toBeNull();
            expect(snapshot.version).toBe(1);
            expect(snapshot.data.count).toBe(5);
        });

        it('increments version optimistically', () => {
            const componentId = 'test-component:1';
            const currentState = {
                data: { count: 5 },
                version: 1
            };

            const optimisticState = manager.applyOptimisticUpdate(
                componentId,
                currentState,
                (state) => ({
                    ...state,
                    data: { ...state.data, count: state.data.count + 1 }
                })
            );

            expect(optimisticState.version).toBe(2);
            expect(optimisticState.data.count).toBe(6);
        });

        it('tracks pending operation', () => {
            const componentId = 'test-component:1';
            const currentState = {
                data: { count: 5 },
                version: 1
            };

            manager.applyOptimisticUpdate(
                componentId,
                currentState,
                (state) => ({ ...state }),
                { action: 'testAction', params: { value: 1 } }
            );

            const pendingOps = manager.getPendingOperations(componentId);
            expect(pendingOps).toHaveLength(1);
            expect(pendingOps[0].metadata.action).toBe('testAction');
            expect(pendingOps[0].status).toBe('pending');
        });

        it('generates unique operation IDs', () => {
            const componentId = 'test-component:1';
            const currentState = {
                data: {},
                version: 1
            };

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            const op1 = manager.getPendingOperations(componentId)[0];

            manager.clear();

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            const op2 = manager.getPendingOperations(componentId)[0];

            expect(op1.id).not.toBe(op2.id);
        });
    });

    describe('confirmOperation', () => {
        it('marks operation as confirmed', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            const operationId = manager.getPendingOperations(componentId)[0].id;

            const serverState = { data: {}, version: 2 };
            manager.confirmOperation(componentId, operationId, serverState);

            // Operation should be removed from pending
            expect(manager.getPendingOperations(componentId)).toHaveLength(0);
        });

        it('clears snapshot when no more pending operations', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            const operationId = manager.getPendingOperations(componentId)[0].id;

            const serverState = { data: {}, version: 2 };
            manager.confirmOperation(componentId, operationId, serverState);

            // Snapshot should be cleared
            expect(manager.getSnapshot(componentId)).toBeNull();
        });

        it('keeps snapshot when pending operations remain', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            // Create two pending operations
            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            manager.applyOptimisticUpdate(componentId, { data: {}, version: 2 }, (s) => s);

            const operations = manager.getPendingOperations(componentId);
            const operationId = operations[0].id;

            // Confirm first operation
            manager.confirmOperation(componentId, operationId, { data: {}, version: 2 });

            // Snapshot should still exist (second operation pending)
            expect(manager.getSnapshot(componentId)).not.toBeNull();
            expect(manager.getPendingOperations(componentId)).toHaveLength(1);
        });
    });

    describe('handleConflict', () => {
        it('marks operation as failed', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            const operationId = manager.getPendingOperations(componentId)[0].id;

            const serverState = { data: {}, version: 3 }; // Conflict: expected 2, got 3
            manager.handleConflict(componentId, operationId, serverState, {
                expectedVersion: 2,
                actualVersion: 3
            });

            // Operation should be removed
            expect(manager.getPendingOperations(componentId)).toHaveLength(0);
        });

        it('returns server state and notification', () => {
            const componentId = 'test-component:1';
            const currentState = { data: { count: 5 }, version: 1 };

            manager.applyOptimisticUpdate(
                componentId,
                currentState,
                (s) => ({ ...s, data: { count: 6 } }),
                { action: 'increment' }
            );
            const operationId = manager.getPendingOperations(componentId)[0].id;

            const serverState = { data: { count: 10 }, version: 3 };
            const result = manager.handleConflict(componentId, operationId, serverState);

            expect(result.state).toEqual(serverState);
            expect(result.notification).toBeDefined();
            expect(result.notification.type).toBe('conflict');
            expect(result.notification.canRetry).toBe(true);
        });

        it('clears all pending operations on conflict', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            // Create multiple pending operations
            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            manager.applyOptimisticUpdate(componentId, { data: {}, version: 2 }, (s) => s);

            const operationId = manager.getPendingOperations(componentId)[0].id;

            manager.handleConflict(componentId, operationId, { data: {}, version: 5 });

            // All operations should be cleared
            expect(manager.getPendingOperations(componentId)).toHaveLength(0);
        });

        it('clears snapshot after conflict', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            const operationId = manager.getPendingOperations(componentId)[0].id;

            manager.handleConflict(componentId, operationId, { data: {}, version: 3 });

            expect(manager.getSnapshot(componentId)).toBeNull();
        });

        it('calls conflict handler if registered', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            const conflictHandler = jest.fn();
            manager.registerConflictHandler(componentId, conflictHandler);

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            const operationId = manager.getPendingOperations(componentId)[0].id;

            const serverState = { data: {}, version: 3 };
            manager.handleConflict(componentId, operationId, serverState);

            expect(conflictHandler).toHaveBeenCalled();
            const callArgs = conflictHandler.mock.calls[0][0];
            expect(callArgs.serverState).toEqual(serverState);
        });
    });

    describe('retryOperation', () => {
        it('executes retry callback with operation metadata', async () => {
            const componentId = 'test-component:1';
            const retryCallback = jest.fn().mockResolvedValue({ success: true });

            const operation = {
                id: 'op-1',
                metadata: { action: 'testAction', params: { value: 1 } }
            };

            const result = await manager.retryOperation(componentId, operation, retryCallback);

            expect(retryCallback).toHaveBeenCalledWith(operation.metadata);
            expect(result.success).toBe(true);
        });

        it('propagates retry callback errors', async () => {
            const componentId = 'test-component:1';
            const error = new Error('Retry failed');
            const retryCallback = jest.fn().mockRejectedValue(error);

            const operation = {
                id: 'op-1',
                metadata: { action: 'testAction' }
            };

            await expect(
                manager.retryOperation(componentId, operation, retryCallback)
            ).rejects.toThrow('Retry failed');
        });
    });

    describe('state management', () => {
        it('tracks multiple components independently', () => {
            const comp1 = 'component:1';
            const comp2 = 'component:2';
            const state = { data: {}, version: 1 };

            manager.applyOptimisticUpdate(comp1, state, (s) => s);
            manager.applyOptimisticUpdate(comp2, state, (s) => s);

            expect(manager.getPendingOperations(comp1)).toHaveLength(1);
            expect(manager.getPendingOperations(comp2)).toHaveLength(1);
            expect(manager.getSnapshot(comp1)).not.toBeNull();
            expect(manager.getSnapshot(comp2)).not.toBeNull();
        });

        it('clears all state correctly', () => {
            const componentId = 'test-component:1';
            const currentState = { data: {}, version: 1 };

            manager.applyOptimisticUpdate(componentId, currentState, (s) => s);
            manager.registerConflictHandler(componentId, () => {});

            manager.clear();

            expect(manager.getPendingOperations(componentId)).toHaveLength(0);
            expect(manager.getSnapshot(componentId)).toBeNull();
            expect(manager.getStats().total_components).toBe(0);
        });
    });

    describe('getStats', () => {
        it('returns accurate statistics', () => {
            const comp1 = 'component:1';
            const comp2 = 'component:2';
            const state = { data: {}, version: 1 };

            manager.applyOptimisticUpdate(comp1, state, (s) => s);
            manager.applyOptimisticUpdate(comp1, { data: {}, version: 2 }, (s) => s);
            manager.applyOptimisticUpdate(comp2, state, (s) => s);

            const stats = manager.getStats();

            expect(stats.total_components).toBe(2);
            expect(stats.total_pending_operations).toBe(3);
            expect(stats.total_snapshots).toBe(2);
            expect(stats.components[comp1].pending).toBe(2);
            expect(stats.components[comp2].pending).toBe(1);
        });
    });
});
