/**
 * Integration smoke tests for AuthenticationModule via NestJS TestingModule.
 *
 * Goals:
 * - Verify that with OAUTH_STRATEGY=none the module compiles WITHOUT key files present.
 * - Verify that JWT_SIGNER resolves to JwtSignerAdapter (dormant in none mode).
 * - Verify that the module's imports: array remains unchanged (leaf — no OAuthModule/IamModule).
 * - (scaffold scenario) Verify that AuthenticationModule bootstraps with the anonymous
 *   ACCOUNT_LOADER wiring that the packager injects — no IAM module, no override needed.
 *
 * The AuroraHubConsumerAdapter imports jwks-rsa (ESM-only); we mock it before imports run
 * to avoid the known ts-jest/ESM limitation (catalyst-backend-testing SKILL.md).
 */

// Mock the ESM-importing adapter BEFORE any module imports run.
jest.mock(
  '@aurora/modules/authentication/infrastructure/strategies/aurora-hub/aurora-hub-consumer.adapter',
  () => ({
    AuroraHubConsumerAdapter: class AuroraHubConsumerAdapterMock {
      getPassportOptions() {
        return {
          jwtFromRequest: () => null,
          secretOrKey: 'mock',
        };
      }
      validate(): Promise<Record<string, never>> {
        return Promise.resolve({});
      }
    },
  }),
);

import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { Test } from '@nestjs/testing';
import { AuthenticationModule } from './authentication.module';
import { ACCOUNT_LOADER, HUB_SESSION_MANAGER, JWT_SIGNER } from './domain/ports';
import { AnonymousAccountLoaderAdapter } from './infrastructure/strategies/local-provider/anonymous-account-loader.adapter';
import { JwtSignerAdapter } from './infrastructure/strategies/local-provider/jwt-signer.adapter';
import { LocalProviderAdapter } from './infrastructure/strategies/local-provider/local-provider.adapter';

// ── Source-repo tests (with explicit ACCOUNT_LOADER override) ─────────────────
// These represent the full-install (with IAM bridge) scenario.

describe('AuthenticationModule — none mode integration', () => {
  function buildModule(): ReturnType<typeof Test.createTestingModule> {
    return (
      Test.createTestingModule({
        imports: [
          ConfigModule.forRoot({ isGlobal: true }),
          // CacheModule provides CACHE_MANAGER which LocalProviderAdapter injects
          CacheModule.register({ isGlobal: true }),
          PassportModule.register({ defaultStrategy: 'jwt' }),
          AuthenticationModule,
        ],
      })
        .overrideProvider(ACCOUNT_LOADER)
        .useValue({ loadById: jest.fn() })
        // Override LocalProviderAdapter so it doesn't need CACHE_MANAGER from a non-global module
        .overrideProvider(LocalProviderAdapter)
        .useValue({
          getPassportOptions: () => ({ jwtFromRequest: () => null, secretOrKey: 'mock' }),
          validate: jest.fn(),
        })
    );
  }

  it('compiles successfully with OAUTH_STRATEGY=none and no key files present', async () => {
    process.env.OAUTH_STRATEGY = 'none';

    const builder = buildModule();
    const module = await builder.compile();

    expect(module).toBeDefined();

    delete process.env.OAUTH_STRATEGY;
  });

  it('JWT_SIGNER resolves to JwtSignerAdapter (dormant signer) when mode is none', async () => {
    process.env.OAUTH_STRATEGY = 'none';

    const builder = buildModule();
    const module = await builder.compile();

    const signer = module.get(JWT_SIGNER);
    expect(signer).toBeInstanceOf(JwtSignerAdapter);

    delete process.env.OAUTH_STRATEGY;
  });

  it('HUB_SESSION_MANAGER resolves to JwtSignerAdapter (same adapter, separate token)', async () => {
    process.env.OAUTH_STRATEGY = 'none';

    const builder = buildModule();
    const module = await builder.compile();

    const hubManager = module.get(HUB_SESSION_MANAGER);
    expect(hubManager).toBeInstanceOf(JwtSignerAdapter);

    delete process.env.OAUTH_STRATEGY;
  });

  it('compiles successfully with OAUTH_STRATEGY=aurora-hub and no key files present', async () => {
    // Reproduces the production crash: a satellite app sets OAUTH_STRATEGY=aurora-hub and
    // provides no local RSA key files. The module MUST compile without ENOENT errors.
    process.env.OAUTH_STRATEGY = 'aurora-hub';
    // Point the key paths at guaranteed-missing files so this test stays discriminating even
    // in a dev env where ./.keys/ exists: reverting the dormant guard makes JwtSignerAdapter's
    // constructor readFileSync the private key → ENOENT, failing module compilation.
    process.env.OAUTH_PRIVATE_KEY_PATH = '/nonexistent/aurora-hub-test-private.key';
    process.env.OAUTH_PUBLIC_KEY_PATH = '/nonexistent/aurora-hub-test-public.key';

    const builder = buildModule();
    const module = await builder.compile();

    expect(module).toBeDefined();

    delete process.env.OAUTH_STRATEGY;
    delete process.env.OAUTH_PRIVATE_KEY_PATH;
    delete process.env.OAUTH_PUBLIC_KEY_PATH;
  });

  it('AuthenticationModule imports: array does not include OAuthModule, IamModule, or any @api/* module', () => {
    const metadata = Reflect.getMetadata('imports', AuthenticationModule) as unknown[] | undefined;
    const importNames = (metadata ?? []).map((m: unknown) => {
      if (typeof m === 'function') return (m as { name: string }).name;
      if (typeof m === 'object' && m !== null && 'module' in m) {
        return (m as { module: { name: string } }).module.name;
      }
      return null;
    });

    expect(importNames).not.toContain('OAuthModule');
    expect(importNames).not.toContain('IamModule');
  });
});

// ── Scaffold scenario (mirrors published scaffold after packager step) ─────────
// Verifies the WIRING that cleanAuthenticationModule() will inject into the scaffold.
//
// Why this describe block uses a scaffold-mirror module, not overrideProvider():
//   `overrideProvider` at the TestingModule root provides a token globally, but NestJS
//   resolves LocalProviderAdapter's ACCOUNT_LOADER dependency within the AuthenticationModule
//   context — a child module that has no ACCOUNT_LOADER in its own providers. The only way
//   to satisfy it without running the actual packager is to declare a test module that
//   already contains the same providers the packager will inject.
//
// NestJS precedence (empirically confirmed in nestjs-provider-precedence.spec.ts):
//   MODULE-WINS — module-scoped providers take precedence over @Global ones.
//   The anonymous adapter must therefore be scaffold-only (packager step) and
//   NEVER registered in the source AuthenticationModule. If it were, it would shadow
//   the @Global BridgesModule's IAM bridge in a full install → auth stays anonymous.

import { Module } from '@nestjs/common';

/**
 * Scaffold-mirror module: mirrors the published scaffold's AuthenticationModule after
 * `cleanAuthenticationModule()` runs (packager step in
 * `scripts/packager/backend/application.mjs`). Used only in tests; never imported by
 * production code.
 *
 * Intentionally includes `LocalProviderAdapter` — the EXACT provider whose
 * `@Inject(ACCOUNT_LOADER)` (3rd constructor param) caused the original DI crash when
 * no ACCOUNT_LOADER binding was present. Without the anonymous binding below, Nest throws:
 *   "Nest can't resolve dependencies of LocalProviderAdapter (ConfigService, CACHE_MANAGER,
 *    ?). Please make sure that the argument ACCOUNT_LOADER at index [2] is available."
 *
 * The anonymous binding MUST satisfy that injection WITHOUT any `overrideProvider` call,
 * because in the real scaffold the anonymous provider IS the module's own provider — not
 * an external override. This validates that the packager step produces a self-sufficient
 * module.
 *
 * Note: We cannot reuse the real AuthenticationModule class here because its @Module
 * metadata is fixed at class-decoration time. This separate class carries the additional
 * providers the packager injects.
 */
@Module({
  imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
  providers: [
    AnonymousAccountLoaderAdapter,
    { provide: ACCOUNT_LOADER, useExisting: AnonymousAccountLoaderAdapter },
    // LocalProviderAdapter is the real consumer of ACCOUNT_LOADER. Including it here
    // is what makes the test meaningful: it proves the anonymous binding resolves the
    // actual crash scenario, not just an abstract token lookup.
    LocalProviderAdapter,
  ],
})
class ScaffoldAuthenticationModuleMirror {}

describe('AuthenticationModule — scaffold scenario (anonymous ACCOUNT_LOADER, no IAM)', () => {
  /**
   * Builds the TestingModule for scaffold-scenario tests.
   *
   * GlobalModule shape:
   *   - ConfigModule.forRoot (isGlobal: true) — needed by LocalProviderAdapter to read
   *     OAUTH_STRATEGY via ConfigService.
   *   - CacheModule.register (isGlobal: true) — provides CACHE_MANAGER, the 2nd param
   *     injected by LocalProviderAdapter.
   *
   * No overrideProvider(ACCOUNT_LOADER) — exactly mirroring the scaffold's own wiring.
   */
  async function buildScaffoldModule() {
    return Test.createTestingModule({
      imports: [
        ConfigModule.forRoot({ isGlobal: true }),
        CacheModule.register({ isGlobal: true }),
        ScaffoldAuthenticationModuleMirror,
      ],
    }).compile();
  }

  beforeEach(() => {
    process.env.OAUTH_STRATEGY = 'none';
  });

  afterEach(() => {
    delete process.env.OAUTH_STRATEGY;
  });

  it('scaffold-mirror boots with OAUTH_STRATEGY=none, no ACCOUNT_LOADER override, and LocalProviderAdapter present', async () => {
    // This is the critical DI-crash scenario: LocalProviderAdapter injects ACCOUNT_LOADER
    // at constructor param index 2. If the anonymous binding is missing, Nest throws at
    // compile() time with:
    //   "Nest can't resolve dependencies of LocalProviderAdapter (..., ?, ...)
    //    Please make sure that the argument ACCOUNT_LOADER at index [2] is available."
    // A successful compile() here proves the anonymous binding satisfies that injection.
    const moduleRef = await buildScaffoldModule();

    expect(moduleRef).toBeDefined();
  });

  it('LocalProviderAdapter is instantiated — the real consumer of ACCOUNT_LOADER', async () => {
    // Verifies the ACTUAL crash consumer (LocalProviderAdapter) was resolved by Nest DI,
    // not just that the ACCOUNT_LOADER token exists. Without the anonymous binding, this
    // assertion would never be reached because compile() would throw first.
    const moduleRef = await buildScaffoldModule();

    const localProvider = moduleRef.get(LocalProviderAdapter);
    expect(localProvider).toBeDefined();
    expect(localProvider).toBeInstanceOf(LocalProviderAdapter);
  });

  it('ACCOUNT_LOADER resolves to AnonymousAccountLoaderAdapter — not the LocalProviderAdapter consumer', async () => {
    // Verifies the anonymous binding wires the token to the right adapter. LocalProviderAdapter
    // is a CONSUMER of ACCOUNT_LOADER, not its implementation.
    const moduleRef = await buildScaffoldModule();

    const accountLoader = moduleRef.get(ACCOUNT_LOADER);
    expect(accountLoader).toBeInstanceOf(AnonymousAccountLoaderAdapter);
  });

  it('source AuthenticationModule providers array does not contain an ACCOUNT_LOADER binding', () => {
    // Regression: verify the SOURCE module is unchanged (packager step is scaffold-only).
    // If the anonymous provider leaked into the source module, it would shadow the @Global
    // BridgesModule's IAM bridge in a full install — silently keeping auth anonymous.
    const providers = Reflect.getMetadata('providers', AuthenticationModule) as unknown[];
    const hasAnonymousBinding = (providers ?? []).some((p: unknown) => {
      if (typeof p === 'object' && p !== null && 'provide' in p) {
        return (p as { provide: symbol }).provide === ACCOUNT_LOADER;
      }
      return false;
    });

    expect(hasAnonymousBinding).toBe(false);
  });
});
