import { existsSync, readFileSync } from 'fs';
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { ConfigLoaderError } from '../src/errors';
import { FileLoader } from '../src/loaders/file.loader';

// Mock fs module
mock.module('fs', () => ({
  existsSync: mock(() => false),
  readFileSync: mock(() => ''),
}));

describe('FileLoader', () => {
  let loader: FileLoader;
  const configPath = '/app/config';
  const environment = 'development';

  beforeEach(() => {
    // Reset mocks
    (existsSync as any).mockReset();
    (readFileSync as any).mockReset();
  });

  describe('constructor', () => {
    it('should have medium priority', () => {
      loader = new FileLoader(configPath, environment);
      expect(loader.priority).toBe(50);
    });

    it('should store config path and environment', () => {
      loader = new FileLoader('/custom/path', 'production');
      expect(loader).toBeDefined();
    });
  });

  describe('load', () => {
    it('should load only default.json when environment file does not exist', () => {
      const defaultConfig = {
        name: 'app',
        port: 3000,
        features: ['auth', 'cache'],
      };

      (existsSync as any).mockImplementation((path: string) => {
        return path.endsWith('default.json');
      });

      (readFileSync as any).mockImplementation((path: string) => {
        if (path.endsWith('default.json')) {
          return JSON.stringify(defaultConfig);
        }
        return '{}';
      });

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(existsSync).toHaveBeenCalledWith('/app/config/default.json');
      expect(existsSync).toHaveBeenCalledWith('/app/config/development.json');
      expect(readFileSync).toHaveBeenCalledWith('/app/config/default.json', 'utf-8');
      expect(config).toEqual(defaultConfig);
    });

    it('should load and merge default and environment configs', () => {
      const defaultConfig = {
        name: 'app',
        port: 3000,
        database: {
          host: 'localhost',
          port: 5432,
        },
      };

      const devConfig = {
        port: 3001,
        database: {
          host: 'dev-db',
        },
        debug: true,
      };

      (existsSync as any).mockReturnValue(true);

      (readFileSync as any).mockImplementation((path: string) => {
        if (path.endsWith('default.json')) {
          return JSON.stringify(defaultConfig);
        }
        if (path.endsWith('development.json')) {
          return JSON.stringify(devConfig);
        }
        return '{}';
      });

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(config).toEqual({
        name: 'app',
        port: 3001, // Overridden by dev config
        database: {
          host: 'dev-db', // Overridden by dev config
          port: 5432, // Preserved from default
        },
        debug: true, // Added by dev config
      });
    });

    it('should handle production environment', () => {
      const defaultConfig = { name: 'app', debug: true };
      const prodConfig = { debug: false, secure: true };

      (existsSync as any).mockReturnValue(true);

      (readFileSync as any).mockImplementation((path: string) => {
        if (path.endsWith('default.json')) {
          return JSON.stringify(defaultConfig);
        }
        if (path.endsWith('production.json')) {
          return JSON.stringify(prodConfig);
        }
        return '{}';
      });

      loader = new FileLoader(configPath, 'production');
      const config = loader.load();

      expect(existsSync).toHaveBeenCalledWith('/app/config/production.json');
      expect(config).toEqual({
        name: 'app',
        debug: false,
        secure: true,
      });
    });

    it('should return empty object when no config files exist', () => {
      (existsSync as any).mockReturnValue(false);

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(config).toEqual({});
      expect(readFileSync).not.toHaveBeenCalled();
    });

    it('should throw ConfigLoaderError on JSON parse error', () => {
      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockReturnValue('{ invalid json');

      loader = new FileLoader(configPath, environment);

      expect(() => loader.load()).toThrow(ConfigLoaderError);
      expect(() => loader.load()).toThrow('Failed to load configuration files');
    });

    it('should throw ConfigLoaderError on file read error', () => {
      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockImplementation(() => {
        throw new Error('Permission denied');
      });

      loader = new FileLoader(configPath, environment);

      expect(() => loader.load()).toThrow(ConfigLoaderError);
      expect(() => loader.load()).toThrow('Failed to load configuration files');
    });

    it('should handle different config paths', () => {
      const customPath = '/custom/config/dir';
      const config = { custom: true };

      (existsSync as any).mockImplementation((path: string) => {
        return path.startsWith(customPath);
      });

      (readFileSync as any).mockReturnValue(JSON.stringify(config));

      loader = new FileLoader(customPath, environment);
      loader.load();

      expect(existsSync).toHaveBeenCalledWith(`${customPath}/default.json`);
      expect(existsSync).toHaveBeenCalledWith(`${customPath}/development.json`);
    });
  });

  describe('deepMerge', () => {
    it('should handle null and undefined values', () => {
      const defaultConfig = {
        a: 'value',
        b: null,
        c: 'default',
      };

      const envConfig = {
        a: null,
        b: 'updated',
        // Note: undefined values are not preserved in JSON
      };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockImplementation((path: string) => {
        if (path.endsWith('default.json')) {
          return JSON.stringify(defaultConfig);
        }
        if (path.endsWith('development.json')) {
          return JSON.stringify(envConfig);
        }
        return '{}';
      });

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(config).toEqual({
        a: null,
        b: 'updated',
        c: 'default', // Preserved from default since envConfig doesn't have 'c'
      });
    });

    it('should handle arrays correctly', () => {
      const defaultConfig = {
        items: [1, 2, 3],
        features: ['auth', 'cache'],
      };

      const envConfig = {
        items: [4, 5],
        features: ['auth', 'cache', 'search'],
      };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockImplementation((path: string) => {
        if (path.endsWith('default.json')) {
          return JSON.stringify(defaultConfig);
        }
        if (path.endsWith('development.json')) {
          return JSON.stringify(envConfig);
        }
        return '{}';
      });

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      // Arrays should be replaced, not merged
      expect(config).toEqual({
        items: [4, 5],
        features: ['auth', 'cache', 'search'],
      });
    });

    it('should handle deeply nested objects', () => {
      const defaultConfig = {
        level1: {
          level2: {
            level3: {
              a: 1,
              b: 2,
            },
            c: 3,
          },
          d: 4,
        },
      };

      const envConfig = {
        level1: {
          level2: {
            level3: {
              b: 22,
              e: 5,
            },
            f: 6,
          },
        },
      };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockImplementation((path: string) => {
        if (path.endsWith('default.json')) {
          return JSON.stringify(defaultConfig);
        }
        if (path.endsWith('development.json')) {
          return JSON.stringify(envConfig);
        }
        return '{}';
      });

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(config).toEqual({
        level1: {
          level2: {
            level3: {
              a: 1,
              b: 22,
              e: 5,
            },
            c: 3,
            f: 6,
          },
          d: 4,
        },
      });
    });

    it('should handle Date and RegExp objects', () => {
      // Dates and RegExps in JSON are serialized as strings
      const defaultConfig = {
        createdAt: '2023-01-01T00:00:00.000Z',
        pattern: '/test/gi',
      };

      const envConfig = {
        updatedAt: '2023-06-01T00:00:00.000Z',
      };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockImplementation((path: string) => {
        if (path.endsWith('default.json')) {
          return JSON.stringify(defaultConfig);
        }
        if (path.endsWith('development.json')) {
          return JSON.stringify(envConfig);
        }
        return '{}';
      });

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(config).toEqual({
        createdAt: '2023-01-01T00:00:00.000Z',
        pattern: '/test/gi',
        updatedAt: '2023-06-01T00:00:00.000Z',
      });
    });
  });

  describe('edge cases', () => {
    it('should handle empty JSON files', () => {
      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockReturnValue('{}');

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(config).toEqual({});
    });

    it('should handle whitespace in JSON files', () => {
      const config = { test: 'value' };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockReturnValue(`  \n\t${JSON.stringify(config)}\n  `);

      loader = new FileLoader(configPath, environment);
      const result = loader.load();

      expect(result).toEqual(config);
    });

    it('should handle very large config files', () => {
      const largeConfig: Record<string, unknown> = {};
      for (let i = 0; i < 1000; i++) {
        largeConfig[`key_${i}`] = {
          value: i,
          nested: { data: `data_${i}` },
        };
      }

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockReturnValue(JSON.stringify(largeConfig));

      loader = new FileLoader(configPath, environment);
      const config = loader.load();

      expect(Object.keys(config)).toHaveLength(1000);
      expect(config.key_500).toEqual({
        value: 500,
        nested: { data: 'data_500' },
      });
    });

    it('should handle unicode in config values', () => {
      const config = {
        emoji: '🚀',
        chinese: '你好',
        arabic: 'مرحبا',
      };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockReturnValue(JSON.stringify(config));

      loader = new FileLoader(configPath, environment);
      const result = loader.load();

      expect(result).toEqual(config);
    });

    it('should handle config with circular reference patterns', () => {
      // JSON doesn't support circular references, but we can have
      // patterns that look circular
      const config = {
        parent: {
          child: {
            ref: 'parent',
          },
        },
      };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockReturnValue(JSON.stringify(config));

      loader = new FileLoader(configPath, environment);
      const result = loader.load();

      expect(result).toEqual(config);
    });

    it('should handle numeric string keys', () => {
      const config = {
        '123': 'numeric key',
        '456': { nested: 'value' },
      };

      (existsSync as any).mockReturnValue(true);
      (readFileSync as any).mockReturnValue(JSON.stringify(config));

      loader = new FileLoader(configPath, environment);
      const result = loader.load();

      expect(result).toEqual(config);
    });
  });
});
