const plugin = jest.fn();
const scopedPlugin = jest.fn();

jest.mock('commitlint-plugin-example', () => plugin, {virtual: true});
jest.mock('@scope/commitlint-plugin-example', () => scopedPlugin, {
	virtual: true,
});

import path from 'path';
import resolveFrom from 'resolve-from';
import {fix, git, npm} from '@commitlint/test';

import load from './load';

const fixBootstrap = (name: string) => fix.bootstrap(name, __dirname);
const gitBootstrap = (name: string) => git.bootstrap(name, __dirname);
const npmBootstrap = (name: string) => npm.bootstrap(name, __dirname);

test('extends-empty should have no rules', async () => {
	const cwd = await gitBootstrap('fixtures/extends-empty');
	const actual = await load({}, {cwd});

	expect(actual.rules).toMatchObject({});
});

test('uses seed as configured', async () => {
	const cwd = await gitBootstrap('fixtures/extends-empty');
	const rules = {'body-case': [1, 'never', 'camel-case'] as any};

	const actual = await load({rules}, {cwd});

	expect(actual.rules['body-case']).toStrictEqual([1, 'never', 'camel-case']);
});

test('rules should be loaded from relative config file', async () => {
	const file = 'config/commitlint.config.js';
	const cwd = await gitBootstrap('fixtures/specify-config-file');
	const rules = {'body-case': [1, 'never', 'camel-case'] as any};

	const actual = await load({rules}, {cwd, file});

	expect(actual.rules['body-case']).toStrictEqual([1, 'never', 'camel-case']);
});

test('rules should be loaded from absolute config file', async () => {
	const cwd = await gitBootstrap('fixtures/specify-config-file');
	const file = path.resolve(cwd, 'config/commitlint.config.js');
	const rules = {'body-case': [1, 'never', 'camel-case'] as any};

	const actual = await load({rules}, {cwd: process.cwd(), file});

	expect(actual.rules['body-case']).toStrictEqual([1, 'never', 'camel-case']);
});

test('plugins should be loaded from seed', async () => {
	const cwd = await gitBootstrap('fixtures/extends-empty');
	const actual = await load({plugins: ['example', '@scope/example']}, {cwd});

	expect(actual.plugins).toMatchObject({
		example: plugin,
		'@scope/example': scopedPlugin,
	});
});

test('plugins should be loaded from local', async () => {
	const actual = await load({
		plugins: [
			{
				rules: {
					test: () => [true, 'asd'],
				},
			},
		],
	});

	expect(actual.plugins).toEqual(
		expect.objectContaining({
			local: {
				rules: {
					test: expect.any(Function),
				},
			},
		})
	);
});

test('plugins should be loaded from config', async () => {
	const cwd = await gitBootstrap('fixtures/extends-plugins');
	const actual = await load({}, {cwd});

	expect(actual.plugins).toMatchObject({
		example: plugin,
		'@scope/example': scopedPlugin,
	});
});

test('plugins should be loaded from shareable config', async () => {
	const cwd = await gitBootstrap('fixtures/extends-with-plugins');
	const actual = await load({}, {cwd});

	expect(actual.plugins).toMatchObject({
		example: plugin,
		'@scope/example': scopedPlugin,
	});
});

test('local plugins should be loaded from shareable configs', async () => {
	const cwd = await gitBootstrap('fixtures/extends-with-local-plugins');
	const actual = await load({}, {cwd});

	expect(actual.plugins).toEqual(
		expect.objectContaining({
			local: {
				rules: {
					'hello-world-rule': expect.any(Function),
					'is-positive': expect.any(Function),
				},
			},
		})
	);
});

test('uses seed with parserPreset', async () => {
	const cwd = await gitBootstrap('fixtures/parser-preset');
	const {parserPreset: actual} = await load(
		{parserPreset: './conventional-changelog-custom'},
		{cwd}
	);

	expect(actual.name).toBe('./conventional-changelog-custom');
	expect(actual.parserOpts).toMatchObject({
		headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/,
	});
});

test('invalid extend should throw', async () => {
	const cwd = await gitBootstrap('fixtures/extends-invalid');

	await expect(load({}, {cwd})).rejects.toThrow();
});

test('empty file should have no rules', async () => {
	const cwd = await gitBootstrap('fixtures/empty-object-file');
	const actual = await load({}, {cwd});

	expect(actual.rules).toMatchObject({});
});

test('empty file should extend nothing', async () => {
	const cwd = await gitBootstrap('fixtures/empty-file');
	const actual = await load({}, {cwd});

	expect(actual.extends).toHaveLength(0);
});

test('respects cwd option', async () => {
	const cwd = await gitBootstrap('fixtures/recursive-extends/first-extended');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: ['./second-extended'],
		plugins: {},
		rules: {
			one: 1,
			two: 2,
		},
	});
});

test('recursive extends', async () => {
	const cwd = await gitBootstrap('fixtures/recursive-extends');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: ['./first-extended'],
		plugins: {},
		rules: {
			zero: 0,
			one: 1,
			two: 2,
		},
	});
});

test('recursive extends with json file', async () => {
	const cwd = await gitBootstrap('fixtures/recursive-extends-json');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: ['./first-extended'],
		plugins: {},
		rules: {
			zero: 0,
			one: 1,
			two: 2,
		},
	});
});

test('recursive extends with yaml file', async () => {
	const cwd = await gitBootstrap('fixtures/recursive-extends-yaml');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: ['./first-extended'],
		plugins: {},
		rules: {
			zero: 0,
			one: 1,
			two: 2,
		},
	});
});

test('recursive extends with js file', async () => {
	const cwd = await gitBootstrap('fixtures/recursive-extends-js');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: ['./first-extended'],
		plugins: {},
		rules: {
			zero: 0,
			one: 1,
			two: 2,
		},
	});
});

test('recursive extends with package.json file', async () => {
	const cwd = await gitBootstrap('fixtures/recursive-extends-package');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: ['./first-extended'],
		plugins: {},
		rules: {
			zero: 0,
			one: 1,
			two: 2,
		},
	});
});

test('parser preset overwrites completely instead of merging', async () => {
	const cwd = await gitBootstrap('fixtures/parser-preset-override');
	const actual = await load({}, {cwd});

	expect(actual.parserPreset.name).toBe('./custom');
	expect(actual.parserPreset.parserOpts).toMatchObject({
		headerPattern: /.*/,
	});
});

test('recursive extends with parserPreset', async () => {
	const cwd = await gitBootstrap('fixtures/recursive-parser-preset');
	const actual = await load({}, {cwd});

	expect(actual.parserPreset.name).toBe('./conventional-changelog-custom');
	expect(actual.parserPreset.parserOpts).toMatchObject({
		headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/,
	});
});

test('ignores unknow keys', async () => {
	const cwd = await gitBootstrap('fixtures/trash-file');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: [],
		plugins: {},
		rules: {
			foo: 'bar',
			baz: 'bar',
		},
	});
});

test('ignores unknow keys recursively', async () => {
	const cwd = await gitBootstrap('fixtures/trash-extend');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: ['./one'],
		plugins: {},
		rules: {
			zero: 0,
			one: 1,
		},
	});
});

test('find up from given cwd', async () => {
	const outer = await fixBootstrap('fixtures/outer-scope');
	await git.init(path.join(outer, 'inner-scope'));
	const cwd = path.join(outer, 'inner-scope', 'child-scope');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: [],
		plugins: {},
		rules: {
			child: true,
			inner: false,
			outer: false,
		},
	});
});

test('find up config from outside current git repo', async () => {
	const outer = await fixBootstrap('fixtures/outer-scope');
	const cwd = await git.init(path.join(outer, 'inner-scope'));
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: '@commitlint/format',
		extends: [],
		plugins: {},
		rules: {
			child: false,
			inner: false,
			outer: true,
		},
	});
});

test('respects formatter option', async () => {
	const cwd = await gitBootstrap('fixtures/formatter');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: 'commitlint-junit',
		extends: [],
		plugins: {},
		rules: {},
	});
});

test('resolves formatter relative from config directory', async () => {
	const cwd = await gitBootstrap('fixtures/formatter-local-module');
	const actual = await load({}, {cwd});

	expect(actual).toMatchObject({
		formatter: resolveFrom(cwd, './formatters/custom.js'),
		extends: [],
		plugins: {},
		rules: {},
	});
});

test('returns formatter name when unable to resolve from config directory', async () => {
	const cwd = await gitBootstrap('fixtures/formatter-local-module');
	const actual = await load({formatter: './doesnt/exists.js'}, {cwd});

	expect(actual).toMatchObject({
		formatter: './doesnt/exists.js',
		extends: [],
		plugins: {},
		rules: {},
	});
});

test('does not mutate config module reference', async () => {
	const file = 'config/commitlint.config.js';
	const cwd = await gitBootstrap('fixtures/specify-config-file');
	const rules = {'body-case': [1, 'never', 'camel-case'] as any};

	const configPath = path.join(cwd, file);
	const before = JSON.stringify(require(configPath));
	await load({rules}, {cwd, file});
	const after = JSON.stringify(require(configPath));

	expect(after).toBe(before);
});

test('resolves parser preset from conventional commits', async () => {
	const cwd = await npmBootstrap('fixtures/parser-preset-conventionalcommits');
	const actual = await load({}, {cwd});

	expect(actual.parserPreset.name).toBe(
		'conventional-changelog-conventionalcommits'
	);
	expect(typeof actual.parserPreset.parserOpts).toBe('object');
	expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
		/^(\w*)(?:\((.*)\))?!?: (.*)$/
	);
});

test('resolves parser preset from conventional angular', async () => {
	const cwd = await npmBootstrap('fixtures/parser-preset-angular');
	const actual = await load({}, {cwd});

	expect(actual.parserPreset.name).toBe('conventional-changelog-angular');
	expect(typeof actual.parserPreset.parserOpts).toBe('object');
	expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
		/^(\w*)(?:\((.*)\))?: (.*)$/
	);
});

test('recursive resolves parser preset from conventional atom', async () => {
	const cwd = await gitBootstrap(
		'fixtures/recursive-parser-preset-conventional-atom'
	);
	await npm.installModules(
		path.resolve(cwd, 'first-extended', 'second-extended')
	);

	const actual = await load({}, {cwd});

	expect(actual.parserPreset.name).toBe('conventional-changelog-atom');
	expect(typeof actual.parserPreset.parserOpts).toBe('object');
	expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
		/^(:.*?:) (.*)$/
	);
});

test('resolves parser preset from conventional commits without factory support', async () => {
	const cwd = await npmBootstrap(
		'fixtures/parser-preset-conventional-without-factory'
	);
	const actual = await load({}, {cwd});

	expect(actual.parserPreset.name).toBe(
		'conventional-changelog-conventionalcommits'
	);
	expect(typeof actual.parserPreset.parserOpts).toBe('object');
	expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
		/^(\w*)(?:\((.*)\))?!?: (.*)$/
	);
});