import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { z } from 'zod';
import {
  checkRequiredEnvVars,
  COMMON_SECRET_PATTERNS,
  createStrictSchema,
  formatValidationResult,
  isSecret,
  isSecretEnvVar,
  mergeSchemas,
  redactSecrets,
  secret,
  secretSchema,
  secretStringSchema,
  SecretValue,
  validateCompleteness,
  validateConfig,
  wrapSecretEnvVars,
  type ValidationResult,
} from '../src';

describe('Config Utils', () => {
  describe('SecretValue', () => {
    it('should create a secret value', () => {
      const secret = new SecretValue('my-secret');
      expect(secret).toBeInstanceOf(SecretValue);
      expect(secret.toString()).toBe('***');
    });

    it('should use custom mask', () => {
      const secret = new SecretValue('my-secret', 'HIDDEN');
      expect(secret.toString()).toBe('HIDDEN');
    });

    it('should reveal value with reason', () => {
      const secret = new SecretValue('my-secret');
      expect(secret.reveal('testing')).toBe('my-secret');
    });

    it('should throw when revealing without reason', () => {
      const secret = new SecretValue('my-secret');
      expect(() => secret.reveal('')).toThrow('Reason required for revealing secret value');
    });

    it('should mask value in JSON', () => {
      const secret = new SecretValue('my-secret');
      expect(JSON.stringify(secret)).toBe('"***"');
      expect(secret.toJSON()).toBe('***');
    });

    it('should compare values without revealing', () => {
      const secret = new SecretValue('my-secret');
      expect(secret.equals('my-secret')).toBe(true);
      expect(secret.equals('other-secret')).toBe(false);
    });

    it('should map secret values', () => {
      const secret = new SecretValue('hello');
      const mapped = secret.map(val => val.toUpperCase(), 'testing transformation');
      expect(mapped.reveal('checking result')).toBe('HELLO');
      expect(mapped.toString()).toBe('***');
    });

    it('should work with non-string types', () => {
      const numberSecret = new SecretValue(12345, 'XXX');
      expect(numberSecret.reveal('test')).toBe(12345);
      expect(numberSecret.toString()).toBe('XXX');

      const objectSecret = new SecretValue({ key: 'value' }, '[OBJECT]');
      expect(objectSecret.reveal('test')).toEqual({ key: 'value' });
      expect(objectSecret.toString()).toBe('[OBJECT]');
    });
  });

  describe('secret helper function', () => {
    it('should create secret values', () => {
      const s = secret('my-secret');
      expect(s).toBeInstanceOf(SecretValue);
      expect(s.reveal('test')).toBe('my-secret');
    });

    it('should accept custom mask', () => {
      const s = secret('my-secret', 'REDACTED');
      expect(s.toString()).toBe('REDACTED');
    });
  });

  describe('isSecret', () => {
    it('should identify secret values', () => {
      expect(isSecret(new SecretValue('test'))).toBe(true);
      expect(isSecret(secret('test'))).toBe(true);
      expect(isSecret('test')).toBe(false);
      expect(isSecret(null)).toBe(false);
      expect(isSecret(undefined)).toBe(false);
      expect(isSecret({})).toBe(false);
    });
  });

  describe('secretSchema', () => {
    it('should validate SecretValue instances', () => {
      const schema = secretSchema(z.string());
      const secretVal = new SecretValue('test');

      expect(() => schema.parse(secretVal)).not.toThrow();
      expect(() => schema.parse('test')).toThrow();
      expect(() => schema.parse(null)).toThrow();
    });
  });

  describe('secretStringSchema', () => {
    it('should transform string to SecretValue', () => {
      const result = secretStringSchema.parse('my-secret');
      expect(result).toBeInstanceOf(SecretValue);
      expect(result.reveal('test')).toBe('my-secret');
    });

    it('should reject non-strings', () => {
      expect(() => secretStringSchema.parse(123)).toThrow();
      expect(() => secretStringSchema.parse(null)).toThrow();
    });
  });

  describe('redactSecrets', () => {
    it('should redact specified paths', () => {
      const obj = {
        username: 'admin',
        password: 'secret123',
        nested: {
          apiKey: 'key123',
          public: 'visible',
        },
      };

      const redacted = redactSecrets(obj, ['password', 'nested.apiKey']);

      expect(redacted).toEqual({
        username: 'admin',
        password: '***REDACTED***',
        nested: {
          apiKey: '***REDACTED***',
          public: 'visible',
        },
      });
    });

    it('should redact SecretValue instances', () => {
      const obj = {
        normal: 'value',
        secret: new SecretValue('hidden', 'MASKED'),
        nested: {
          anotherSecret: secret('also-hidden'),
        },
      };

      const redacted = redactSecrets(obj);

      expect(redacted).toEqual({
        normal: 'value',
        secret: 'MASKED',
        nested: {
          anotherSecret: '***',
        },
      });
    });

    it('should handle arrays', () => {
      const obj = {
        items: [
          { name: 'item1', secret: new SecretValue('s1') },
          { name: 'item2', secret: new SecretValue('s2') },
        ],
      };

      const redacted = redactSecrets(obj);

      expect(redacted.items).toEqual([
        { name: 'item1', secret: '***' },
        { name: 'item2', secret: '***' },
      ]);
    });

    it('should handle null and undefined', () => {
      const obj = {
        nullValue: null,
        undefinedValue: undefined,
        secret: new SecretValue('test'),
      };

      const redacted = redactSecrets(obj);

      expect(redacted).toEqual({
        nullValue: null,
        undefinedValue: undefined,
        secret: '***',
      });
    });

    it('should handle non-existent paths gracefully', () => {
      const obj = { a: 'value' };
      const redacted = redactSecrets(obj, ['b.c.d']);
      expect(redacted).toEqual({ a: 'value' });
    });

    it('should not modify original object', () => {
      const obj = { password: 'secret' };
      const original = { ...obj };
      redactSecrets(obj, ['password']);
      expect(obj).toEqual(original);
    });
  });

  describe('isSecretEnvVar', () => {
    it('should identify common secret patterns', () => {
      // Positive cases
      expect(isSecretEnvVar('PASSWORD')).toBe(true);
      expect(isSecretEnvVar('DB_PASSWORD')).toBe(true);
      expect(isSecretEnvVar('API_KEY')).toBe(true);
      expect(isSecretEnvVar('API-KEY')).toBe(true);
      expect(isSecretEnvVar('SECRET_TOKEN')).toBe(true);
      expect(isSecretEnvVar('AUTH_TOKEN')).toBe(true);
      expect(isSecretEnvVar('PRIVATE_KEY')).toBe(true);
      expect(isSecretEnvVar('CREDENTIAL')).toBe(true);
      expect(isSecretEnvVar('password')).toBe(true); // Case insensitive

      // Negative cases
      expect(isSecretEnvVar('USERNAME')).toBe(false);
      expect(isSecretEnvVar('PORT')).toBe(false);
      expect(isSecretEnvVar('DEBUG')).toBe(false);
      expect(isSecretEnvVar('NODE_ENV')).toBe(false);
    });
  });

  describe('wrapSecretEnvVars', () => {
    it('should wrap secret environment variables', () => {
      const env = {
        USERNAME: 'admin',
        PASSWORD: 'secret123',
        API_KEY: 'key123',
        PORT: '3000',
      };

      const wrapped = wrapSecretEnvVars(env);

      expect(wrapped.USERNAME).toBe('admin');
      expect(wrapped.PORT).toBe('3000');

      expect(isSecret(wrapped.PASSWORD)).toBe(true);
      expect(isSecret(wrapped.API_KEY)).toBe(true);

      const passwordSecret = wrapped.PASSWORD as SecretValue;
      expect(passwordSecret.reveal('test')).toBe('secret123');
      expect(passwordSecret.toString()).toBe('***PASSWORD***');
    });

    it('should handle undefined values', () => {
      const env = {
        PASSWORD: undefined,
        USERNAME: 'admin',
      };

      const wrapped = wrapSecretEnvVars(env);

      expect(wrapped.PASSWORD).toBeUndefined();
      expect(wrapped.USERNAME).toBe('admin');
    });
  });

  describe('validateConfig', () => {
    const schema = z.object({
      name: z.string(),
      port: z.number(),
      optional: z.string().optional(),
    });

    it('should validate valid config', () => {
      const result = validateConfig({ name: 'app', port: 3000 }, schema);
      expect(result.valid).toBe(true);
      expect(result.errors).toBeUndefined();
    });

    it('should return errors for invalid config', () => {
      const result = validateConfig({ name: 'app', port: 'invalid' }, schema);
      expect(result.valid).toBe(false);
      expect(result.errors).toBeDefined();
      expect(result.errors![0].path).toBe('port');
      expect(result.errors![0].message).toContain('Expected number');
    });

    it('should handle missing required fields', () => {
      const result = validateConfig({ port: 3000 }, schema);
      expect(result.valid).toBe(false);
      expect(result.errors).toBeDefined();
      expect(result.errors![0].path).toBe('name');
    });

    it('should rethrow non-Zod errors', () => {
      const badSchema = {
        parse: () => {
          throw new Error('Not a Zod error');
        },
      } as any;

      expect(() => validateConfig({}, badSchema)).toThrow('Not a Zod error');
    });
  });

  describe('checkRequiredEnvVars', () => {
    const originalEnv = { ...process.env };

    beforeEach(() => {
      // Clear environment
      for (const key in process.env) {
        delete process.env[key];
      }
    });

    afterEach(() => {
      // Restore environment
      for (const key in process.env) {
        delete process.env[key];
      }
      Object.assign(process.env, originalEnv);
    });

    it('should pass when all required vars are set', () => {
      process.env.API_KEY = 'key123';
      process.env.DATABASE_URL = 'postgres://...';

      const result = checkRequiredEnvVars(['API_KEY', 'DATABASE_URL']);
      expect(result.valid).toBe(true);
      expect(result.errors).toBeUndefined();
    });

    it('should fail when required vars are missing', () => {
      process.env.API_KEY = 'key123';

      const result = checkRequiredEnvVars(['API_KEY', 'DATABASE_URL', 'MISSING_VAR']);
      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(2);
      expect(result.errors![0].path).toBe('env.DATABASE_URL');
      expect(result.errors![1].path).toBe('env.MISSING_VAR');
    });

    it('should handle empty required list', () => {
      const result = checkRequiredEnvVars([]);
      expect(result.valid).toBe(true);
      expect(result.errors).toBeUndefined();
    });
  });

  describe('validateCompleteness', () => {
    it('should validate complete config', () => {
      const config = {
        database: {
          host: 'localhost',
          port: 5432,
          credentials: {
            username: 'admin',
            password: 'secret',
          },
        },
      };

      const result = validateCompleteness(config, [
        'database.host',
        'database.port',
        'database.credentials.username',
      ]);

      expect(result.valid).toBe(true);
      expect(result.errors).toBeUndefined();
    });

    it('should detect missing values', () => {
      const config = {
        database: {
          host: 'localhost',
          credentials: {},
        },
      };

      const result = validateCompleteness(config, [
        'database.host',
        'database.port',
        'database.credentials.username',
      ]);

      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(2);
      expect(result.errors![0].path).toBe('database.port');
      expect(result.errors![1].path).toBe('database.credentials.username');
    });

    it('should handle null and undefined as missing', () => {
      const config = {
        a: null,
        b: undefined,
        c: 'value',
      };

      const result = validateCompleteness(config, ['a', 'b', 'c']);
      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(2);
    });

    it('should handle non-existent paths', () => {
      const config = { a: 'value' };
      const result = validateCompleteness(config, ['b.c.d']);
      expect(result.valid).toBe(false);
      expect(result.errors![0].path).toBe('b.c.d');
    });
  });

  describe('formatValidationResult', () => {
    it('should format valid result', () => {
      const result: ValidationResult = { valid: true };
      const formatted = formatValidationResult(result);
      expect(formatted).toBe('✅ Configuration is valid');
    });

    it('should format errors', () => {
      const result: ValidationResult = {
        valid: false,
        errors: [
          { path: 'port', message: 'Expected number' },
          {
            path: 'database.host',
            message: 'Invalid value',
            expected: 'string',
            received: 'number',
          },
        ],
      };

      const formatted = formatValidationResult(result);
      expect(formatted).toContain('❌ Configuration validation failed');
      expect(formatted).toContain('Errors:');
      expect(formatted).toContain('- port: Expected number');
      expect(formatted).toContain('- database.host: Invalid value');
      expect(formatted).toContain('Expected: string, Received: number');
    });

    it('should format warnings', () => {
      const result: ValidationResult = {
        valid: true,
        warnings: [{ path: 'deprecated.feature', message: 'This feature is deprecated' }],
      };

      const formatted = formatValidationResult(result);
      expect(formatted).toContain('✅ Configuration is valid');
      expect(formatted).toContain('Warnings:');
      expect(formatted).toContain('- deprecated.feature: This feature is deprecated');
    });
  });

  describe('createStrictSchema', () => {
    it('should create strict schema', () => {
      const schema = createStrictSchema({
        name: z.string(),
        age: z.number(),
      });

      expect(() => schema.parse({ name: 'John', age: 30 })).not.toThrow();
      expect(() => schema.parse({ name: 'John', age: 30, extra: 'field' })).toThrow();
    });
  });

  describe('mergeSchemas', () => {
    it('should merge two schemas', () => {
      const schema1 = z.object({ a: z.string() });
      const schema2 = z.object({ b: z.number() });

      const merged = mergeSchemas(schema1, schema2);
      const result = merged.parse({ a: 'test', b: 123 });

      expect(result).toEqual({ a: 'test', b: 123 });
    });

    it('should merge multiple schemas', () => {
      const schema1 = z.object({ a: z.string() });
      const schema2 = z.object({ b: z.number() });
      const schema3 = z.object({ c: z.boolean() });

      const merged = mergeSchemas(schema1, schema2, schema3);
      const result = merged.parse({ a: 'test', b: 123, c: true });

      expect(result).toEqual({ a: 'test', b: 123, c: true });
    });

    it('should throw with less than two schemas', () => {
      expect(() => mergeSchemas(z.object({}))).toThrow('At least two schemas required');
      expect(() => mergeSchemas()).toThrow('At least two schemas required');
    });

    it('should handle overlapping fields', () => {
      const schema1 = z.object({ a: z.string(), shared: z.string() });
      const schema2 = z.object({ b: z.number(), shared: z.string() });

      const merged = mergeSchemas(schema1, schema2);

      // Both schemas require 'shared' to be a string
      expect(() => merged.parse({ a: 'test', b: 123, shared: 'value' })).not.toThrow();
      expect(() => merged.parse({ a: 'test', b: 123, shared: 123 })).toThrow();
    });
  });

  describe('COMMON_SECRET_PATTERNS', () => {
    it('should be an array of RegExp', () => {
      expect(Array.isArray(COMMON_SECRET_PATTERNS)).toBe(true);
      expect(COMMON_SECRET_PATTERNS.length).toBeGreaterThan(0);

      for (const pattern of COMMON_SECRET_PATTERNS) {
        expect(pattern).toBeInstanceOf(RegExp);
      }
    });
  });
});
