import { Test } from '@nestjs/testing';
import { LoggerModule, Logger } from 'nestjs-pino';
import { REDIS_CLIENT } from '../redis/redis.token';
import { UserSessionIndexService } from './user-session-index.service';

interface RedisStub {
  sadd: jest.Mock;
  srem: jest.Mock;
  smembers: jest.Mock;
}

function makeRedisStub(): RedisStub {
  return {
    sadd: jest.fn().mockResolvedValue(1),
    srem: jest.fn().mockResolvedValue(1),
    smembers: jest.fn().mockResolvedValue([]),
  };
}

async function setup(redis: RedisStub) {
  const ref = await Test.createTestingModule({
    imports: [LoggerModule.forRoot({ pinoHttp: { level: 'silent' } })],
    providers: [UserSessionIndexService, { provide: REDIS_CLIENT, useValue: redis }],
  }).compile();
  return {
    service: ref.get(UserSessionIndexService),
    logger: ref.get(Logger) as Logger & { warn: jest.Mock },
  };
}

describe('UserSessionIndexService', () => {
  describe('add()', () => {
    it('SADDs the session id to user_sessions:<userId>', async () => {
      const redis = makeRedisStub();
      const { service } = await setup(redis);
      await service.add('user-oid', 'session-abc');
      expect(redis.sadd).toHaveBeenCalledWith('user_sessions:user-oid', 'session-abc');
    });

    it('swallows redis failures (best-effort) and logs a warning', async () => {
      const redis = makeRedisStub();
      redis.sadd.mockRejectedValueOnce(new Error('redis unreachable'));
      const { service, logger } = await setup(redis);
      const warnSpy = jest.spyOn(logger, 'warn');
      await expect(service.add('user-oid', 'session-abc')).resolves.toBeUndefined();
      expect(warnSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          event: 'user_session_index.add_failed',
          userId: 'user-oid',
          message: 'redis unreachable',
        }),
        'UserSessionIndex',
      );
    });
  });

  describe('remove()', () => {
    it('SREMs the session id from user_sessions:<userId>', async () => {
      const redis = makeRedisStub();
      const { service } = await setup(redis);
      await service.remove('user-oid', 'session-abc');
      expect(redis.srem).toHaveBeenCalledWith('user_sessions:user-oid', 'session-abc');
    });

    it('swallows redis failures and logs a warning', async () => {
      const redis = makeRedisStub();
      redis.srem.mockRejectedValueOnce(new Error('boom'));
      const { service, logger } = await setup(redis);
      const warnSpy = jest.spyOn(logger, 'warn');
      await service.remove('user-oid', 'session-abc');
      expect(warnSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          event: 'user_session_index.remove_failed',
          userId: 'user-oid',
        }),
        'UserSessionIndex',
      );
    });
  });

  describe('list()', () => {
    it('SMEMBERS the user_sessions set and returns the raw ids (orphans included)', async () => {
      const redis = makeRedisStub();
      redis.smembers.mockResolvedValueOnce(['session-a', 'session-b', 'orphan']);
      const { service } = await setup(redis);
      const ids = await service.list('user-oid');
      expect(redis.smembers).toHaveBeenCalledWith('user_sessions:user-oid');
      expect(ids).toEqual(['session-a', 'session-b', 'orphan']);
    });
  });
});
