/* eslint-disable node/no-unpublished-import */
/*---------------------------------------------------------
 * Copyright 2023 The Go Authors. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for license information.
 *--------------------------------------------------------*/

import { EventEmitter } from 'events';
import * as path from 'path';
import sinon = require('sinon');
import * as vscode from 'vscode';
import { LanguageClient } from 'vscode-languageclient/node';
import { getGoConfig } from '../../src/config';
import {
	buildLanguageClient,
	BuildLanguageClientOption,
	buildLanguageServerConfig,
	toServerInfo
} from '../../src/language/goLanguageServer';
import { GoExtensionContext } from '../../src/context';

// FakeOutputChannel is a fake output channel used to buffer
// the output of the tested language client in an in-memory
// string array until cleared.
export 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[];

	private eventEmitter = new EventEmitter();
	private registeredPatterns = new Set<string>();
	public onPattern(msg: string, listener: () => void) {
		this.registeredPatterns.add(msg);
		this.eventEmitter.once(msg, () => {
			this.registeredPatterns.delete(msg);
			listener();
		});
	}

	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) => {
		this.registeredPatterns?.forEach((p) => {
			if (v.includes(p)) {
				this.eventEmitter.emit(p);
			}
		});

		if (this.buf.length > 1024) {
			this.buf.shift();
		}
		this.buf.push(v.trim());
	};
}
// Env is a collection of test-related variables and lsp client.
// Currently, this works only in module-aware mode.
export class Env {
	public languageClient?: LanguageClient;
	public goCtx: GoExtensionContext = {};

	private fakeOutputChannel?: FakeOutputChannel;
	private disposables = [] as { dispose(): void }[];

	public flushTrace(print: boolean) {
		if (print) {
			console.log(this.fakeOutputChannel?.toString());
		}
		this.fakeOutputChannel?.clear();
	}

	// This is a hack to check the progress of package loading.
	// TODO(hyangah): use progress message middleware hook instead
	// once it becomes available.
	public onMessageInTrace(msg: string, timeoutMS: number): Promise<void> {
		return new Promise((resolve, reject) => {
			const timeout = setTimeout(() => {
				this.flushTrace(true);
				reject(`Timed out while waiting for '${msg}'`);
			}, timeoutMS);
			this.fakeOutputChannel?.onPattern(msg, () => {
				clearTimeout(timeout);
				resolve();
			});
		});
	}

	// Start the language server with the fakeOutputChannel.
	// if workspaceFolder is provided, gopls will start with it.
	public async startGopls(
		filePath: string | undefined,
		goConfig?: vscode.WorkspaceConfiguration,
		workspaceFolder?: string
	) {
		// file path to open.
		this.fakeOutputChannel = new FakeOutputChannel();
		const pkgLoadingDone = this.onMessageInTrace('Finished loading packages.', 60000);

		if (!goConfig) {
			goConfig = getGoConfig();
		}
		const cfg: BuildLanguageClientOption = await buildLanguageServerConfig(
			Object.create(goConfig, {
				useLanguageServer: { value: true },
				languageServerFlags: { value: ['-rpc.trace'] } // enable rpc tracing to monitor progress reports
			})
		);
		cfg.outputChannel = this.fakeOutputChannel; // inject our fake output channel.
		this.goCtx.latestConfig = cfg;
		this.languageClient = await buildLanguageClient(this.goCtx, cfg);
		if (!this.languageClient) {
			throw new Error('Language client not initialized.');
		}

		if (workspaceFolder) {
			this.languageClient.clientOptions.workspaceFolder = {
				uri: vscode.Uri.file(workspaceFolder),
				index: 0,
				name: path.basename(workspaceFolder)
			};
		}
		await this.languageClient.start();
		this.goCtx.languageClient = this.languageClient;
		this.goCtx.serverInfo = toServerInfo(this.languageClient.initializeResult);
		this.goCtx.serverOutputChannel = this.fakeOutputChannel;
		this.goCtx.languageServerIsRunning = this.languageClient?.isRunning();

		if (filePath) {
			await this.openDoc(filePath);
		}
		await pkgLoadingDone;
	}

	public async teardown() {
		try {
			await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
			await this.languageClient?.stop(10000); // 10s timeout
		} catch (e) {
			console.log(`failed to stop gopls within 10sec: ${e}`);
			this.flushTrace(true);
		} finally {
			for (const d of this.disposables) {
				d.dispose();
			}
			this.languageClient = undefined;
			this.goCtx = {};
		}
	}

	public async openDoc(...paths: string[]) {
		const uri = vscode.Uri.file(path.resolve(...paths));
		const doc = await vscode.workspace.openTextDocument(uri);
		return { uri, doc };
	}
}
