#!/usr/bin/env node

/**
 * ✅ COMPREHENSIVE Unit Tests for Configuration Management (TypeScript)
 * Tests environment variable parsing, validation, and default values
 */

import assert from 'node:assert';
import { afterEach, beforeEach, describe, it } from 'node:test';

async function importConfigFresh(): Promise<typeof import('../../dist/infrastructure/config.js')> {
  const suffix = `${Date.now()}-${Math.random()}`;
  return await import(`../../dist/infrastructure/config.js?t=${suffix}`);
}

describe('Configuration Management (TypeScript)', () => {
  let originalEnv: Record<string, string | undefined>;

  beforeEach(() => {
    originalEnv = { ...process.env };
  });

  afterEach(() => {
    process.env = originalEnv;
  });

  describe('Environment Variable Parsing', () => {
    it('should parse environment variables correctly', async () => {
      // Set test environment variables
      process.env.NODE_ENV = 'test';
      process.env.CACHE_ENABLED = 'true';
      process.env.CACHE_TTL = '600';
      process.env.CACHE_MAX_SIZE = '2000';
      process.env.LOG_LEVEL = 'debug';
      process.env.LOG_FORMAT = 'json';
      process.env.METRICS_ENABLED = 'true';
      process.env.METRICS_PORT = '3001';
      process.env.COURTLISTENER_TIMEOUT = '45000';
      process.env.COURTLISTENER_RATE_LIMIT = '150';

      // Import config after setting env vars
      const { getConfig } = await import('../../dist/infrastructure/config.js');
      const config = getConfig();

      assert.strictEqual(config.cache.enabled, true);
      assert.strictEqual(config.cache.ttl, 600);
      assert.strictEqual(config.cache.maxSize, 2000);
      assert.strictEqual(config.logging.level, 'debug');
      assert.strictEqual(config.logging.format, 'json');
      assert.strictEqual(config.metrics.enabled, true);
      assert.strictEqual(config.metrics.port, 3001);
      assert.strictEqual(config.courtListener.timeout, 45000);
      assert.strictEqual(config.courtListener.rateLimitPerMinute, 150);
    });

    it('should use default values when env vars are missing', async () => {
      // Clear all relevant env vars
      delete process.env.CACHE_ENABLED;
      delete process.env.CACHE_TTL;
      delete process.env.LOG_LEVEL;
      delete process.env.METRICS_ENABLED;
      delete process.env.CACHE_MAX_SIZE;

      // Use timestamp to force fresh import
      const { getConfig } = await importConfigFresh();
      const config = getConfig();

      // Use actual defaults from config.ts
      assert.strictEqual(config.cache.enabled, true); // CACHE_ENABLED !== 'false' defaults to true
      assert.strictEqual(config.cache.ttl, 300); // Default: 5 minutes (300 seconds)
      assert.strictEqual(config.cache.maxSize, 1000); // Default: 1000
      assert.strictEqual(config.logging.level, 'info'); // Default: info
      assert.strictEqual(config.metrics.enabled, false); // METRICS_ENABLED === 'true' defaults to false
    });

    it('should handle boolean environment variables', async () => {
      // Test actual boolean parsing behavior from config.ts
      process.env.CACHE_ENABLED = 'false'; // !== 'false' = false
      process.env.METRICS_ENABLED = 'true'; // === 'true' = true
      process.env.LOGGING_ENABLED = 'false'; // !== 'false' = false

      const { getConfig } = await importConfigFresh();
      const config = getConfig();

      assert.strictEqual(config.cache.enabled, false);
      assert.strictEqual(config.metrics.enabled, true);
      assert.strictEqual(config.logging.enabled, false);

      // Clean up
      delete process.env.CACHE_ENABLED;
      delete process.env.METRICS_ENABLED;
      delete process.env.LOGGING_ENABLED;
    });

    it('should handle numeric environment variables', async () => {
      process.env.CACHE_TTL = '900';
      process.env.CACHE_MAX_SIZE = '5000';
      process.env.METRICS_PORT = '4001';
      process.env.COURTLISTENER_TIMEOUT = '60000';

      // Use timestamp to force fresh import
      const { getConfig } = await importConfigFresh();
      const config = getConfig();

      assert.strictEqual(config.cache.ttl, 900);
      assert.strictEqual(config.cache.maxSize, 5000);
      assert.strictEqual(config.metrics.port, 4001);
      assert.strictEqual(config.courtListener.timeout, 60000);

      // Clean up
      delete process.env.CACHE_TTL;
      delete process.env.CACHE_MAX_SIZE;
      delete process.env.METRICS_PORT;
      delete process.env.COURTLISTENER_TIMEOUT;
    });
  });

  describe('Configuration Validation', () => {
    it('should validate log levels', async () => {
      process.env.LOG_LEVEL = 'invalid';

      const { getConfig } = await import('../../dist/infrastructure/config.js');

      // Should either throw error or fallback to default
      assert.doesNotThrow(() => {
        const config = getConfig();
        // Should fallback to default if invalid
        assert.ok(['error', 'warn', 'info', 'debug'].includes(config.logging.level));
      });
    });

    it('should validate numeric ranges', async () => {
      process.env.CACHE_TTL = '-100'; // Invalid negative TTL
      process.env.METRICS_PORT = '99999'; // Invalid port range

      const { getConfig } = await import('../../dist/infrastructure/config.js');

      assert.doesNotThrow(() => {
        const config = getConfig();
        // Should use sensible defaults for invalid values
        assert.ok(config.cache.ttl > 0);
        assert.ok(
          config.metrics.port === undefined ||
            (config.metrics.port >= 1024 && config.metrics.port <= 65535),
        );
      });

      // Clean up
      delete process.env.CACHE_TTL;
      delete process.env.METRICS_PORT;
    });

    it('should validate required string fields', async () => {
      process.env.COURTLISTENER_BASE_URL = '';

      const { getConfig } = await import('../../dist/infrastructure/config.js');

      // Should either throw error or use default
      assert.doesNotThrow(() => {
        const config = getConfig();
        assert.ok(
          typeof config.courtListener.baseUrl === 'string' &&
            config.courtListener.baseUrl.length > 0,
        );
      });

      delete process.env.COURTLISTENER_BASE_URL;
    });
  });

  describe('Security Configuration', () => {
    it('should parse security settings', async () => {
      process.env.AUTH_ENABLED = 'true';
      process.env.AUTH_API_KEYS = 'key1,key2,key3';
      process.env.RATE_LIMIT_ENABLED = 'true';
      process.env.RATE_LIMIT_MAX_REQUESTS = '50';
      process.env.SANITIZATION_ENABLED = 'false';

      const { getConfig } = await importConfigFresh();
      const config = getConfig();

      assert.strictEqual(config.security.authEnabled, true);
      assert.strictEqual(config.security.apiKeys.length, 3);
      assert.strictEqual(config.security.apiKeys[0], 'key1');
      assert.strictEqual(config.security.rateLimitEnabled, true);
      assert.strictEqual(config.security.maxRequestsPerMinute, 50);
      assert.strictEqual(config.security.sanitizationEnabled, false);

      // Clean up
      delete process.env.AUTH_ENABLED;
      delete process.env.AUTH_API_KEYS;
      delete process.env.RATE_LIMIT_ENABLED;
      delete process.env.RATE_LIMIT_MAX_REQUESTS;
      delete process.env.SANITIZATION_ENABLED;
    });

    it('should handle empty API keys array', async () => {
      process.env.AUTH_ENABLED = 'false';

      const { getConfig } = await importConfigFresh();
      const config = getConfig();

      assert.strictEqual(config.security.authEnabled, false);
      assert.strictEqual(config.security.apiKeys.length, 0);

      delete process.env.AUTH_ENABLED;
    });
  });

  describe('Auth Policy Matrix', () => {
    it('should fail fast when OAuth and OIDC are both enabled', async () => {
      process.env.OAUTH_ENABLED = 'true';
      process.env.OIDC_ISSUER = 'https://issuer.example.com';

      const { getConfig } = await importConfigFresh();

      assert.throws(() => getConfig(), /OAuth and OIDC auth cannot both be enabled at startup/);
    });

    it('should expose canonical auth precedence without leaking secrets', async () => {
      process.env.MCP_AUTH_TOKEN = 'static-secret-token';
      process.env.OIDC_ISSUER = 'https://issuer.example.com';

      const { getStartupDiagnostics } = await importConfigFresh();
      const diagnostics = getStartupDiagnostics() as {
        authPolicy?: { effectivePrimary?: string; precedence?: string[] };
      };

      assert.strictEqual(diagnostics.authPolicy?.effectivePrimary, 'oidc');
      assert.deepStrictEqual(diagnostics.authPolicy?.precedence, ['oauth', 'serviceToken', 'oidc']);

      const serialized = JSON.stringify(diagnostics);
      assert.strictEqual(serialized.includes('static-secret-token'), false);
    });

    it('should ignore deprecated static fallback env vars without failing startup', async () => {
      process.env.MCP_AUTH_TOKEN = 'static-only-token';
      process.env.MCP_AUTH_PRIMARY = 'oidc';
      process.env.MCP_ALLOW_STATIC_FALLBACK = 'true';

      const { getStartupDiagnostics } = await importConfigFresh();
      const diagnostics = getStartupDiagnostics() as {
        authPolicy?: { effectivePrimary?: string | null };
        invariants?: { warnings?: string[] };
      };

      assert.strictEqual(diagnostics.authPolicy?.effectivePrimary, null);
      assert.ok(
        diagnostics.invariants?.warnings?.some((warning) =>
          warning.includes('MCP_AUTH_PRIMARY is deprecated and ignored'),
        ),
      );
      assert.ok(
        diagnostics.invariants?.warnings?.some((warning) =>
          warning.includes('MCP_ALLOW_STATIC_FALLBACK is deprecated and ignored'),
        ),
      );
    });

    it('should surface dev fallback risk in startup diagnostics when fully enabled', async () => {
      process.env.MCP_OAUTH_DEV_USER_ID = 'dev-user-123';
      process.env.MCP_ALLOW_DEV_FALLBACK = 'true';

      const { getStartupDiagnostics } = await importConfigFresh();
      const diagnostics = getStartupDiagnostics() as {
        authPolicy?: {
          devFallback?: {
            enabled?: boolean;
            riskLevel?: string;
            productionLike?: boolean;
          };
        };
        invariants?: { warnings?: string[] };
      };

      assert.strictEqual(diagnostics.authPolicy?.devFallback?.enabled, true);
      assert.strictEqual(diagnostics.authPolicy?.devFallback?.riskLevel, 'enabled');
      assert.strictEqual(diagnostics.authPolicy?.devFallback?.productionLike, false);
      assert.ok(
        diagnostics.invariants?.warnings?.some((warning) =>
          warning.includes('OAuth dev fallback is enabled'),
        ),
      );
    });

    it('should warn when dev fallback env is only partially configured', async () => {
      process.env.MCP_OAUTH_DEV_USER_ID = 'dev-user-123';

      const { getStartupDiagnostics } = await importConfigFresh();
      const diagnostics = getStartupDiagnostics() as {
        authPolicy?: {
          devFallback?: {
            enabled?: boolean;
            riskLevel?: string;
            allowFlagEnabled?: boolean;
          };
        };
        invariants?: { warnings?: string[] };
      };

      assert.strictEqual(diagnostics.authPolicy?.devFallback?.enabled, false);
      assert.strictEqual(diagnostics.authPolicy?.devFallback?.allowFlagEnabled, false);
      assert.strictEqual(diagnostics.authPolicy?.devFallback?.riskLevel, 'misconfigured');
      assert.ok(
        diagnostics.invariants?.warnings?.some((warning) =>
          warning.includes('MCP_OAUTH_DEV_USER_ID is configured but inert'),
        ),
      );
    });

    it('should warn when deprecated broad Cloudflare Access trust is configured', async () => {
      process.env.MCP_TRUST_CLOUDFLARE_ACCESS_HEADERS = 'true';

      const { getStartupDiagnostics } = await importConfigFresh();
      const diagnostics = getStartupDiagnostics() as {
        invariants?: { warnings?: string[] };
      };

      assert.ok(
        diagnostics.invariants?.warnings?.some((warning) =>
          warning.includes('MCP_TRUST_CLOUDFLARE_ACCESS_HEADERS is deprecated and ignored'),
        ),
      );
    });

    it('should warn when scoped Cloudflare Access trust is enabled', async () => {
      process.env.MCP_TRUST_CLOUDFLARE_ACCESS_JWT_ASSERTION = 'true';
      process.env.MCP_TRUST_CLOUDFLARE_ACCESS_IDENTITY_HEADERS = 'true';

      const { getStartupDiagnostics } = await importConfigFresh();
      const diagnostics = getStartupDiagnostics() as {
        invariants?: { warnings?: string[] };
      };

      assert.ok(
        diagnostics.invariants?.warnings?.some((warning) =>
          warning.includes('MCP_TRUST_CLOUDFLARE_ACCESS_JWT_ASSERTION is enabled'),
        ),
      );
      assert.ok(
        diagnostics.invariants?.warnings?.some((warning) =>
          warning.includes('MCP_TRUST_CLOUDFLARE_ACCESS_IDENTITY_HEADERS is enabled'),
        ),
      );
    });

    it('should fail fast when dev fallback is enabled in production mode', async () => {
      process.env.NODE_ENV = 'production';
      process.env.MCP_OAUTH_DEV_USER_ID = 'dev-user-123';
      process.env.MCP_ALLOW_DEV_FALLBACK = 'true';

      const { getConfig, getStartupDiagnostics } = await importConfigFresh();

      assert.throws(
        () => getConfig(),
        /OAuth dev fallback cannot be enabled when NODE_ENV=production/,
      );

      const diagnostics = getStartupDiagnostics() as {
        status?: string;
        authPolicy?: {
          devFallback?: {
            enabled?: boolean;
            productionLike?: boolean;
            riskLevel?: string;
          };
        };
        invariants?: { errors?: string[] };
      };

      assert.strictEqual(diagnostics.status, 'error');
      assert.strictEqual(diagnostics.authPolicy?.devFallback?.enabled, true);
      assert.strictEqual(diagnostics.authPolicy?.devFallback?.productionLike, true);
      assert.strictEqual(diagnostics.authPolicy?.devFallback?.riskLevel, 'enabled');
      assert.ok(
        diagnostics.invariants?.errors?.some((error) =>
          error.includes('OAuth dev fallback cannot be enabled when NODE_ENV=production'),
        ),
      );
    });

    it('should fail fast when hosted auth client config is present without MCP_UI_SESSION_SECRET', async () => {
      process.env.OIDC_ISSUER = 'https://issuer.example.com';
      process.env.MCP_AUTH_OIDC_CLIENT_ID = 'worker-client-id';
      process.env.MCP_AUTH_OIDC_CLIENT_SECRET = 'worker-client-secret';

      const { getConfig, getStartupDiagnostics } = await importConfigFresh();

      assert.throws(() => getConfig(), /Hosted auth requires MCP_UI_SESSION_SECRET/);

      const diagnostics = getStartupDiagnostics() as {
        hostedAuth?: { ready?: boolean; errors?: string[] };
      };
      assert.strictEqual(diagnostics.hostedAuth?.ready, false);
      assert.ok(
        diagnostics.hostedAuth?.errors?.some((error) => error.includes('MCP_UI_SESSION_SECRET')),
      );
    });

    it('should fail fast on partial Worker-native hosted auth config', async () => {
      process.env.OIDC_ISSUER = 'https://issuer.example.com';
      process.env.MCP_AUTH_OIDC_CLIENT_ID = 'worker-client-id';
      process.env.MCP_UI_SESSION_SECRET = 'session-secret';

      const { getConfig, getStartupDiagnostics } = await importConfigFresh();

      assert.throws(() => getConfig(), /Hosted auth upstream OIDC config is incomplete/);

      const diagnostics = getStartupDiagnostics() as {
        hostedAuth?: { ready?: boolean; credentialSource?: string | null; errors?: string[] };
      };
      assert.strictEqual(diagnostics.hostedAuth?.ready, false);
      assert.strictEqual(diagnostics.hostedAuth?.credentialSource, null);
      assert.ok(
        diagnostics.hostedAuth?.errors?.some((error) => error.includes('MCP_AUTH_OIDC_CLIENT_ID')),
      );
    });
  });

  describe('Circuit Breaker Configuration', () => {
    it('should parse circuit breaker settings', async () => {
      process.env.CIRCUIT_BREAKER_ENABLED = 'true';
      process.env.CIRCUIT_BREAKER_FAILURE_THRESHOLD = '10';
      process.env.CIRCUIT_BREAKER_SUCCESS_THRESHOLD = '5';
      process.env.CIRCUIT_BREAKER_TIMEOUT = '15000';
      process.env.CIRCUIT_BREAKER_RESET_TIMEOUT = '120000';

      const { getConfig } = await importConfigFresh();
      const config = getConfig();

      assert.strictEqual(config.circuitBreaker.enabled, true);
      assert.strictEqual(config.circuitBreaker.failureThreshold, 10);
      assert.strictEqual(config.circuitBreaker.successThreshold, 5);
      assert.strictEqual(config.circuitBreaker.timeout, 15000);
      assert.strictEqual(config.circuitBreaker.resetTimeout, 120000);

      // Clean up
      delete process.env.CIRCUIT_BREAKER_ENABLED;
      delete process.env.CIRCUIT_BREAKER_FAILURE_THRESHOLD;
      delete process.env.CIRCUIT_BREAKER_SUCCESS_THRESHOLD;
      delete process.env.CIRCUIT_BREAKER_TIMEOUT;
      delete process.env.CIRCUIT_BREAKER_RESET_TIMEOUT;
    });
  });

  describe('Configuration Summary', () => {
    it('should provide configuration summary', async () => {
      const { getConfig } = await import('../../dist/infrastructure/config.js');
      const config = getConfig();

      // Verify config has expected structure
      assert.ok(config.courtListener);
      assert.ok(config.cache);
      assert.ok(config.logging);
      assert.ok(config.metrics);
      assert.ok(config.security);
      assert.ok(config.audit);
      assert.ok(config.circuitBreaker);
      assert.ok(config.compression);
    });
  });
});
