/******************************************************************************
 * Copyright (c) 2016 TypeFox and others.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 * 
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 ******************************************************************************/
package org.eclipse.lsp4j.test.launch;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.Endpoint;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.services.ServiceEndpoints;
import org.eclipse.lsp4j.launch.LSPLauncher;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.xtext.xbase.lib.Pair;
import org.eclipse.xtext.xbase.lib.util.ToStringBuilder;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class LauncherTest {
	
	private static final long TIMEOUT = 2000;
	
	@Test public void testNotification() throws IOException {
		MessageParams p = new MessageParams();
		p.setMessage("Hello World");
		p.setType(MessageType.Info);
		
		client.expectedNotifications.put("window/logMessage", p);
		serverLauncher.getRemoteProxy().logMessage(p);
		client.joinOnEmpty();
	}
	
	@Test public void testRequest() throws Exception {
		CompletionParams p = new CompletionParams();
		p.setPosition(new Position(1,1));
		p.setTextDocument(new TextDocumentIdentifier("test/foo.txt"));
		
		CompletionList result = new CompletionList();
		result.setIsIncomplete(true);
		result.setItems(new ArrayList<>());
		
		CompletionItem item = new CompletionItem();
		item.setDetail("test");
		item.setDocumentation("doc");
		item.setFilterText("filter");
		item.setInsertText("insert");
		item.setKind(CompletionItemKind.Field);
		result.getItems().add(item);
		
		server.expectedRequests.put("textDocument/completion", new Pair<>(p, result));
		CompletableFuture<Either<List<CompletionItem>, CompletionList>> future = clientLauncher.getRemoteProxy().getTextDocumentService().completion(p);
		Assert.assertEquals(Either.forRight(result).toString(), future.get(TIMEOUT, TimeUnit.MILLISECONDS).toString());
		client.joinOnEmpty();
	}
	
	
	static class AssertingEndpoint implements Endpoint {
		public Map<String, Pair<Object, Object>> expectedRequests = new LinkedHashMap<>();
		
		@Override
		public CompletableFuture<?> request(String method, Object parameter) {
			Assert.assertTrue(expectedRequests.containsKey(method));
			Pair<Object, Object> result = expectedRequests.remove(method);
			Assert.assertEquals(result.getKey().toString(), parameter.toString());
			return CompletableFuture.completedFuture(result.getValue());
		}

		public Map<String, Object> expectedNotifications = new LinkedHashMap<>();
		
		@Override
		public void notify(String method, Object parameter) {
			Assert.assertTrue(expectedNotifications.containsKey(method));
			Object object = expectedNotifications.remove(method);
			Assert.assertEquals(object.toString(), parameter.toString());
		}
		
		/**
		 * wait max 1 sec for all expectations to be removed
		 */
		public void joinOnEmpty() {
			long before = System.currentTimeMillis();
			do {
				if (expectedNotifications.isEmpty() && expectedNotifications.isEmpty()) {
					return;
				}
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} while ( System.currentTimeMillis() < before + 1000);
			Assert.fail("expectations weren't empty "+toString());
		}
		
		@Override
		public String toString() {
			return new ToStringBuilder(this).addAllFields().toString();
		}
		
	}

	private AssertingEndpoint server;
	private Launcher<LanguageClient> serverLauncher;
	private Future<?> serverListening;
	
	private AssertingEndpoint client;
	private Launcher<LanguageServer> clientLauncher;
	private Future<?> clientListening;
	
	private Level logLevel;
	
	@Before public void setup() throws IOException {
		PipedInputStream inClient = new PipedInputStream();
		PipedOutputStream outClient = new PipedOutputStream();
		PipedInputStream inServer = new PipedInputStream();
		PipedOutputStream outServer = new PipedOutputStream();
		
		inClient.connect(outServer);
		outClient.connect(inServer);
		server = new AssertingEndpoint();
		serverLauncher = LSPLauncher.createServerLauncher(ServiceEndpoints.toServiceObject(server, LanguageServer.class), inServer, outServer);
		serverListening = serverLauncher.startListening();
		
		client = new AssertingEndpoint();
		clientLauncher = LSPLauncher.createClientLauncher(ServiceEndpoints.toServiceObject(client, LanguageClient.class), inClient, outClient);
		clientListening = clientLauncher.startListening();
		
		Logger logger = Logger.getLogger(StreamMessageProducer.class.getName());
		logLevel = logger.getLevel();
		logger.setLevel(Level.SEVERE);
	}
	
	@After public void teardown() throws InterruptedException, ExecutionException {
		clientListening.cancel(true);
		serverListening.cancel(true);
		Thread.sleep(10);
		Logger logger = Logger.getLogger(StreamMessageProducer.class.getName());
		logger.setLevel(logLevel);
	}

}