/*
 * Copyright 2012-2017 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 org.springframework.boot.devtools.livereload;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.tomcat.websocket.WsWebSocketContainer;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

import org.springframework.util.SocketUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PingMessage;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Tests for {@link LiveReloadServer}.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 */
public class LiveReloadServerTests {

	private static final String HANDSHAKE = "{command: 'hello', "
			+ "protocols: ['http://livereload.com/protocols/official-7']}";

	private int port = SocketUtils.findAvailableTcpPort();

	private MonitoredLiveReloadServer server;

	@Before
	public void setUp() throws Exception {
		this.server = new MonitoredLiveReloadServer(this.port);
		this.server.start();
	}

	@After
	public void tearDown() throws Exception {
		this.server.stop();
	}

	@Test
	@Ignore
	public void servesLivereloadJs() throws Exception {
		RestTemplate template = new RestTemplate();
		URI uri = new URI("http://localhost:" + this.port + "/livereload.js");
		String script = template.getForObject(uri, String.class);
		assertThat(script).contains("livereload.com/protocols/official-7");
	}

	@Test
	public void triggerReload() throws Exception {
		LiveReloadWebSocketHandler handler = connect();
		this.server.triggerReload();
		Thread.sleep(200);
		this.server.stop();
		assertThat(handler.getMessages().get(0))
				.contains("http://livereload.com/protocols/official-7");
		assertThat(handler.getMessages().get(1)).contains("command\":\"reload\"");
	}

	@Test
	public void pingPong() throws Exception {
		LiveReloadWebSocketHandler handler = connect();
		handler.sendMessage(new PingMessage());
		Thread.sleep(200);
		assertThat(handler.getPongCount()).isEqualTo(1);
		this.server.stop();
	}

	@Test
	public void clientClose() throws Exception {
		LiveReloadWebSocketHandler handler = connect();
		handler.close();
		awaitClosedException();
		assertThat(this.server.getClosedExceptions().size()).isGreaterThan(0);
	}

	private void awaitClosedException() throws InterruptedException {
		long startTime = System.currentTimeMillis();
		while (this.server.getClosedExceptions().isEmpty()
				&& System.currentTimeMillis() - startTime < 10000) {
			Thread.sleep(100);
		}
	}

	@Test
	public void serverClose() throws Exception {
		LiveReloadWebSocketHandler handler = connect();
		this.server.stop();
		Thread.sleep(200);
		assertThat(handler.getCloseStatus().getCode()).isEqualTo(1006);
	}

	private LiveReloadWebSocketHandler connect() throws Exception {
		WebSocketClient client = new StandardWebSocketClient(new WsWebSocketContainer());
		LiveReloadWebSocketHandler handler = new LiveReloadWebSocketHandler();
		client.doHandshake(handler, "ws://localhost:" + this.port + "/livereload");
		handler.awaitHello();
		return handler;
	}

	/**
	 * Useful main method for manual testing against a real browser.
	 * @param args main args
	 * @throws IOException in case of I/O errors
	 */
	public static void main(String[] args) throws IOException {
		LiveReloadServer server = new LiveReloadServer();
		server.start();
		while (true) {
			try {
				Thread.sleep(1000);
			}
			catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
			}
			server.triggerReload();
		}
	}

	/**
	 * {@link LiveReloadServer} with additional monitoring.
	 */
	private static class MonitoredLiveReloadServer extends LiveReloadServer {

		private final List<ConnectionClosedException> closedExceptions = new ArrayList<>();

		private final Object monitor = new Object();

		MonitoredLiveReloadServer(int port) {
			super(port);
		}

		@Override
		protected Connection createConnection(java.net.Socket socket,
				InputStream inputStream, OutputStream outputStream) throws IOException {
			return new MonitoredConnection(socket, inputStream, outputStream);
		}

		public List<ConnectionClosedException> getClosedExceptions() {
			synchronized (this.monitor) {
				return new ArrayList<>(this.closedExceptions);
			}
		}

		private class MonitoredConnection extends Connection {

			MonitoredConnection(java.net.Socket socket, InputStream inputStream,
					OutputStream outputStream) throws IOException {
				super(socket, inputStream, outputStream);
			}

			@Override
			public void run() throws Exception {
				try {
					super.run();
				}
				catch (ConnectionClosedException ex) {
					ex.printStackTrace();
					synchronized (MonitoredLiveReloadServer.this.monitor) {
						MonitoredLiveReloadServer.this.closedExceptions.add(ex);
					}
					throw ex;
				}
			}

		}

	}

	private static class LiveReloadWebSocketHandler extends TextWebSocketHandler {

		private WebSocketSession session;

		private final CountDownLatch helloLatch = new CountDownLatch(2);

		private final List<String> messages = new ArrayList<>();

		private int pongCount;

		private CloseStatus closeStatus;

		@Override
		public void afterConnectionEstablished(WebSocketSession session)
				throws Exception {
			this.session = session;
			session.sendMessage(new TextMessage(HANDSHAKE));
			this.helloLatch.countDown();
		}

		public void awaitHello() throws InterruptedException {
			this.helloLatch.await(1, TimeUnit.MINUTES);
			Thread.sleep(200);
		}

		@Override
		protected void handleTextMessage(WebSocketSession session, TextMessage message)
				throws Exception {
			if (message.getPayload().contains("hello")) {
				this.helloLatch.countDown();
			}
			this.messages.add(message.getPayload());
		}

		@Override
		protected void handlePongMessage(WebSocketSession session, PongMessage message)
				throws Exception {
			this.pongCount++;
		}

		@Override
		public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
				throws Exception {
			this.closeStatus = status;
		}

		public void sendMessage(WebSocketMessage<?> message) throws IOException {
			this.session.sendMessage(message);
		}

		public void close() throws IOException {
			this.session.close();
		}

		public List<String> getMessages() {
			return this.messages;
		}

		public int getPongCount() {
			return this.pongCount;
		}

		public CloseStatus getCloseStatus() {
			return this.closeStatus;
		}

	}

}
