import { describe, it, expect, vi } from 'vitest';
import { createOfflineQueue } from './index.js';

function createMemoryStorage() {
  const store: Record<string, string> = {};
  return {
    getItem: (k: string) => store[k] ?? null,
    setItem: (k: string, v: string) => {
      store[k] = v;
    },
    _store: store,
  };
}

describe('createOfflineQueue', () => {
  it('should start with zero length', () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });
    expect(queue.length()).toBe(0);
  });

  it('should enqueue items', () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });

    queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: { name: 'test' } });
    expect(queue.length()).toBe(1);

    queue.enqueue({ id: '2', action: 'update', path: '/sessions/2', payload: { name: 'test2' } });
    expect(queue.length()).toBe(2);
  });

  it('should replace existing entry with same id + action', () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });

    queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: { v: 1 } });
    queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: { v: 2 } });
    expect(queue.length()).toBe(1);
  });

  it('should not replace entries with different action', () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });

    queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: {} });
    queue.enqueue({ id: '1', action: 'update', path: '/sessions/1', payload: {} });
    expect(queue.length()).toBe(2);
  });

  it('should flush all items successfully', async () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });

    queue.enqueue({ id: '1', action: 'create', path: '/a', payload: { n: 1 } });
    queue.enqueue({ id: '2', action: 'create', path: '/b', payload: { n: 2 } });

    const executor = vi.fn().mockResolvedValue(undefined);
    const result = await queue.flush(executor);

    expect(result.flushed).toBe(2);
    expect(result.failed).toBe(0);
    expect(queue.length()).toBe(0);
    expect(executor).toHaveBeenCalledTimes(2);
  });

  it('should retry failed items on next flush', async () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage, maxRetries: 3 });

    queue.enqueue({ id: '1', action: 'create', path: '/a', payload: {} });

    const failingExecutor = vi.fn().mockRejectedValue(new Error('offline'));
    const result = await queue.flush(failingExecutor);

    expect(result.flushed).toBe(0);
    expect(result.failed).toBe(1);
    expect(queue.length()).toBe(1);
  });

  it('should drop items after max retries', async () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage, maxRetries: 2 });

    queue.enqueue({ id: '1', action: 'create', path: '/a', payload: {} });

    const failingExecutor = vi.fn().mockRejectedValue(new Error('offline'));

    // Retry 1
    await queue.flush(failingExecutor);
    expect(queue.length()).toBe(1);

    // Retry 2 — should be dropped
    await queue.flush(failingExecutor);
    expect(queue.length()).toBe(0);
  });

  it('should cap queue at maxQueueSize', () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage, maxQueueSize: 3 });

    for (let i = 0; i < 5; i++) {
      queue.enqueue({ id: `${i}`, action: 'create', path: `/item/${i}`, payload: {} });
    }

    expect(queue.length()).toBe(3);
  });

  it('should persist to storage', () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });

    queue.enqueue({ id: '1', action: 'create', path: '/a', payload: { x: 1 } });

    const stored = JSON.parse(storage._store['test-q']);
    expect(stored).toHaveLength(1);
    expect(stored[0].id).toBe('1');
  });

  it('should clear the queue', () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });

    queue.enqueue({ id: '1', action: 'create', path: '/a', payload: {} });
    queue.enqueue({ id: '2', action: 'create', path: '/b', payload: {} });
    expect(queue.length()).toBe(2);

    queue.clear();
    expect(queue.length()).toBe(0);
  });

  it('should return empty result when flushing empty queue', async () => {
    const storage = createMemoryStorage();
    const queue = createOfflineQueue({ storageKey: 'test-q', storage });

    const executor = vi.fn();
    const result = await queue.flush(executor);

    expect(result.flushed).toBe(0);
    expect(result.failed).toBe(0);
    expect(executor).not.toHaveBeenCalled();
  });
});
