/*
 * Copyright 2002-2019 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
 *
 *      https://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.http.codec.json;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.Consumer;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import org.json.JSONException;
import org.junit.Before;
import org.junit.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;

import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase;
import org.springframework.core.io.buffer.DataBuffer;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;

/**
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @author Juergen Hoeller
 */
public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase {

	private JsonFactory jsonFactory;

	private ObjectMapper objectMapper;


	@Before
	public void createParser() {
		this.jsonFactory = new JsonFactory();
		this.objectMapper = new ObjectMapper(this.jsonFactory);
	}


	@Test
	public void doNotTokenizeArrayElements() {
		testTokenize(
				singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"),
				singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), false);

		testTokenize(
				asList("{\"foo\": \"foofoo\"",
						", \"bar\": \"barbar\"}"),
				singletonList("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), false);

		testTokenize(
				singletonList("[" +
						"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}," +
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
				singletonList("[" +
						"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}," +
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"), false);

		testTokenize(
				singletonList("[{\"foo\": \"bar\"},{\"foo\": \"baz\"}]"),
				singletonList("[{\"foo\": \"bar\"},{\"foo\": \"baz\"}]"), false);

		testTokenize(
				asList("[" +
						"{\"foo\": \"foofoo\", \"bar\"", ": \"barbar\"}," +
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
				singletonList("[" +
						"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}," +
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"), false);

		testTokenize(
				asList("[",
						"{\"id\":1,\"name\":\"Robert\"}", ",",
						"{\"id\":2,\"name\":\"Raide\"}", ",",
						"{\"id\":3,\"name\":\"Ford\"}", "]"),
				singletonList("[" +
						"{\"id\":1,\"name\":\"Robert\"}," +
						"{\"id\":2,\"name\":\"Raide\"}," +
						"{\"id\":3,\"name\":\"Ford\"}]"), false);

		// SPR-16166: top-level JSON values
		testTokenize(asList("\"foo", "bar\""),singletonList("\"foobar\""), false);

		testTokenize(asList("12", "34"),singletonList("1234"), false);

		testTokenize(asList("12.", "34"),singletonList("12.34"), false);

		// note that we do not test for null, true, or false, which are also valid top-level values,
		// but are unsupported by JSONassert
	}

	@Test
	public void tokenizeArrayElements() {
		testTokenize(
				singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"),
				singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), true);

		testTokenize(
				asList("{\"foo\": \"foofoo\"", ", \"bar\": \"barbar\"}"),
				singletonList("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), true);

		testTokenize(
				singletonList("[" +
						"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}," +
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
				asList(
						"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}",
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"), true);

		testTokenize(
				singletonList("[{\"foo\": \"bar\"},{\"foo\": \"baz\"}]"),
				asList("{\"foo\": \"bar\"}", "{\"foo\": \"baz\"}"), true);

		// SPR-15803: nested array
		testTokenize(
				singletonList("[" +
						"{\"id\":\"0\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}," +
						"{\"id\":\"1\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}," +
						"{\"id\":\"2\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}" +
						"]"),
				asList(
						"{\"id\":\"0\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}",
						"{\"id\":\"1\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}",
						"{\"id\":\"2\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}"), true);

		// SPR-15803: nested array, no top-level array
		testTokenize(
				singletonList("{\"speakerIds\":[\"tastapod\"],\"language\":\"ENGLISH\"}"),
				singletonList("{\"speakerIds\":[\"tastapod\"],\"language\":\"ENGLISH\"}"), true);

		testTokenize(
				asList("[" +
						"{\"foo\": \"foofoo\", \"bar\"", ": \"barbar\"}," +
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
				asList(
						"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}",
						"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"), true);

		testTokenize(
				asList("[",
						"{\"id\":1,\"name\":\"Robert\"}",
						",",
						"{\"id\":2,\"name\":\"Raide\"}",
						",",
						"{\"id\":3,\"name\":\"Ford\"}",
						"]"),
				asList("{\"id\":1,\"name\":\"Robert\"}",
						"{\"id\":2,\"name\":\"Raide\"}",
						"{\"id\":3,\"name\":\"Ford\"}"), true);

		// SPR-16166: top-level JSON values
		testTokenize(asList("\"foo", "bar\""),singletonList("\"foobar\""), true);

		testTokenize(asList("12", "34"),singletonList("1234"), true);

		testTokenize(asList("12.", "34"),singletonList("12.34"), true);

		// SPR-16407
		testTokenize(asList("[1", ",2,", "3]"), asList("1", "2", "3"), true);
	}

	@Test
	public void errorInStream() {
		DataBuffer buffer = stringBuffer("{\"id\":1,\"name\":");
		Flux<DataBuffer> source = Flux.just(buffer).concatWith(Flux.error(new RuntimeException()));
		Flux<TokenBuffer> result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true);

		StepVerifier.create(result)
				.expectError(RuntimeException.class)
				.verify();
	}

	@Test  // SPR-16521
	public void jsonEOFExceptionIsWrappedAsDecodingError() {
		Flux<DataBuffer> source = Flux.just(stringBuffer("{\"status\": \"noClosingQuote}"));
		Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false);

		StepVerifier.create(tokens)
				.expectError(DecodingException.class)
				.verify();
	}


	private void testTokenize(List<String> source, List<String> expected, boolean tokenizeArrayElements) {
		Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(
				Flux.fromIterable(source).map(this::stringBuffer),
				this.jsonFactory, this.objectMapper, tokenizeArrayElements);

		Flux<String> result = tokens
				.map(tokenBuffer -> {
					try {
						TreeNode root = this.objectMapper.readTree(tokenBuffer.asParser());
						return this.objectMapper.writeValueAsString(root);
					}
					catch (IOException ex) {
						throw new UncheckedIOException(ex);
					}
				});

		StepVerifier.FirstStep<String> builder = StepVerifier.create(result);
		expected.forEach(s -> builder.assertNext(new JSONAssertConsumer(s)));
		builder.verifyComplete();
	}

	private DataBuffer stringBuffer(String value) {
		byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
		DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
		buffer.write(bytes);
		return buffer;
	}


	private static class JSONAssertConsumer implements Consumer<String> {

		private final String expected;

		JSONAssertConsumer(String expected) {
			this.expected = expected;
		}

		@Override
		public void accept(String s) {
			try {
				JSONAssert.assertEquals(this.expected, s, true);
			}
			catch (JSONException ex) {
				throw new RuntimeException(ex);
			}
		}
	}

}