const RealEngine = require('../../../lib/engine');
const flags = require('../../../lib/flags');
const { LEVELS } = require('../../../lib/logger');

const TYPE_ERROR = 'TypeError';

// Wrapper to prevent reuse of the event bus, which leads to `MaxListenersExceededWarning` messages.
class Engine extends RealEngine {
  constructor(opts) {
    opts = opts || {};
    opts._cacheEventBus = false;

    super(opts);
  }
}

describe('@jsdoc/cli/lib/engine', () => {
  it('exists', () => {
    expect(Engine).toBeFunction();
  });

  it('works with no input', () => {
    expect(() => new Engine()).not.toThrow();
  });

  it('has a static LOG_LEVELS property', () => {
    expect(Engine.LOG_LEVELS).toBeObject();
  });

  it('has an empty array of flags by default', () => {
    expect(new Engine().flags).toBeEmptyArray();
  });

  it('has a property that contains the known flags', () => {
    expect(new Engine().knownFlags).toBe(flags);
  });

  it('has a logLevel property that defaults to LEVELS.WARN', () => {
    expect(new Engine().logLevel).toBe(LEVELS.WARN);
  });

  it('has an undefined revision property by default', () => {
    expect(new Engine().revision).toBeUndefined();
  });

  it('has an undefined version property by default', () => {
    expect(new Engine().version).toBeUndefined();
  });

  it('has a versionDetails property that is an empty string by default', () => {
    expect(new Engine().versionDetails).toBeEmptyString();
  });

  it('throws if the input is not an object', () => {
    expect(() => new Engine('hi')).toThrow();
  });

  it('sets the logLevel if provided', () => {
    const logLevel = LEVELS.VERBOSE;
    const instance = new Engine({ logLevel });

    expect(instance.logLevel).toBe(logLevel);
  });

  it('throws if the logLevel is invalid', () => {
    const logLevel = LEVELS.VERBOSE + 1;

    expect(() => new Engine({ logLevel })).toThrowErrorOfType(TYPE_ERROR);
  });

  it('sets the revision if provided', () => {
    const revision = new Date();
    const instance = new Engine({ revision });

    expect(instance.revision).toBe(revision);
  });

  it('throws if the revision is not a date', () => {
    expect(() => new Engine({ revision: '1' })).toThrow();
  });

  it('sets the version if provided', () => {
    expect(new Engine({ version: '1.2.3' }).version).toBe('1.2.3');
  });

  it('throws if the version is not a string', () => {
    expect(() => new Engine({ version: 1 })).toThrow();
  });

  describe('help', () => {
    const instance = new Engine();

    it('works with no input', () => {
      expect(() => instance.help()).not.toThrow();
    });

    it('throws on bad input', () => {
      expect(() => instance.help('hi')).toThrow();
    });

    it('returns a string', () => {
      expect(instance.help()).toBeNonEmptyString();
    });

    it('honors a reasonable maxLength option', () => {
      const max = 70;
      const help = instance.help({ maxLength: max }).split('\n');

      for (let line of help) {
        expect(line.length).toBeLessThanOrEqualTo(max);
      }
    });

    it('throws on a bad maxLength option', () => {
      expect(() => instance.help({ maxLength: 'long' })).toThrow();
    });
  });

  describe('LOG_LEVELS', () => {
    it('is lib/logger.LEVELS', () => {
      expect(Engine.LOG_LEVELS).toBe(LEVELS);
    });
  });

  describe('parseFlags', () => {
    it('throws with no input', () => {
      expect(() => new Engine().parseFlags()).toThrow();
    });

    it('throws if the input is not an array', () => {
      expect(() => new Engine().parseFlags({ foo: 'bar' })).toThrow();
    });

    it('parses flags with no values', () => {
      expect(new Engine().parseFlags(['--help']).help).toBeTrue();
    });

    it('parses flags with values', () => {
      const parsed = new Engine().parseFlags(['--configure', 'conf.json']);

      expect(parsed.configure).toBe('conf.json');
    });

    it('stores the flags in the `flags` property', () => {
      const instance = new Engine();

      instance.parseFlags(['--help']);

      expect(instance.flags.help).toBeTrue();
    });

    it('throws on unrecognized flags', () => {
      expect(() => new Engine().parseFlags(['--notarealflag'])).toThrow();
    });

    it('throws on invalid flag values', () => {
      expect(() => new Engine().parseFlags(['--access', 'maybe'])).toThrow();
    });

    it('includes the long and short name in the error if a value is invalid', () => {
      let error;

      try {
        new Engine().parseFlags(['--access', 'just-this-once']);
      } catch (e) {
        error = e;
      }

      expect(error.message).toContain('-a/--access');
    });

    it('includes the allowed values in the error if a value is invalid', () => {
      let error;

      try {
        new Engine().parseFlags(['--access', 'maybe-later']);
      } catch (e) {
        error = e;
      }

      expect(error.message).toContain(flags.access.choices.join(', '));
    });

    it('throws if a required value is missing', () => {
      expect(() => new Engine().parseFlags(['--template'])).toThrow();
    });

    it('always uses the long flag name in the parsed flags', () => {
      expect(new Engine().parseFlags(['-h']).help).toBeTrue();
    });

    it('coerces values to other types when appropriate', () => {
      const parsed = new Engine().parseFlags(['--query', 'foo=bar&baz=true']);

      expect(parsed.query).toEqual({
        foo: 'bar',
        baz: true,
      });
    });
  });

  describe('versionDetails', () => {
    it('works with a version but no revision', () => {
      const instance = new Engine({ version: '1.2.3' });

      expect(instance.versionDetails).toBe('JSDoc 1.2.3');
    });

    it('contains an empty string with a revision but no version', () => {
      const revision = new Date();
      const instance = new Engine({ revision });

      expect(instance.versionDetails).toBeEmptyString();
    });

    it('works with a version and a revision', () => {
      const revision = new Date();
      const instance = new Engine({
        version: '1.2.3',
        revision,
      });

      expect(instance.versionDetails).toBe(`JSDoc 1.2.3 (${revision.toUTCString()})`);
    });
  });
});
