import { describe, it, expect, vi } from 'vitest';
import {
  DEADLINE_ABORT_REASON,
  ABORT_CODE_CANCELLED,
  ABORT_CODE_DEADLINE,
  isDeadlineAbort,
  isCancellationAbortCode,
  forwardAbort,
  raceWithGrace,
} from './cancellation.js';

describe('cancellation reason plumbing', () => {
  describe('isDeadlineAbort', () => {
    it('is true for a signal aborted with the deadline reason', () => {
      const c = new AbortController();
      c.abort(DEADLINE_ABORT_REASON);
      expect(isDeadlineAbort(c.signal)).toBe(true);
    });

    it('is false for a plain (user) abort', () => {
      const c = new AbortController();
      c.abort();
      expect(isDeadlineAbort(c.signal)).toBe(false);
    });

    it('is false for a not-yet-aborted signal', () => {
      const c = new AbortController();
      expect(isDeadlineAbort(c.signal)).toBe(false);
    });

    it('is false for undefined/null', () => {
      expect(isDeadlineAbort(undefined)).toBe(false);
      expect(isDeadlineAbort(null)).toBe(false);
    });
  });

  describe('isCancellationAbortCode', () => {
    it('treats user-cancel and deadline codes as cancellations', () => {
      expect(isCancellationAbortCode(ABORT_CODE_CANCELLED)).toBe(true);
      expect(isCancellationAbortCode(ABORT_CODE_DEADLINE)).toBe(true);
    });

    it('does not treat genuine failures as cancellations', () => {
      expect(isCancellationAbortCode('llm_error')).toBe(false);
      expect(isCancellationAbortCode('agent_self_abort')).toBe(false);
      expect(isCancellationAbortCode('movement_abort')).toBe(false);
      expect(isCancellationAbortCode(undefined)).toBe(false);
    });
  });

  describe('forwardAbort', () => {
    it('aborts the local controller when the external signal fires, preserving reason', () => {
      const external = new AbortController();
      const local = new AbortController();
      const cleanup = forwardAbort(local, external.signal);
      expect(local.signal.aborted).toBe(false);
      external.abort(DEADLINE_ABORT_REASON);
      expect(local.signal.aborted).toBe(true);
      expect(local.signal.reason).toBe(DEADLINE_ABORT_REASON);
      cleanup();
    });

    it('aborts immediately when the external signal is already aborted', () => {
      const external = new AbortController();
      external.abort();
      const local = new AbortController();
      forwardAbort(local, external.signal);
      expect(local.signal.aborted).toBe(true);
    });

    it('is a no-op for an undefined source and returns a callable cleanup', () => {
      const local = new AbortController();
      const cleanup = forwardAbort(local, undefined);
      expect(local.signal.aborted).toBe(false);
      expect(() => cleanup()).not.toThrow();
    });

    it('cleanup detaches the listener so a later external abort does not propagate', () => {
      const external = new AbortController();
      const local = new AbortController();
      const cleanup = forwardAbort(local, external.signal);
      cleanup();
      external.abort();
      expect(local.signal.aborted).toBe(false);
    });
  });

  describe('raceWithGrace (②B hard-kill)', () => {
    it('resolves ok with the value when work settles and no abort fires', async () => {
      const c = new AbortController();
      await expect(raceWithGrace(Promise.resolve(42), c.signal, 1000)).resolves.toEqual({
        ok: true,
        value: 42,
      });
      expect(c.signal.aborted).toBe(false);
    });

    it('re-throws when work rejects before any abort (preserves the fail path)', async () => {
      const c = new AbortController();
      await expect(raceWithGrace(Promise.reject(new Error('boom')), c.signal, 1000)).rejects.toThrow(
        'boom',
      );
    });

    it('hard-kills with deadline=true when work hangs past the grace after a deadline abort', async () => {
      vi.useFakeTimers();
      try {
        const work = new Promise<string>(() => {}); // never settles
        const c = new AbortController();
        const p = raceWithGrace(work, c.signal, 15_000);
        c.abort(DEADLINE_ABORT_REASON);
        await vi.advanceTimersByTimeAsync(15_000);
        await expect(p).resolves.toEqual({ ok: false, deadline: true });
      } finally {
        vi.useRealTimers();
      }
    });

    it('hard-kills with deadline=false for a plain (user) cancel abort', async () => {
      vi.useFakeTimers();
      try {
        const work = new Promise<string>(() => {});
        const c = new AbortController();
        const p = raceWithGrace(work, c.signal, 1_000);
        c.abort();
        await vi.advanceTimersByTimeAsync(1_000);
        await expect(p).resolves.toEqual({ ok: false, deadline: false });
      } finally {
        vi.useRealTimers();
      }
    });

    it('lets a cooperative unwind win when work settles within the grace window', async () => {
      vi.useFakeTimers();
      try {
        let resolveWork!: (v: string) => void;
        const work = new Promise<string>((r) => { resolveWork = r; });
        const c = new AbortController();
        const p = raceWithGrace(work, c.signal, 15_000);
        c.abort(DEADLINE_ABORT_REASON);
        await vi.advanceTimersByTimeAsync(5_000); // still within grace
        resolveWork('done');
        await vi.advanceTimersByTimeAsync(0); // flush the microtask
        await expect(p).resolves.toEqual({ ok: true, value: 'done' });
        // Advancing past the original grace must NOT flip the already-settled race.
        await vi.advanceTimersByTimeAsync(20_000);
      } finally {
        vi.useRealTimers();
      }
    });

    it('arms the grace immediately when the signal is already aborted at call time', async () => {
      vi.useFakeTimers();
      try {
        const c = new AbortController();
        c.abort(DEADLINE_ABORT_REASON);
        const work = new Promise<string>(() => {});
        const p = raceWithGrace(work, c.signal, 500);
        await vi.advanceTimersByTimeAsync(500);
        await expect(p).resolves.toEqual({ ok: false, deadline: true });
      } finally {
        vi.useRealTimers();
      }
    });

    it('swallows a late rejection from the detached work after a hard-kill', async () => {
      vi.useFakeTimers();
      try {
        let rejectWork!: (e: unknown) => void;
        const work = new Promise<string>((_, rej) => { rejectWork = rej; });
        const c = new AbortController();
        const p = raceWithGrace(work, c.signal, 1_000);
        c.abort();
        await vi.advanceTimersByTimeAsync(1_000);
        await expect(p).resolves.toEqual({ ok: false, deadline: false });
        // The zombie work rejects afterwards — must not throw / resolve twice.
        rejectWork(new Error('late'));
        await expect(vi.advanceTimersByTimeAsync(0)).resolves.not.toThrow();
      } finally {
        vi.useRealTimers();
      }
    });
  });
});
