package com.taxonic.carml.engine;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.StringContains.containsString;
import static org.hamcrest.core.StringStartsWith.startsWith;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.taxonic.carml.logical_source_resolver.CsvResolver;
import com.taxonic.carml.logical_source_resolver.JsonPathResolver;
import com.taxonic.carml.logical_source_resolver.LogicalSourceResolver;
import com.taxonic.carml.logical_source_resolver.XPathResolver;
import com.taxonic.carml.model.NameableStream;
import com.taxonic.carml.model.TriplesMap;
import com.taxonic.carml.model.impl.CarmlLogicalSource;
import com.taxonic.carml.model.impl.CarmlStream;
import com.taxonic.carml.util.RmlMappingLoader;
import com.taxonic.carml.vocab.Rdf;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Optional;
import org.apache.commons.io.IOUtils;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class RmlMapperTest {

	RmlMapper mapper;
	NameableStream stream;
	final String input = "test input";
	final String secondInput = "second test input";

	@Before
	public void prepareMapper() {
		mapper = RmlMapper.newBuilder()
				.setLogicalSourceResolver(Rdf.Ql.Csv, new CsvResolver())
				.setLogicalSourceResolver(Rdf.Ql.JsonPath, new JsonPathResolver())
				.setLogicalSourceResolver(Rdf.Ql.XPath, new XPathResolver())
				.build();
	}

	@Test
	public void mapper_boundWithUnnamedInputStream_shouldReadInputStream() throws IOException {
		stream = new CarmlStream();
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		mapper.bindInputStream(inputStream);
		assertThat(mapper.getSourceManager().getSource(RmlMapper.DEFAULT_STREAM_NAME), is(input));
	}

	@Test
	public void mapper_boundWithNamedInputStream_shouldReadCorrespondingInputStream() {
		String streamName = "foo";
		stream = new CarmlStream(streamName);
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		mapper.bindInputStream(streamName, inputStream);
		assertThat(mapper.getSourceManager().getSource(streamName), is(input));
	}

	@Test
	public void mapper_boundWithUnnamedInputStream_shouldThrowErrorOnUnknownStream() {
		String streamName = "foo";
		stream = new CarmlStream(streamName);
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		mapper.bindInputStream(inputStream);

		RmlMappingLoader loader = RmlMappingLoader.build();
		InputStream input = RmlMapperTest.class.getResourceAsStream("simple.namedcarml.rml.ttl");

		RuntimeException exception = assertThrows(RuntimeException.class, () -> mapper.map(loader.load(RDFFormat.TURTLE, input)));
		assertThat(exception.getMessage(), is(String.format("attempting to get source by "
				+ "name [%s], but no such binding is present", streamName)));
	}

	@Test
	public void mapper_boundWithNamedInputStream_shouldThrowErrorOnUnknownStream() {
		String streamName = "bar";
		String unknownStreamName = "foo";
		stream = new CarmlStream(unknownStreamName);
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		mapper.bindInputStream(streamName, inputStream);

		RmlMappingLoader loader = RmlMappingLoader.build();
		InputStream input = RmlMapperTest.class.getResourceAsStream("simple.namedcarml.rml.ttl");

		RuntimeException exception = assertThrows(RuntimeException.class, () -> mapper.map(loader.load(RDFFormat.TURTLE, input)));
		assertThat(exception.getMessage(), is(String.format("attempting to get source by "
				+ "name [%s], but no such binding is present", unknownStreamName)));
	}

	@Test
	public void mapper_boundWithMultipleNamedInputStreams_shouldReadCorrespondingStreams() {
		String streamName = "foo";
		String secondStreamName = "bar";
		stream = new CarmlStream(streamName);
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		String secondInput = "second test input";
		InputStream secondInputStream = IOUtils.toInputStream(secondInput, Charset.defaultCharset());
		mapper.bindInputStream(streamName, inputStream);
		mapper.bindInputStream(secondStreamName, secondInputStream);
		assertThat(mapper.getSourceManager().getSource(streamName), is(input));
		assertThat(mapper.getSourceManager().getSource(secondStreamName), is(secondInput));
	}

	@Test
	public void mapper_boundWithMultipleNamedAndOneUnnamedInputStreams_shouldReadCorrespondingStreams() {
		String streamName = "foo";
		String secondStreamName = "bar";
		stream = new CarmlStream(streamName);
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		InputStream secondInputStream = IOUtils.toInputStream(secondInput, Charset.defaultCharset());
		String unnamedInput = "unnamed test input";
		InputStream unnamedInputStream = IOUtils.toInputStream(unnamedInput, Charset.defaultCharset());
		mapper.bindInputStream(streamName, inputStream);
		mapper.bindInputStream(secondStreamName, secondInputStream);
		mapper.bindInputStream(unnamedInputStream);
		assertThat(mapper.getSourceManager().getSource(streamName), is(input));
		assertThat(mapper.getSourceManager().getSource(secondStreamName), is(secondInput));
		assertThat(mapper.getSourceManager().getSource(RmlMapper.DEFAULT_STREAM_NAME), is(unnamedInput));
	}

	@Test
	public void mapper_boundWithMultipleUnnamedInputStreams_shouldReadLastBoundStream() {
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		InputStream secondInputStream = IOUtils.toInputStream(secondInput, Charset.defaultCharset());
		mapper.bindInputStream(inputStream);
		assertThat(mapper.getSourceManager().getSource(RmlMapper.DEFAULT_STREAM_NAME), is(input));
		mapper.bindInputStream(secondInputStream);
		assertThat(mapper.getSourceManager().getSource(RmlMapper.DEFAULT_STREAM_NAME), is(secondInput));
	}

	@Test
	public void mapper_boundWithMultipleNamedInputStreams_shouldReadLastBoundStream() {
		String streamName = "foo";
		String secondStreamName = "bar";
		InputStream inputStream = IOUtils.toInputStream(input, Charset.defaultCharset());
		InputStream secondInputStream = IOUtils.toInputStream(secondInput, Charset.defaultCharset());
		mapper.bindInputStream(streamName, inputStream);
		assertThat(mapper.getSourceManager().getSource(streamName), is(input));
		mapper.bindInputStream(secondStreamName, secondInputStream);
		assertThat(mapper.getSourceManager().getSource(secondStreamName), is(secondInput));
	}

	@Test
	public void mapper_notFindingBoundUnnamedInputStream_shouldThrowException() {
		RmlMappingLoader loader = RmlMappingLoader.build();
		InputStream input = RmlMapperTest.class.getResourceAsStream("simple.carml.rml.ttl");

		RuntimeException exception = assertThrows(RuntimeException.class, () -> mapper.map(loader.load(RDFFormat.TURTLE, input)));
		assertThat(exception.getMessage(), is("attempting to get source, but no binding was present"));
	}

	@Test
	public void mapper_notFindingBoundNamedInputStreams_shouldThrowException() {
		String streamName = "foo";
		RmlMappingLoader loader = RmlMappingLoader.build();
		InputStream input = RmlMapperTest.class.getResourceAsStream("simple.namedcarml.rml.ttl");

		RuntimeException exception = assertThrows(RuntimeException.class, () -> mapper.map(loader.load(RDFFormat.TURTLE, input)));
		assertThat(exception.getMessage(), is(String.format("attempting to get source by "
				+ "name [%s], but no such binding is present", streamName)));
	}

	@Test
	public void mapper_withNoBoundSource_shouldThrowException() throws IOException {
		RmlMappingLoader loader = RmlMappingLoader.build();
		InputStream input = RmlMapperTest.class.getResourceAsStream("simple.carml.rml.ttl");

		RuntimeException exception = assertThrows(RuntimeException.class, () -> mapper.map(loader.load(RDFFormat.TURTLE, input)));
		assertThat(exception.getMessage(), is("attempting to get source, but no binding was present"));
	}

	@Test
	public void mapper_builtWithLogicalSourceResolvers_shouldUseTheCorrectResolver() throws IOException {
		ValueFactory f = SimpleValueFactory.getInstance();
		IRI expectedIRI = f.createIRI("http://this.iri/isUsed");
		Iterable<String> expectedSourceData = Collections.singletonList("expected");
		LogicalSourceResolver.SourceIterator<String> expectedSourceIterator = (a, b) -> expectedSourceData;
		LogicalSourceResolver.ExpressionEvaluatorFactory<String> expectedFactory = a -> null;
		LogicalSourceResolver<String> expectedResolver = new LogicalSourceResolverContainer<>(
				expectedSourceIterator, expectedFactory);

		IRI unusedIRI = f.createIRI("http://this.iri/isNotUsed");
		LogicalSourceResolver<String> unusedResolver = new LogicalSourceResolverContainer<>(null, null);

		SourceResolver sourceResolver = mock(SourceResolver.class);
		String input = "foo";
		when(sourceResolver.apply(any())).thenReturn(Optional.of(input));

		RmlMapper mapper = RmlMapper.newBuilder()
				.sourceResolver(sourceResolver)
				.setLogicalSourceResolver(expectedIRI, expectedResolver)
				.setLogicalSourceResolver(unusedIRI, unusedResolver)
				.build();

		TriplesMap logicalSourceMap = mock(TriplesMap.class);
		when(logicalSourceMap.getLogicalSource())
				.thenReturn(new CarmlLogicalSource(null, null, expectedIRI));

		TriplesMapperComponents<?> components = mapper.getTriplesMapperComponents(logicalSourceMap);

		Assert.assertSame(expectedFactory, components.getExpressionEvaluatorFactory());
		Assert.assertSame(expectedSourceData, components.getIterator().get());

	}

	@Test
	public void mapper_usedWithUnknownReferenceFormulation_shouldThrowException() throws IOException {
		ValueFactory f = SimpleValueFactory.getInstance();
		IRI unusedIRI = f.createIRI("http://this.iri/isNotUsed");
		LogicalSourceResolver<?> unusedResolver = new LogicalSourceResolverContainer<>(null, null);

		SourceResolver sourceResolver = mock(SourceResolver.class);
		String input = "foo";
		when(sourceResolver.apply(any())).thenReturn(Optional.of(input));

		RmlMapper mapper = RmlMapper.newBuilder()
				.sourceResolver(sourceResolver)
				.setLogicalSourceResolver(unusedIRI, unusedResolver)
				.build();

		IRI unsupportedRefFormulation = f.createIRI("http://this.iri/isNotSupported");

		TriplesMap logicalSourceMap = mock(TriplesMap.class);
		when(logicalSourceMap.asRdf())
				.thenReturn(new LinkedHashModel());
		when(logicalSourceMap.getLogicalSource())
				.thenReturn(new CarmlLogicalSource(null, null, unsupportedRefFormulation));

		RuntimeException exception = assertThrows(RuntimeException.class, () -> mapper.getTriplesMapperComponents(logicalSourceMap));
		assertThat(exception.getMessage(), startsWith("Unsupported reference formulation"));
		assertThat(exception.getMessage(), containsString(unsupportedRefFormulation.toString()));


	}

	private static class LogicalSourceResolverContainer<T> implements LogicalSourceResolver<T> {

		SourceIterator<T> sourceIterator;
		ExpressionEvaluatorFactory<T> evaluatorFactory;

		public LogicalSourceResolverContainer(SourceIterator<T> sourceIterator, ExpressionEvaluatorFactory<T> evaluatorFactory) {
			this.sourceIterator = sourceIterator;
			this.evaluatorFactory = evaluatorFactory;
		}

		@Override
		public SourceIterator<T> getSourceIterator() {
			return sourceIterator;
		}

		@Override
		public ExpressionEvaluatorFactory<T> getExpressionEvaluatorFactory() {
			return evaluatorFactory;
		}
	}
}
