import { describe, it, expect, vi } from 'vitest';
import { createStickyBackendResolver, type StickyBackendLogger } from './sticky-backend.js';

function makeLogger(): StickyBackendLogger & {
  calls: { debug: string[]; info: string[]; warn: string[] };
} {
  const calls = { debug: [] as string[], info: [] as string[], warn: [] as string[] };
  return {
    calls,
    debug: (m) => calls.debug.push(m),
    info: (m) => calls.info.push(m),
    warn: (m) => calls.warn.push(m),
  };
}

describe('createStickyBackendResolver', () => {
  it('persists the first backend, sets sticky, and logs at info', async () => {
    const logger = makeLogger();
    const persist = vi.fn().mockResolvedValue(undefined);
    const resolve = createStickyBackendResolver({
      initial: null,
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    await resolve({ backendId: 'gpu-a', cacheKey: null });

    expect(persist).toHaveBeenCalledTimes(1);
    expect(persist).toHaveBeenCalledWith('gpu-a');
    expect(logger.calls.info).toHaveLength(1);
    expect(logger.calls.info[0]).toContain('gpu-a');
  });

  it('short-circuits subsequent events once sticky is set', async () => {
    const logger = makeLogger();
    const persist = vi.fn().mockResolvedValue(undefined);
    const resolve = createStickyBackendResolver({
      initial: null,
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    await resolve({ backendId: 'gpu-a', cacheKey: null });
    await resolve({ backendId: 'gpu-b', cacheKey: 'sha:xyz' });
    await resolve({ backendId: 'gpu-a', cacheKey: null });

    expect(persist).toHaveBeenCalledTimes(1);
    // gpu-b ≠ sticky → debug; gpu-a == sticky → no log
    expect(logger.calls.debug).toHaveLength(1);
    expect(logger.calls.debug[0]).toContain('gpu-b');
  });

  it('honors initial sticky from DB without re-persisting', async () => {
    const logger = makeLogger();
    const persist = vi.fn().mockResolvedValue(undefined);
    const resolve = createStickyBackendResolver({
      initial: 'gpu-seed',
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    await resolve({ backendId: 'gpu-other', cacheKey: null });
    await resolve({ backendId: 'gpu-seed', cacheKey: null });

    expect(persist).not.toHaveBeenCalled();
    expect(logger.calls.debug).toHaveLength(1);
    expect(logger.calls.debug[0]).toContain('gpu-other');
  });

  it('does NOT set sticky when persist fails — next event retries', async () => {
    const logger = makeLogger();
    const persist = vi
      .fn()
      .mockRejectedValueOnce(new Error('SQLITE_BUSY'))
      .mockResolvedValueOnce(undefined);
    const resolve = createStickyBackendResolver({
      initial: null,
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    // First call: DB write fails → sticky unset → warn logged
    await resolve({ backendId: 'gpu-a', cacheKey: null });
    expect(persist).toHaveBeenCalledTimes(1);
    expect(logger.calls.warn).toHaveLength(1);
    expect(logger.calls.warn[0]).toContain('SQLITE_BUSY');
    expect(logger.calls.info).toHaveLength(0);

    // Second call: DB write succeeds → sticky set
    await resolve({ backendId: 'gpu-b', cacheKey: null });
    expect(persist).toHaveBeenCalledTimes(2);
    expect(persist).toHaveBeenLastCalledWith('gpu-b');
    expect(logger.calls.info).toHaveLength(1);
    expect(logger.calls.info[0]).toContain('gpu-b');

    // Third call: sticky is now set → no further persist
    await resolve({ backendId: 'gpu-c', cacheKey: null });
    expect(persist).toHaveBeenCalledTimes(2);
  });

  it('retries on every event until persist succeeds (multiple failures)', async () => {
    const logger = makeLogger();
    const persist = vi
      .fn()
      .mockRejectedValueOnce(new Error('fail 1'))
      .mockRejectedValueOnce(new Error('fail 2'))
      .mockResolvedValueOnce(undefined);
    const resolve = createStickyBackendResolver({
      initial: null,
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    await resolve({ backendId: 'gpu-a', cacheKey: null });
    await resolve({ backendId: 'gpu-b', cacheKey: null });
    await resolve({ backendId: 'gpu-c', cacheKey: null });

    expect(persist).toHaveBeenCalledTimes(3);
    expect(logger.calls.warn).toHaveLength(2);
    expect(logger.calls.info).toHaveLength(1);
    expect(logger.calls.info[0]).toContain('gpu-c');
  });
});
