/* eslint-disable @typescript-eslint/no-explicit-any */
/*---------------------------------------------------------
 * Copyright (C) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for license information.
 *--------------------------------------------------------*/
'use strict';

import path = require('path');
import vscode = require('vscode');
import { CommandFactory } from './commands';
import { getGoConfig } from './config';
import { GoExtensionContext } from './context';
import { isModSupported } from './goModules';
import { escapeSubTestName } from './subTestUtils';
import {
	extractInstanceTestName,
	findAllTestSuiteRuns,
	getBenchmarkFunctions,
	getTestFlags,
	getTestFunctionDebugArgs,
	getTestFunctionsAndTestSuite,
	getTestTags,
	goTest,
	TestConfig,
	SuiteToTestMap,
	getTestFunctions
} from './testUtils';

// lastTestConfig holds a reference to the last executed TestConfig which allows
// the last test to be easily re-executed.
let lastTestConfig: TestConfig | undefined;

// lastDebugConfig holds a reference to the last executed DebugConfiguration which allows
// the last test to be easily re-executed and debugged.
let lastDebugConfig: vscode.DebugConfiguration | undefined;
let lastDebugWorkspaceFolder: vscode.WorkspaceFolder | undefined;

export type TestAtCursorCmd = 'debug' | 'test' | 'benchmark';
export type SubTestAtCursorCmd = Exclude<TestAtCursorCmd, 'benchmark'>;

class NotFoundError extends Error {}

async function _testAtCursor(
	goCtx: GoExtensionContext,
	goConfig: vscode.WorkspaceConfiguration,
	cmd: TestAtCursorCmd,
	args: any
) {
	const editor = vscode.window.activeTextEditor;
	if (!editor) {
		throw new NotFoundError('No editor is active.');
	}
	if (!editor.document.fileName.endsWith('_test.go')) {
		throw new NotFoundError('No tests found. Current file is not a test file.');
	}

	const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(
		cmd === 'benchmark',
		goCtx,
		editor.document
	);
	// We use functionName if it was provided as argument
	// Otherwise find any test function containing the cursor.
	const testFunctionName =
		args && args.functionName
			? args.functionName
			: testFunctions?.filter((func) => func.range.contains(editor.selection.start)).map((el) => el.name)[0];
	if (!testFunctionName) {
		throw new NotFoundError('No test function found at cursor.');
	}

	await editor.document.save();

	if (cmd === 'debug') {
		return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig);
	} else if (cmd === 'benchmark' || cmd === 'test') {
		return runTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, cmd, args);
	} else {
		throw new Error(`Unsupported command: ${cmd}`);
	}
}

async function _subTestAtCursor(
	goCtx: GoExtensionContext,
	goConfig: vscode.WorkspaceConfiguration,
	cmd: SubTestAtCursorCmd,
	args: any
) {
	const editor = vscode.window.activeTextEditor;
	if (!editor) {
		vscode.window.showInformationMessage('No editor is active.');
		return;
	}
	if (!editor.document.fileName.endsWith('_test.go')) {
		vscode.window.showInformationMessage('No tests found. Current file is not a test file.');
		return;
	}

	await editor.document.save();
	const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(false, goCtx, editor.document);
	// We use functionName if it was provided as argument
	// Otherwise find any test function containing the cursor.
	const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
	const testFunctionName =
		args && args.functionName ? args.functionName : currentTestFunctions.map((el) => el.name)[0];

	if (!testFunctionName || currentTestFunctions.length === 0) {
		vscode.window.showInformationMessage('No test function found at cursor.');
		return;
	}

	let subTestName: string | undefined = args?.subTestName;

	if (!subTestName) {
		const testFunction = currentTestFunctions[0];
		const simpleRunRegex = /t.Run\("([^"]+)",/;
		const runRegex = /t.Run\(/;
		let lineText: string;
		let runMatch: RegExpMatchArray | null | undefined;
		let simpleMatch: RegExpMatchArray | null | undefined;
		for (let i = editor.selection.start.line; i >= testFunction.range.start.line; i--) {
			lineText = editor.document.lineAt(i).text;
			simpleMatch = lineText.match(simpleRunRegex);
			runMatch = lineText.match(runRegex);
			if (simpleMatch || (runMatch && !simpleMatch)) {
				break;
			}
		}

		if (!simpleMatch) {
			const input = await vscode.window.showInputBox({
				prompt: 'Enter sub test name'
			});
			if (input) {
				subTestName = input;
			} else {
				vscode.window.showInformationMessage('No subtest function with a simple subtest name found at cursor.');
				return;
			}
		} else {
			subTestName = simpleMatch[1];
		}
	}

	await editor.document.save();

	const escapedName = escapeSubTestName(testFunctionName, subTestName);

	if (cmd === 'debug') {
		return debugTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig);
	} else if (cmd === 'test') {
		return runTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig, cmd, args);
	} else {
		throw new Error(`Unsupported command: ${cmd}`);
	}
}

/**
 * Executes the unit test at the primary cursor using `go test`. Output
 * is sent to the 'Go' channel.
 * @param goConfig Configuration for the Go extension.
 * @param cmd Whether the command is test, benchmark, or debug.
 * @param args
 */
export function testAtCursor(cmd: TestAtCursorCmd): CommandFactory {
	return (ctx, goCtx) => (args: any) => {
		const goConfig = getGoConfig();
		return _testAtCursor(goCtx, goConfig, cmd, args).catch((err) => {
			if (err instanceof NotFoundError) {
				vscode.window.showInformationMessage(err.message);
			} else {
				console.error(err);
			}
		});
	};
}

/**
 * Executes the unit test at the primary cursor if found, otherwise re-runs the previous test.
 * @param goConfig Configuration for the Go extension.
 * @param cmd Whether the command is test, benchmark, or debug.
 * @param args
 */
export function testAtCursorOrPrevious(cmd: TestAtCursorCmd): CommandFactory {
	return (ctx, goCtx) => async (args: any) => {
		const goConfig = getGoConfig();
		try {
			await _testAtCursor(goCtx, goConfig, cmd, args);
		} catch (err) {
			if (err instanceof NotFoundError) {
				const editor = vscode.window.activeTextEditor;
				if (editor) {
					await editor.document.save();
				}
				await testPrevious(ctx, goCtx)();
			} else {
				console.error(err);
			}
		}
	};
}

/**
 * Runs the test at cursor.
 */
async function runTestAtCursor(
	editor: vscode.TextEditor,
	testFunctionName: string,
	testFunctions: vscode.DocumentSymbol[],
	suiteToTest: SuiteToTestMap,
	goConfig: vscode.WorkspaceConfiguration,
	cmd: TestAtCursorCmd,
	args: any
) {
	const testConfigFns = [testFunctionName];
	if (cmd !== 'benchmark' && extractInstanceTestName(testFunctionName)) {
		testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions, suiteToTest).map((t) => t.name));
	}

	const isMod = await isModSupported(editor.document.uri);
	const testConfig: TestConfig = {
		goConfig,
		dir: path.dirname(editor.document.fileName),
		flags: getTestFlags(goConfig, args),
		functions: testConfigFns,
		isBenchmark: cmd === 'benchmark',
		isMod,
		applyCodeCoverage: goConfig.get<boolean>('coverOnSingleTest')
	};
	// Remember this config as the last executed test.
	lastTestConfig = testConfig;
	return goTest(testConfig);
}

/**
 * Executes the sub unit test at the primary cursor.
 *
 * @param cmd Whether the command is test or debug.
 */
export function subTestAtCursor(cmd: SubTestAtCursorCmd): CommandFactory {
	return (_, goCtx) => async (args: string[]) => {
		try {
			return await _subTestAtCursor(goCtx, getGoConfig(), cmd, args);
		} catch (err) {
			if (err instanceof NotFoundError) {
				vscode.window.showInformationMessage(err.message);
			} else {
				console.error(err);
			}
		}
	};
}

/**
 * Debugs the test at cursor.
 * @param editorOrDocument The text document (or editor) that defines the test.
 * @param testFunctionName The name of the test function.
 * @param testFunctions All test function symbols defined by the document.
 * @param goConfig Go configuration, i.e. flags, tags, environment, etc.
 * @param sessionID If specified, `sessionID` is added to the debug
 * configuration and can be used to identify the debug session.
 * @returns Whether the debug session was successfully started.
 */
export async function debugTestAtCursor(
	editorOrDocument: vscode.TextEditor | vscode.TextDocument,
	testFunctionName: string,
	testFunctions: vscode.DocumentSymbol[],
	suiteToFunc: SuiteToTestMap,
	goConfig: vscode.WorkspaceConfiguration,
	sessionID?: string
) {
	const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument;
	const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc);
	const tags = getTestTags(goConfig);
	const buildFlags = tags ? ['-tags', tags] : [];
	const flagsFromConfig = getTestFlags(goConfig);
	let foundArgsFlag = false;
	flagsFromConfig.forEach((x) => {
		if (foundArgsFlag) {
			args.push(x);
			return;
		}
		if (x === '-args') {
			foundArgsFlag = true;
			return;
		}
		buildFlags.push(x);
	});
	const workspaceFolder = vscode.workspace.getWorkspaceFolder(doc.uri);
	const debugConfig: vscode.DebugConfiguration = {
		name: 'Debug Test',
		type: 'go',
		request: 'launch',
		mode: 'test',
		program: path.dirname(doc.fileName),
		env: goConfig.get('testEnvVars', {}),
		envFile: goConfig.get('testEnvFile'),
		args,
		buildFlags: buildFlags.join(' '),
		sessionID
	};
	lastDebugConfig = debugConfig;
	lastDebugWorkspaceFolder = workspaceFolder;
	vscode.commands.executeCommand('workbench.debug.action.focusRepl');
	return await vscode.debug.startDebugging(workspaceFolder, debugConfig);
}

/**
 * Runs all tests in the package of the source of the active editor.
 *
 * @param goConfig Configuration for the Go extension.
 */
export function testCurrentPackage(isBenchmark: boolean): CommandFactory {
	return () => async (args: any) => {
		const goConfig = getGoConfig();
		const editor = vscode.window.activeTextEditor;
		if (!editor) {
			vscode.window.showInformationMessage('No editor is active.');
			return;
		}

		const isMod = await isModSupported(editor.document.uri);
		const testConfig: TestConfig = {
			goConfig,
			dir: path.dirname(editor.document.fileName),
			flags: getTestFlags(goConfig, args),
			isBenchmark,
			isMod,
			applyCodeCoverage: goConfig.get<boolean>('coverOnTestPackage')
		};
		// Remember this config as the last executed test.
		lastTestConfig = testConfig;
		return goTest(testConfig);
	};
}

/**
 * Runs all tests from all directories in the workspace.
 *
 * @param goConfig Configuration for the Go extension.
 */
export const testWorkspace: CommandFactory = () => (args: any) => {
	const goConfig = getGoConfig();
	if (!vscode.workspace.workspaceFolders?.length) {
		vscode.window.showInformationMessage('No workspace is open to run tests.');
		return;
	}
	let workspaceUri: vscode.Uri | undefined = vscode.workspace.workspaceFolders[0].uri;
	if (
		vscode.window.activeTextEditor &&
		vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri)
	) {
		workspaceUri = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri)!.uri;
	}

	const testConfig: TestConfig = {
		goConfig,
		dir: workspaceUri.fsPath,
		flags: getTestFlags(goConfig, args),
		includeSubDirectories: true
	};
	// Remember this config as the last executed test.
	lastTestConfig = testConfig;

	isModSupported(workspaceUri, true).then((isMod) => {
		testConfig.isMod = isMod;
		goTest(testConfig).then(null, (err) => {
			console.error(err);
		});
	});
};

/**
 * Runs all tests in the source of the active editor.
 *
 * @param goConfig Configuration for the Go extension.
 * @param isBenchmark Boolean flag indicating if these are benchmark tests or not.
 */
export function testCurrentFile(isBenchmark: boolean, getConfig = getGoConfig): CommandFactory {
	return (ctx, goCtx) => async (args: string[]) => {
		const goConfig = getConfig();
		const editor = vscode.window.activeTextEditor;
		if (!editor) {
			vscode.window.showInformationMessage('No editor is active.');
			return false;
		}
		if (!editor.document.fileName.endsWith('_test.go')) {
			vscode.window.showInformationMessage('No tests found. Current file is not a test file.');
			return false;
		}

		const getFunctions = isBenchmark ? getBenchmarkFunctions : getTestFunctions;
		const isMod = await isModSupported(editor.document.uri);

		return editor.document
			.save()
			.then(() => {
				return getFunctions(goCtx, editor.document).then((testFunctions) => {
					const testConfig: TestConfig = {
						goConfig,
						dir: path.dirname(editor.document.fileName),
						flags: getTestFlags(goConfig, args),
						functions: testFunctions?.map((sym) => sym.name),
						isBenchmark,
						isMod,
						applyCodeCoverage: goConfig.get<boolean>('coverOnSingleTestFile')
					};
					// Remember this config as the last executed test.
					lastTestConfig = testConfig;
					return goTest(testConfig);
				});
			})
			.then(undefined, (err) => {
				console.error(err);
				return Promise.resolve(false);
			});
	};
}

/**
 * Runs the previously executed test.
 */
export const testPrevious: CommandFactory = () => () => {
	if (!lastTestConfig) {
		vscode.window.showInformationMessage('No test has been recently executed.');
		return;
	}
	goTest(lastTestConfig).then(null, (err) => {
		console.error(err);
	});
};

/**
 * Runs the previously executed test.
 */
export const debugPrevious: CommandFactory = () => () => {
	if (!lastDebugConfig) {
		vscode.window.showInformationMessage('No test has been recently debugged.');
		return;
	}
	return vscode.debug.startDebugging(lastDebugWorkspaceFolder, lastDebugConfig);
};
