/*
 * Copyright 2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.bearchoke.platform.tests.web.websocket;

import com.bearchoke.platform.server.common.ServerConstants;
import com.bearchoke.platform.server.frontend.FrontendWebApplicationInitializer;
import com.bearchoke.platform.tests.web.websocket.support.client.StompMessageHandler;
import com.bearchoke.platform.tests.web.websocket.support.client.StompSession;
import com.bearchoke.platform.tests.web.websocket.support.client.WebSocketStompClient;
import com.bearchoke.platform.tests.web.websocket.support.server.TomcatWebSocketTestServer;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.test.util.JsonPathExpectationsHelper;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.SocketUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport;
import org.springframework.web.socket.sockjs.client.SockJsClient;
import org.springframework.web.socket.sockjs.client.Transport;
import org.springframework.web.socket.sockjs.client.WebSocketTransport;

import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.Assert.fail;

@Log4j2
public class QuotesWebSocketIntegrationTest {

	private static int port;

	private static TomcatWebSocketTestServer server;

	private static SockJsClient sockJsClient;

	private final static WebSocketHttpHeaders headers = new WebSocketHttpHeaders();

	@BeforeClass
	public static void setup() throws Exception {
		log.info("Setting up Quotes Web Socket Integration test....");
		port = SocketUtils.findAvailableTcpPort();

		server = new TomcatWebSocketTestServer(port);
		server.deployConfig(FrontendWebApplicationInitializer.class);
		server.start();

		loginAndSaveXAuthToken("harrymitchell", "HarryMitchell5!", headers);

		List<Transport> transports = new ArrayList<>();
		transports.add(new WebSocketTransport(new StandardWebSocketClient()));
		RestTemplateXhrTransport xhrTransport = new RestTemplateXhrTransport(new RestTemplate());
		xhrTransport.setRequestHeaders(headers);
		transports.add(xhrTransport);

		sockJsClient = new SockJsClient(transports);
        sockJsClient.setHttpHeaderNames("X-Auth-Token");
		log.info("Setup complete!");
	}

	private static void loginAndSaveXAuthToken(final String user, final String password,
											   final HttpHeaders headersToUpdate) {

		log.info("Authenticating user before subscribing to web socket");

		String url = "http://dev.bearchoke.com:" + port + "/api/authenticate";

		try {
			new RestTemplate().execute(url, HttpMethod.POST,

					request -> {
						MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
						map.add("username", user);
						map.add("password", password);
						new FormHttpMessageConverter().write(map, MediaType.APPLICATION_FORM_URLENCODED, request);
					},

					response -> {
						String xAuthToken = response.getHeaders().getFirst(ServerConstants.X_AUTH_TOKEN);
						log.info("Retrieved x-auth-token: " + xAuthToken);
						headersToUpdate.add(ServerConstants.X_AUTH_TOKEN, xAuthToken);
						return null;
					});
		} catch (Exception ex) {
			log.error(ex.getMessage(), ex);
		}
	}

	@AfterClass
	public static void teardown() throws Exception {
		log.info("Tearing down server after integration test complete");

		if (server != null) {
			try {
				server.undeployConfig();
			}
			catch (Throwable t) {
				log.error("Failed to undeploy application", t);
			}

			try {
				server.stop();
			}
			catch (Throwable t) {
				log.error("Failed to stop server", t);
			}
		}
	}


	@Test
	public void getQuotes() throws Exception {
		log.info("Testing getting stock quotes from QuoteService...");
		final CountDownLatch latch = new CountDownLatch(1);
		final AtomicReference<Throwable> failure = new AtomicReference<Throwable>();

		URI uri = new URI("ws://dev.bearchoke.com:" + port + "/ws");
		WebSocketStompClient stompClient = new WebSocketStompClient(uri, this.headers, sockJsClient);
		stompClient.setMessageConverter(new MappingJackson2MessageConverter());

		stompClient.connect(new StompMessageHandler() {

			private StompSession stompSession;

			@Override
			public void afterConnected(StompSession stompSession, StompHeaderAccessor headers) {
				String channel = "/topic/price.stock.*";
				QuotesWebSocketIntegrationTest.log.info("Subscribing to channel: " + channel);
				stompSession.subscribe(channel, RandomStringUtils.randomAlphabetic(10));
				this.stompSession = stompSession;
			}

			@Override
			public void handleMessage(Message<byte[]> message) {
				StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
				if (!headers.getDestination().startsWith("/topic/price.stock.")) {
					 failure.set(new IllegalStateException("Unexpected message: " + message));
				}

				QuotesWebSocketIntegrationTest.log.debug("Got \n" + new String(message.getPayload()));
				try {
					String json = new String(message.getPayload(), Charset.forName("UTF-8"));
					new JsonPathExpectationsHelper("$.company").assertValue(json, "Citrix Systems, Inc.");
					new JsonPathExpectationsHelper("$.ticker").assertValue(json, "CTXS");
				}
				catch (Throwable t) {
					failure.set(t);
				}
				finally {
					this.stompSession.disconnect();
					latch.countDown();
				}
			}

			@Override
			public void handleError(Message<byte[]> message) {
				failure.set(new Exception(new String(message.getPayload(), Charset.forName("UTF-8"))));
			}

			@Override
			public void handleReceipt(String receiptId) {}

			@Override
			public void afterDisconnected() {
				QuotesWebSocketIntegrationTest.log.info("Successfully disconnected from Web Socket channel");
			}
		});

		if (failure.get() != null) {
			throw new AssertionError("", failure.get());
		}

		if (!latch.await(5, TimeUnit.SECONDS)) {
			Assert.fail("Quotes not received");
		}
	}

}