import 'reflect-metadata';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import * as nodeOs from 'node:os';
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
import * as nodeUrl from 'node:url';
import { MODULE_METADATA } from '@nestjs/common/constants.js';
import { describe, expect, it, vi } from 'vitest';
import type { MosaicConfig } from '@mosaicstack/config';

interface ComposedModuleGraph {
  imports: readonly unknown[];
  federationModule: unknown;
  bootLogLines: readonly string[];
  mosaicConfig: MosaicConfig;
  resolvedConfigPath: string;
}

type StorageTier = 'local' | 'standalone' | 'federated';

interface ModuleGraphFixture {
  tempRoot: string;
  anchor: string;
  homePath: string;
  cwdPath: string;
  monorepoRootEnvPath: string;
  gatewayLocalEnvPath: string;
  daemonEnvPath: string;
  monorepoRootConfigPath: string;
  gatewayLocalConfigPath: string;
}

interface ModuleGraphFixtureOptions {
  rootEnvMode?: 'present' | 'absent';
  rootTier?: StorageTier;
  rootEnvContents?: string;
  redactionMarker?: string;
  gatewayLocalTier?: StorageTier;
  gatewayLocalEnvContents?: string;
  daemonEnvContents?: string;
  inheritedTier?: StorageTier;
  expectedProcessTier?: string;
  setup?: (fixture: ModuleGraphFixture) => Promise<void>;
}

// Each case uses vi.resetModules() and re-imports the full gateway graph for distinct ambient FS/env; CI needs headroom, while this still guards genuine hangs.
const MODULE_IMPORT_TIMEOUT_MS = 120_000;
const MONOREPO_ROOT_DOTENV_LABEL = 'monorepo-root .env';
const DAEMON_DOTENV_LABEL = 'daemon .env';

function configJson(tier: StorageTier): string {
  if (tier === 'local') {
    return JSON.stringify({
      tier,
      storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' },
      queue: { type: 'local', dataDir: '.mosaic/queue' },
      memory: { type: 'keyword' },
    });
  }

  return JSON.stringify({
    tier,
    storage: { type: 'postgres', url: 'postgresql://fixture.invalid/mosaic' },
    queue: { type: 'bullmq' },
    memory: { type: tier === 'federated' ? 'pgvector' : 'keyword' },
  });
}

function snapshotProcessEnv(): Record<string, string | undefined> {
  return { ...process.env };
}

function restoreProcessEnv(snapshot: Record<string, string | undefined>): void {
  for (const key of Object.keys(process.env)) {
    if (!(key in snapshot)) {
      delete process.env[key];
    }
  }

  for (const [key, value] of Object.entries(snapshot)) {
    if (value === undefined) {
      delete process.env[key];
      continue;
    }

    process.env[key] = value;
  }
}

function expectPathUnderTempRoot(path: string, tempRoot: string): void {
  const relativePath = relative(tempRoot, path);
  expect(relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))).toBe(
    true,
  );
}

async function writeFixture(path: string, contents: string, tempRoot: string): Promise<void> {
  expectPathUnderTempRoot(path, tempRoot);
  await mkdir(dirname(path), { recursive: true });
  await writeFile(path, contents, 'utf8');
}

interface ConfigModuleProvider {
  provide: string;
  useFactory: () => MosaicConfig;
}

function isConfigModuleProvider(value: unknown): value is ConfigModuleProvider {
  if (typeof value !== 'object' || value === null) {
    return false;
  }

  if (!('provide' in value) || typeof value.provide !== 'string') {
    return false;
  }

  return 'useFactory' in value && typeof value.useFactory === 'function';
}

function singleBootLogLine(bootLogLines: readonly string[]): string {
  expect(bootLogLines).toHaveLength(1);
  const [bootLogLine] = bootLogLines;
  if (bootLogLine === undefined) {
    throw new Error('Expected a single boot log line');
  }

  return bootLogLine;
}

function expectBootLogLine(
  bootLogLines: readonly string[],
  tier: StorageTier,
  source: string,
): void {
  const bootLogLine = singleBootLogLine(bootLogLines);

  expect(bootLogLine).toContain(`storage tier=${tier}`);
  expect(bootLogLine).toContain(`source=${source}`);
}

async function loadModuleGraphFromDotenv(
  options: ModuleGraphFixtureOptions,
): Promise<ComposedModuleGraph> {
  const originalEnv = snapshotProcessEnv();
  const tempRoot = await mkdtemp(join(nodeOs.tmpdir(), 'mosaic-gateway-module-'));
  let consoleInfoSpy: ReturnType<typeof vi.spyOn> | undefined;
  let cwdSpy: ReturnType<typeof vi.spyOn> | undefined;

  try {
    const anchor = join(tempRoot, 'anchored', 'apps', 'gateway', 'src');
    const homePath = join(tempRoot, 'home');
    const cwdPath = join(tempRoot, 'ambient', 'parent', 'cwd');
    const fixture: ModuleGraphFixture = {
      tempRoot,
      anchor,
      homePath,
      cwdPath,
      monorepoRootEnvPath: resolve(anchor, '../../..', '.env'),
      gatewayLocalEnvPath: resolve(anchor, '..', '.env'),
      daemonEnvPath: join(homePath, '.config', 'mosaic', 'gateway', '.env'),
      monorepoRootConfigPath: resolve(anchor, '../../..', 'mosaic.config.json'),
      gatewayLocalConfigPath: resolve(anchor, '..', 'mosaic.config.json'),
    };
    consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation((): void => undefined);

    for (const path of Object.values(fixture)) {
      expectPathUnderTempRoot(path, tempRoot);
    }

    await mkdir(anchor, { recursive: true });
    await mkdir(cwdPath, { recursive: true });

    if ((options.rootEnvMode ?? 'present') === 'absent') {
      if (
        options.rootEnvContents !== undefined ||
        options.rootTier !== undefined ||
        options.redactionMarker !== undefined
      ) {
        throw new Error('Expected no root env fixture values when rootEnvMode is absent');
      }
    } else {
      if (options.rootEnvContents === undefined && options.rootTier === undefined) {
        throw new Error('Expected rootTier or rootEnvContents');
      }

      const rootFixture = options.rootEnvContents ?? `MOSAIC_STORAGE_TIER=${options.rootTier}\n`;
      const rootFixtureWithMarker = options.redactionMarker
        ? `${rootFixture}BETTER_AUTH_SECRET=${options.redactionMarker}\n`
        : rootFixture;
      await writeFixture(fixture.monorepoRootEnvPath, rootFixtureWithMarker, tempRoot);
    }

    if (options.daemonEnvContents !== undefined) {
      await writeFixture(fixture.daemonEnvPath, options.daemonEnvContents, tempRoot);
    }

    if (options.gatewayLocalEnvContents !== undefined) {
      await writeFixture(fixture.gatewayLocalEnvPath, options.gatewayLocalEnvContents, tempRoot);
    } else if (options.gatewayLocalTier !== undefined) {
      await writeFixture(
        fixture.gatewayLocalEnvPath,
        `MOSAIC_STORAGE_TIER=${options.gatewayLocalTier}\n`,
        tempRoot,
      );
    }

    process.env['HOME'] = homePath;
    delete process.env['MOSAIC_STORAGE_TIER'];
    delete process.env['DATABASE_URL'];
    delete process.env['VALKEY_URL'];
    delete process.env['MOSAIC_GATEWAY_HOME'];

    await options.setup?.(fixture);

    if (options.inheritedTier !== undefined) {
      process.env['MOSAIC_STORAGE_TIER'] = options.inheritedTier;
    }

    vi.resetModules();
    vi.doMock('node:os', () => ({ ...nodeOs, homedir: (): string => homePath }));
    vi.doMock('node:url', () => ({
      ...nodeUrl,
      fileURLToPath: (url: string | URL): string => {
        const actualPath = nodeUrl.fileURLToPath(url);
        if (
          actualPath.endsWith('/apps/gateway/src/env.ts') ||
          actualPath.endsWith('/apps/gateway/src/env.js')
        ) {
          return join(anchor, 'env.ts');
        }
        return actualPath;
      },
    }));
    cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(cwdPath);

    if (options.inheritedTier === undefined) {
      expect(process.env['MOSAIC_STORAGE_TIER']).toBeUndefined();
    } else {
      expect(process.env['MOSAIC_STORAGE_TIER']).toBe(options.inheritedTier);
    }

    const envModule = await import('./env.js');
    expect(process.env['MOSAIC_STORAGE_TIER']).toBe(
      options.expectedProcessTier ?? options.rootTier,
    );

    const { AppModule } = await import('./app.module.js');
    const { FederationModule } = await import('./federation/federation.module.js');
    const imports: unknown = Reflect.getMetadata(MODULE_METADATA.IMPORTS, AppModule);

    if (!Array.isArray(imports)) {
      throw new Error('AppModule imports metadata is not an array');
    }

    const { ConfigModule, MOSAIC_CONFIG } = await import('./config/config.module.js');
    const providers: unknown = Reflect.getMetadata(MODULE_METADATA.PROVIDERS, ConfigModule);

    if (!Array.isArray(providers)) {
      throw new Error('ConfigModule providers metadata is not an array');
    }

    const configProvider = providers
      .filter(isConfigModuleProvider)
      .find((provider: ConfigModuleProvider): boolean => provider.provide === MOSAIC_CONFIG);

    if (!configProvider) {
      throw new Error('MOSAIC_CONFIG provider factory not found');
    }

    return {
      imports,
      federationModule: FederationModule,
      bootLogLines: consoleInfoSpy.mock.calls.map((args: readonly unknown[]): string =>
        args.map((value: unknown): string => String(value)).join(' '),
      ),
      mosaicConfig: configProvider.useFactory(),
      resolvedConfigPath: envModule.resolveGatewayConfigPath(),
    };
  } finally {
    cwdSpy?.mockRestore();
    vi.doUnmock('node:url');
    vi.doUnmock('node:os');
    vi.resetModules();
    consoleInfoSpy?.mockRestore();
    restoreProcessEnv(originalEnv);
    await rm(tempRoot, { recursive: true, force: true });
  }
}

describe('AppModule federation gating', (): void => {
  it('loads dotenv before tracing and AppModule evaluation', async (): Promise<void> => {
    const mainSource = await readFile(new URL('./main.ts', import.meta.url), 'utf8');
    const envImportIndex = mainSource.indexOf("import './env.js';");
    const tracingImportIndex = mainSource.indexOf("import './tracing.js';");
    const appModuleImportIndex = mainSource.indexOf("import { AppModule } from './app.module.js';");

    expect(envImportIndex).toBeGreaterThan(-1);
    expect(envImportIndex).toBeLessThan(tracingImportIndex);
    expect(envImportIndex).toBeLessThan(appModuleImportIndex);
  });

  it(
    'ignores ambient cwd/.env and cwd/../.env files',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          await writeFixture(
            join(fixture.cwdPath, '.env'),
            'MOSAIC_STORAGE_TIER=federated\n',
            fixture.tempRoot,
          );
          await writeFixture(
            resolve(fixture.cwdPath, '..', '.env'),
            'MOSAIC_STORAGE_TIER=federated\n',
            fixture.tempRoot,
          );
        },
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'local', MONOREPO_ROOT_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'ignores an ambient cwd/mosaic.config.json federated config',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          await writeFixture(
            join(fixture.cwdPath, 'mosaic.config.json'),
            configJson('federated'),
            fixture.tempRoot,
          );
        },
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'local', MONOREPO_ROOT_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'ignores an ambient cwd/../../mosaic.config.json federated config',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          await writeFixture(
            resolve(fixture.cwdPath, '../..', 'mosaic.config.json'),
            configJson('federated'),
            fixture.tempRoot,
          );
        },
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'local', MONOREPO_ROOT_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'anchored gateway-local config wins monorepo-root config and registers FederationModule',
    async (): Promise<void> => {
      let gatewayLocalConfigPath = '';
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          gatewayLocalConfigPath = fixture.gatewayLocalConfigPath;
          await writeFixture(
            fixture.gatewayLocalConfigPath,
            configJson('federated'),
            fixture.tempRoot,
          );
          await writeFixture(fixture.monorepoRootConfigPath, configJson('local'), fixture.tempRoot);
        },
      });

      expect(graph.resolvedConfigPath).toBe(gatewayLocalConfigPath);
      expect(graph.mosaicConfig.tier).toBe('federated');
      expect(graph.imports).toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'federated', 'mosaic.config.json');
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'resolves the daemon-installed GATEWAY_HOME/mosaic.config.json ahead of gateway-local and monorepo-root configs',
    async (): Promise<void> => {
      let daemonConfigPath = '';
      const graph = await loadModuleGraphFromDotenv({
        rootEnvMode: 'absent',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          const externalGatewayHome = join(fixture.tempRoot, 'external-gateway-home');
          daemonConfigPath = join(externalGatewayHome, 'mosaic.config.json');
          await writeFixture(daemonConfigPath, configJson('federated'), fixture.tempRoot);
          await writeFixture(
            fixture.gatewayLocalConfigPath,
            configJson('standalone'),
            fixture.tempRoot,
          );
          await writeFixture(fixture.monorepoRootConfigPath, configJson('local'), fixture.tempRoot);
          process.env['MOSAIC_GATEWAY_HOME'] = externalGatewayHome;
          process.env['DATABASE_URL'] = 'postgresql://fixture.invalid/mosaic';
        },
      });

      expect(graph.resolvedConfigPath).toBe(daemonConfigPath);
      expect(graph.mosaicConfig.tier).toBe('federated');
      expect(graph.imports).toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'federated', 'mosaic.config.json');
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'logs mosaic.config.json when anchored config and env tiers are both federated',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'federated',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          await writeFixture(
            fixture.monorepoRootConfigPath,
            configJson('federated'),
            fixture.tempRoot,
          );
        },
      });

      expect(graph.imports).toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'federated', 'mosaic.config.json');
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'logs standalone from a monorepo-root .env DATABASE_URL fallback',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootEnvContents: 'DATABASE_URL=fixture-database-url\n',
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'standalone', MONOREPO_ROOT_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'attributes an invalid monorepo-root dotenv tier to the default',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootEnvContents: 'MOSAIC_STORAGE_TIER=invalid\n',
        expectedProcessTier: 'invalid',
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'local', 'default');
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'attributes DATABASE_URL fallback to daemon .env ahead of inherited local tier',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootEnvMode: 'absent',
        daemonEnvContents: 'DATABASE_URL=fixture-database-url\n',
        inheritedTier: 'local',
        expectedProcessTier: 'local',
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'standalone', DAEMON_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'daemon .env wins over monorepo-root and gateway-local tier values',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        gatewayLocalTier: 'federated',
        daemonEnvContents: 'MOSAIC_STORAGE_TIER=standalone\n',
        expectedProcessTier: 'standalone',
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'standalone', DAEMON_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'inherits process.env.MOSAIC_STORAGE_TIER over daemon, monorepo-root, and gateway-local dotenv values',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        gatewayLocalTier: 'federated',
        daemonEnvContents: 'MOSAIC_STORAGE_TIER=federated\n',
        inheritedTier: 'standalone',
        expectedProcessTier: 'standalone',
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'standalone', 'process environment');
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'gateway-local .env configures the tier and source when the monorepo-root .env is absent',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootEnvMode: 'absent',
        gatewayLocalTier: 'federated',
        expectedProcessTier: 'federated',
      });

      expect(graph.imports).toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'federated', 'gateway-local .env');
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'monorepo-root .env wins over gateway-local tier values',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'standalone',
        gatewayLocalTier: 'federated',
      });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'standalone', MONOREPO_ROOT_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it.each(['local', 'standalone'] as const)(
    'does not register FederationModule for the %s tier',
    async (tier): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({ rootTier: tier });

      expect(graph.imports).not.toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, tier, MONOREPO_ROOT_DOTENV_LABEL);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'registers FederationModule when federated tier is supplied by the anchored monorepo root .env',
    async (): Promise<void> => {
      const redactionMarker = 'redaction-fixture-marker';
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'federated',
        redactionMarker,
      });

      expect(graph.imports).toContain(graph.federationModule);
      expectBootLogLine(graph.bootLogLines, 'federated', MONOREPO_ROOT_DOTENV_LABEL);
      expect(singleBootLogLine(graph.bootLogLines)).not.toContain(redactionMarker);
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'MOSAIC_CONFIG provider ignores an ambient cwd/mosaic.config.json config',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          await writeFixture(
            join(fixture.cwdPath, 'mosaic.config.json'),
            JSON.stringify({
              tier: 'federated',
              storage: {
                type: 'postgres',
                url: 'postgresql://ambient-attacker.invalid/mosaic',
                enableVector: true,
              },
              queue: { type: 'bullmq' },
              memory: { type: 'pgvector' },
            }),
            fixture.tempRoot,
          );
        },
      });

      expect(graph.mosaicConfig.tier).toBe('local');
      expect(graph.mosaicConfig.storage).not.toEqual(
        expect.objectContaining({ url: 'postgresql://ambient-attacker.invalid/mosaic' }),
      );
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );

  it(
    'MOSAIC_CONFIG provider resolves from the anchored monorepo-root mosaic.config.json',
    async (): Promise<void> => {
      const graph = await loadModuleGraphFromDotenv({
        rootTier: 'local',
        setup: async (fixture: ModuleGraphFixture): Promise<void> => {
          await writeFixture(
            fixture.monorepoRootConfigPath,
            configJson('federated'),
            fixture.tempRoot,
          );
        },
      });

      expect(graph.mosaicConfig.tier).toBe('federated');
      expect(graph.mosaicConfig.storage).toEqual(
        expect.objectContaining({ url: 'postgresql://fixture.invalid/mosaic' }),
      );
    },
    MODULE_IMPORT_TIMEOUT_MS,
  );
});
