import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

vi.mock('../../src/env', () => ({ env: { boxIdleMs: 1000 } }));
vi.mock('../../src/runtime/docker', () => ({
  ensureUserContainer: vi.fn((args: { username: string }) =>
    Promise.resolve(`spoon-box-${args.username}`),
  ),
  stopWorkspaceContainer: vi.fn(() => Promise.resolve()),
  userContainerName: (username: string) => `spoon-box-${username}`,
  listWorkspaceContainerNames: vi.fn(() => Promise.resolve([])),
}));

const load = async () => await import('../../src/user-container');

describe('box registry', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(async () => {
    const module = await load();
    module._resetBoxRegistryForTests();
    vi.useRealTimers();
    vi.resetModules();
    vi.clearAllMocks();
  });

  test('serializes concurrent acquire into a single docker run', async () => {
    const module = await load();
    const docker = await import('../../src/runtime/docker');
    const [a, b] = await Promise.all([
      module.acquireUserBox({
        username: 'alice',
        workdir: '/w',
        containerHome: '/home/alice',
      }),
      module.acquireUserBox({
        username: 'alice',
        workdir: '/w',
        containerHome: '/home/alice',
      }),
    ]);
    expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
    expect(a.boxName).toBe('spoon-box-alice');
    a.release();
    await vi.advanceTimersByTimeAsync(2000);
    expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
    b.release();
    await vi.advanceTimersByTimeAsync(2000);
    expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
  });

  test('double-release of one handle does not reap a box held elsewhere', async () => {
    const module = await load();
    const docker = await import('../../src/runtime/docker');
    const a = await module.acquireUserBox({
      username: 'bob',
      workdir: '/w',
      containerHome: '/home/bob',
    });
    const b = await module.acquireUserBox({
      username: 'bob',
      workdir: '/w',
      containerHome: '/home/bob',
    });
    a.release();
    a.release();
    await vi.advanceTimersByTimeAsync(2000);
    expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
    b.release();
    await vi.advanceTimersByTimeAsync(2000);
    expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
  });

  test('reconcileExistingBoxes adopts orphans with zero refs and reaps them', async () => {
    const module = await load();
    const docker = await import('../../src/runtime/docker');
    (
      docker.listWorkspaceContainerNames as unknown as ReturnType<typeof vi.fn>
    ).mockResolvedValueOnce(['spoon-box-carol']);
    await module.reconcileExistingBoxes();
    expect(module.runningBoxUsernames().has('carol')).toBe(true);
    await vi.advanceTimersByTimeAsync(2000);
    expect(docker.stopWorkspaceContainer).toHaveBeenCalledWith(
      'spoon-box-carol',
    );
  });

  test('ensures an adopted box once before returning acquire handles', async () => {
    const module = await load();
    const docker = await import('../../src/runtime/docker');
    vi.mocked(docker.listWorkspaceContainerNames).mockResolvedValueOnce([
      'spoon-box-casey',
    ]);
    await module.reconcileExistingBoxes();

    const first = await module.acquireUserBox({
      username: 'casey',
      workdir: '/w',
      containerHome: '/home/casey',
    });
    const second = await module.acquireUserBox({
      username: 'casey',
      workdir: '/w',
      containerHome: '/home/casey',
    });

    expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
    expect(first.boxName).toBe('spoon-box-casey');
    expect(second.boxName).toBe('spoon-box-casey');
  });

  test('retries ensure after the first acquire fails', async () => {
    const module = await load();
    const docker = await import('../../src/runtime/docker');
    const ensure = vi.mocked(docker.ensureUserContainer);
    ensure
      .mockRejectedValueOnce(new Error('docker failed'))
      .mockResolvedValueOnce('spoon-box-dana');

    await expect(
      module.acquireUserBox({
        username: 'dana',
        workdir: '/w',
        containerHome: '/home/dana',
      }),
    ).rejects.toThrow('docker failed');

    const handle = await module.acquireUserBox({
      username: 'dana',
      workdir: '/w',
      containerHome: '/home/dana',
    });
    expect(ensure).toHaveBeenCalledTimes(2);
    expect(handle.boxName).toBe('spoon-box-dana');
  });

  test('finishes an old idle reap before a new acquire can ensure the box', async () => {
    const module = await load();
    const docker = await import('../../src/runtime/docker');
    const ensure = vi.mocked(docker.ensureUserContainer);
    const stop = vi.mocked(docker.stopWorkspaceContainer);
    const events: string[] = [];
    let finishStop!: () => void;
    const stopDone = new Promise<void>((resolve) => {
      finishStop = resolve;
    });
    ensure.mockReset().mockImplementation((args: { username: string }) => {
      events.push('ensure');
      return Promise.resolve(`spoon-box-${args.username}`);
    });
    stop.mockReset().mockImplementationOnce(() => {
      events.push('stop:start');
      return stopDone.then(() => {
        events.push('stop:end');
      });
    });

    const first = await module.acquireUserBox({
      username: 'erin',
      workdir: '/w',
      containerHome: '/home/erin',
    });
    first.release();
    await vi.advanceTimersByTimeAsync(1000);

    const secondPromise = module.acquireUserBox({
      username: 'erin',
      workdir: '/w',
      containerHome: '/home/erin',
    });
    await Promise.resolve();
    await Promise.resolve();
    expect(events).toEqual(['ensure', 'stop:start']);

    finishStop();
    const second = await secondPromise;
    expect(second.boxName).toBe('spoon-box-erin');
    expect(events).toEqual(['ensure', 'stop:start', 'stop:end', 'ensure']);
  });
});
