import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { ConfigManager } from '../src/config-manager';
import { ConfigValidationError } from '../src/errors';
import { initializeConfig, resetConfig } from '../src/index';
import { EnvLoader } from '../src/loaders/env.loader';
import { FileLoader } from '../src/loaders/file.loader';
import { appConfigSchema } from '../src/schemas';

const TEST_DIR = join(__dirname, 'edge-case-tests');

describe('Edge Cases and Error Handling', () => {
  let originalEnv: NodeJS.ProcessEnv;
  let originalCwd: string;

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

    resetConfig();

    if (existsSync(TEST_DIR)) {
      rmSync(TEST_DIR, { recursive: true, force: true });
    }
    mkdirSync(TEST_DIR, { recursive: true });
  });

  afterEach(() => {
    process.env = originalEnv;
    process.chdir(originalCwd);
    resetConfig();

    if (existsSync(TEST_DIR)) {
      rmSync(TEST_DIR, { recursive: true, force: true });
    }
  });

  test('should handle missing .env files gracefully', async () => {
    // No .env file exists
    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    // Should not throw even without .env file
    const config = await manager.initialize(appConfigSchema);
    expect(config).toBeDefined();
  });

  test('should handle corrupted JSON config files', async () => {
    const configDir = join(TEST_DIR, 'config');
    mkdirSync(configDir, { recursive: true });

    // Create corrupted JSON file
    writeFileSync(join(configDir, 'development.json'), '{ "app": { "name": "test", invalid json }');

    const manager = new ConfigManager({
      loaders: [new FileLoader(configDir, 'development')],
    });

    // Should throw error for invalid JSON
    await expect(manager.initialize(appConfigSchema)).rejects.toThrow();
  });

  test('should handle missing config directories', async () => {
    const nonExistentDir = join(TEST_DIR, 'nonexistent');

    const manager = new ConfigManager({
      loaders: [new FileLoader(nonExistentDir, 'development')],
    });

    // Should not throw, should return empty config
    const config = await manager.initialize();
    expect(config).toBeDefined();
  });

  test('should handle permission denied on config files', async () => {
    const configDir = join(TEST_DIR, 'config');
    mkdirSync(configDir, { recursive: true });

    const configFile = join(configDir, 'development.json');
    writeFileSync(configFile, JSON.stringify({ app: { name: 'test' } }));

    // Make file unreadable (this might not work on all systems)
    try {
      chmodSync(configFile, 0o000);

      const manager = new ConfigManager({
        loaders: [new FileLoader(configDir, 'development')],
      });

      // Should handle permission error gracefully
      const config = await manager.initialize();
      expect(config).toBeDefined();
    } finally {
      // Restore permissions for cleanup
      try {
        chmodSync(configFile, 0o644);
      } catch {
        // Ignore errors during cleanup
      }
    }
  });

  test('should handle circular references in config merging', async () => {
    // This tests deep merge with potential circular references
    const configDir = join(TEST_DIR, 'config');
    mkdirSync(configDir, { recursive: true });

    writeFileSync(
      join(configDir, 'development.json'),
      JSON.stringify({
        app: {
          name: 'test',
          settings: {
            ref: 'settings',
          },
        },
      })
    );

    process.env.APP_SETTINGS_NESTED_VALUE = 'deep-value';

    const manager = new ConfigManager({
      loaders: [new FileLoader(configDir, 'development'), new EnvLoader('')],
    });

    const config = await manager.initialize(appConfigSchema);
    expect(config.app.name).toBe('test');
  });

  test('should handle extremely deep nesting in environment variables', async () => {
    // Test very deep nesting
    process.env.LEVEL1_LEVEL2_LEVEL3_LEVEL4_LEVEL5_VALUE = 'deep-value';

    const manager = new ConfigManager({
      loaders: [new EnvLoader('', { nestedDelimiter: '_' })],
    });

    const config = await manager.initialize();

    // Should create nested structure
    expect((config as any).level1?.level2?.level3?.level4?.level5?.value).toBe('deep-value');
  });

  test('should handle conflicting data types in config merging', async () => {
    const configDir = join(TEST_DIR, 'config');
    mkdirSync(configDir, { recursive: true });

    // File config has object
    writeFileSync(
      join(configDir, 'development.json'),
      JSON.stringify({
        database: {
          host: 'localhost',
          port: 5432,
        },
      })
    );

    // Environment variable tries to override with string
    process.env.DATABASE = 'simple-string';

    const manager = new ConfigManager({
      loaders: [new FileLoader(configDir, 'development'), new EnvLoader('')],
    });

    const config = await manager.initialize(appConfigSchema);

    // Environment variable should win
    expect(config.database).toBe('simple-string');
  });

  test('should handle different working directories', async () => {
    // Create multiple config setups in different directories
    const dir1 = join(TEST_DIR, 'dir1');
    const dir2 = join(TEST_DIR, 'dir2');

    mkdirSync(join(dir1, 'config'), { recursive: true });
    mkdirSync(join(dir2, 'config'), { recursive: true });

    writeFileSync(
      join(dir1, 'config', 'development.json'),
      JSON.stringify({ app: { name: 'dir1-app' } })
    );

    writeFileSync(
      join(dir2, 'config', 'development.json'),
      JSON.stringify({ app: { name: 'dir2-app' } })
    );

    // Test from dir1
    process.chdir(dir1);
    resetConfig();
    let config = await initializeConfig();
    expect(config.app.name).toBe('dir1-app');

    // Test from dir2
    process.chdir(dir2);
    resetConfig();
    config = await initializeConfig();
    expect(config.app.name).toBe('dir2-app');
  });

  test('should handle malformed .env files', async () => {
    // Create malformed .env file
    writeFileSync(
      join(TEST_DIR, '.env'),
      `# Good line
VALID_KEY=valid_value
# Malformed lines
MISSING_VALUE=
=MISSING_KEY
SPACES IN KEY=value
KEY_WITH_QUOTES="quoted value"
KEY_WITH_SINGLE_QUOTES='single quoted'
# Complex value
JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
`
    );

    process.chdir(TEST_DIR);

    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    const _config = await manager.initialize();

    // Should handle valid entries
    expect(process.env.VALID_KEY).toBe('valid_value');
    expect(process.env.KEY_WITH_QUOTES).toBe('quoted value');
    expect(process.env.KEY_WITH_SINGLE_QUOTES).toBe('single quoted');
  });

  test('should handle empty config files', async () => {
    const configDir = join(TEST_DIR, 'config');
    mkdirSync(configDir, { recursive: true });

    // Create empty JSON file
    writeFileSync(join(configDir, 'development.json'), '{}');

    const manager = new ConfigManager({
      loaders: [new FileLoader(configDir, 'development')],
    });

    const config = await manager.initialize(appConfigSchema);
    expect(config).toBeDefined();
    expect(config.environment).toBe('development'); // Should have default
  });

  test('should handle config initialization without schema', async () => {
    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    // Initialize without schema
    const config = await manager.initialize();
    expect(config).toBeDefined();
    expect(typeof config).toBe('object');
  });

  test('should handle accessing config before initialization', () => {
    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    // Should throw error when accessing uninitialized config
    expect(() => manager.get()).toThrow('Configuration not initialized');
    expect(() => manager.getValue('some.path')).toThrow('Configuration not initialized');
    expect(() => manager.has('some.path')).toThrow('Configuration not initialized');
  });

  test('should handle invalid config paths in getValue', async () => {
    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    const _config = await manager.initialize(appConfigSchema);

    // Should throw for invalid paths
    expect(() => manager.getValue('nonexistent.path')).toThrow('Configuration key not found');
    expect(() => manager.getValue('app.nonexistent')).toThrow('Configuration key not found');

    // Should work for valid paths
    expect(() => manager.getValue('environment')).not.toThrow();
  });

  test('should handle null and undefined values in config', async () => {
    process.env.NULL_VALUE = 'null';
    process.env.UNDEFINED_VALUE = 'undefined';
    process.env.EMPTY_VALUE = '';

    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    const config = await manager.initialize();

    expect((config as any).null_value).toBe(null);
    expect((config as any).undefined_value).toBe(undefined);
    expect((config as any).empty_value).toBe('');
  });

  test('should handle schema validation failures', async () => {
    // Set up config that will fail schema validation
    process.env.APP_NAME = 'valid-name';
    process.env.APP_VERSION = 'valid-version';
    process.env.SERVICE_PORT = 'not-a-number'; // This should cause validation to fail

    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    await expect(manager.initialize(appConfigSchema)).rejects.toThrow(ConfigValidationError);
  });

  test('should handle config updates with invalid schema', async () => {
    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    await manager.initialize(appConfigSchema);

    // Try to update with invalid data
    expect(() => {
      manager.set({
        service: {
          port: 'invalid-port' as any,
        },
      });
    }).toThrow(ConfigValidationError);
  });

  test('should handle loader priority conflicts', async () => {
    const configDir = join(TEST_DIR, 'config');
    mkdirSync(configDir, { recursive: true });

    writeFileSync(
      join(configDir, 'development.json'),
      JSON.stringify({ app: { name: 'file-config' } })
    );

    process.env.APP_NAME = 'env-config';

    // Create loaders with different priorities
    const manager = new ConfigManager({
      loaders: [
        new FileLoader(configDir, 'development'), // priority 50
        new EnvLoader(''), // priority 100
      ],
    });

    const config = await manager.initialize(appConfigSchema);

    // Environment should win due to higher priority
    expect(config.app.name).toBe('env-config');
  });

  test('should handle readonly environment variables', async () => {
    // Some system environment variables might be readonly
    const originalPath = process.env.PATH;

    // This should not cause the loader to fail
    const manager = new ConfigManager({
      loaders: [new EnvLoader('')],
    });

    const config = await manager.initialize();
    expect(config).toBeDefined();

    // PATH should not be modified
    expect(process.env.PATH).toBe(originalPath);
  });
});
