import resolveExtends, {ResolveExtendsContext} from '.';

const id = (id: unknown) => id;

test('returns empty object when called without params', () => {
	const actual = resolveExtends();
	expect(actual).toEqual({});
});

test('returns an equivalent object as passed in', () => {
	const expected = {foo: 'bar'};
	const actual = resolveExtends(expected);
	expect(actual).toEqual(expected);
});

test('falls back to global install', async () => {
	const resolveGlobal = jest.fn(() => '@commitlint/foo-bar');
	const require = jest.fn(() => ({}));

	const ctx = {resolveGlobal, require} as ResolveExtendsContext;

	resolveExtends({extends: ['@commitlint/foo-bar']}, ctx);
	expect(ctx.resolveGlobal).toBeCalledWith('@commitlint/foo-bar');
});

test('fails for missing extends', async () => {
	expect(() => resolveExtends({extends: ['@commitlint/foo-bar']})).toThrow(
		/Cannot find module "@commitlint\/foo-bar" from/
	);
});

test('resolves extends for single config', () => {
	const input = {extends: 'extender-name'};
	const ctx = {
		resolve: id,
		require: jest.fn(() => ({})),
	} as ResolveExtendsContext;
	resolveExtends(input, ctx);

	expect(ctx.require).toHaveBeenCalledWith('extender-name');
});

test('uses empty prefix by default', () => {
	const input = {extends: ['extender-name']};
	const ctx = {
		resolve: id,
		require: jest.fn(() => ({})),
	} as ResolveExtendsContext;
	resolveExtends(input, ctx);

	expect(ctx.require).toHaveBeenCalledWith('extender-name');
});

test('uses prefix as configured', () => {
	const input = {extends: ['extender-name']};
	const ctx = {
		resolve: id,
		require: jest.fn(() => ({})),
	} as ResolveExtendsContext;

	resolveExtends(input, {
		...ctx,
		prefix: 'prefix',
	});

	expect(ctx.require).toHaveBeenCalledWith('prefix-extender-name');
});

test('ignores prefix for scoped extends', () => {
	const input = {extends: ['@scope/extender-name']};
	const ctx = {
		resolve: id,
		require: jest.fn(() => ({})),
	} as ResolveExtendsContext;

	resolveExtends(input, {
		...ctx,
		prefix: 'prefix',
	});

	expect(ctx.require).toHaveBeenCalledWith('@scope/extender-name');
});

test('adds prefix as suffix for scopes only', () => {
	const input = {extends: ['@scope']};
	const ctx = {
		resolve: id,
		require: jest.fn(() => ({})),
	} as ResolveExtendsContext;

	resolveExtends(input, {
		...ctx,
		prefix: 'prefix',
	});

	expect(ctx.require).toHaveBeenCalledWith('@scope/prefix');
});

test('ignores prefix for relative extends', () => {
	const input = {extends: ['./extender']};
	const ctx = {
		resolve: id,
		require: jest.fn(() => ({})),
	} as ResolveExtendsContext;

	resolveExtends(input, {
		...ctx,
		prefix: 'prefix',
	});

	expect(ctx.require).toHaveBeenCalledWith('./extender');
});

test('ignores prefix for absolute extends', () => {
	const absolutePath = require.resolve('@commitlint/config-angular');
	const input = {extends: [absolutePath]};
	const ctx = {
		resolve: id,
		require: jest.fn(() => ({})),
	} as ResolveExtendsContext;

	resolveExtends(input, {
		...ctx,
		prefix: 'prefix',
	});

	expect(ctx.require).toHaveBeenCalledWith(absolutePath);
});

test('propagates return value of require function', () => {
	const input = {extends: ['extender-name']};
	const propagated = {foo: 'bar'};
	const ctx = {
		resolve: id,
		require: jest.fn(() => propagated),
	} as ResolveExtendsContext;

	const actual = resolveExtends(input, ctx);
	expect(actual).toEqual(expect.objectContaining(propagated));
});

test('resolves extends recursively', () => {
	const input = {extends: ['extender-name']};

	const require = (id: string) => {
		switch (id) {
			case 'extender-name':
				return {extends: ['recursive-extender-name']};
			case 'recursive-extender-name':
				return {foo: 'bar'};
			default:
				return {};
		}
	};

	const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext;
	resolveExtends(input, ctx);

	expect(ctx.require).toHaveBeenCalledWith('extender-name');
	expect(ctx.require).toHaveBeenCalledWith('recursive-extender-name');
});

test('uses prefix key recursively', () => {
	const input = {extends: ['extender-name']};

	const require = (id: string) => {
		switch (id) {
			case 'prefix-extender-name':
				return {extends: ['recursive-extender-name']};
			case 'prefix-recursive-extender-name':
				return {foo: 'bar'};
			default:
				return {};
		}
	};

	const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext;

	resolveExtends(input, {
		...ctx,
		prefix: 'prefix',
	});

	expect(ctx.require).toHaveBeenCalledWith('prefix-extender-name');
	expect(ctx.require).toHaveBeenCalledWith('prefix-recursive-extender-name');
});

test('propagates contents recursively', () => {
	const input = {extends: ['extender-name']};

	const require = (id: string) => {
		switch (id) {
			case 'extender-name':
				return {extends: ['recursive-extender-name'], foo: 'bar'};
			case 'recursive-extender-name':
				return {baz: 'bar'};
			default:
				return {};
		}
	};

	const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext;

	const actual = resolveExtends(input, ctx);

	const expected = {
		extends: ['extender-name'],
		foo: 'bar',
		baz: 'bar',
	};

	expect(actual).toEqual(expected);
});

test('propagates contents recursively with overlap', () => {
	const input = {extends: ['extender-name']};

	const require = (id: string) => {
		switch (id) {
			case 'extender-name':
				return {
					extends: ['recursive-extender-name'],
					rules: {rule: ['zero', 'one']},
				};
			case 'recursive-extender-name':
				return {rules: {rule: ['two', 'three', 'four']}};
			default:
				return {};
		}
	};

	const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext;

	const actual = resolveExtends(input, ctx);

	const expected = {
		extends: ['extender-name'],
		rules: {
			rule: ['zero', 'one'],
		},
	};

	expect(actual).toEqual(expected);
});

test('extends rules from left to right with overlap', () => {
	const input = {extends: ['left', 'right']};

	const require = (id: string) => {
		switch (id) {
			case 'left':
				return {rules: {a: true}};
			case 'right':
				return {rules: {a: false, b: true}};
			default:
				return {};
		}
	};

	const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext;

	const actual = resolveExtends(input, ctx);

	const expected = {
		extends: ['left', 'right'],
		rules: {
			a: false,
			b: true,
		},
	};

	expect(actual).toEqual(expected);
});

test('extending contents should take precedence', () => {
	const input = {extends: ['extender-name'], zero: 'root'};

	const require = (id: string) => {
		switch (id) {
			case 'extender-name':
				return {extends: ['recursive-extender-name'], zero: id, one: id};
			case 'recursive-extender-name':
				return {
					extends: ['second-recursive-extender-name'],
					zero: id,
					one: id,
					two: id,
				};
			case 'second-recursive-extender-name':
				return {zero: id, one: id, two: id, three: id};
			default:
				return {};
		}
	};

	const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext;

	const actual = resolveExtends(input, ctx);

	const expected = {
		extends: ['extender-name'],
		zero: 'root',
		one: 'extender-name',
		two: 'recursive-extender-name',
		three: 'second-recursive-extender-name',
	};

	expect(actual).toEqual(expected);
});

test('should fall back to conventional-changelog-lint-config prefix', () => {
	const input = {extends: ['extender-name']};

	const resolve = (id: string) => {
		if (id === 'conventional-changelog-lint-config-extender-name') {
			return 'conventional-changelog-lint-config-extender-name';
		}
		throw new Error(`Could not find module "*${id}"`);
	};

	const require = (id: string) => {
		switch (id) {
			case 'conventional-changelog-lint-config-extender-name':
				return {
					rules: {
						fallback: true,
					},
				};
			default:
				return {};
		}
	};

	const ctx = {
		resolve: jest.fn(resolve),
		require: jest.fn(require),
	} as ResolveExtendsContext;

	const actual = resolveExtends(input, {
		...ctx,
		prefix: 'prefix',
	});

	expect(actual).toEqual({
		extends: ['extender-name'],
		rules: {
			fallback: true,
		},
	});
});