/* eslint-disable @typescript-eslint/no-explicit-any */
/*---------------------------------------------------------
 * Copyright 2020 The Go Authors. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for license information.
 *--------------------------------------------------------*/

'use strict';

import assert from 'assert';
import { getGoConfig } from '../../src/config';
import { computeTestCommand, getTestFlags, goTest } from '../../src/testUtils';
import { rmdirRecursive } from '../../src/util';
import fs = require('fs-extra');
import os = require('os');
import path = require('path');
import sinon = require('sinon');
import vscode = require('vscode');

suite('Test Go Test Args', () => {
	function runTest(param: {
		expectedArgs: string;
		expectedOutArgs: string;
		flags?: string[];
		functions?: string[];
		isBenchmark?: boolean;
	}) {
		const { args, outArgs } = computeTestCommand(
			{
				dir: '',
				goConfig: getGoConfig(),
				flags: param.flags || [],
				functions: param.functions || [],
				isBenchmark: param.isBenchmark || false,
				applyCodeCoverage: false
			},
			['./...']
		);

		assert.strictEqual(args.join(' '), param.expectedArgs, 'actual command');
		assert.strictEqual(outArgs.join(' '), param.expectedOutArgs, 'displayed command');
	}

	test('default config', () => {
		runTest({
			expectedArgs: 'test -timeout 30s ./...',
			expectedOutArgs: 'test -timeout 30s ./...'
		});
	});
	test('user flag [-v] enables -json flag', () => {
		runTest({
			expectedArgs: 'test -timeout 30s -json ./... -v',
			expectedOutArgs: 'test -timeout 30s ./... -v',
			flags: ['-v']
		});
	});
	test('user flag [-json -v] prevents -json flag addition', () => {
		runTest({
			expectedArgs: 'test -timeout 30s ./... -json -v',
			expectedOutArgs: 'test -timeout 30s ./... -json -v',
			flags: ['-json', '-v']
		});
	});
	test('user flag [-args] does not crash', () => {
		runTest({
			expectedArgs: 'test -timeout 30s ./... -args',
			expectedOutArgs: 'test -timeout 30s ./... -args',
			flags: ['-args']
		});
	});
	test('user flag [-args -v] does not enable -json flag', () => {
		runTest({
			expectedArgs: 'test -timeout 30s ./... -args -v',
			expectedOutArgs: 'test -timeout 30s ./... -args -v',
			flags: ['-args', '-v']
		});
	});
	test('specifying functions adds -run flags', () => {
		runTest({
			expectedArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
			expectedOutArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
			functions: ['TestA', 'TestB']
		});
	});
	test('functions & benchmark adds -bench flags and skips timeout', () => {
		runTest({
			expectedArgs: 'test -benchmem -run=^$ -bench ^(TestA|TestB)$ ./...',
			expectedOutArgs: 'test -benchmem -run=^$ -bench ^(TestA|TestB)$ ./...',
			functions: ['TestA', 'TestB'],
			isBenchmark: true
		});
	});
	test('user -run flag is ignored when functions are provided', () => {
		runTest({
			expectedArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
			expectedOutArgs: 'test -timeout 30s -run ^(TestA|TestB)$ ./...',
			functions: ['TestA', 'TestB'],
			flags: ['-run', 'TestC']
		});
	});
	test('use -testify.m for methods', () => {
		runTest({
			expectedArgs:
				'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
			expectedOutArgs:
				'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
			functions: [
				'(*ExampleTestSuite).TestExample',
				'(*ExampleTestSuite).TestAnotherExample',
				'TestExampleTestSuite'
			]
		});
	});
});

suite('Test Go Test', function () {
	this.timeout(50000);

	const sourcePath = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'goTestTest');

	let tmpGopath: string;
	let repoPath: string;

	let previousEnv: any;

	setup(() => {
		previousEnv = Object.assign({}, process.env);
	});

	teardown(async () => {
		process.env = previousEnv;
		rmdirRecursive(tmpGopath);
	});

	function setupRepo(isModuleMode: boolean) {
		tmpGopath = fs.mkdtempSync(path.join(os.tmpdir(), 'go-test-test'));
		fs.mkdirSync(path.join(tmpGopath, 'src'));
		repoPath = path.join(tmpGopath, 'src', 'goTestTest');
		fs.copySync(sourcePath, repoPath, {
			recursive: true,
			filter: (src: string): boolean => {
				if (isModuleMode) {
					return true;
				}
				return path.basename(src) !== 'go.mod'; // skip go.mod file.
			}
		});
		process.env.GOPATH = tmpGopath;
		process.env.GO111MODULE = isModuleMode ? 'on' : 'off';
	}

	async function runTest(
		input: { isMod: boolean; includeSubDirectories: boolean; testFlags?: string[]; applyCodeCoverage?: boolean },
		wantFiles: string[]
	) {
		const config = Object.create(getGoConfig());
		const outputChannel = new FakeOutputChannel();

		const testConfig = {
			goConfig: config,
			outputChannel,
			dir: repoPath,
			flags: input.testFlags ? input.testFlags : getTestFlags(config),
			isMod: input.isMod,
			includeSubDirectories: input.includeSubDirectories,
			applyCodeCoverage: input.applyCodeCoverage
		};
		try {
			// TODO: instead of calling goTest, consider to test
			// testCurrentPackage, testCurrentWorkspace, testCurrentFile
			// which are closer to the features exposed to users.
			const result = await goTest(testConfig);
			assert.equal(result, false); // we expect tests to fail.
		} catch (e) {
			console.log('exception: ${e}');
		}

		const testOutput = outputChannel.toString();
		for (const want of wantFiles) {
			assert.ok(testOutput.includes(want), `\nFully resolved file path "${want}" not found in \n${testOutput}`);
		}
	}

	test('resolves file names in logs (modules)', async () => {
		setupRepo(true);
		await runTest({ isMod: true, includeSubDirectories: true }, [
			path.join(repoPath, 'a_test.go'),
			path.join(repoPath, 'b', 'b_test.go')
		]);
		await runTest({ isMod: true, includeSubDirectories: false }, [path.join(repoPath, 'a_test.go')]);
		await runTest({ isMod: true, includeSubDirectories: true, testFlags: ['-v'] }, [
			path.join(repoPath, 'a_test.go'),
			path.join(repoPath, 'b', 'b_test.go')
		]);
		await runTest({ isMod: true, includeSubDirectories: true, testFlags: ['-race'], applyCodeCoverage: true }, [
			path.join(repoPath, 'a_test.go'),
			path.join(repoPath, 'b', 'b_test.go')
		]);
		await runTest({ isMod: true, includeSubDirectories: false, testFlags: ['-v'] }, [
			path.join(repoPath, 'a_test.go')
		]);
	});

	test('resolves file names in logs (GOPATH)', async () => {
		setupRepo(false);
		await runTest({ isMod: false, includeSubDirectories: true }, [
			path.join(repoPath, 'a_test.go'),
			path.join(repoPath, 'b', 'b_test.go')
		]);
		await runTest({ isMod: false, includeSubDirectories: false }, [path.join(repoPath, 'a_test.go')]);
		await runTest({ isMod: false, includeSubDirectories: true, testFlags: ['-v'] }, [
			path.join(repoPath, 'a_test.go'),
			path.join(repoPath, 'b', 'b_test.go')
		]);
		await runTest({ isMod: false, includeSubDirectories: true, testFlags: ['-race'], applyCodeCoverage: true }, [
			path.join(repoPath, 'a_test.go'),
			path.join(repoPath, 'b', 'b_test.go')
		]);
		await runTest({ isMod: false, includeSubDirectories: false, testFlags: ['-v'] }, [
			path.join(repoPath, 'a_test.go')
		]);
	});
});

// FakeOutputChannel is a fake output channel used to buffer
// the output of the tested language client in an in-memory
// string array until cleared.
class FakeOutputChannel implements vscode.OutputChannel {
	public name = 'FakeOutputChannel';
	public show = sinon.fake(); // no-empty
	public hide = sinon.fake(); // no-empty
	public dispose = sinon.fake(); // no-empty
	public replace = sinon.fake(); // no-empty

	private buf = [] as string[];

	public append = (v: string) => this.enqueue(v);
	public appendLine = (v: string) => this.enqueue(v);
	public clear = () => {
		this.buf = [];
	};
	public toString = () => {
		return this.buf.join('\n');
	};

	private enqueue = (v: string) => {
		if (this.buf.length > 1024) {
			this.buf.shift();
		}
		this.buf.push(v.trim());
	};
}
