import { randomBytes } from 'node:crypto';
import { Global, Module } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { ClsService } from 'nestjs-cls';
import { LoggerModule } from 'nestjs-pino';
import { PrismaService } from 'nestjs-prisma';
import { AuditModule } from '../audit/audit.module';
import { SessionModule } from './session.module';
import { SESSION_MIDDLEWARE, type RequestHandler } from './session.token';

@Global()
@Module({
  providers: [
    { provide: PrismaService, useValue: {} },
    { provide: ClsService, useValue: { get: () => undefined } },
  ],
  exports: [PrismaService, ClsService],
})
class TestStubsModule {}

const STRONG_KEY = randomBytes(32).toString('base64url');
const STRONG_SECRET = randomBytes(32).toString('base64url');

interface OriginalEnv {
  REDIS_URL: string | undefined;
  SESSION_SECRET: string | undefined;
  SESSION_ENCRYPTION_KEY: string | undefined;
  LOG_USER_ID_SALT: string | undefined;
  NODE_ENV: string | undefined;
}

// SessionModule's absolute-timeout middleware factory now depends
// on `AuditWriter` (provided by AuditModule via @Global() in
// production). The spec compiles a slice of the graph so we import
// AuditModule explicitly and stub its DB-touching deps.
async function compile() {
  return Test.createTestingModule({
    imports: [
      LoggerModule.forRoot({ pinoHttp: { level: 'silent' } }),
      TestStubsModule,
      AuditModule,
      SessionModule,
    ],
  }).compile();
}

describe('SessionModule', () => {
  const original: OriginalEnv = {
    REDIS_URL: process.env['REDIS_URL'],
    SESSION_SECRET: process.env['SESSION_SECRET'],
    SESSION_ENCRYPTION_KEY: process.env['SESSION_ENCRYPTION_KEY'],
    LOG_USER_ID_SALT: process.env['LOG_USER_ID_SALT'],
    NODE_ENV: process.env['NODE_ENV'],
  };

  beforeEach(() => {
    // Well-formed but unreachable URL — `ioredis` opens its socket
    // lazily so the module compiles without any network access.
    process.env['REDIS_URL'] = 'redis://default:test-pass@127.0.0.1:65535/0';
    process.env['SESSION_SECRET'] = STRONG_SECRET;
    process.env['SESSION_ENCRYPTION_KEY'] = STRONG_KEY;
    process.env['LOG_USER_ID_SALT'] = randomBytes(32).toString('base64url');
    process.env['NODE_ENV'] = 'development';
  });

  afterEach(() => {
    restore('REDIS_URL', original.REDIS_URL);
    restore('SESSION_SECRET', original.SESSION_SECRET);
    restore('SESSION_ENCRYPTION_KEY', original.SESSION_ENCRYPTION_KEY);
    restore('LOG_USER_ID_SALT', original.LOG_USER_ID_SALT);
    restore('NODE_ENV', original.NODE_ENV);
  });

  it('provides a request handler via SESSION_MIDDLEWARE', async () => {
    const ref = await compile();
    try {
      const middleware = ref.get<RequestHandler>(SESSION_MIDDLEWARE);
      expect(typeof middleware).toBe('function');
      // Express request handlers always declare 3 named params
      // (req, res, next) — useful smoke check that the factory
      // returned the expected shape rather than a `Store` or
      // a configured options object.
      expect(middleware.length).toBe(3);
    } finally {
      await disposeRedis(ref);
      await ref.close();
    }
  });

  it('fails to compile when SESSION_ENCRYPTION_KEY is missing', async () => {
    delete process.env['SESSION_ENCRYPTION_KEY'];
    await expect(compile()).rejects.toThrow(/SESSION_ENCRYPTION_KEY is not set/);
  });

  it('fails to compile when SESSION_SECRET is missing', async () => {
    delete process.env['SESSION_SECRET'];
    await expect(compile()).rejects.toThrow(/SESSION_SECRET is not set/);
  });
});

function restore(name: string, value: string | undefined): void {
  if (value === undefined) {
    delete process.env[name];
  } else {
    process.env[name] = value;
  }
}

async function disposeRedis(ref: Awaited<ReturnType<typeof compile>>): Promise<void> {
  // `RedisModule` keeps an `ioredis` client open; explicitly
  // disconnect so Jest doesn't hang on its reconnect timer.
  const redisToken = 'REDIS_CLIENT';
  const client = ref.get<{ disconnect: () => void }>(redisToken);
  client.disconnect();
}
