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

const hoisted = vi.hoisted(() => {
  const skipResponsesForOps = new Set<string>();
  const workers: Array<{
    messages: unknown[];
    handlers: Map<string, (value: unknown) => void>;
    postMessage: (message: unknown) => void;
    on: (event: string, handler: (value: unknown) => void) => void;
    terminate: ReturnType<typeof vi.fn>;
  }> = [];
  const createMockWorker = vi.fn().mockImplementation(() => {
    const worker = {
      messages: [] as unknown[],
      handlers: new Map<string, (value: unknown) => void>(),
      postMessage(message: unknown) {
        worker.messages.push(message);
        const request = message as { id: string; op: string };
        if (skipResponsesForOps.has(request.op)) return;
        queueMicrotask(() => {
          const handler = worker.handlers.get('message');
          if (!handler) return;
          handler({
            id: request.id,
            ok: true,
            result: request.op === 'listTeams' || request.op === 'getAllTasks' ? [] : null,
            diag: { op: request.op, totalMs: 0 },
          });
        });
      },
      on(event: string, handler: (value: unknown) => void) {
        worker.handlers.set(event, handler);
      },
      terminate: vi.fn(async () => undefined),
    };
    workers.push(worker);
    return worker;
  });
  return {
    workers,
    createMockWorker,
    skipResponsesForOps,
  };
});

vi.mock('node:fs', async () => {
  const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
  return {
    ...actual,
    existsSync: vi.fn(() => true),
  };
});

vi.mock('node:worker_threads', () => ({
  Worker: hoisted.createMockWorker,
  default: {
    Worker: hoisted.createMockWorker,
  },
}));

describe('TeamFsWorkerClient', () => {
  afterEach(() => {
    vi.resetModules();
    vi.clearAllMocks();
    vi.useRealTimers();
    hoisted.workers.length = 0;
    hoisted.skipResponsesForOps.clear();
  });

  it('prewarms the worker without running a scan', async () => {
    const { TeamFsWorkerClient } = await import(
      '../../../../src/main/services/team/TeamFsWorkerClient'
    );
    const client = new TeamFsWorkerClient();

    await client.prewarm();

    expect(hoisted.workers).toHaveLength(1);
    expect(hoisted.workers[0].messages).toHaveLength(1);
    expect(hoisted.workers[0].messages[0]).toMatchObject({
      op: 'warmup',
      payload: {},
    });
  });

  it('does not queue warmup behind an already running worker', async () => {
    const { TeamFsWorkerClient } = await import(
      '../../../../src/main/services/team/TeamFsWorkerClient'
    );
    const client = new TeamFsWorkerClient();

    await client.listTeams({
      largeConfigBytes: 8 * 1024,
      configHeadBytes: 4 * 1024,
      maxConfigBytes: 256 * 1024,
      maxMembersMetaBytes: 256 * 1024,
      maxSessionHistoryInSummary: 10,
      maxProjectPathHistoryInSummary: 10,
    });
    await client.prewarm();

    expect(hoisted.workers).toHaveLength(1);
    expect(hoisted.workers[0].messages).toHaveLength(1);
    expect(hoisted.workers[0].messages[0]).toMatchObject({
      op: 'listTeams',
    });
  });

  it('ignores stale worker exit after timeout when a replacement worker owns pending work', async () => {
    vi.useFakeTimers();
    hoisted.skipResponsesForOps.add('warmup');
    hoisted.skipResponsesForOps.add('listTeams');
    const { TeamFsWorkerClient } = await import(
      '../../../../src/main/services/team/TeamFsWorkerClient'
    );
    const client = new TeamFsWorkerClient();

    const prewarmResult = client.prewarm().catch((error: unknown) => error);
    await vi.advanceTimersByTimeAsync(20_001);
    const prewarmError = await prewarmResult;
    expect(prewarmError).toBeInstanceOf(Error);
    expect((prewarmError as Error).message).toContain('Worker call timeout');
    expect(hoisted.workers).toHaveLength(1);

    const listPromise = client.listTeams({
      largeConfigBytes: 8 * 1024,
      configHeadBytes: 4 * 1024,
      maxConfigBytes: 256 * 1024,
      maxMembersMetaBytes: 256 * 1024,
      maxSessionHistoryInSummary: 10,
      maxProjectPathHistoryInSummary: 10,
    });

    expect(hoisted.workers).toHaveLength(2);
    const staleWorker = hoisted.workers[0];
    const replacementWorker = hoisted.workers[1];
    const listRequest = replacementWorker.messages[0] as { id: string };

    staleWorker.handlers.get('exit')?.(1);
    replacementWorker.handlers.get('message')?.({
      id: listRequest.id,
      ok: true,
      result: [{ teamName: 'fresh-team', displayName: 'Fresh Team' }],
      diag: { op: 'listTeams', totalMs: 1 },
    });

    await expect(listPromise).resolves.toEqual({
      teams: [{ teamName: 'fresh-team', displayName: 'Fresh Team' }],
      diag: { op: 'listTeams', totalMs: 1 },
    });
  });
});
