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 (follow-current semantics)', () => {
  it('persists the first backend, advances current, and logs at info', async () => {
    const logger = makeLogger();
    const persist = vi.fn().mockResolvedValue(undefined);
    const tracker = createStickyBackendResolver({
      initial: null,
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    await tracker.onEvent({ backendId: 'gpu-a', cacheKey: null });

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

  it('follows backend switches: each CHANGE persists; repeats do not', async () => {
    const logger = makeLogger();
    const persist = vi.fn().mockResolvedValue(undefined);
    const tracker = createStickyBackendResolver({
      initial: null,
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    await tracker.onEvent({ backendId: 'gpu-a', cacheKey: null });
    await tracker.onEvent({ backendId: 'gpu-a', cacheKey: 'sha:1' }); // same — no persist
    await tracker.onEvent({ backendId: 'gpu-b', cacheKey: null });    // switch — persists
    await tracker.onEvent({ backendId: 'gpu-b', cacheKey: null });    // same — no persist

    expect(persist).toHaveBeenCalledTimes(2);
    expect(persist).toHaveBeenNthCalledWith(1, 'gpu-a');
    expect(persist).toHaveBeenNthCalledWith(2, 'gpu-b');
    expect(tracker.current()).toBe('gpu-b');
    // The switch log mentions both ends
    expect(logger.calls.info[1]).toContain('gpu-a');
    expect(logger.calls.info[1]).toContain('gpu-b');
  });

  it('honors initial value from DB: same backend does not re-persist, a switch does', async () => {
    const logger = makeLogger();
    const persist = vi.fn().mockResolvedValue(undefined);
    const tracker = createStickyBackendResolver({
      initial: 'gpu-seed',
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

    expect(tracker.current()).toBe('gpu-seed');
    await tracker.onEvent({ backendId: 'gpu-seed', cacheKey: null });
    expect(persist).not.toHaveBeenCalled();

    await tracker.onEvent({ backendId: 'gpu-other', cacheKey: null });
    expect(persist).toHaveBeenCalledTimes(1);
    expect(tracker.current()).toBe('gpu-other');
  });

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

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

    // Second call (same backend again): retries because current ≠ backendId
    await tracker.onEvent({ backendId: 'gpu-a', cacheKey: null });
    expect(persist).toHaveBeenCalledTimes(2);
    expect(tracker.current()).toBe('gpu-a');
    expect(logger.calls.info).toHaveLength(1);
  });

  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 tracker = createStickyBackendResolver({
      initial: null,
      persist,
      logger,
      workerId: 'w1',
      jobId: 'j1',
    });

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

    expect(persist).toHaveBeenCalledTimes(3);
    expect(logger.calls.warn).toHaveLength(2);
    expect(logger.calls.info).toHaveLength(1);
    expect(tracker.current()).toBe('gpu-c');
  });
});
