import { promises as fs } from 'fs';
import { createConfig } from './index';

// Test JSON data
const jsonDefaults = {
  setting: 'defaultvalue',
  another: {
    setting: 'avalue',
  },
};

const jsonLocals = {
  setting: 'localvalue',
  another: {
    more: 'stuff',
  },
  even: {
    deeper: {
      key: 'sodeep',
    },
  },
};

describe('Config', () => {
  it('should only have values from config.defaults.json', async () => {
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    const config = createConfig();
    await fs.unlink('config.defaults.json');

    expect(config.setting).toBe('defaultvalue');
    expect(config.another.setting).toBe('avalue');
  });

  it('should only have values from config.json', async () => {
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));
    const config = createConfig();
    await fs.unlink('config.json');

    expect(config.setting).toBe('localvalue');
    expect(config.another.more).toBe('stuff');
  });

  it('should have both values with preference to config.json', async () => {
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));
    const config = createConfig();
    await fs.unlink('config.json');
    await fs.unlink('config.defaults.json');

    expect(config.setting).toBe('localvalue');
    expect(config.another.more).toBe('stuff');
    expect(config.another.setting).toBe('avalue');
  });

  it('should respect a given basePath', async () => {
    // Create the tmp directory only if it doesn't exist
    await fs.mkdir('tmp', { recursive: true });
    
    await fs.writeFile('tmp/config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('tmp/config.json', JSON.stringify(jsonLocals));
    
    const config = createConfig('tmp');
    
    await fs.unlink('tmp/config.json');
    await fs.unlink('tmp/config.defaults.json');
    
    // Remove the tmp directory after the test
    await fs.rmdir('tmp', { recursive: true });
  
    expect(config.setting).toBe('localvalue');
    expect(config.another.more).toBe('stuff');
    expect(config.another.setting).toBe('avalue');
  });

  it('should have all values with preference to env', async () => {
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));

    process.env['SETTING'] = 'overwritten-by-env';
    process.env['ANOTHER_MORE'] = 'false';
    process.env['EVEN_DEEPER_KEY'] = 'true';

    const config = createConfig();

    await fs.unlink('config.json');
    await fs.unlink('config.defaults.json');

    delete process.env['SETTING'];
    delete process.env['ANOTHER_MORE'];
    delete process.env['EVEN_DEEPER_KEY'];

    expect(config.setting).toBe('overwritten-by-env');
    expect(config.another.more).toBe(false);
    expect(config.even.deeper.key).toBe(true);
    expect(config.another.setting).toBe('avalue');
  });

  it('should have all values with preference to env and prefix', async () => {
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));

    process.env['P_SETTING'] = 'overwritten-by-env';
    process.env['P_ANOTHER_MORE'] = 'false';
    process.env['P_EVEN_DEEPER_KEY'] = 'true';

    const config = createConfig(undefined, 'p');

    await fs.unlink('config.json');
    await fs.unlink('config.defaults.json');

    delete process.env['P_SETTING'];
    delete process.env['P_ANOTHER_MORE'];
    delete process.env['P_EVEN_DEEPER_KEY'];

    expect(config.setting).toBe('overwritten-by-env');
    expect(config.another.more).toBe(false);
    expect(config.even.deeper.key).toBe(true);
    expect(config.another.setting).toBe('avalue');
  });

  it('should read in a file when given', async () => {
    jsonLocals.setting = 'file:file.txt';
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));
    await fs.writeFile('file.txt', 'value-from-file');

    const config = createConfig();

    await fs.unlink('config.json');
    await fs.unlink('config.defaults.json');
    await fs.unlink('file.txt');
    jsonLocals.setting = 'localvalue';

    expect(config.setting).toBe('value-from-file');
  });

  it('should show a message and keep the setting when file given but not found', async () => {
    jsonLocals.setting = 'file:file.txt';
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));

    const config = createConfig();

    await fs.unlink('config.json');
    await fs.unlink('config.defaults.json');
    jsonLocals.setting = 'localvalue';

    expect(config.setting).toBe('file:file.txt');
  });

  it('should _reload if asked', async () => {
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));

    const config = createConfig();

    expect(config.setting).toBe('localvalue');

    jsonLocals.setting = 'reloaded-value';
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));

    config._reload();

    await fs.unlink('config.json');
    await fs.unlink('config.defaults.json');
    jsonLocals.setting = 'localvalue';

    expect(config.setting).toBe('reloaded-value');
  });

  it('should redact passwords if using _show', async () => {
    jsonLocals.even.deeper.key = 'password123';
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));

    const config = createConfig();

    await fs.unlink('config.json');
    await fs.unlink('config.defaults.json');
    jsonLocals.even.deeper.key = 'sodeep';

    expect(config._show().even.deeper.key).toBe('REDACTED');
  });

  it('should handle missing config.defaults.json gracefully', async () => {
    // Ensure config.defaults.json does not exist
    await fs.unlink('config.defaults.json').catch(() => {}); // Ignore errors if the file doesn't exist
  
    const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
  
    const config = createConfig();
  
    // Ensure no crash and empty config
    expect(config.setting).toBeUndefined();
  
    // Check if the missing file message was logged
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining(`Defaults file missing`)
    );
  
    consoleSpy.mockRestore();
  });

  it('should handle missing config.json gracefully', async () => {
    // Create config.defaults.json but ensure config.json does not exist
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.unlink('config.json').catch(() => {});
  
    const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
  
    const config = createConfig();
    await fs.unlink('config.defaults.json');
  
    // Expect log message for the missing file
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('local file missing')
    );
  
    // Defaults should still be loaded
    expect(config.setting).toBe('defaultvalue');
    expect(config.another.setting).toBe('avalue');

    consoleSpy.mockRestore();
  });

  it('should handle invalid JSON content in config.defaults.json gracefully', async () => {
    // Write invalid JSON to config.defaults.json
    await fs.writeFile('config.defaults.json', '{ invalid json }');
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));

    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
  
    const config = createConfig();

    // Expect a log message for invalid JSON
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('Error reading defaults file')
    );
  
    // Clean up files
    await fs.unlink('config.defaults.json');
    await fs.unlink('config.json');
  
  
    // Expect config.json to still load successfully
    expect(config.setting).toBe('localvalue');
    expect(config.another.more).toBe('stuff');

    consoleSpy.mockRestore();
  });

  it('should handle invalid JSON content in config.json gracefully', async () => {
    // Write invalid JSON to config.json
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', '{ invalid json }');

    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
  
    const config = createConfig();

    // Expect a log message for invalid JSON
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('Error reading local config file')
    );
  
    // Clean up files
    await fs.unlink('config.defaults.json');
    await fs.unlink('config.json');
  
    // Expect defaults to still load successfully
    expect(config.setting).toBe('defaultvalue');
    expect(config.another.setting).toBe('avalue');

    consoleSpy.mockRestore();
  });

  it('should serialize only configData using toJSON()', async () => {
    // Create test configuration files
    await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults));
    await fs.writeFile('config.json', JSON.stringify(jsonLocals));
  
    const config = createConfig();
  
    // Serialize the config object to JSON
    const serializedConfig = JSON.stringify(config);
  
    // Clean up test files
    await fs.unlink('config.defaults.json');
    await fs.unlink('config.json');
  
    // Parse the serialized JSON
    const parsedConfig = JSON.parse(serializedConfig);
  
    // Check that all expected configData properties are present
    expect(parsedConfig.setting).toBe('localvalue');
    expect(parsedConfig.another.setting).toBe('avalue'); // From defaults
    expect(parsedConfig.another.more).toBe('stuff'); // From local
    expect(parsedConfig.even.deeper.key).toBe('sodeep'); // From local
  
    // Ensure internal properties are not present
    expect(parsedConfig).not.toHaveProperty('configDefaults');
    expect(parsedConfig).not.toHaveProperty('configLocal');
    expect(parsedConfig).not.toHaveProperty('envPrefix');
  });

  it('should log an error if a file referenced by "file:" is missing', async () => {
    // Write test configuration file
    await fs.writeFile(
      'config.json',
      JSON.stringify({
        setting: 'file:nonexistent-file.txt',
      })
    );
  
    // Spy on console.error
    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
  
    const config = createConfig();
  
    // Clean up test files
    await fs.unlink('config.json');
  
    // Ensure the original "file:" reference remains unchanged
    expect(config.setting).toBe('file:nonexistent-file.txt');
  
    // Ensure error is logged for the missing file
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('An unknown error occurred')
    );
  
    consoleSpy.mockRestore();
  });

});