package com.googlecode.jsonrpc4j.server;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.googlecode.jsonrpc4j.ErrorResolver;
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
import com.googlecode.jsonrpc4j.JsonRpcServer;
import com.googlecode.jsonrpc4j.util.Util;
import org.easymock.EasyMock;
import org.easymock.EasyMockRunner;
import org.easymock.Mock;
import org.easymock.MockType;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.*;
import static com.googlecode.jsonrpc4j.util.Util.*;
import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;

@RunWith(EasyMockRunner.class)
public class JsonRpcServerTest {

	@Mock(type = MockType.NICE)
	private ServiceInterface mockService;
	@Mock(type = MockType.NICE)
	private JsonRpcInterceptor mockInterceptor;
	private ByteArrayOutputStream byteArrayOutputStream;
	private JsonRpcServer jsonRpcServer;

	@Before
	public void setup() {
		jsonRpcServer = new JsonRpcServer(Util.mapper, mockService, ServiceInterface.class);
		jsonRpcServer.setInterceptorList(new ArrayList<JsonRpcInterceptor>() {{
			add(mockInterceptor);
		}});
		byteArrayOutputStream = new ByteArrayOutputStream();
	}

	@Test
	public void testGetMethod_badRequest_corruptParams() throws Exception {
		EasyMock.expect(mockService.testMethod("Whirinaki")).andReturn("Forest");
		EasyMock.replay(mockService);

		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test-get");
		MockHttpServletResponse response = new MockHttpServletResponse();

		request.addParameter("id", Integer.toString(123));
		request.addParameter("method", "testMethod");
		request.addParameter("params", "{BROKEN}");

		jsonRpcServer.handle(request, response);

		assertTrue(MockHttpServletResponse.SC_BAD_REQUEST == response.getStatus());

		JsonNode errorNode = error(toByteArrayOutputStream(response.getContentAsByteArray()));

		assertNotNull(errorNode);
		assertEquals(errorCode(errorNode).asLong(), (long) ErrorResolver.JsonError.PARSE_ERROR.code);
	}

	@Test
	public void testGetMethod_badRequest_noMethod() throws Exception {
		EasyMock.expect(mockService.testMethod("Whirinaki")).andReturn("Forest");
		EasyMock.replay(mockService);

		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test-get");
		MockHttpServletResponse response = new MockHttpServletResponse();

		request.addParameter("id", Integer.toString(123));
		// no method!
		request.addParameter("params", net.iharder.Base64.encodeBytes("[\"Whirinaki\"]".getBytes(StandardCharsets.UTF_8)));

		jsonRpcServer.handle(request, response);

		assertTrue(MockHttpServletResponse.SC_NOT_FOUND == response.getStatus());

		JsonNode errorNode = error(toByteArrayOutputStream(response.getContentAsByteArray()));

		assertNotNull(errorNode);
		assertEquals(errorCode(errorNode).asLong(), (long) ErrorResolver.JsonError.METHOD_NOT_FOUND.code);
	}

	@Test
	public void test_contentType() throws Exception {
		EasyMock.expect(mockService.testMethod("Whir?inaki")).andReturn("For?est");
		EasyMock.replay(mockService);

		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test-post");
		MockHttpServletResponse response = new MockHttpServletResponse();

		request.setContentType("application/json");
		request.setContent("{\"jsonrpc\":\"2.0\",\"id\":123,\"method\":\"testMethod\",\"params\":[\"Whir?inaki\"]}".getBytes(StandardCharsets.UTF_8));

		jsonRpcServer.setContentType("flip/flop");

		jsonRpcServer.handle(request, response);

		assertTrue("flip/flop".equals(response.getContentType()));
		checkSuccessfulResponse(response);
	}

	private void checkSuccessfulResponse(MockHttpServletResponse response) throws IOException {
		assertTrue(HttpServletResponse.SC_OK == response.getStatus());

		JsonNode responseEnvelope = decodeAnswer(toByteArrayOutputStream(response.getContentAsByteArray()));
		assertTrue(responseEnvelope.get(ID).isIntegralNumber());
		assertEquals(responseEnvelope.get(ID).asLong(), 123L);
		assertTrue(responseEnvelope.get(RESULT).isTextual());
		assertEquals(responseEnvelope.get(RESULT).asText(), "For?est");
	}

	@Test
	public void testGetMethod_base64Params() throws Exception {
		EasyMock.expect(mockService.testMethod("Whir?inaki")).andReturn("For?est");
		EasyMock.replay(mockService);

		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test-get");
		MockHttpServletResponse response = new MockHttpServletResponse();

		request.addParameter("id", Integer.toString(123));
		request.addParameter("method", "testMethod");
		request.addParameter("params", net.iharder.Base64.encodeBytes("[\"Whir?inaki\"]".getBytes(StandardCharsets.UTF_8)));

		jsonRpcServer.handle(request, response);

		assertTrue("application/json-rpc".equals(response.getContentType()));
		checkSuccessfulResponse(response);
	}

	@Test
	public void testGetMethod_unencodedParams() throws Exception {
		EasyMock.expect(mockService.testMethod("Whir?inaki")).andReturn("For?est");
		EasyMock.replay(mockService);

		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test-get");
		MockHttpServletResponse response = new MockHttpServletResponse();

		request.addParameter("id", Integer.toString(123));
		request.addParameter("method", "testMethod");
		request.addParameter("params", "[\"Whir?inaki\"]");

		jsonRpcServer.handle(request, response);

		assertTrue("application/json-rpc".equals(response.getContentType()));
		checkSuccessfulResponse(response);
	}

	@Test
	public void testNullRequest() throws Exception {
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test-get");
		MockHttpServletResponse response = new MockHttpServletResponse();

		jsonRpcServer.handle(request, response);
		assertTrue(MockHttpServletResponse.SC_BAD_REQUEST == response.getStatus());
	}

	@Test
	public void testGzipResponse() throws IOException {
		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test-post");
		request.addHeader(ACCEPT_ENCODING, "gzip");
		request.setContentType("application/json");
		request.setContent("{\"jsonrpc\":\"2.0\",\"id\":123,\"method\":\"testMethod\",\"params\":[\"Whir?inaki\"]}".getBytes(StandardCharsets.UTF_8));

		MockHttpServletResponse response = new MockHttpServletResponse();

		jsonRpcServer = new JsonRpcServer(Util.mapper, mockService, ServiceInterface.class, true);
		jsonRpcServer.handle(request, response);

		byte[] compressed = response.getContentAsByteArray();
		String sb = getCompressedResponseContent(compressed);

		Assert.assertEquals(sb, "{\"jsonrpc\":\"2.0\",\"id\":123,\"result\":null}");
		Assert.assertEquals("gzip", response.getHeader(CONTENT_ENCODING));
	}

	@Test
	public void testGzipResponseMultipleAcceptEncoding() throws IOException {
		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test-post");
		request.addHeader(ACCEPT_ENCODING, "gzip,deflate");
		request.setContentType("application/json");
		request.setContent("{\"jsonrpc\":\"2.0\",\"id\":123,\"method\":\"testMethod\",\"params\":[\"Whir?inaki\"]}".getBytes(StandardCharsets.UTF_8));

		MockHttpServletResponse response = new MockHttpServletResponse();

		jsonRpcServer = new JsonRpcServer(Util.mapper, mockService, ServiceInterface.class, true);
		jsonRpcServer.handle(request, response);

		byte[] compressed = response.getContentAsByteArray();
		String sb = getCompressedResponseContent(compressed);

		Assert.assertEquals(sb, "{\"jsonrpc\":\"2.0\",\"id\":123,\"result\":null}");
		Assert.assertEquals("gzip", response.getHeader(CONTENT_ENCODING));
	}

	@Test
	public void testCorruptRequest() throws Exception {
		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test-post");
		request.setContentType("application/json");
		request.setContent("{NOT JSON}".getBytes(StandardCharsets.UTF_8));

		MockHttpServletResponse response = new MockHttpServletResponse();

		jsonRpcServer = new JsonRpcServer(Util.mapper, mockService, ServiceInterface.class, true);
		jsonRpcServer.handle(request, response);

		String content = response.getContentAsString();

		Assert.assertEquals(content, "{\"jsonrpc\":\"2.0\",\"id\":\"null\"," +
				"\"error\":{\"code\":-32700,\"message\":\"JSON parse error\"}}\n");
	}

	private String getCompressedResponseContent(byte[] compressed) throws IOException {
		GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(compressed));
		InputStreamReader inputStreamReader = new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8);
		BufferedReader bufferedReader = new BufferedReader(inputStreamReader, 2048);
		StringBuilder sb = new StringBuilder();
		String readed;
		while ((readed = bufferedReader.readLine()) != null) {
			sb.append(readed);
		}
		return sb.toString();
	}

	@Test
	public void testGzipRequest() throws IOException {
		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test-post");
		request.addHeader(CONTENT_ENCODING, "gzip");
		request.setContentType("application/json");
		byte[] bytes = "{\"jsonrpc\":\"2.0\",\"id\":123,\"method\":\"testMethod\",\"params\":[\"Whir?inaki\"]}".getBytes(StandardCharsets.UTF_8);

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		GZIPOutputStream gos = new GZIPOutputStream(baos);
		gos.write(bytes);
		gos.close();

		request.setContent(baos.toByteArray());

		MockHttpServletResponse response = new MockHttpServletResponse();
		jsonRpcServer = new JsonRpcServer(Util.mapper, mockService, ServiceInterface.class, true);
		jsonRpcServer.handle(request, response);

		String responseContent = new String(response.getContentAsByteArray(), StandardCharsets.UTF_8);
		Assert.assertEquals(responseContent, "{\"jsonrpc\":\"2.0\",\"id\":123,\"result\":null}\n");
		Assert.assertNull(response.getHeader(CONTENT_ENCODING));
	}

	@Test
	public void testGzipRequestAndResponse() throws IOException {
		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test-post");
		request.addHeader(CONTENT_ENCODING, "gzip");
		request.addHeader(ACCEPT_ENCODING, "gzip");
		request.setContentType("application/json");
		byte[] bytes = "{\"jsonrpc\":\"2.0\",\"id\":123,\"method\":\"testMethod\",\"params\":[\"Whir?inaki\"]}".getBytes(StandardCharsets.UTF_8);

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		GZIPOutputStream gos = new GZIPOutputStream(baos);
		gos.write(bytes);
		gos.close();

		request.setContent(baos.toByteArray());

		MockHttpServletResponse response = new MockHttpServletResponse();
		jsonRpcServer = new JsonRpcServer(Util.mapper, mockService, ServiceInterface.class, true);
		jsonRpcServer.handle(request, response);

		byte[] compressed = response.getContentAsByteArray();
		String sb = getCompressedResponseContent(compressed);

		Assert.assertEquals(sb, "{\"jsonrpc\":\"2.0\",\"id\":123,\"result\":null}");
		Assert.assertEquals("gzip", response.getHeader(CONTENT_ENCODING));
	}

	@Test
	public void interceptorsPreHandleJsonExceptionTest() throws IOException {
		String requestNotRpc = "{\"test\": 1}";
		String exceptionMessage = "123";

		//
		mockInterceptor.preHandleJson(mapper.readTree(requestNotRpc));
		expectLastCall().andThrow(new RuntimeException(exceptionMessage));

		replay(mockInterceptor);
		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/v1/zone");
		request.setContent(requestNotRpc.getBytes(StandardCharsets.UTF_8));
		MockHttpServletResponse response = new MockHttpServletResponse();
		jsonRpcServer.handle(request, response);
		assertEquals(400, response.getStatus());
		assertEquals("", response.getContentAsString());
		verify(mockInterceptor);
	}

	@Test
	public void interceptorsPreHandleExceptionTest() throws IOException {
		final String requestGood = "{\n" +
				"  \"id\": 0,\n" +
				"  \"jsonrpc\": \"2.0\",\n" +
				"  \"method\": \"testMethod\",\n" +
				"  \"params\": [\"test.cool\"]\n" +
				"  }\n" +
				"}";
		String exceptionMessage = "123";
		String responseError = "{\"jsonrpc\":\"2.0\",\"id\":0,\"error\":{\"code\":-32001,\"message\":\"" +
				exceptionMessage + "\",\"data\":{\"exceptionTypeName\":\"java.lang.RuntimeException\",\"message\":\"" +
				exceptionMessage + "\"}}}";

		//
		mockInterceptor.preHandle(
				anyObject(),
				anyObject(Method.class),
				eq(new ArrayList<JsonNode>() {{
					add(mapper.readTree(requestGood).at("/params/0"));
				}})
		);
		expectLastCall().andThrow(new RuntimeException(exceptionMessage));

		replay(mockInterceptor);
		jsonRpcServer.handleRequest(new ByteArrayInputStream(requestGood.getBytes(StandardCharsets.UTF_8)), byteArrayOutputStream);
		assertEquals(responseError, byteArrayOutputStream.toString("UTF-8").trim());
		verify(mockInterceptor);
	}

	@Test
	public void interceptorsPostHandleExceptionTest() throws IOException {
		final String requestGood = "{\n" +
				"  \"id\": 0,\n" +
				"  \"jsonrpc\": \"2.0\",\n" +
				"  \"method\": \"testMethod\",\n" +
				"  \"params\": [\"test.cool\"]\n" +
				"  }\n" +
				"}";
		String exceptionMessage = "123";
		String returnString = "test";
		String responseError = "{\"jsonrpc\":\"2.0\",\"id\":0,\"error\":{\"code\":-32001,\"message\":\"" +
				exceptionMessage + "\",\"data\":{\"exceptionTypeName\":\"java.lang.RuntimeException\",\"message\":\"" +
				exceptionMessage + "\"}}}";

		//
		expect(mockService.testMethod(mapper.readTree(requestGood).at("/params/0").asText())).andReturn(returnString);
		mockInterceptor.postHandle(
				anyObject(),
				anyObject(Method.class),
				eq(new ArrayList<JsonNode>() {{
					add(mapper.readTree(requestGood).at("/params/0"));
				}}),
				eq(new TextNode(returnString))
		);
		expectLastCall().andThrow(new RuntimeException(exceptionMessage));

		replay(mockService, mockInterceptor);
		jsonRpcServer.handleRequest(new ByteArrayInputStream(requestGood.getBytes(StandardCharsets.UTF_8)), byteArrayOutputStream);
		assertEquals(responseError, byteArrayOutputStream.toString("UTF-8").trim());
		verify(mockService, mockInterceptor);
	}

	@Test
	public void interceptorsPostHandleJsonExceptionTest() throws IOException {
		final String requestGood = "{\n" +
				"  \"id\": 0,\n" +
				"  \"jsonrpc\": \"2.0\",\n" +
				"  \"method\": \"testMethod\",\n" +
				"  \"params\": [\"test.cool\"]\n" +
				"  }\n" +
				"}";
		String exceptionMessage = "123";
		String returnString = "test";
		String responseError = "{\"jsonrpc\":\"2.0\",\"id\":0,\"error\":{\"code\":-32001,\"message\":\"" +
				exceptionMessage + "\",\"data\":{\"exceptionTypeName\":\"java.lang.RuntimeException\",\"message\":\"" +
				exceptionMessage + "\"}}}";

		//
		expect(mockService.testMethod(mapper.readTree(requestGood).at("/params/0").asText())).andReturn(returnString);
		mockInterceptor.postHandleJson(anyObject(JsonNode.class));
		expectLastCall().andThrow(new RuntimeException(exceptionMessage));

		replay(mockService, mockInterceptor);
		jsonRpcServer.handleRequest(new ByteArrayInputStream(requestGood.getBytes(StandardCharsets.UTF_8)), byteArrayOutputStream);
		assertEquals(responseError, byteArrayOutputStream.toString("UTF-8").trim());
		verify(mockService, mockInterceptor);
	}

	// Service and service interfaces used in test

	public interface ServiceInterface {
		String testMethod(String param1);
	}

}