import IORedis from 'ioredis';
import { adaptIoredisForConnectRedis } from './ioredis-connect-redis-adapter';

/**
 * Unit-level tests that pin the **shape** of each adapted call —
 * `connect-redis` v9 invokes them in a specific way and any drift
 * breaks the session store silently. The behavioural round-trip is
 * exercised by `connect-redis`' own integration tests once its
 * `RedisStore` is wired in `session.module.spec.ts`.
 */
describe('adaptIoredisForConnectRedis', () => {
  function fakeClient(): {
    spy: Record<string, jest.Mock>;
    client: IORedis;
  } {
    const spy = {
      get: jest.fn().mockResolvedValue('v'),
      set: jest.fn().mockResolvedValue('OK'),
      expire: jest.fn().mockResolvedValue(1),
      del: jest.fn().mockResolvedValue(2),
      mget: jest.fn().mockResolvedValue(['v1', null, 'v3']),
      scanStream: jest.fn(),
    };
    // The adapter only touches the 6 methods above — we deliberately
    // don't construct a real `ioredis` instance to keep the unit
    // pure and avoid hitting the network.
    return { spy, client: spy as unknown as IORedis };
  }

  it('forwards get(key) unchanged', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    await adapter.get('sid');
    expect(spy['get']).toHaveBeenCalledWith('sid');
  });

  it('translates set with EX expiration to ioredis varargs', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    await adapter.set('sid', 'value', { expiration: { type: 'EX', value: 1800 } });
    expect(spy['set']).toHaveBeenCalledWith('sid', 'value', 'EX', 1800);
  });

  it('translates set without options to a plain ioredis set', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    await adapter.set('sid', 'value');
    expect(spy['set']).toHaveBeenCalledWith('sid', 'value');
  });

  it('forwards expire(key, seconds) unchanged', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    await adapter.expire('sid', 1800);
    expect(spy['expire']).toHaveBeenCalledWith('sid', 1800);
  });

  it('spreads del([keys]) into ioredis varargs', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    await adapter.del(['a', 'b', 'c']);
    expect(spy['del']).toHaveBeenCalledWith('a', 'b', 'c');
  });

  it('short-circuits del([]) without touching the client', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    const result = await adapter.del([]);
    expect(result).toBe(0);
    expect(spy['del']).not.toHaveBeenCalled();
  });

  it('spreads mGet([keys]) into ioredis varargs and returns the array', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    const result = await adapter.mGet(['a', 'b', 'c']);
    expect(spy['mget']).toHaveBeenCalledWith('a', 'b', 'c');
    expect(result).toEqual(['v1', null, 'v3']);
  });

  it('short-circuits mGet([]) without touching the client', async () => {
    const { spy, client } = fakeClient();
    const adapter = adaptIoredisForConnectRedis(client);
    expect(await adapter.mGet([])).toEqual([]);
    expect(spy['mget']).not.toHaveBeenCalled();
  });

  it('wraps scanStream into an async iterable of key arrays', async () => {
    const { spy, client } = fakeClient();
    const fakeStream = makeReadable([['session:a', 'session:b'], ['session:c']]);
    (spy['scanStream'] as jest.Mock).mockReturnValueOnce(fakeStream);

    const adapter = adaptIoredisForConnectRedis(client);
    const iterator = adapter.scanIterator({ MATCH: 'session:*', COUNT: 100 });

    const collected: string[][] = [];
    for await (const batch of iterator) {
      collected.push(batch);
    }

    expect(spy['scanStream']).toHaveBeenCalledWith({ match: 'session:*', count: 100 });
    expect(collected).toEqual([['session:a', 'session:b'], ['session:c']]);
  });
});

function makeReadable<T>(chunks: T[]): NodeJS.ReadableStream {
  // Minimal async iterable that mimics `ioredis.scanStream`'s
  // for-await contract — the adapter only needs `Symbol.asyncIterator`.
  return {
    async *[Symbol.asyncIterator]() {
      for (const chunk of chunks) {
        yield chunk;
      }
    },
  } as unknown as NodeJS.ReadableStream;
}
