import { describe, it, expect } from 'vitest';
import InterceptorManager from '../../../lib/core/InterceptorManager.js';

describe('core::InterceptorManager', () => {
  it('does not accumulate trailing tombstones during repeated use and eject', () => {
    const manager = new InterceptorManager();
    const persistent = manager.use(() => {});
    const transient = () => {};

    for (let i = 0; i < 10000; i++) {
      const id = manager.use(transient);
      manager.eject(id);
    }

    expect(Array.isArray(manager.handlers)).toBe(true);
    expect(manager.handlers).toHaveLength(1);

    manager.eject(persistent);

    expect(manager.handlers).toHaveLength(0);
  });

  it('preserves interior tombstones until trailing handlers are removed', () => {
    const manager = new InterceptorManager();
    const first = manager.use(() => {});
    const second = manager.use(() => {});
    const third = manager.use(() => {});

    manager.eject(second);

    expect(manager.handlers).toHaveLength(3);
    expect(manager.handlers[second]).toBeNull();

    manager.eject(third);

    expect(manager.handlers).toHaveLength(1);
    expect(manager.handlers[first]).not.toBeNull();
  });

  it('does not reuse an ejected interceptor ID after trimming its handler', () => {
    const manager = new InterceptorManager();
    const active = () => {};
    const staleId = manager.use(() => {});

    manager.eject(staleId);

    const activeId = manager.use(active);

    expect(activeId).not.toBe(staleId);

    manager.eject(staleId);

    expect(manager.handlers).toHaveLength(1);
    expect(manager.handlers[0].fulfilled).toBe(active);

    manager.eject(activeId);

    expect(manager.handlers).toHaveLength(0);
  });

  it('invalidates interceptor IDs when the handlers array is replaced', () => {
    const manager = new InterceptorManager();
    const stale = () => {};
    const active = () => {};
    const staleId = manager.use(stale);

    manager.handlers = [...manager.handlers];

    const activeId = manager.use(active);

    expect(activeId).not.toBe(staleId);

    manager.eject(staleId);

    expect(manager.handlers.map((handler) => handler.fulfilled)).toEqual([stale, active]);

    manager.eject(activeId);

    expect(manager.handlers.map((handler) => handler.fulfilled)).toEqual([stale]);
  });

  it('does not retain interceptor entries when the handlers array is cleared in place', () => {
    const manager = new InterceptorManager();
    const handler = () => {};

    for (let i = 0; i < 10000; i++) {
      manager.handlers.length = 0;
      manager.use(handler);
    }

    const internals = manager[Object.getOwnPropertySymbols(manager)[0]];

    expect(manager.handlers).toHaveLength(1);
    expect(internals.handlerEntries.size).toBe(1);
  });

  it('defers compaction while iterating so newly registered handlers are not visited', () => {
    const manager = new InterceptorManager();
    const first = () => {};
    const removed = () => {};
    const added = () => {};
    const visited = [];
    let mutated = false;

    manager.use(first);
    const removedId = manager.use(removed);

    manager.forEach((handler) => {
      visited.push(handler.fulfilled);

      if (!mutated && handler.fulfilled === first) {
        mutated = true;
        manager.eject(removedId);
        manager.use(added);
      }
    });

    expect(visited).toEqual([first]);

    const nextVisited = [];
    manager.forEach((handler) => nextVisited.push(handler.fulfilled));

    expect(nextVisited).toEqual([first, added]);
  });

  it('compacts handlers ejected during iteration after iteration completes', () => {
    const manager = new InterceptorManager();
    const first = () => {};
    const second = () => {};
    const visited = [];

    manager.use(first);
    const secondId = manager.use(second);

    manager.forEach((handler) => {
      visited.push(handler.fulfilled);

      if (handler.fulfilled === first) {
        manager.eject(secondId);
      }
    });

    expect(visited).toEqual([first]);
    expect(manager.handlers).toHaveLength(1);
  });

  it('preserves iteration over the current handlers when cleared during iteration', () => {
    const manager = new InterceptorManager();
    const first = () => {};
    const second = () => {};
    const visited = [];

    manager.use(first);
    manager.use(second);

    manager.forEach((handler) => {
      visited.push(handler.fulfilled);

      if (handler.fulfilled === first) {
        manager.clear();
      }
    });

    expect(visited).toEqual([first, second]);
    expect(manager.handlers).toHaveLength(0);
  });

  // Regression for #11114: `handlers` is public and `clear()` has always
  // tolerated a nullish value, but syncHandlerEntries read `.length` before
  // checking it, so every later request threw.
  describe.each([
    ['null', null],
    ['undefined', undefined],
  ])('with handlers set to %s', (_label, value) => {
    it('forEach is a no-op instead of throwing', () => {
      const manager = new InterceptorManager();
      manager.use(() => {});
      manager.handlers = value;

      const visited = [];
      expect(() => manager.forEach((h) => visited.push(h))).not.toThrow();
      expect(visited).toEqual([]);
    });

    it('stays usable across repeated forEach calls', () => {
      const manager = new InterceptorManager();
      manager.use(() => {});
      manager.handlers = value;

      // The second pass takes the `handlers === handlersRef` branch, which is
      // where the original report's stack trace landed.
      expect(() => {
        manager.forEach(() => {});
        manager.forEach(() => {});
      }).not.toThrow();
    });

    it('eject does not throw', () => {
      const manager = new InterceptorManager();
      const id = manager.use(() => {});
      manager.handlers = value;

      expect(() => manager.eject(id)).not.toThrow();
    });

    it('accepts and invokes a new handler', () => {
      const manager = new InterceptorManager();
      manager.use(() => {});
      manager.handlers = value;

      const fulfilled = () => {};
      manager.use(fulfilled);

      const visited = [];
      manager.forEach((h) => visited.push(h.fulfilled));
      expect(visited).toEqual([fulfilled]);
    });
  });
});
