import { randomBytes } from 'node:crypto';
import { ConfidentialClientApplication } from '@azure/msal-node';
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 { UsersModule } from '../users/users.module';
import { AuthModule } from './auth.module';
import { ENTRA_CONFIG, type EntraConfig } from './entra-config.token';
import { MSAL_CLIENT } from './msal-client.token';

// Production app makes PrismaService + ClsService globally available
// via `PrismaModule.forRoot({ isGlobal: true })` and ObservabilityModule.
// In a slice-of-graph spec we stub both via a tiny @Global() module
// so AuditModule's transitive resolution of AuditWriter succeeds
// without booting Prisma or nestjs-cls for real.
@Global()
@Module({
  providers: [
    { provide: PrismaService, useValue: {} },
    { provide: ClsService, useValue: { get: () => undefined } },
  ],
  exports: [PrismaService, ClsService],
})
class TestStubsModule {}

// AuthModule imports SessionModule + (transitively, via AuditModule
// being @Global) needs the AuditWriter providers. compile() walks
// the full import graph so the spec has to satisfy every validator
// at boot time, including:
//   - SessionModule's RedisModule (REDIS_URL)
//   - SessionModule's middleware factory (SESSION_SECRET +
//     SESSION_ENCRYPTION_KEY)
//   - AuditModule's HashUserIdService (LOG_USER_ID_SALT)
// Lesson from PR #115/#116: CI starts with a clean env, masking the
// failure that nx-loaded apps/portal-bff/.env hides locally.
const VALID = {
  ENTRA_INSTANCE_URL: 'https://login.microsoftonline.com/',
  ENTRA_TENANT_ID: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
  ENTRA_CLIENT_ID: '11111111-2222-3333-4444-555555555555',
  ENTRA_CLIENT_SECRET: 's3cret-value-from-entra',
  ENTRA_REDIRECT_URI: 'http://localhost:3000/api/auth/callback',
  ENTRA_POST_LOGOUT_REDIRECT_URI: 'http://localhost:4200/',
  ENTRA_ADMIN_REDIRECT_URI: 'http://localhost:3000/api/admin/auth/callback',
  ENTRA_ADMIN_POST_LOGOUT_REDIRECT_URI: 'http://localhost:4300/',
  // Well-formed but unreachable Redis URL — `ioredis` opens the
  // socket lazily so the module compiles without any network access.
  REDIS_URL: 'redis://default:test-pass@127.0.0.1:65535/0',
  SESSION_SECRET: randomBytes(32).toString('base64url'),
  SESSION_ENCRYPTION_KEY: randomBytes(32).toString('base64url'),
  LOG_USER_ID_SALT: randomBytes(32).toString('base64url'),
  // PrismaModule reads this at boot; an unreachable URL is fine in
  // tests because we never issue queries.
  DATABASE_URL: 'postgresql://test:test@127.0.0.1:65535/test',
};

// AuthModule's MSAL_CLIENT factory injects the Pino-backed `Logger`
// (which the production app supplies through `ObservabilityModule`).
// `AuditModule` is `@Global()` in production via AppModule; here we
// import it explicitly because we compile a slice of the graph.
// AuditWriter's deps (PrismaService, ClsService) are stubbed — the
// spec doesn't need a working DB, only that the wiring resolves.
async function compile() {
  return Test.createTestingModule({
    imports: [
      LoggerModule.forRoot({ pinoHttp: { level: 'silent' } }),
      TestStubsModule,
      AuditModule,
      UsersModule,
      AuthModule,
    ],
  }).compile();
}

describe('AuthModule', () => {
  const originalEnv: Record<string, string | undefined> = {};
  let ref: Awaited<ReturnType<typeof compile>> | undefined;

  beforeEach(() => {
    for (const key of Object.keys(VALID) as Array<keyof typeof VALID>) {
      originalEnv[key] = process.env[key];
      process.env[key] = VALID[key];
    }
  });

  afterEach(async () => {
    // SessionModule (transitive) opens an `ioredis` client at module
    // init; explicitly disconnect so Jest doesn't hang on its
    // reconnect timer between tests.
    if (ref) {
      const redis = ref.get<{ disconnect: () => void }>('REDIS_CLIENT');
      redis.disconnect();
      await ref.close();
      ref = undefined;
    }
    for (const key of Object.keys(VALID) as Array<keyof typeof VALID>) {
      const saved = originalEnv[key];
      if (saved === undefined) {
        delete process.env[key];
      } else {
        process.env[key] = saved;
      }
    }
  });

  it('provides EntraConfig via the ENTRA_CONFIG token', async () => {
    ref = await compile();
    const config = ref.get<EntraConfig>(ENTRA_CONFIG);
    expect(config.clientId).toBe(VALID.ENTRA_CLIENT_ID);
    expect(config.authority).toBe(`${VALID.ENTRA_INSTANCE_URL}${VALID.ENTRA_TENANT_ID}`);
  });

  it('provides a ConfidentialClientApplication via the MSAL_CLIENT token', async () => {
    ref = await compile();
    const client = ref.get<ConfidentialClientApplication>(MSAL_CLIENT);
    expect(client).toBeInstanceOf(ConfidentialClientApplication);
  });

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