/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable node/no-unpublished-import */
/*---------------------------------------------------------
 * Copyright (C) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for license information.
 *--------------------------------------------------------*/
import assert from 'assert';
import * as path from 'path';
import * as vscode from 'vscode';
import { getGoConfig } from '../../src/config';
import sinon = require('sinon');
import { getGoVersion, GoVersion } from '../../src/util';
import { GOPLS_MAYBE_PROMPT_FOR_TELEMETRY, recordTelemetryStartTime, TelemetryService } from '../../src/goTelemetry';
import { MockMemento } from '../mocks/MockMemento';
import { Env } from './goplsTestEnv.utils';

async function sleep(ms: number) {
	return new Promise((resolve) => setTimeout(resolve, ms));
}

suite('Go Extension Tests With Gopls', function () {
	this.timeout(300000);
	const projectDir = path.join(__dirname, '..', '..', '..');
	const testdataDir = path.join(projectDir, 'test', 'testdata');
	const env = new Env();
	const sandbox = sinon.createSandbox();
	let goVersion: GoVersion;

	suiteSetup(async () => {
		goVersion = await getGoVersion();
	});

	this.afterEach(async function () {
		await env.teardown();
		// Note: this shouldn't use () => {...}. Arrow functions do not have 'this'.
		// I don't know why but this.currentTest.state does not have the expected value when
		// used with teardown.
		env.flushTrace(this.currentTest?.state === 'failed');
		sandbox.restore();
	});

	test('HoverProvider', async () => {
		const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
		await env.startGopls(path.join(workspaceDir, 'test.go'), undefined, workspaceDir);
		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
		const testCases: [string, vscode.Position, string | null, string | null][] = [
			// [new vscode.Position(3,3), '/usr/local/go/src/fmt'],
			['keyword', new vscode.Position(0, 3), null, null], // keyword
			['inside a string', new vscode.Position(23, 14), null, null], // inside a string
			['just a }', new vscode.Position(20, 0), null, null], // just a }
			['inside a number', new vscode.Position(28, 16), null, null], // inside a number
			['func main()', new vscode.Position(22, 5), 'func main()', null],
			['import "math"', new vscode.Position(40, 23), 'package math', '`math` on'],
			[
				'func Println()',
				new vscode.Position(19, 6),
				goVersion.lt('1.18')
					? 'func fmt.Println(a ...interface{}) (n int, err error)'
					: 'func fmt.Println(a ...any) (n int, err error)',
				'Println formats '
			],
			['func print()', new vscode.Position(23, 4), 'func print(txt string)', 'This is an unexported function ']
		];

		const promises = testCases.map(async ([name, position, expectedSignature, expectedDoc]) => {
			const hovers = (await vscode.commands.executeCommand(
				'vscode.executeHoverProvider',
				uri,
				position
			)) as vscode.Hover[];

			if (expectedSignature === null && expectedDoc === null) {
				assert.equal(hovers.length, 0, `check hovers over ${name} failed: unexpected non-empty hover message.`);
				return;
			}

			const hover = hovers[0];
			assert.equal(
				hover.contents.length,
				1,
				`check hovers over ${name} failed: unexpected number of hover messages.`
			);
			const gotMessage = (<vscode.MarkdownString>hover.contents[0]).value;
			assert.ok(
				gotMessage.includes('```go\n' + expectedSignature + '\n```') &&
					(!expectedDoc || gotMessage.includes(expectedDoc)),
				`check hovers over ${name} failed: got ${gotMessage}`
			);
		});
		return Promise.all(promises);
	});

	test('Completion middleware', async () => {
		const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
		await env.startGopls(path.join(workspaceDir, 'test.go'), undefined, workspaceDir);
		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
		const testCases: [string, vscode.Position, string, vscode.CompletionItemKind][] = [
			['fmt.P<>', new vscode.Position(19, 6), 'Print', vscode.CompletionItemKind.Function],
			['xyz.H<>', new vscode.Position(41, 13), 'Hello', vscode.CompletionItemKind.Method]
		];

		for (const [name, position, wantFilterText, wantItemKind] of testCases) {
			let list: vscode.CompletionList<vscode.CompletionItem> | undefined;
			// Query completion items. We expect the hard coded filter text hack
			// has been applied and gopls returns an incomplete list by default
			// to avoid reordering by vscode. But, if the query is made before
			// gopls is ready, we observed that gopls returns an empty result
			// as a complete result, and vscode returns a general completion list instead.
			// Retry a couple of times if we see a complete result as a workaround.
			// (github.com/golang/vscode-go/issues/363)
			for (let i = 0; i < 3; i++) {
				list = (await vscode.commands.executeCommand(
					'vscode.executeCompletionItemProvider',
					uri,
					position
				)) as vscode.CompletionList;
				if (list.isIncomplete) {
					break;
				}
				await sleep(100);
				console.log(`${new Date()}: retrying...`);
			}
			// Confirm that the hardcoded filter text hack has been applied.
			if (!list || !list.isIncomplete) {
				assert.fail('gopls should provide an incomplete list by default');
			}

			// vscode.executeCompletionItemProvider will return results from all
			// registered completion item providers, not only gopls but also snippets.
			// Alternative is to directly query the language client, but that will
			// prevent us from detecting problems caused by issues between the language
			// client library and the vscode.
			let itemKindFound = false;
			for (const item of list.items) {
				if (item.kind === vscode.CompletionItemKind.Snippet) {
					continue;
				} // gopls does not supply Snippet yet.
				assert.strictEqual(
					item.filterText ?? item.label,
					wantFilterText,
					`${uri}:${name} failed, unexpected filter text ` +
						`(got ${item.filterText ?? item.label}, want ${wantFilterText})\n` +
						`${JSON.stringify(item, null, 2)}`
				);
				if (item.kind === wantItemKind) {
					itemKindFound = true;
				}
				if (
					item.kind === vscode.CompletionItemKind.Method ||
					item.kind === vscode.CompletionItemKind.Function
				) {
					assert.ok(
						item.command,
						`${uri}:${name}: expected command associated with ${item.label}, found none`
					);
				}
			}
			assert(itemKindFound, `failed to find expected item kind ${wantItemKind}: got ${JSON.stringify(list)}`);
		}
	});

	async function testCustomFormatter(goConfig: vscode.WorkspaceConfiguration, customFormatter: string) {
		const config = require('../../src/config');
		sandbox.stub(config, 'getGoConfig').returns(goConfig);
		const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
		await env.startGopls(path.join(workspaceDir, 'test.go'), goConfig, workspaceDir);
		const { doc } = await env.openDoc(testdataDir, 'gogetdocTestData', 'format.go');
		await vscode.window.showTextDocument(doc);

		const formatFeature = env.languageClient?.getFeature('textDocument/formatting');
		const formatter = formatFeature?.getProvider(doc);
		const tokensrc = new vscode.CancellationTokenSource();
		try {
			const result = await formatter?.provideDocumentFormattingEdits(
				doc,
				{} as vscode.FormattingOptions,
				tokensrc.token
			);
			assert.fail(`formatter unexpectedly succeeded and returned a result: ${JSON.stringify(result)}`);
		} catch (e) {
			assert(`${e}`.includes(`errors when formatting with ${customFormatter}`), `${e}`);
		}
	}

	test('Nonexistent formatter', async () => {
		const customFormatter = 'nonexistent';
		const goConfig = Object.create(getGoConfig(), {
			formatTool: { value: customFormatter } // this should make the formatter fail.
		}) as vscode.WorkspaceConfiguration;

		await testCustomFormatter(goConfig, customFormatter);
	});

	test('Custom formatter', async () => {
		const customFormatter = 'coolCustomFormatter';
		const goConfig = Object.create(getGoConfig(), {
			formatTool: { value: 'custom' }, // this should make the formatter fail.
			alternateTools: { value: { customFormatter: customFormatter } } // this should make the formatter fail.
		}) as vscode.WorkspaceConfiguration;

		await testCustomFormatter(goConfig, customFormatter);
	});

	test('Prompt For telemetry', async () => {
		const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
		await env.startGopls(path.join(workspaceDir, 'test.go'), undefined, workspaceDir);
		const memento = new MockMemento();
		recordTelemetryStartTime(memento, new Date('2000-01-01'));
		const sut = new TelemetryService(env.languageClient, memento, [GOPLS_MAYBE_PROMPT_FOR_TELEMETRY]);
		try {
			await Promise.all([
				// we want to see the prompt command flowing.
				env.onMessageInTrace(GOPLS_MAYBE_PROMPT_FOR_TELEMETRY, 60_000),
				sut.promptForTelemetry(
					false /* not a preview */,
					true /* vscode telemetry not disabled */,
					1000 /* 1000 out of 1000 users */
				)
			]);
		} catch (e) {
			assert(false, `unexpected failure: ${e}`);
		}
	});
});
