import { EventEmitter } from 'events';
import {
  DEFAULT_RELEASE_FAILURE_THRESHOLD,
  OWNER_SELF_RELEASE_EXIT_CODE,
  releaseOwnerAfterStartupFailure,
  releaseOwnerIfChromeUnreachable,
  wireOwnerSelfRelease,
} from '../src/chrome/owner-self-release';
import { DebugPortTimeoutError } from '../src/chrome/launcher-debug-port';

const flush = () => new Promise((resolve) => setImmediate(resolve));

describe('owner self-release (#1474)', () => {
  function setup(over: {
    releaseLock?: () => void;
    probeChromeReachable?: () => Promise<boolean>;
    failureThreshold?: number;
  } = {}) {
    const watchdog = new EventEmitter();
    const releaseLock = over.releaseLock ?? jest.fn();
    const exit = jest.fn();
    const log = jest.fn();
    const probeChromeReachable = over.probeChromeReachable ?? jest.fn(async () => false);
    wireOwnerSelfRelease(watchdog, {
      releaseLock,
      exit,
      log,
      probeChromeReachable,
      failureThreshold: over.failureThreshold,
    });
    return { watchdog, releaseLock, exit, log, probeChromeReachable };
  }

  test('releases + exits after threshold consecutive failures with CDP unreachable', async () => {
    const { watchdog, releaseLock, exit, probeChromeReachable } = setup();

    for (let i = 0; i < DEFAULT_RELEASE_FAILURE_THRESHOLD; i++) {
      watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    }
    await flush();

    expect(probeChromeReachable).toHaveBeenCalled();
    expect(releaseLock).toHaveBeenCalledTimes(1);
    expect(exit).toHaveBeenCalledWith(OWNER_SELF_RELEASE_EXIT_CODE);
    expect(OWNER_SELF_RELEASE_EXIT_CODE).not.toBe(0);
  });

  test('does NOT tear down a Chrome that is actually reachable (Codex P2)', async () => {
    const { watchdog, releaseLock, exit } = setup({ probeChromeReachable: jest.fn(async () => true) });

    for (let i = 0; i < DEFAULT_RELEASE_FAILURE_THRESHOLD; i++) {
      watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    }
    await flush();

    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('watchdog-exhausted (emitted after a SUCCESSFUL relaunch) never self-releases', async () => {
    const { watchdog, releaseLock, exit, probeChromeReachable } = setup();

    watchdog.emit('watchdog-exhausted', { count: 10, timestamp: 0 });
    await flush();

    expect(probeChromeReachable).not.toHaveBeenCalled();
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('a single transient relaunch-failed does not act (below threshold)', async () => {
    const { watchdog, releaseLock, exit, probeChromeReachable } = setup({ failureThreshold: 2 });

    watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    await flush();

    expect(probeChromeReachable).not.toHaveBeenCalled();
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('a successful relaunch between failures resets the counter', async () => {
    const { watchdog, releaseLock, exit } = setup({ failureThreshold: 2 });

    watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    watchdog.emit('chrome-relaunched', { pid: 1, timestamp: 0 }); // recovery → reset
    watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    await flush();

    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('aborts self-release if Chrome recovers while the probe is in flight', async () => {
    let resolveProbe!: (v: boolean) => void;
    const probeChromeReachable = jest.fn(
      () => new Promise<boolean>((resolve) => { resolveProbe = resolve; }),
    );
    const { watchdog, releaseLock, exit } = setup({ probeChromeReachable });

    for (let i = 0; i < DEFAULT_RELEASE_FAILURE_THRESHOLD; i++) {
      watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    }
    // Probe is in flight. Chrome recovers, then the probe resolves a STALE false
    // (the new Chrome has not bound its debug port yet). Must NOT tear it down.
    watchdog.emit('chrome-relaunched', { pid: 1, timestamp: 0 });
    resolveProbe(false);
    await flush();

    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('after an inconclusive probe, one more failure re-probes and can release', async () => {
    let calls = 0;
    const probeChromeReachable = jest.fn(async () => {
      calls += 1;
      if (calls === 1) throw new Error('probe exploded'); // inconclusive
      return false; // second probe: confirmed unreachable
    });
    const { watchdog, releaseLock, exit } = setup({ probeChromeReachable, failureThreshold: 2 });

    watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 }); // reaches threshold → inconclusive
    await flush();
    expect(releaseLock).not.toHaveBeenCalled();

    watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 }); // steps back to threshold → re-probes
    await flush();
    expect(probeChromeReachable).toHaveBeenCalledTimes(2);
    expect(releaseLock).toHaveBeenCalledTimes(1);
    expect(exit).toHaveBeenCalledWith(OWNER_SELF_RELEASE_EXIT_CODE);
  });

  test('does NOT surrender ownership on a recoverable chrome-died', async () => {
    const { watchdog, releaseLock, exit } = setup();

    watchdog.emit('chrome-died', { pid: 123, timestamp: 0 });
    await flush();

    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('stays up when the reachability probe itself errors (inconclusive)', async () => {
    const probeChromeReachable = jest.fn(async () => {
      throw new Error('probe exploded');
    });
    const { watchdog, releaseLock, exit, log } = setup({ probeChromeReachable });

    for (let i = 0; i < DEFAULT_RELEASE_FAILURE_THRESHOLD; i++) {
      watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    }
    await flush();

    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
    expect(log).toHaveBeenCalledWith(expect.stringContaining('probe errored'));
  });

  test('still exits even if lock release throws (best-effort)', async () => {
    const releaseLock = jest.fn(() => {
      throw new Error('unlink failed');
    });
    const { watchdog, exit, log } = setup({ releaseLock });

    for (let i = 0; i < DEFAULT_RELEASE_FAILURE_THRESHOLD; i++) {
      watchdog.emit('relaunch-failed', { error: new Error('boom'), timestamp: 0 });
    }
    await flush();

    expect(exit).toHaveBeenCalledWith(OWNER_SELF_RELEASE_EXIT_CODE);
    expect(log).toHaveBeenCalledWith(expect.stringContaining('release failed'));
  });

  test('primitive releases only after a confirming unreachable probe', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();
    const result = await releaseOwnerIfChromeUnreachable({
      releaseLock,
      exit,
      log: jest.fn(),
      probeChromeReachable: jest.fn(async () => false),
    }, { trigger: 'startup connect failure' });

    expect(result).toBe('released');
    expect(releaseLock).toHaveBeenCalledTimes(1);
    expect(exit).toHaveBeenCalledWith(OWNER_SELF_RELEASE_EXIT_CODE);
  });

  test('primitive keeps ownership when Chrome is reachable', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();
    const onKept = jest.fn();
    const result = await releaseOwnerIfChromeUnreachable({
      releaseLock,
      exit,
      log: jest.fn(),
      probeChromeReachable: jest.fn(async () => true),
    }, { trigger: 'startup connect failure', onKept });

    expect(result).toBe('reachable');
    expect(onKept).toHaveBeenCalledTimes(1);
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('primitive aborts release when a recovery race makes the probe stale', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();
    const onKept = jest.fn();
    const result = await releaseOwnerIfChromeUnreachable({
      releaseLock,
      exit,
      log: jest.fn(),
      probeChromeReachable: jest.fn(async () => false),
    }, { trigger: 'startup connect failure', abortIfRecovered: () => true, onKept });

    expect(result).toBe('aborted');
    expect(onKept).toHaveBeenCalledTimes(1);
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('primitive treats probe errors as inconclusive and keeps ownership', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();
    const onInconclusive = jest.fn();
    const result = await releaseOwnerIfChromeUnreachable({
      releaseLock,
      exit,
      log: jest.fn(),
      probeChromeReachable: jest.fn(async () => { throw new Error('probe failed'); }),
    }, { trigger: 'startup connect failure', onInconclusive });

    expect(result).toBe('inconclusive');
    expect(onInconclusive).toHaveBeenCalledTimes(1);
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('startup failure helper logs the startup failure and uses the same release gate', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();
    const log = jest.fn();

    const result = await releaseOwnerAfterStartupFailure(new Error('launch timeout'), {
      releaseLock,
      exit,
      log,
      probeChromeReachable: jest.fn(async () => false),
    });

    expect(result).toBe('released');
    expect(log).toHaveBeenCalledWith(expect.stringContaining('Startup Chrome launch failed'));
    expect(releaseLock).toHaveBeenCalledTimes(1);
    expect(exit).toHaveBeenCalledWith(OWNER_SELF_RELEASE_EXIT_CODE);
  });

  test('startup failure helper keeps ownership when CDP is reachable', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();

    const result = await releaseOwnerAfterStartupFailure(new Error('launch timeout'), {
      releaseLock,
      exit,
      log: jest.fn(),
      probeChromeReachable: jest.fn(async () => true),
    });

    expect(result).toBe('reachable');
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('startup failure helper treats probe errors as inconclusive', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();

    const result = await releaseOwnerAfterStartupFailure(new Error('launch timeout'), {
      releaseLock,
      exit,
      log: jest.fn(),
      probeChromeReachable: jest.fn(async () => { throw new Error('probe failed'); }),
    });

    expect(result).toBe('inconclusive');
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });

  test('startup failure helper treats debug-port timeout as inconclusive without probing', async () => {
    const releaseLock = jest.fn();
    const exit = jest.fn();
    const probeChromeReachable = jest.fn(async () => false);

    const result = await releaseOwnerAfterStartupFailure(new DebugPortTimeoutError(9222, 1000, 3), {
      releaseLock,
      exit,
      log: jest.fn(),
      probeChromeReachable,
    });

    expect(result).toBe('inconclusive');
    expect(probeChromeReachable).not.toHaveBeenCalled();
    expect(releaseLock).not.toHaveBeenCalled();
    expect(exit).not.toHaveBeenCalled();
  });
});
