/**
 * ConfigManager unit tests
 * Tests comprehensive configuration management with multiple sources and validation
 */

import { ConfigManager, A11yAnalyzeConfig, ConfigLoadResult, ConfigSource } from './config-manager';
import { writeFileSync, unlinkSync, existsSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';

// Mock filesystem operations for testing
jest.mock('fs', () => ({
  readFileSync: jest.fn(),
  writeFileSync: jest.fn(),
  existsSync: jest.fn(),
  unlinkSync: jest.fn(),
  mkdirSync: jest.fn(),
  rmSync: jest.fn(),
}));

jest.mock('path', () => ({
  ...jest.requireActual('path'),
  resolve: jest.fn(),
  join: jest.fn(),
}));

jest.mock('os', () => ({
  homedir: jest.fn(() => '/home/user'),
}));

const mockFs = require('fs');
const mockPath = require('path');

describe('ConfigManager', () => {
  let configManager: ConfigManager;
  let originalEnv: NodeJS.ProcessEnv;

  beforeEach(() => {
    configManager = new ConfigManager();
    originalEnv = { ...process.env };
    
    // Reset mocks
    jest.clearAllMocks();
    
    // Setup default mock behaviors
    mockFs.existsSync.mockReturnValue(false);
    mockPath.resolve.mockImplementation((...args: string[]) => args.join('/'));
    mockPath.join.mockImplementation((...args: string[]) => args.join('/'));
  });

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

  describe('Default Configuration', () => {
    it('should provide sensible default configuration', async () => {
      const result = await configManager.loadConfig();
      
      expect(result.config).toBeDefined();
      expect(result.config.scanning.wcagLevel).toBe('AA');
      expect(result.config.scanning.includeAAA).toBe(false);
      expect(result.config.scanning.includeARIA).toBe(true);
      expect(result.config.scanning.timeout).toBe(30000);
      expect(result.config.browser.headless).toBe(true);
      expect(result.config.browser.viewport.width).toBe(1280);
      expect(result.config.browser.viewport.height).toBe(720);
      expect(result.config.crawling.maxDepth).toBe(2);
      expect(result.config.crawling.maxPages).toBe(50);
      expect(result.config.output.format).toBe('console');
      expect(result.config.scoring.profile).toBe('balanced');
    });

         it('should include only default source when no other config found', async () => {
       const result = await configManager.loadConfig();
       
       expect(result.sources).toHaveLength(1);
       expect(result.sources[0]?.type).toBe('defaults');
       expect(result.sources[0]?.loaded).toBe(true);
       expect(result.sources[0]?.priority).toBe(0);
     });

    it('should provide static access to default config', () => {
      const defaultConfig = ConfigManager.getDefaultConfig();
      
      expect(defaultConfig.scanning.wcagLevel).toBe('AA');
      expect(defaultConfig.browser.headless).toBe(true);
      expect(defaultConfig.scoring.profile).toBe('balanced');
    });
  });

  describe('Configuration File Loading', () => {
    it('should load JSON configuration file', async () => {
      const configData = {
        scanning: {
          wcagLevel: 'AAA',
          timeout: 45000,
        },
        output: {
          format: 'json',
          verbose: true,
        },
      };

      mockFs.existsSync.mockImplementation((path: string) => 
        path.includes('.a11yanalyzerc.json')
      );
      mockFs.readFileSync.mockReturnValue(JSON.stringify(configData));

      const result = await configManager.loadConfig();

      expect(result.config.scanning.wcagLevel).toBe('AAA');
      expect(result.config.scanning.timeout).toBe(45000);
      expect(result.config.output.format).toBe('json');
      expect(result.config.output.verbose).toBe(true);
      expect(result.sources.some(s => s.type === 'file')).toBe(true);
    });

    it('should load JavaScript configuration file', async () => {
      const configPath = '/test/.a11yanalyzerc.js';
      
      mockFs.existsSync.mockImplementation((path: string) => 
        path === configPath
      );

      // Mock require for JS config
      const mockConfig = {
        scanning: { wcagLevel: 'A' },
        browser: { headless: false },
      };
      
      const originalRequire = require;
      (global as any).require = jest.fn().mockImplementation((path: string) => {
        if (path === configPath) return mockConfig;
        return originalRequire(path);
      });

             // Mock require.cache and require.resolve
       require.cache = {};
       (require.resolve as any) = jest.fn().mockReturnValue(configPath);

      try {
        const result = await configManager.loadConfig(['/test']);

        expect(result.config.scanning.wcagLevel).toBe('A');
        expect(result.config.browser.headless).toBe(false);
      } finally {
        (global as any).require = originalRequire;
      }
    });

    it('should load configuration from package.json', async () => {
      const packageData = {
        name: 'test-project',
        a11yanalyze: {
          scanning: {
            wcagLevel: 'AAA',
            includeAAA: true,
          },
          scoring: {
            profile: 'strict',
          },
        },
      };

      mockFs.existsSync.mockImplementation((path: string) => 
        path.includes('package.json')
      );
      mockFs.readFileSync.mockReturnValue(JSON.stringify(packageData));

      const result = await configManager.loadConfig();

      expect(result.config.scanning.wcagLevel).toBe('AAA');
      expect(result.config.scanning.includeAAA).toBe(true);
      expect(result.config.scoring.profile).toBe('strict');
    });

    it('should handle file loading errors gracefully', async () => {
      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockImplementation(() => {
        throw new Error('File read error');
      });

      const result = await configManager.loadConfig();

      expect(result.errors.length).toBeGreaterThan(0);
      expect(result.errors[0]).toContain('File read error');
      expect(result.config).toBeDefined(); // Should fall back to defaults
    });

    it('should handle invalid JSON gracefully', async () => {
      mockFs.existsSync.mockImplementation((path: string) => 
        path.includes('.a11yanalyzerc.json')
      );
      mockFs.readFileSync.mockReturnValue('{ invalid json }');

      const result = await configManager.loadConfig();

      expect(result.errors.length).toBeGreaterThan(0);
      expect(result.config).toBeDefined(); // Should fall back to defaults
    });
  });

  describe('Environment Variable Loading', () => {
    it('should load configuration from environment variables', async () => {
      process.env.A11Y_WCAG_LEVEL = 'AAA';
      process.env.A11Y_INCLUDE_AAA = 'true';
      process.env.A11Y_TIMEOUT = '60000';
      process.env.A11Y_HEADLESS = 'false';
      process.env.A11Y_VIEWPORT_WIDTH = '1920';
      process.env.A11Y_VIEWPORT_HEIGHT = '1080';
      process.env.A11Y_OUTPUT_FORMAT = 'json';
      process.env.A11Y_VERBOSE = 'true';

      const result = await configManager.loadConfig();

      expect(result.config.scanning.wcagLevel).toBe('AAA');
      expect(result.config.scanning.includeAAA).toBe(true);
      expect(result.config.scanning.timeout).toBe(60000);
      expect(result.config.browser.headless).toBe(false);
      expect(result.config.browser.viewport.width).toBe(1920);
      expect(result.config.browser.viewport.height).toBe(1080);
      expect(result.config.output.format).toBe('json');
      expect(result.config.output.verbose).toBe(true);
      expect(result.sources.some(s => s.type === 'environment')).toBe(true);
    });

    it('should parse different environment variable types correctly', async () => {
      process.env.A11Y_INCLUDE_AAA = 'true';
      process.env.A11Y_TIMEOUT = '45000';
      process.env.A11Y_VIEWPORT_WIDTH = '1600';

      const result = await configManager.loadConfig();

      expect(typeof result.config.scanning.includeAAA).toBe('boolean');
      expect(typeof result.config.scanning.timeout).toBe('number');
      expect(typeof result.config.browser.viewport.width).toBe('number');
      expect(result.config.scanning.includeAAA).toBe(true);
      expect(result.config.scanning.timeout).toBe(45000);
      expect(result.config.browser.viewport.width).toBe(1600);
    });

    it('should handle invalid environment variables with warnings', async () => {
      process.env.A11Y_TIMEOUT = 'invalid-number';

      const result = await configManager.loadConfig();

      expect(result.warnings.length).toBeGreaterThan(0);
      expect(result.warnings.some(w => w.includes('A11Y_TIMEOUT'))).toBe(true);
    });
  });

  describe('CLI Options Loading', () => {
    it('should load configuration from CLI options', async () => {
      const cliOptions = {
        wcagLevel: 'AAA',
        includeAaa: true,
        timeout: '45000',
        viewport: '1920x1080',
        format: 'json',
        verbose: true,
        profile: 'strict',
      };

      const result = await configManager.loadConfig([], cliOptions);

      expect(result.config.scanning.wcagLevel).toBe('AAA');
      expect(result.config.scanning.includeAAA).toBe(true);
      expect(result.config.scanning.timeout).toBe(45000);
      expect(result.config.browser.viewport.width).toBe(1920);
      expect(result.config.browser.viewport.height).toBe(1080);
      expect(result.config.output.format).toBe('json');
      expect(result.config.output.verbose).toBe(true);
      expect(result.config.scoring.profile).toBe('strict');
      expect(result.sources.some(s => s.type === 'cli')).toBe(true);
    });

    it('should parse viewport string correctly', async () => {
      const cliOptions = {
        viewport: '1600x900',
      };

      const result = await configManager.loadConfig([], cliOptions);

      expect(result.config.browser.viewport.width).toBe(1600);
      expect(result.config.browser.viewport.height).toBe(900);
    });

    it('should handle invalid viewport format gracefully', async () => {
      const cliOptions = {
        viewport: 'invalid-format',
      };

      const result = await configManager.loadConfig([], cliOptions);

      // Should not crash and should use defaults for viewport
      expect(result.config.browser.viewport.width).toBe(1280);
      expect(result.config.browser.viewport.height).toBe(720);
    });
  });

  describe('Configuration Priority and Merging', () => {
    it('should apply configuration sources in correct priority order', async () => {
      // Setup file config
      const fileConfig = {
        scanning: { wcagLevel: 'A', timeout: 20000 },
        output: { format: 'json' },
      };
      mockFs.existsSync.mockImplementation((path: string) => 
        path.includes('.a11yanalyzerc.json')
      );
      mockFs.readFileSync.mockReturnValue(JSON.stringify(fileConfig));

      // Setup environment config
      process.env.A11Y_WCAG_LEVEL = 'AAA';
      process.env.A11Y_VERBOSE = 'true';

      // Setup CLI config
      const cliOptions = {
        wcagLevel: 'AA',
        quiet: true,
      };

      const result = await configManager.loadConfig([], cliOptions);

      // CLI should override everything
      expect(result.config.scanning.wcagLevel).toBe('AA');
      expect(result.config.output.quiet).toBe(true);

      // Environment should override file but not CLI
      expect(result.config.output.verbose).toBe(true);

      // File should override defaults but not environment/CLI
      expect(result.config.output.format).toBe('json');
      expect(result.config.scanning.timeout).toBe(20000);

      // Verify source priorities
      const sources = result.sources;
      const defaultSource = sources.find(s => s.type === 'defaults');
      const fileSource = sources.find(s => s.type === 'file');
      const envSource = sources.find(s => s.type === 'environment');
      const cliSource = sources.find(s => s.type === 'cli');

      expect(defaultSource?.priority).toBe(0);
      expect(fileSource?.priority).toBe(1);
      expect(envSource?.priority).toBe(2);
      expect(cliSource?.priority).toBe(3);
    });

    it('should deeply merge nested configuration objects', async () => {
      const fileConfig = {
        browser: {
          headless: false,
          viewport: { width: 1600 },
        },
        scanning: {
          wcagLevel: 'A',
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(fileConfig));

      const cliOptions = {
        timeout: '45000',
      };

      const result = await configManager.loadConfig([], cliOptions);

      // Should merge browser config
      expect(result.config.browser.headless).toBe(false); // From file
      expect(result.config.browser.viewport.width).toBe(1600); // From file
      expect(result.config.browser.viewport.height).toBe(720); // From default
      expect(result.config.browser.userAgent).toContain('A11yAnalyze'); // From default

      // Should merge scanning config
      expect(result.config.scanning.wcagLevel).toBe('A'); // From file
      expect(result.config.scanning.timeout).toBe(45000); // From CLI
      expect(result.config.scanning.includeARIA).toBe(true); // From default
    });
  });

  describe('Configuration Validation', () => {
    it('should validate WCAG level', async () => {
      const cliOptions = {
        wcagLevel: 'INVALID_LEVEL',
      };

      const result = await configManager.loadConfig([], cliOptions);

      expect(result.errors.some(e => e.includes('Invalid WCAG level'))).toBe(true);
    });

    it('should validate scoring profile', async () => {
      const cliOptions = {
        profile: 'invalid_profile',
      };

      const result = await configManager.loadConfig([], cliOptions);

      expect(result.errors.some(e => e.includes('Invalid scoring profile'))).toBe(true);
    });

    it('should validate timeout values', async () => {
      const fileConfig = {
        scanning: { timeout: -1000 },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(fileConfig));

      const result = await configManager.loadConfig();

      expect(result.errors.some(e => e.includes('timeout must be greater than 0'))).toBe(true);
    });

    it('should validate viewport dimensions', async () => {
      const fileConfig = {
        browser: {
          viewport: { width: -100, height: 0 },
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(fileConfig));

      const result = await configManager.loadConfig();

      expect(result.errors.some(e => e.includes('viewport dimensions must be greater than 0'))).toBe(true);
    });

    it('should validate crawling configuration', async () => {
      const fileConfig = {
        crawling: {
          maxDepth: -5,
          maxConcurrency: 0,
          maxPages: -10,
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(fileConfig));

      const result = await configManager.loadConfig();

      expect(result.errors.some(e => e.includes('depth cannot be negative'))).toBe(true);
      expect(result.errors.some(e => e.includes('concurrency must be greater than 0'))).toBe(true);
      expect(result.warnings.some(w => w.includes('Max pages is 0 or negative'))).toBe(true);
    });
  });

  describe('Configuration Generation and Saving', () => {
    it('should generate sample JSON configuration', () => {
      const jsonConfig = configManager.generateSampleConfig('json');
      
      expect(() => JSON.parse(jsonConfig)).not.toThrow();
      const parsed = JSON.parse(jsonConfig);
      expect(parsed.scanning).toBeDefined();
      expect(parsed.browser).toBeDefined();
      expect(parsed.output).toBeDefined();
    });

    it('should generate sample JavaScript configuration', () => {
      const jsConfig = configManager.generateSampleConfig('js');
      
      expect(jsConfig).toContain('module.exports = {');
      expect(jsConfig).toContain('scanning');
      expect(jsConfig).toContain('browser');
    });

    it('should generate sample YAML configuration', () => {
      const yamlConfig = configManager.generateSampleConfig('yaml');
      
      expect(yamlConfig).toContain('scanning:');
      expect(yamlConfig).toContain('browser:');
      expect(yamlConfig).toContain('wcagLevel:');
    });

    it('should save configuration to file', async () => {
      const customConfig = {
        scanning: { wcagLevel: 'AAA' as const },
        output: { format: 'json' as const },
      };

      await configManager.saveConfig(customConfig, 'test-config.json');

      expect(mockFs.writeFileSync).toHaveBeenCalledWith(
        'test-config.json',
        expect.stringContaining('"wcagLevel": "AAA"'),
        'utf8'
      );
    });

    it('should handle save errors gracefully', async () => {
      mockFs.writeFileSync.mockImplementation(() => {
        throw new Error('Write failed');
      });

      await expect(configManager.saveConfig({}, 'test.json')).rejects.toThrow('Failed to save configuration');
    });
  });

  describe('Configuration Access and State', () => {
    it('should return null when no config is loaded', () => {
      const config = configManager.getConfig();
      expect(config).toBeNull();
    });

    it('should return loaded config after loading', async () => {
      await configManager.loadConfig();
      const config = configManager.getConfig();
      
      expect(config).toBeDefined();
      expect(config?.scanning.wcagLevel).toBe('AA');
    });

    it('should return configuration sources information', async () => {
      const result = await configManager.loadConfig();
      const sources = configManager.getConfigSources();
      
             expect(sources).toEqual(result.sources);
       expect(sources.length).toBeGreaterThan(0);
       expect(sources[0]?.type).toBe('defaults');
    });

    it('should preserve configuration between calls', async () => {
      await configManager.loadConfig();
      const config1 = configManager.getConfig();
      const config2 = configManager.getConfig();
      
      expect(config1).toBe(config2); // Same reference
    });
  });

  describe('Error Handling and Edge Cases', () => {
    it('should handle empty configuration files', async () => {
      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue('{}');

      const result = await configManager.loadConfig();

      expect(result.config).toBeDefined();
      expect(result.errors).toHaveLength(0);
      // Should use defaults for missing values
      expect(result.config.scanning.wcagLevel).toBe('AA');
    });

    it('should handle malformed package.json', async () => {
      mockFs.existsSync.mockImplementation((path: string) => 
        path.includes('package.json')
      );
      mockFs.readFileSync.mockReturnValue('{ "name": "test" }'); // No a11yanalyze key

      const result = await configManager.loadConfig();

      expect(result.config).toBeDefined();
      expect(result.config.scanning.wcagLevel).toBe('AA'); // Should use defaults
    });

    it('should handle empty search paths gracefully', async () => {
      const result = await configManager.loadConfig([]);

      expect(result.config).toBeDefined();
      expect(result.sources.some(s => s.type === 'defaults')).toBe(true);
    });

    it('should handle undefined CLI options gracefully', async () => {
      const cliOptions = {
        wcagLevel: undefined,
        timeout: null,
        verbose: false,
      };

      const result = await configManager.loadConfig([], cliOptions);

      expect(result.config).toBeDefined();
      expect(result.config.scanning.wcagLevel).toBe('AA'); // Should use default
      expect(result.config.output.verbose).toBe(false); // Should use CLI value
    });

    it('should recover from total configuration failure', async () => {
      // Simulate complete failure
      mockFs.existsSync.mockImplementation(() => {
        throw new Error('Filesystem error');
      });

      const result = await configManager.loadConfig();

      expect(result.errors.length).toBeGreaterThan(0);
      expect(result.config).toBeDefined();
      expect(result.config.scanning.wcagLevel).toBe('AA'); // Should fall back to defaults
    });
  });
}); 