import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
  registerShutdownHook,
  runShutdown,
  installSignalHandlers,
  __resetShutdownForTests,
  __getRegisteredHookCountForTests,
} from './shutdown.js';

describe('shutdown registry', () => {
  let exitCalls: number[];

  beforeEach(() => {
    exitCalls = [];
    __resetShutdownForTests({ exitFn: (code) => { exitCalls.push(code); } });
  });

  it('runs all registered hooks concurrently', async () => {
    const order: string[] = [];
    registerShutdownHook('slow', async () => {
      await new Promise((r) => setTimeout(r, 20));
      order.push('slow');
    });
    registerShutdownHook('fast', async () => {
      order.push('fast');
    });
    await runShutdown('SIGTERM');
    // Concurrent: fast resolves before slow even though it registered second.
    expect(order).toEqual(['fast', 'slow']);
    expect(exitCalls).toEqual([0]);
  });

  it('continues other hooks when one rejects', async () => {
    const ran: string[] = [];
    registerShutdownHook('broken', async () => { throw new Error('boom'); });
    registerShutdownHook('ok', async () => { ran.push('ok'); });
    await runShutdown('SIGTERM');
    expect(ran).toEqual(['ok']);
    // exit still called once with 0 — a hook failure must not block exit.
    expect(exitCalls).toEqual([0]);
  });

  it('is idempotent across repeated invocations', async () => {
    let hookCalls = 0;
    registerShutdownHook('counter', async () => { hookCalls++; });
    await runShutdown('SIGTERM');
    await runShutdown('SIGINT');
    expect(hookCalls).toBe(1);
    expect(exitCalls).toEqual([0]);
  });

  it('treats sync hooks identically to async hooks', async () => {
    const ran: string[] = [];
    registerShutdownHook('sync', () => { ran.push('sync'); });
    registerShutdownHook('async', async () => { ran.push('async'); });
    await runShutdown('SIGTERM');
    expect(ran.sort()).toEqual(['async', 'sync']);
    expect(exitCalls).toEqual([0]);
  });

  it('exits even when no hooks are registered', async () => {
    await runShutdown('SIGTERM');
    expect(exitCalls).toEqual([0]);
  });

  it('counts each registerShutdownHook call', () => {
    registerShutdownHook('a', () => {});
    registerShutdownHook('b', () => {});
    expect(__getRegisteredHookCountForTests()).toBe(2);
  });

  it('installSignalHandlers installs exactly one listener per signal even when called twice', () => {
    // Snapshot existing listener counts so we don't false-positive on
    // listeners installed by the test harness itself.
    const sigtermBefore = process.listenerCount('SIGTERM');
    const sigintBefore = process.listenerCount('SIGINT');
    try {
      installSignalHandlers();
      installSignalHandlers();
      installSignalHandlers();
      expect(process.listenerCount('SIGTERM') - sigtermBefore).toBe(1);
      expect(process.listenerCount('SIGINT') - sigintBefore).toBe(1);
    } finally {
      // Remove the listeners we just installed so they don't leak
      // into subsequent tests (vitest shares the process). We can't
      // grab a handle to the wrapper, so removeAllListeners (capped
      // to the count we added) is the safe option.
      const sigtermNow = process.listenerCount('SIGTERM');
      const sigintNow = process.listenerCount('SIGINT');
      const sigtermAdded = sigtermNow - sigtermBefore;
      const sigintAdded = sigintNow - sigintBefore;
      const sigtermListeners = process.listeners('SIGTERM');
      const sigintListeners = process.listeners('SIGINT');
      for (let i = sigtermListeners.length - sigtermAdded; i < sigtermListeners.length; i++) {
        process.removeListener('SIGTERM', sigtermListeners[i]! as () => void);
      }
      for (let i = sigintListeners.length - sigintAdded; i < sigintListeners.length; i++) {
        process.removeListener('SIGINT', sigintListeners[i]! as () => void);
      }
    }
  });

  it('logs and surfaces sync throws as rejections', async () => {
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    registerShutdownHook('syncthrow', () => { throw new Error('sync-boom'); });
    await runShutdown('SIGTERM');
    expect(exitCalls).toEqual([0]);
    warn.mockRestore();
  });
});
