import path from 'path';
import {fix, git} from '@commitlint/test';
import execa from 'execa';
import merge from 'lodash/merge';
import fs from 'fs-extra';

const bin = require.resolve('../cli.js');

interface TestOptions {
	cwd: string;
	env?: Record<string, string>;
}

const cli = (args: string[], options: TestOptions) => {
	return (input = '') => {
		return execa(bin, args, {
			cwd: options.cwd,
			env: options.env,
			input: input,
			reject: false,
		});
	};
};

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

test('should throw when called without [input]', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli([], {cwd})();
	expect(actual.exitCode).toBe(1);
});

test('should reprint input from stdin', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli([], {cwd})('foo: bar');
	expect(actual.stdout).toContain('foo: bar');
});

test('should produce success output with --verbose flag', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli(['--verbose'], {cwd})('type: bar');
	expect(actual.stdout).toContain('0 problems, 0 warnings');
	expect(actual.stderr).toEqual('');
});

test('should produce no output with --quiet flag', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli(['--quiet'], {cwd})('foo: bar');
	expect(actual.stdout).toEqual('');
	expect(actual.stderr).toEqual('');
});

test('should produce no output with -q flag', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli(['-q'], {cwd})('foo: bar');
	expect(actual.stdout).toEqual('');
	expect(actual.stderr).toEqual('');
});

test('should produce help for empty config', async () => {
	const cwd = await gitBootstrap('fixtures/empty');
	const actual = await cli([], {cwd})('foo: bar');
	expect(actual.stdout).toContain('Please add rules');
	expect(actual.exitCode).toBe(1);
});

test('should produce help for problems', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli([], {cwd})('foo: bar');
	expect(actual.stdout).toContain(
		'Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint'
	);
	expect(actual.exitCode).toBe(1);
});

test('should produce help for problems with correct helpurl', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli(
		['-H https://github.com/conventional-changelog/commitlint/#testhelpurl'],
		{cwd}
	)('foo: bar');
	expect(actual.stdout).toContain(
		'Get help: https://github.com/conventional-changelog/commitlint/#testhelpurl'
	);
	expect(actual.exitCode).toBe(1);
});

test('should fail for input from stdin without rules', async () => {
	const cwd = await gitBootstrap('fixtures/empty');
	const actual = await cli([], {cwd})('foo: bar');
	expect(actual.exitCode).toBe(1);
});

test('should succeed for input from stdin with rules', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli([], {cwd})('type: bar');
	expect(actual.exitCode).toBe(0);
});

test('should fail for input from stdin with rule from rc', async () => {
	const cwd = await gitBootstrap('fixtures/simple');
	const actual = await cli([], {cwd})('foo: bar');
	expect(actual.stdout).toContain('type must not be one of [foo]');
	expect(actual.exitCode).toBe(1);
});

test('should work with --config option', async () => {
	const file = 'config/commitlint.config.js';
	const cwd = await gitBootstrap('fixtures/specify-config-file');
	const actual = await cli(['--config', file], {cwd})('foo: bar');
	expect(actual.stdout).toContain('type must not be one of [foo]');
	expect(actual.exitCode).toBe(1);
});

test('should fail for input from stdin with rule from js', async () => {
	const cwd = await gitBootstrap('fixtures/extends-root');
	const actual = await cli(['--extends', './extended'], {cwd})('foo: bar');
	expect(actual.stdout).toContain('type must not be one of [foo]');
	expect(actual.exitCode).toBe(1);
});

test('should output help URL defined in config file', async () => {
	const cwd = await gitBootstrap('fixtures/help-url');
	const actual = await cli([], {cwd})('foo: bar');
	expect(actual.stdout).toContain('Get help: https://www.example.com/foo');
	expect(actual.exitCode).toBe(1);
});

test('should produce no error output with --quiet flag', async () => {
	const cwd = await gitBootstrap('fixtures/simple');
	const actual = await cli(['--quiet'], {cwd})('foo: bar');
	expect(actual.stdout).toEqual('');
	expect(actual.stderr).toEqual('');
	expect(actual.exitCode).toBe(1);
});

test('should produce no error output with -q flag', async () => {
	const cwd = await gitBootstrap('fixtures/simple');
	const actual = await cli(['-q'], {cwd})('foo: bar');
	expect(actual.stdout).toEqual('');
	expect(actual.stderr).toEqual('');
	expect(actual.exitCode).toBe(1);
});

test('should work with husky commitmsg hook and git commit', async () => {
	const cwd = await gitBootstrap('fixtures/husky/integration');
	await writePkg({husky: {hooks: {'commit-msg': `'${bin}' -e`}}}, {cwd});

	// await execa('npm', ['install'], {cwd}); // npm install is failing on windows machines
	await execa('git', ['add', 'package.json'], {cwd});
	const commit = await execa(
		'git',
		['commit', '-m', '"test: this should work"'],
		{cwd}
	);

	expect(commit).toBeTruthy();
});

test('should work with husky commitmsg hook in sub packages', async () => {
	const upper = await gitBootstrap('fixtures/husky');
	const cwd = path.join(upper, 'integration');
	await writePkg({husky: {hooks: {'commit-msg': `'${bin}' -e`}}}, {cwd: upper});

	// await execa('npm', ['install'], {cwd}); // npm install is failing on windows machines
	await execa('git', ['add', 'package.json'], {cwd});
	const commit = await execa(
		'git',
		['commit', '-m', '"test: this should work"'],
		{cwd}
	);
	expect(commit).toBeTruthy();
});

test('should work with husky via commitlint -e $GIT_PARAMS', async () => {
	const cwd = await gitBootstrap('fixtures/husky/integration');
	await writePkg(
		{husky: {hooks: {'commit-msg': `'${bin}' -e $GIT_PARAMS`}}},
		{cwd}
	);

	// await execa('npm', ['install'], {cwd}); // npm install is failing on windows machines
	await execa('git', ['add', 'package.json'], {cwd});
	const commit = await execa(
		'git',
		['commit', '-m', '"test: this should work"'],
		{cwd}
	);
	expect(commit).toBeTruthy();
});

test('should work with husky via commitlint -e %GIT_PARAMS%', async () => {
	const cwd = await gitBootstrap('fixtures/husky/integration');
	await writePkg(
		{husky: {hooks: {'commit-msg': `'${bin}' -e %GIT_PARAMS%`}}},
		{cwd}
	);

	// await execa('npm', ['install'], {cwd}); // npm install is failing on windows machines
	await execa('git', ['add', 'package.json'], {cwd});
	const commit = await execa(
		'git',
		['commit', '-m', '"test: this should work"'],
		{cwd}
	);
	expect(commit).toBeTruthy();
});

test('should work with husky via commitlint -e $HUSKY_GIT_PARAMS', async () => {
	const cwd = await gitBootstrap('fixtures/husky/integration');
	await writePkg(
		{husky: {hooks: {'commit-msg': `'${bin}' -e $HUSKY_GIT_PARAMS`}}},
		{cwd}
	);

	// await execa('npm', ['install'], {cwd}); // npm install is failing on windows machines
	await execa('git', ['add', 'package.json'], {cwd});
	const commit = await execa(
		'git',
		['commit', '-m', '"test: this should work"'],
		{cwd}
	);
	expect(commit).toBeTruthy();
});

test('should work with husky via commitlint -e %HUSKY_GIT_PARAMS%', async () => {
	const cwd = await gitBootstrap('fixtures/husky/integration');
	await writePkg(
		{husky: {hooks: {'commit-msg': `'${bin}' -e %HUSKY_GIT_PARAMS%`}}},
		{cwd}
	);

	// await execa('npm', ['install'], {cwd}); // npm install is failing on windows machines
	await execa('git', ['add', 'package.json'], {cwd});
	const commit = await execa(
		'git',
		['commit', '-m', '"test: this should work"'],
		{cwd}
	);
	expect(commit).toBeTruthy();
});

test('should allow reading of environment variables for edit file, succeeding if valid', async () => {
	const cwd = await gitBootstrap('fixtures/simple');
	await fs.writeFile(path.join(cwd, 'commit-msg-file'), 'foo');
	const actual = await cli(['--env', 'variable'], {
		cwd,
		env: {variable: 'commit-msg-file'},
	})();
	expect(actual.exitCode).toBe(0);
});

test('should allow reading of environment variables for edit file, failing if invalid', async () => {
	const cwd = await gitBootstrap('fixtures/simple');
	await fs.writeFile(
		path.join(cwd, 'commit-msg-file'),
		'foo: bar\n\nFoo bar bizz buzz.\n\nCloses #123.'
	);
	const actual = await cli(['--env', 'variable'], {
		cwd,
		env: {variable: 'commit-msg-file'},
	})();
	expect(actual.exitCode).toBe(1);
});

test('should pick up parser preset and fail accordingly', async () => {
	const cwd = await gitBootstrap('fixtures/parser-preset');
	const actual = await cli(['--parser-preset', './parser-preset'], {cwd})(
		'type(scope): subject'
	);
	expect(actual.exitCode).toBe(1);
	expect(actual.stdout).toContain('may not be empty');
});

test('should pick up parser preset and succeed accordingly', async () => {
	const cwd = await gitBootstrap('fixtures/parser-preset');
	const actual = await cli(['--parser-preset', './parser-preset'], {cwd})(
		'----type(scope): subject'
	);
	expect(actual.exitCode).toBe(0);
});

test('should pick up config from outside git repo and fail accordingly', async () => {
	const outer = await fixBootstrap('fixtures/outer-scope');
	const cwd = await git.init(path.join(outer, 'inner-scope'));

	const actual = await cli([], {cwd})('inner: bar');
	expect(actual.exitCode).toBe(1);
});

test('should pick up config from outside git repo and succeed accordingly', async () => {
	const outer = await fixBootstrap('fixtures/outer-scope');
	const cwd = await git.init(path.join(outer, 'inner-scope'));

	const actual = await cli([], {cwd})('outer: bar');
	expect(actual.exitCode).toBe(0);
});

test('should pick up config from inside git repo with precedence and succeed accordingly', async () => {
	const outer = await fixBootstrap('fixtures/inner-scope');
	const cwd = await git.init(path.join(outer, 'inner-scope'));

	const actual = await cli([], {cwd})('inner: bar');
	expect(actual.exitCode).toBe(0);
});

test('should pick up config from inside git repo with precedence and fail accordingly', async () => {
	const outer = await fixBootstrap('fixtures/inner-scope');
	const cwd = await git.init(path.join(outer, 'inner-scope'));

	const actual = await cli([], {cwd})('outer: bar');
	expect(actual.exitCode).toBe(1);
});

test('should handle --amend with signoff', async () => {
	const cwd = await gitBootstrap('fixtures/signoff');
	await writePkg({husky: {hooks: {'commit-msg': `'${bin}' -e`}}}, {cwd});

	// await execa('npm', ['install'], {cwd}); // npm install is failing on windows machines
	await execa('git', ['add', 'package.json'], {cwd});
	await execa(
		'git',
		['commit', '-m', '"test: this should work"', '--signoff'],
		{cwd}
	);
	const commit = await execa('git', ['commit', '--amend', '--no-edit'], {cwd});

	expect(commit).toBeTruthy();
}, 10000);

test('should handle linting with issue prefixes', async () => {
	const cwd = await gitBootstrap('fixtures/issue-prefixes');
	const actual = await cli([], {cwd})('foobar REF-1');
	expect(actual.exitCode).toBe(0);
}, 10000);

test('should print full commit message when input from stdin fails', async () => {
	const cwd = await gitBootstrap('fixtures/simple');
	const input = 'foo: bar\n\nFoo bar bizz buzz.\n\nCloses #123.';
	// output text in plain text so we can compare it
	const actual = await cli(['--color=false'], {cwd})(input);

	expect(actual.stdout).toContain(input);
	expect(actual.exitCode).toBe(1);
});

test('should not print commit message fully or partially when input succeeds', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const message = 'type: bar\n\nFoo bar bizz buzz.\n\nCloses #123.';
	// output text in plain text so we can compare it
	const actual = await cli(['--color=false'], {cwd})(message);

	expect(actual.stdout).not.toContain(message);
	expect(actual.stdout).not.toContain(message.split('\n')[0]);
	expect(actual.exitCode).toBe(0);
});

test('should fail for invalid formatters from configuration', async () => {
	const cwd = await gitBootstrap('fixtures/custom-formatter');
	const actual = await cli([], {cwd})('foo: bar');

	expect(actual.stderr).toContain(
		'Using format custom-formatter, but cannot find the module'
	);
	expect(actual.stdout).toEqual('');
	expect(actual.exitCode).toBe(1);
});

test('should skip linting if message matches ignores config', async () => {
	const cwd = await gitBootstrap('fixtures/ignores');
	const actual = await cli([], {cwd})('WIP');
	expect(actual.exitCode).toBe(0);
});

test('should not skip linting if message does not match ignores config', async () => {
	const cwd = await gitBootstrap('fixtures/ignores');
	const actual = await cli([], {cwd})('foo');
	expect(actual.exitCode).toBe(1);
});

test('should not skip linting if defaultIgnores is false', async () => {
	const cwd = await gitBootstrap('fixtures/default-ignores-false');
	const actual = await cli([], {cwd})('fixup! foo: bar');
	expect(actual.exitCode).toBe(1);
});

test('should skip linting if defaultIgnores is true', async () => {
	const cwd = await gitBootstrap('fixtures/default-ignores-true');
	const actual = await cli([], {cwd})('fixup! foo: bar');
	expect(actual.exitCode).toBe(0);
});

test('should skip linting if defaultIgnores is unset', async () => {
	const cwd = await gitBootstrap('fixtures/default-ignores-unset');
	const actual = await cli([], {cwd})('fixup! foo: bar');
	expect(actual.exitCode).toBe(0);
});

test('should fail for invalid formatters from flags', async () => {
	const cwd = await gitBootstrap('fixtures/custom-formatter');
	const actual = await cli(['--format', 'through-flag'], {cwd})('foo: bar');

	expect(actual.stderr).toContain(
		'Using format through-flag, but cannot find the module'
	);
	expect(actual.stdout).toEqual('');
	expect(actual.exitCode).toBe(1);
});

test('should work with absolute formatter path', async () => {
	const formatterPath = path.resolve(
		__dirname,
		'../fixtures/custom-formatter/formatters/custom.js'
	);
	const cwd = await gitBootstrap('fixtures/custom-formatter');
	const actual = await cli(['--format', formatterPath], {cwd})(
		'test: this should work'
	);

	expect(actual.stdout).toContain('custom-formatter-ok');
	expect(actual.exitCode).toBe(0);
});

test('should work with relative formatter path', async () => {
	const cwd = path.resolve(
		await gitBootstrap('fixtures/custom-formatter'),
		'./formatters'
	);
	const actual = await cli(['--format', './custom.js'], {cwd})(
		'test: this should work'
	);

	expect(actual.stdout).toContain('custom-formatter-ok');
	expect(actual.exitCode).toBe(0);
});

test('should print help', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli(['--help'], {cwd})();
	const version = require('../package.json').version;
	const stdout = actual.stdout.replace(`@${version}`, '@dev');
	expect(stdout).toMatchInlineSnapshot(`
		"@commitlint/cli@dev - Lint your commit messages

		[input] reads from stdin if --edit, --env, --from and --to are omitted

		Options:
		  -c, --color          toggle colored output           [boolean] [default: true]
		  -g, --config         path to the config file                          [string]
		      --print-config   print resolved config          [boolean] [default: false]
		  -d, --cwd            directory to execute in
		                                         [string] [default: (Working Directory)]
		  -e, --edit           read last commit message from the specified file or
		                       fallbacks to ./.git/COMMIT_EDITMSG               [string]
		  -E, --env            check message in the file at path given by environment
		                       variable value                                   [string]
		  -x, --extends        array of shareable configurations to extend       [array]
		  -H, --help-url       help url in error message                        [string]
		  -f, --from           lower end of the commit range to lint; applies if
		                       edit=false                                       [string]
		  -o, --format         output format of the results                     [string]
		  -p, --parser-preset  configuration preset to use for
		                       conventional-commits-parser                      [string]
		  -q, --quiet          toggle console output          [boolean] [default: false]
		  -t, --to             upper end of the commit range to lint; applies if
		                       edit=false                                       [string]
		  -V, --verbose        enable verbose output for reports without problems
		                                                                       [boolean]
		  -v, --version        display version information                     [boolean]
		  -h, --help           Show help                                       [boolean]"
	`);
});

test('should print version', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli(['--version'], {cwd})();
	expect(actual.stdout).toMatch('@commitlint/cli@');
});

test('should print config', async () => {
	const cwd = await gitBootstrap('fixtures/default');
	const actual = await cli(['--print-config', '--no-color'], {cwd})();
	const stdout = actual.stdout
		.replace(/^{[^\n]/g, '{\n  ')
		.replace(/[^\n]}$/g, '\n}')
		.replace(/(helpUrl:)\n[ ]+/, '$1 ');
	expect(stdout).toMatchInlineSnapshot(`
		"{
		  extends: [],
		  formatter: '@commitlint/format',
		  parserPreset: undefined,
		  ignores: undefined,
		  defaultIgnores: undefined,
		  plugins: {},
		  rules: { 'type-enum': [ 2, 'never', [ 'foo' ] ] },
		  helpUrl: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'
		}"
	`);
});

async function writePkg(payload: unknown, options: TestOptions) {
	const pkgPath = path.join(options.cwd, 'package.json');
	const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
	const result = merge(pkg, payload);
	await fs.writeFile(pkgPath, JSON.stringify(result, null, '  '));
}