/*
 * Copyright 2002-2018 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.core.convert.support;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;

/**
 * Tests for {@link StreamConverter}.
 *
 * @author Stephane Nicoll
 * @since 4.2
 */
public class StreamConverterTests {

	@Rule
	public final ExpectedException thrown = ExpectedException.none();

	private final GenericConversionService conversionService = new GenericConversionService();

	private final StreamConverter streamConverter = new StreamConverter(this.conversionService);


	@Before
	public void setup() {
		this.conversionService.addConverter(new CollectionToCollectionConverter(this.conversionService));
		this.conversionService.addConverter(new ArrayToCollectionConverter(this.conversionService));
		this.conversionService.addConverter(new CollectionToArrayConverter(this.conversionService));
		this.conversionService.addConverter(this.streamConverter);
	}


	@Test
	public void convertFromStreamToList() throws NoSuchFieldException {
		this.conversionService.addConverter(Number.class, String.class, new ObjectToStringConverter());
		Stream<Integer> stream = Arrays.asList(1, 2, 3).stream();
		TypeDescriptor listOfStrings = new TypeDescriptor(Types.class.getField("listOfStrings"));
		Object result = this.conversionService.convert(stream, listOfStrings);

		assertNotNull("Converted object must not be null", result);
		assertTrue("Converted object must be a list", result instanceof List);
		@SuppressWarnings("unchecked")
		List<String> content = (List<String>) result;
		assertEquals("1", content.get(0));
		assertEquals("2", content.get(1));
		assertEquals("3", content.get(2));
		assertEquals("Wrong number of elements", 3, content.size());
	}

	@Test
	public void convertFromStreamToArray() throws NoSuchFieldException {
		this.conversionService.addConverterFactory(new NumberToNumberConverterFactory());
		Stream<Integer> stream = Arrays.asList(1, 2, 3).stream();
		TypeDescriptor arrayOfLongs = new TypeDescriptor(Types.class.getField("arrayOfLongs"));
		Object result = this.conversionService.convert(stream, arrayOfLongs);

		assertNotNull("Converted object must not be null", result);
		assertTrue("Converted object must be an array", result.getClass().isArray());
		Long[] content = (Long[]) result;
		assertEquals(Long.valueOf(1L), content[0]);
		assertEquals(Long.valueOf(2L), content[1]);
		assertEquals(Long.valueOf(3L), content[2]);
		assertEquals("Wrong number of elements", 3, content.length);
	}

	@Test
	public void convertFromStreamToRawList() throws NoSuchFieldException {
		Stream<Integer> stream = Arrays.asList(1, 2, 3).stream();
		TypeDescriptor listOfStrings = new TypeDescriptor(Types.class.getField("rawList"));
		Object result = this.conversionService.convert(stream, listOfStrings);

		assertNotNull("Converted object must not be null", result);
		assertTrue("Converted object must be a list", result instanceof List);
		@SuppressWarnings("unchecked")
		List<Object> content = (List<Object>) result;
		assertEquals(1, content.get(0));
		assertEquals(2, content.get(1));
		assertEquals(3, content.get(2));
		assertEquals("Wrong number of elements", 3, content.size());
	}

	@Test
	public void convertFromStreamToArrayNoConverter() throws NoSuchFieldException {
		Stream<Integer> stream = Arrays.asList(1, 2, 3).stream();
		TypeDescriptor arrayOfLongs = new TypeDescriptor(Types.class.getField("arrayOfLongs"));

		thrown.expect(ConversionFailedException.class);
		thrown.expectCause(is(instanceOf(ConverterNotFoundException.class)));
		this.conversionService.convert(stream, arrayOfLongs);
	}

	@Test
	@SuppressWarnings("resource")
	public void convertFromListToStream() throws NoSuchFieldException {
		this.conversionService.addConverterFactory(new StringToNumberConverterFactory());
		List<String> stream = Arrays.asList("1", "2", "3");
		TypeDescriptor streamOfInteger = new TypeDescriptor(Types.class.getField("streamOfIntegers"));
		Object result = this.conversionService.convert(stream, streamOfInteger);

		assertNotNull("Converted object must not be null", result);
		assertTrue("Converted object must be a stream", result instanceof Stream);
		@SuppressWarnings("unchecked")
		Stream<Integer> content = (Stream<Integer>) result;
		assertEquals(6, content.mapToInt((x) -> x).sum());
	}

	@Test
	@SuppressWarnings("resource")
	public void convertFromArrayToStream() throws NoSuchFieldException {
		Integer[] stream = new Integer[] {1, 0, 1};
		this.conversionService.addConverter(new Converter<Integer, Boolean>() {
			@Override
			public Boolean convert(Integer source) {
				return source == 1;
			}
		});
		TypeDescriptor streamOfBoolean = new TypeDescriptor(Types.class.getField("streamOfBooleans"));
		Object result = this.conversionService.convert(stream, streamOfBoolean);

		assertNotNull("Converted object must not be null", result);
		assertTrue("Converted object must be a stream", result instanceof Stream);
		@SuppressWarnings("unchecked")
		Stream<Boolean> content = (Stream<Boolean>) result;
		assertEquals(2, content.filter(x -> x).count());
	}

	@Test
	@SuppressWarnings("resource")
	public void convertFromListToRawStream() throws NoSuchFieldException {
		List<String> stream = Arrays.asList("1", "2", "3");
		TypeDescriptor streamOfInteger = new TypeDescriptor(Types.class.getField("rawStream"));
		Object result = this.conversionService.convert(stream, streamOfInteger);

		assertNotNull("Converted object must not be null", result);
		assertTrue("Converted object must be a stream", result instanceof Stream);
		@SuppressWarnings("unchecked")
		Stream<Object> content = (Stream<Object>) result;
		StringBuilder sb = new StringBuilder();
		content.forEach(sb::append);
		assertEquals("123", sb.toString());
	}

	@Test
	public void doesNotMatchIfNoStream() throws NoSuchFieldException {
		assertFalse("Should not match non stream type", this.streamConverter.matches(
				new TypeDescriptor(Types.class.getField("listOfStrings")),
				new TypeDescriptor(Types.class.getField("arrayOfLongs"))));
	}

	@Test
	public void shouldFailToConvertIfNoStream() throws NoSuchFieldException {
		thrown.expect(IllegalStateException.class);
		this.streamConverter.convert(new Object(), new TypeDescriptor(Types.class.getField("listOfStrings")),
				new TypeDescriptor(Types.class.getField("arrayOfLongs")));
	}


	@SuppressWarnings({ "rawtypes" })
	static class Types {

		public List<String> listOfStrings;

		public Long[] arrayOfLongs;

		public Stream<Integer> streamOfIntegers;

		public Stream<Boolean> streamOfBooleans;

		public Stream rawStream;

		public List rawList;
	}

}
