import { describe, expect, test } from 'vitest';

type LifecycleRunner = (args: string[]) => boolean;
type LifecycleRegister = (api: unknown, runner: LifecycleRunner) => void;
type Handler = (event: Record<string, unknown>, ctx: Record<string, unknown>) => unknown;

const lifecycleModuleUrl = new URL('../../framework/runtime/pi/lease-lifecycle.ts', import.meta.url)
  .href;
const { registerLeaseLifecycleHooks } = (await import(lifecycleModuleUrl)) as {
  registerLeaseLifecycleHooks: LifecycleRegister;
};

function fakePi() {
  const handlers = new Map<string, Handler[]>();
  return {
    handlers,
    api: {
      on(event: string, handler: Handler) {
        handlers.set(event, [...(handlers.get(event) ?? []), handler]);
      },
    },
    async emit(event: string, value: Record<string, unknown> = {}) {
      const results = [];
      for (const handler of handlers.get(event) ?? []) {
        results.push(await handler(value, {}));
      }
      return results;
    },
  };
}

describe('Pi compaction and runtime-generation lease lifecycle', () => {
  test('pre-compaction and first post-compaction context independently revoke', async () => {
    const pi = fakePi();
    const calls: string[][] = [];
    registerLeaseLifecycleHooks(pi.api as never, (args) => {
      calls.push(args);
      return true;
    });

    expect(await pi.emit('session_before_compact', { reason: 'threshold' })).toEqual([undefined]);
    expect(calls.at(-1)).toEqual([
      '--runtime',
      'pi',
      '--reason',
      'pi-session-before-compact:threshold',
    ]);

    await pi.emit('session_compact', { reason: 'threshold' });
    await pi.emit('context', { messages: [] });
    expect(calls.at(-1)).toEqual([
      '--runtime',
      'pi',
      '--reason',
      'pi-context-after-compact:threshold',
    ]);
    const afterFirstContext = calls.length;
    await pi.emit('context', { messages: [] });
    expect(calls).toHaveLength(afterFirstContext);
  });

  test.each(['reload', 'new', 'resume', 'fork'])(
    '%s bumps generation before reuse',
    async (reason) => {
      const pi = fakePi();
      const calls: string[][] = [];
      registerLeaseLifecycleHooks(pi.api as never, (args) => {
        calls.push(args);
        return true;
      });

      await pi.emit('session_start', { reason });
      expect(calls).toEqual([
        ['--runtime', 'pi', '--reason', `pi-session-start:${reason}`, '--bump-generation'],
      ]);
    },
  );

  test('startup leaves the launcher generation intact and tools locally open', async () => {
    const pi = fakePi();
    const calls: string[][] = [];
    registerLeaseLifecycleHooks(pi.api as never, (args) => {
      calls.push(args);
      return true;
    });

    await pi.emit('session_start', { reason: 'startup' });
    await pi.emit('session_start');
    expect(calls).toEqual([]);
    expect(await pi.emit('tool_call', { toolName: 'bash' })).toEqual([undefined]);
  });

  test('failed post-compaction revoke blocks tools until context retries successfully', async () => {
    const pi = fakePi();
    const outcomes = [false, true];
    registerLeaseLifecycleHooks(pi.api as never, () => outcomes.shift() ?? true);

    await pi.emit('session_compact');
    await pi.emit('context');
    expect(await pi.emit('tool_call', { toolName: 'bash' })).toEqual([
      {
        block: true,
        reason: expect.stringContaining('lease lifecycle revoke failed'),
      },
    ]);

    await pi.emit('context');
    expect(await pi.emit('tool_call', { toolName: 'bash' })).toEqual([undefined]);
  });

  test('failed lifecycle revoke cancels compaction and closes later tool calls', async () => {
    const pi = fakePi();
    registerLeaseLifecycleHooks(pi.api as never, () => false);

    const compact = await pi.emit('session_before_compact', { reason: 'manual' });
    expect(compact).toEqual([{ cancel: true }]);

    await pi.emit('session_start', { reason: 'resume' });
    const tool = await pi.emit('tool_call', { toolName: 'bash' });
    expect(tool).toEqual([
      {
        block: true,
        reason: expect.stringContaining('lease lifecycle revoke failed'),
      },
    ]);
  });
});
