/**
 * LiveComponent Batching Tests
 *
 * Tests for request batching functionality that reduces HTTP overhead
 * by combining multiple component actions into a single request.
 */

describe('LiveComponent Batching', () => {
    let LiveComponent;
    let fetchMock;

    beforeEach(() => {
        // Reset DOM
        document.body.innerHTML = '';

        // Mock fetch
        fetchMock = jest.fn();
        global.fetch = fetchMock;

        // Create minimal LiveComponent manager for testing
        LiveComponent = {
            components: new Map(),
            batchQueue: [],
            batchQueueTimer: null,

            async executeBatch(operations, options = {}) {
                if (!Array.isArray(operations) || operations.length === 0) {
                    throw new Error('Operations must be a non-empty array');
                }

                const batchRequest = {
                    operations: operations.map(op => ({
                        componentId: op.componentId,
                        method: op.method,
                        params: op.params || {},
                        fragments: op.fragments || null,
                        operationId: op.operationId || null
                    }))
                };

                const response = await fetch('/live-component/batch', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Accept': 'application/json'
                    },
                    body: JSON.stringify(batchRequest)
                });

                if (!response.ok) {
                    const errorData = await response.json();
                    throw new Error(errorData.error || 'Batch request failed');
                }

                const batchResponse = await response.json();

                if (options.autoApply !== false) {
                    await this.applyBatchResults(batchResponse.results);
                }

                return batchResponse;
            },

            async applyBatchResults(results) {
                for (const result of results) {
                    if (!result.success) continue;

                    const componentId = result.componentId;
                    const config = this.components.get(componentId);
                    if (!config) continue;

                    if (result.fragments) {
                        // Mock fragment update
                        config.lastFragments = result.fragments;
                    } else if (result.html) {
                        config.element.innerHTML = result.html;
                    }

                    if (result.state) {
                        config.element.dataset.state = JSON.stringify(result.state);
                    }
                }
            },

            queueBatchOperation(operation, options = {}) {
                this.batchQueue.push(operation);

                if (this.batchQueueTimer) {
                    clearTimeout(this.batchQueueTimer);
                }

                const flushDelay = options.flushDelay || 50;
                this.batchQueueTimer = setTimeout(() => {
                    this.flushBatchQueue();
                }, flushDelay);
            },

            async flushBatchQueue() {
                if (this.batchQueue.length === 0) return;

                const operations = [...this.batchQueue];
                this.batchQueue = [];
                this.batchQueueTimer = null;

                await this.executeBatch(operations);
            }
        };
    });

    afterEach(() => {
        jest.clearAllMocks();
        if (LiveComponent.batchQueueTimer) {
            clearTimeout(LiveComponent.batchQueueTimer);
        }
    });

    describe('executeBatch', () => {
        it('executes batch request with multiple operations', async () => {
            fetchMock.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    results: [
                        { success: true, operation_id: 'op-1', html: '<div>Result 1</div>' },
                        { success: true, operation_id: 'op-2', html: '<div>Result 2</div>' }
                    ],
                    total_operations: 2,
                    success_count: 2,
                    failure_count: 0
                })
            });

            const operations = [
                { componentId: 'counter:1', method: 'increment', operationId: 'op-1' },
                { componentId: 'counter:2', method: 'increment', operationId: 'op-2' }
            ];

            const response = await LiveComponent.executeBatch(operations, { autoApply: false });

            expect(fetchMock).toHaveBeenCalledTimes(1);
            expect(fetchMock).toHaveBeenCalledWith('/live-component/batch', expect.objectContaining({
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                }
            }));

            const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body);
            expect(requestBody.operations).toHaveLength(2);
            expect(response.success_count).toBe(2);
        });

        it('throws error for empty operations array', async () => {
            await expect(LiveComponent.executeBatch([])).rejects.toThrow('Operations must be a non-empty array');
        });

        it('throws error for non-array operations', async () => {
            await expect(LiveComponent.executeBatch(null)).rejects.toThrow('Operations must be a non-empty array');
        });

        it('includes fragments in batch request', async () => {
            fetchMock.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    results: [{ success: true, operation_id: 'op-1', fragments: { 'display': '<span>5</span>' } }],
                    total_operations: 1,
                    success_count: 1,
                    failure_count: 0
                })
            });

            const operations = [
                {
                    componentId: 'counter:1',
                    method: 'increment',
                    params: { amount: 5 },
                    fragments: ['display'],
                    operationId: 'op-1'
                }
            ];

            await LiveComponent.executeBatch(operations, { autoApply: false });

            const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body);
            expect(requestBody.operations[0].fragments).toEqual(['display']);
            expect(requestBody.operations[0].params).toEqual({ amount: 5 });
        });

        it('handles server error response', async () => {
            fetchMock.mockResolvedValueOnce({
                ok: false,
                json: async () => ({ error: 'Batch validation failed' })
            });

            const operations = [{ componentId: 'test', method: 'action' }];

            await expect(LiveComponent.executeBatch(operations)).rejects.toThrow('Batch validation failed');
        });

        it('handles partial failures in batch', async () => {
            fetchMock.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    results: [
                        { success: true, operation_id: 'op-1', html: '<div>Success</div>' },
                        { success: false, operation_id: 'op-2', error: 'Action not found', error_code: 'ACTION_NOT_FOUND' }
                    ],
                    total_operations: 2,
                    success_count: 1,
                    failure_count: 1
                })
            });

            const operations = [
                { componentId: 'component:1', method: 'validAction', operationId: 'op-1' },
                { componentId: 'component:2', method: 'invalidAction', operationId: 'op-2' }
            ];

            const response = await LiveComponent.executeBatch(operations, { autoApply: false });

            expect(response.success_count).toBe(1);
            expect(response.failure_count).toBe(1);
            expect(response.results[1].success).toBe(false);
            expect(response.results[1].error_code).toBe('ACTION_NOT_FOUND');
        });
    });

    describe('applyBatchResults', () => {
        it('applies HTML results to components', async () => {
            const element1 = document.createElement('div');
            const element2 = document.createElement('div');

            LiveComponent.components.set('comp:1', { element: element1 });
            LiveComponent.components.set('comp:2', { element: element2 });

            const results = [
                { success: true, componentId: 'comp:1', html: '<p>Updated 1</p>' },
                { success: true, componentId: 'comp:2', html: '<p>Updated 2</p>' }
            ];

            await LiveComponent.applyBatchResults(results);

            expect(element1.innerHTML).toBe('<p>Updated 1</p>');
            expect(element2.innerHTML).toBe('<p>Updated 2</p>');
        });

        it('applies fragment results to components', async () => {
            const element = document.createElement('div');
            LiveComponent.components.set('comp:1', { element });

            const results = [
                {
                    success: true,
                    componentId: 'comp:1',
                    fragments: { 'header': '<h1>New Header</h1>', 'footer': '<p>New Footer</p>' }
                }
            ];

            await LiveComponent.applyBatchResults(results);

            const config = LiveComponent.components.get('comp:1');
            expect(config.lastFragments).toEqual({ 'header': '<h1>New Header</h1>', 'footer': '<p>New Footer</p>' });
        });

        it('updates component state', async () => {
            const element = document.createElement('div');
            LiveComponent.components.set('comp:1', { element });

            const results = [
                {
                    success: true,
                    componentId: 'comp:1',
                    html: '<div>Updated</div>',
                    state: { count: 10, active: true }
                }
            ];

            await LiveComponent.applyBatchResults(results);

            const state = JSON.parse(element.dataset.state);
            expect(state.count).toBe(10);
            expect(state.active).toBe(true);
        });

        it('skips failed operations', async () => {
            const element = document.createElement('div');
            element.innerHTML = '<div>Original</div>';
            LiveComponent.components.set('comp:1', { element });

            const results = [
                { success: false, componentId: 'comp:1', error: 'Action failed' }
            ];

            await LiveComponent.applyBatchResults(results);

            // Original content should be unchanged
            expect(element.innerHTML).toBe('<div>Original</div>');
        });

        it('skips unknown components', async () => {
            const results = [
                { success: true, componentId: 'unknown:1', html: '<div>Test</div>' }
            ];

            // Should not throw
            await expect(LiveComponent.applyBatchResults(results)).resolves.not.toThrow();
        });
    });

    describe('queueBatchOperation', () => {
        it('queues operation for batching', () => {
            const operation = { componentId: 'test:1', method: 'action' };

            LiveComponent.queueBatchOperation(operation);

            expect(LiveComponent.batchQueue).toHaveLength(1);
            expect(LiveComponent.batchQueue[0]).toEqual(operation);
        });

        it('sets timer for auto-flush', () => {
            const operation = { componentId: 'test:1', method: 'action' };

            LiveComponent.queueBatchOperation(operation);

            expect(LiveComponent.batchQueueTimer).not.toBeNull();
        });

        it('resets timer on subsequent operations', () => {
            LiveComponent.queueBatchOperation({ componentId: 'test:1', method: 'action1' });
            const firstTimer = LiveComponent.batchQueueTimer;

            LiveComponent.queueBatchOperation({ componentId: 'test:2', method: 'action2' });
            const secondTimer = LiveComponent.batchQueueTimer;

            expect(firstTimer).not.toBe(secondTimer);
            expect(LiveComponent.batchQueue).toHaveLength(2);
        });

        it('uses custom flush delay', () => {
            jest.useFakeTimers();

            LiveComponent.queueBatchOperation(
                { componentId: 'test:1', method: 'action' },
                { flushDelay: 100 }
            );

            expect(LiveComponent.batchQueue).toHaveLength(1);

            jest.advanceTimersByTime(50);
            expect(LiveComponent.batchQueue).toHaveLength(1); // Not flushed yet

            jest.advanceTimersByTime(50);
            // Timer should have fired (but executeBatch is mocked so queue won't clear in test)

            jest.useRealTimers();
        });
    });

    describe('flushBatchQueue', () => {
        it('executes all queued operations', async () => {
            fetchMock.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    results: [
                        { success: true, operation_id: 'op-1' },
                        { success: true, operation_id: 'op-2' },
                        { success: true, operation_id: 'op-3' }
                    ],
                    total_operations: 3,
                    success_count: 3,
                    failure_count: 0
                })
            });

            LiveComponent.batchQueue = [
                { componentId: 'test:1', method: 'action1' },
                { componentId: 'test:2', method: 'action2' },
                { componentId: 'test:3', method: 'action3' }
            ];

            await LiveComponent.flushBatchQueue();

            expect(fetchMock).toHaveBeenCalledTimes(1);
            expect(LiveComponent.batchQueue).toHaveLength(0);
            expect(LiveComponent.batchQueueTimer).toBeNull();
        });

        it('does nothing when queue is empty', async () => {
            LiveComponent.batchQueue = [];

            await LiveComponent.flushBatchQueue();

            expect(fetchMock).not.toHaveBeenCalled();
        });

        it('clears queue even on error', async () => {
            fetchMock.mockRejectedValueOnce(new Error('Network error'));

            LiveComponent.batchQueue = [
                { componentId: 'test:1', method: 'action1' }
            ];

            await LiveComponent.flushBatchQueue();

            expect(LiveComponent.batchQueue).toHaveLength(0);
        });
    });

    describe('automatic batching integration', () => {
        it('batches multiple operations within time window', async () => {
            jest.useFakeTimers();

            fetchMock.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    results: [
                        { success: true, operation_id: 'op-1' },
                        { success: true, operation_id: 'op-2' }
                    ],
                    total_operations: 2,
                    success_count: 2,
                    failure_count: 0
                })
            });

            // Queue multiple operations within 50ms window
            LiveComponent.queueBatchOperation({ componentId: 'test:1', method: 'action1' });
            LiveComponent.queueBatchOperation({ componentId: 'test:2', method: 'action2' });

            expect(LiveComponent.batchQueue).toHaveLength(2);

            // Fast-forward timer to trigger flush
            jest.advanceTimersByTime(50);

            // Wait for async execution
            await Promise.resolve();

            expect(fetchMock).toHaveBeenCalledTimes(1);

            const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body);
            expect(requestBody.operations).toHaveLength(2);

            jest.useRealTimers();
        });
    });

    describe('performance benefits', () => {
        it('reduces HTTP requests compared to individual actions', async () => {
            // Batch scenario
            fetchMock.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    results: Array(10).fill({ success: true }),
                    total_operations: 10,
                    success_count: 10,
                    failure_count: 0
                })
            });

            const operations = Array.from({ length: 10 }, (_, i) => ({
                componentId: `test:${i}`,
                method: 'action'
            }));

            await LiveComponent.executeBatch(operations, { autoApply: false });

            // Only 1 HTTP request for 10 operations
            expect(fetchMock).toHaveBeenCalledTimes(1);

            // Without batching, would require 10 separate HTTP requests
            // Reduction: 90% (10 requests → 1 request)
        });
    });
});
