/*******************************************************************************
 * Copyright (c) 2020 Eclipse RDF4J contributors.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 ******************************************************************************/
package org.eclipse.rdf4j.model;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.YearMonth;
import java.time.temporal.ChronoField;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import org.junit.Test;

/**
 * Abstract {@link Literal} test suite.
 */
public abstract class LiteralTest {

	private static final String XSD = "http://www.w3.org/2001/XMLSchema#";
	private static final String RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";

	static final String XSD_BOOLEAN = XSD + "boolean";

	static final String XSD_BYTE = XSD + "byte";
	static final String XSD_SHORT = XSD + "short";
	static final String XSD_INT = XSD + "int";
	static final String XSD_LONG = XSD + "long";
	static final String XSD_FLOAT = XSD + "float";
	static final String XSD_DOUBLE = XSD + "double";
	static final String XSD_INTEGER = XSD + "integer";
	static final String XSD_DECIMAL = XSD + "decimal";

	static final String XSD_STRING = XSD + "string";

	static final String XSD_DATETIME = XSD + "dateTime";
	static final String XSD_TIME = XSD + "time";
	static final String XSD_DATE = XSD + "date";
	static final String XSD_GYEARMONTH = XSD + "gYearMonth";
	static final String XSD_GYEAR = XSD + "gYear";
	static final String XSD_GMONTHDAY = XSD + "gMonthDay";
	static final String XSD_GDAY = XSD + "gDay";
	static final String XSD_GMONTH = XSD + "gMonth";
	static final String XSD_DURATION = XSD + "duration";
	static final String XSD_DURATION_DAYTIME = XSD + "dayTimeDuration";
	static final String XSD_DURATION_YEARMONTH = XSD + "yearMonthDuration";

	static final String RDF_LANG_STRING = RDF + "langString";

	/**
	 * Creates a test literal instance.
	 *
	 * @param label the label of the literal
	 *
	 * @return a new instance of the concrete literal class under test
	 */
	protected abstract Literal literal(String label);

	/**
	 * Creates a test literal instance.
	 *
	 * @param label    the label of the literal
	 * @param language the language of the literal
	 *
	 * @return a new instance of the concrete literal class under test
	 */
	protected abstract Literal literal(String label, String language);

	/**
	 * Creates a test literal instance.
	 *
	 * @param label    the label of the literal
	 * @param datatype the datatype of the literal
	 *
	 * @return a new instance of the concrete literal class under test
	 */
	protected abstract Literal literal(String label, IRI datatype);

	/**
	 * Creates a test datatype IRI instance.
	 *
	 * @param iri the IRI of the datatype
	 *
	 * @return a new instance of the concrete datatype class under test
	 */
	protected abstract IRI datatype(String iri);

	//// Constructors //////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public final void testPlainConstructor() {

		final String label = "label";

		final Literal literal = literal(label);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_STRING);

		assertThatNullPointerException().isThrownBy(() -> literal(null));
	}

	@Test
	public final void testPlainConstructorWithLongLabel() {

		final StringBuilder label = new StringBuilder(1000000);

		for (int i = 0; i < 1000000; i++) {
			label.append(Integer.toHexString(i % 16));
		}

		final Literal literal = literal(label.toString());

		assertThat(literal.getLabel()).isEqualTo(label.toString());
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_STRING);

	}

	@Test
	public final void testTaggedConstructor() {

		final String label = "label";
		final String language = "en";

		final Literal literal = literal(label, language);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).contains(language);
		assertThat(literal.getDatatype().stringValue()).isEqualTo(RDF_LANG_STRING);

		assertThatNullPointerException().isThrownBy(() -> literal(null, (String) null));
		assertThatNullPointerException().isThrownBy(() -> literal("", (String) null));
		assertThatNullPointerException().isThrownBy(() -> literal(null, ""));
		assertThatNullPointerException().isThrownBy(() -> literal(null, (IRI) null));

		assertThatIllegalArgumentException().isThrownBy(() -> literal("", ""));

	}

	@Test
	public final void testTypedConstructor() {

		final String label = "label";
		final String datatype = "http://examplle.org/datatype";

		final Literal literal = literal(label, datatype(datatype));

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(datatype);

		assertThatNullPointerException().isThrownBy(() -> literal(null, (IRI) null));
		assertThatNullPointerException().isThrownBy(() -> literal(null, datatype(XSD_STRING)));
		assertThatNullPointerException().isThrownBy(() -> literal(null, datatype(RDF_LANG_STRING)));

		assertThatIllegalArgumentException().isThrownBy(() -> literal("", datatype(RDF_LANG_STRING)));

	}

	@Test
	public final void testTypedConstructorNullDatatype() {

		final String label = "label";
		final IRI datatype = null;

		final Literal literal = literal(label, datatype);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_STRING);

	}

	//// String Value //////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testStringValue() {

		final String label = "literal";
		final String language = "en";
		final IRI datatype = datatype(XSD_DECIMAL);

		assertThat(literal(label).stringValue()).isEqualTo(label);
		assertThat(literal(label, language).stringValue()).isEqualTo(label);
		assertThat(literal(label, datatype).stringValue()).isEqualTo(label);
	}

	//// Object Values /////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testBooleanValue() {

		final IRI datatype = datatype(XSD_BOOLEAN);

		assertThat(literal("true", datatype).booleanValue()).isTrue();
		assertThat(literal("false", datatype).booleanValue()).isFalse();

		assertThat(literal("1", datatype).booleanValue()).isTrue();
		assertThat(literal("0", datatype).booleanValue()).isFalse();

		assertThat(literal("\ttrue", datatype).booleanValue()).isTrue();
		assertThat(literal("false\t", datatype).booleanValue()).isFalse();

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).booleanValue());

	}

	@Test
	public final void testByteValue() {

		final IRI datatype = datatype(XSD_BYTE);
		final Class<Byte> type = Byte.class;

		assertThat(literal("100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) 100);
		assertThat(literal("+100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) 100);
		assertThat(literal("-100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) -100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).byteValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).booleanValue());

	}

	@Test
	public final void testShortValue() {

		final IRI datatype = datatype(XSD_SHORT);
		final Class<Short> type = Short.class;

		assertThat(literal("100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) 100);
		assertThat(literal("+100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) 100);
		assertThat(literal("-100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) -100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).shortValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).shortValue());

	}

	@Test
	public final void testIntValue() {

		final IRI datatype = datatype(XSD_INT);
		final Class<Integer> type = Integer.class;

		assertThat(literal("100", datatype).intValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).intValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).intValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).intValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).intValue());

	}

	@Test
	public final void testLongValue() {

		final IRI datatype = datatype(XSD_LONG);
		final Class<Long> type = Long.class;

		assertThat(literal("100", datatype).longValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).longValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).longValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).longValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).longValue());

	}

	@Test
	public final void testFloatValue() {

		final IRI datatype = datatype(XSD_FLOAT);
		final Class<Float> type = Float.class;

		assertThat(literal("100", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).floatValue()).isInstanceOf(type).isEqualTo(-100);
		assertThat(literal("100.0", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("10e1", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);

		assertThat(literal("INF", datatype).floatValue()).isInstanceOf(type).isEqualTo(Float.POSITIVE_INFINITY);
		assertThat(literal("-INF", datatype).floatValue()).isInstanceOf(type).isEqualTo(Float.NEGATIVE_INFINITY);
		assertTrue(Float.isNaN(literal("NaN", datatype).floatValue()));

		// assertThatIllegalArgumentException().as("not normalized")
		// .isThrownBy(() -> literal("\t100", datatype).floatValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).floatValue());

	}

	@Test
	public final void testDoubleValue() {

		final IRI datatype = datatype(XSD_DOUBLE);
		final Class<Double> type = Double.class;

		assertThat(literal("100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(-100);
		assertThat(literal("100.0", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("10e1", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);

		assertThat(literal("INF", datatype).doubleValue()).isInstanceOf(type).isEqualTo(Double.POSITIVE_INFINITY);
		assertThat(literal("-INF", datatype).doubleValue()).isInstanceOf(type).isEqualTo(Double.NEGATIVE_INFINITY);
		assertTrue(Double.isNaN(literal("NaN", datatype).doubleValue()));

		// assertThatIllegalArgumentException().as("not normalized")
		// .isThrownBy(() -> literal("\t100", datatype).doubleValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).doubleValue());

	}

	@Test
	public final void testIntegerValue() {

		final IRI datatype = datatype(XSD_INTEGER);
		final Class<BigInteger> type = BigInteger.class;

		assertThat(literal("100", datatype).integerValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).integerValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).integerValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).integerValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).integerValue());

	}

	@Test
	public final void testDecimalValue() {

		final IRI datatype = datatype(XSD_DECIMAL);
		final Class<BigDecimal> type = BigDecimal.class;

		assertThat(literal("100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100"));
		assertThat(literal("+100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100"));
		assertThat(literal("-100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("-100"));
		assertThat(literal("100.0", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100.0"));
		assertThat(literal("10e1", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("1.0e2"));

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).decimalValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).decimalValue());

	}

	@Test
	public final void testTemporalDateTimeValue() {

		final String integral = "2020-09-29T01:02:03";
		final String fractional = "2020-09-29T01:02:03.004";

		final String offset = "2020-09-29T01:02:03+05:00";
		final String zero = "2020-09-29T01:02:03Z";

		assertThat(LocalDateTime.from(literal(integral, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(LocalDateTime.parse(integral));

		assertThat(LocalDateTime.from(literal(fractional, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(LocalDateTime.parse(fractional));

		assertThat(OffsetDateTime.from(literal(offset, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(OffsetDateTime.parse(offset));

		assertThat(OffsetDateTime.from(literal(zero, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(OffsetDateTime.parse(zero));

		Stream.of(

				"0001-01-01T00:00:00",
				"0001-01-01T00:00:00.0",
				"0001-01-01T00:00:00Z",
				"0001-01-01T00:00:00.0Z",
				"0001-01-01T00:00:00+00:00",
				"0001-01-01T00:00:00.0+00:00",
				"0001-01-01T00:00:00.0-00:00",
				"0001-01-01T00:00:00.0+14:00",
				"0001-01-01T00:00:00.0-14:00",
				"0001-05-31T00:00:00.00",
				"0001-07-31T00:00:00.00",
				"0001-08-31T00:00:00.00",
				"0001-10-31T00:00:00.00",
				"0001-12-31T00:00:00.00",
				"-0001-01-01T00:00:00",
				"1234-12-31T23:59:59",
				"1234-12-31T24:00:00",
				// "12345-12-31T24:00:00",
				// "1234-12-31T24:00:00.1234567890",
				"2004-02-29T00:00:00"

		)
				.forEach(value -> assertThatCode(() -> literal(value, XSD_DATETIME).temporalAccessorValue())
						.as(value)
						.doesNotThrowAnyException()
				);

	}

	@Test
	public final void testTemporalTimeValue() {

		final String integral = "01:02:03";
		final String fractional = "01:02:03.004";

		final String offset = "01:02:03+05:00";
		final String zero = "01:02:03Z";

		assertThat(LocalTime.from(literal(integral, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(LocalTime.parse(integral));

		assertThat(LocalTime.from(literal(fractional, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(LocalTime.parse(fractional));

		assertThat(OffsetTime.from(literal(offset, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(OffsetTime.parse(offset));

		assertThat(OffsetTime.from(literal(zero, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(OffsetTime.parse(zero));
	}

	@Test
	public final void testTemporalDateValue() {

		final String local = "2020-11-14";
		final String offset = "2020-11-14+05:00";
		final String zero = "2020-11-14Z";

		assertThat(LocalDate.from(literal(local, XSD_DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(local));

		assertThat(LocalDate.from(literal(offset, XSD_DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(offset.substring(0, 10))); // OffsetDate not supported by java.time

		assertThat(LocalDate.from(literal(zero, XSD_DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(offset.substring(0, 10))); // OffsetDate not supported by java.time

	}

	@Test
	public final void testTemporalGYearMonthValue() {

		final String base = "2020-11";

		assertThat(YearMonth.from(literal(base, XSD_GYEARMONTH).temporalAccessorValue()))
				.isEqualTo(YearMonth.parse(base));

	}

	@Test
	public final void testTemporalGYearValue() {

		final String local = "2020";

		assertThat(Year.from(literal(local, XSD_GYEAR).temporalAccessorValue()))
				.isEqualTo(Year.parse(local));

	}

	@Test
	public final void testTemporalGMonthDayValue() {

		final String local = "--11-14";

		assertThat(MonthDay.from(literal(local, XSD_GMONTHDAY).temporalAccessorValue()))
				.isEqualTo(MonthDay.parse(local));

	}

	@Test
	public final void testTemporalGDayValue() {

		final String local = "---14";

		assertThat(literal(local, XSD_GDAY).temporalAccessorValue().get(ChronoField.DAY_OF_MONTH))
				.isEqualTo(14);

	}

	@Test
	public final void testTemporalGMonthValue() {

		final String local = "--11";

		assertThat(Month.from(literal(local, XSD_GMONTH).temporalAccessorValue()))
				.isEqualTo(Month.NOVEMBER);

	}

	@Test
	public final void testTemporalAccessorMalformedValue() {

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("--", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("2020-11-16T", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("2020-11-16T11:12:13.", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("malformed", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("2020-11-16T", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("2020-11-16T11:12:13.", XSD_DATETIME).temporalAccessorValue());

		Stream.of(

				"foo", "Mon, 11 Jul 2005 09:22:29 +0200",
				"0001-01-01T00:00",
				"0001-01-01T00:00.00",
				"0001-13-01T00:00:00.00",
				"0001-01-32T00:00:00.00",
				// "0001-02-30T00:00:00.00",
				// "2005-02-29T00:00:00", // not a leap year
				// "0001-04-31T00:00:00.00",
				"0001-01-01T25:00:00.00",
				"0001-01-01T00:61:00.00",
				"0001-01-01T00:00:61.00",
				"0001-01-01T00:00.00+15:00",
				"0001-01-01T00:00.00-15:00",
				"001-01-01T00:00:00.0",
				"0001-1-01T00:00:00.0",
				"0001-01-1T00:00:00.0",
				"0001-01-01T0:00:00.0",
				"0001-01-01T00:0:00.0",
				"0001-01-01T00:00:0.0",
				"0001/01-01T00:00:00.0",
				"0001-01/01T00:00:00.0",
				"0001-01-01t00:00:00.0",
				"0001-01-01T00.00:00.0",
				"0001-01-01T00:00.00.0",
				"0001-01-01T00:00:00:0",
				"0001-01-01T00:00.00+0:00",
				"0001-01-01T00:00.00+00:0",
				"0001-jan-01T00:00:00",
				"0001-01-01T00:00:00+00:00Z",
				"0001-01-01T24:01:00", "0001-01-01T24:00:01",
				"00001-01-01T00:00:00",
				"0001-001-01T00:00:00",
				"0001-01-001T00:00:00",
				"0001-01-01T000:00:00",
				"0001-01-01T00:000:00",
				"0001-01-01T00:00:000",
				"0001-01-01T00:00:000",
				"0001-01-01T00:00:00z",
				"0001-01-01T00:00:00+05",
				"0001-01-01T00:00:00+0500",
				"0001-01-01T00:00:00GMT",
				"0001-01-01T00:00:00PST",
				"0001-01-01T00:00:00GMT+05",
				// "0000-01-01T00:00:00",
				"-0000-01-01T00:00:00",
				"+0001-01-01T00:00:00"

		)
				.forEach(value -> assertThatExceptionOfType(DateTimeException.class)
						.as(value)
						.isThrownBy(() -> literal(value, XSD_DATETIME).temporalAccessorValue())
				);

	}

	@Test
	public final void testTemporalDurationValue() {

		final String period = "P1Y2M3D";
		final String duration = "PT1H2M3.4S";

		assertThat(Period.from(literal(period, XSD_DURATION).temporalAmountValue()))
				.isEqualTo(Period.parse(period));

		assertThat(Period.from(literal("-P1Y2M3D", XSD_DURATION).temporalAmountValue()))
				.isEqualTo(Period.parse(period).negated());

		assertThat(Duration.from(literal(duration, XSD_DURATION).temporalAmountValue()))
				.isEqualTo(Duration.parse(duration));

	}

	@Test
	public final void testTemporalAmountMalformedValue() {

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("malformed", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no  components")
				.isThrownBy(() -> literal("P", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("P1Y2MT", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("negative component")
				.isThrownBy(() -> literal("P-1347M ", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time separator")
				.isThrownBy(() -> literal("P1Y1S ", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("PT1.S", XSD_DURATION).temporalAmountValue());
	}

	@Test
	public final void testCalendarValue() throws DatatypeConfigurationException {

		final Class<XMLGregorianCalendar> type = XMLGregorianCalendar.class;

		final DatatypeFactory factory = DatatypeFactory.newInstance();

		final Function<Consumer<XMLGregorianCalendar>, XMLGregorianCalendar> setup = consumer -> {

			final XMLGregorianCalendar calendar = factory.newXMLGregorianCalendar();

			consumer.accept(calendar);

			return calendar;

		};

		assertThat(literal("2020-09-29T01:02:03.004Z", XSD_DATETIME).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
					calendar.setDay(29);
					calendar.setTime(1, 2, 3, 4);
					calendar.setTimezone(0);
				}));

		assertThat(literal("01:02:03.004", XSD_TIME).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setTime(1, 2, 3, 4);
				}));

		assertThat(literal("2020-09-29", XSD_DATE).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
					calendar.setDay(29);
				}));

		assertThat(literal("2020-09", XSD_GYEARMONTH).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
				}));

		assertThat(literal("--09-29", XSD_GMONTHDAY).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setMonth(9);
					calendar.setDay(29);
				}));

		assertThat(literal("2020", XSD_GYEAR).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
				}));

		assertThat(literal("--09", XSD_GMONTH).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setMonth(9);
				}));

		assertThat(literal("---29", XSD_GDAY).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setDay(29);
				}));

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", XSD_DATETIME).calendarValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", XSD_DATETIME).calendarValue());

	}

	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testEqualsAndHashCode() {

		final Literal plain = literal("plain");
		final Literal tagged = literal("tagged", "en");
		final Literal typed = literal("typed", datatype("http://example.org/datatype"));

		final Literal _plain = literal(plain.getLabel());
		final Literal _tagged = literal(tagged.getLabel(), tagged.getLanguage().orElse(""));
		final Literal _typed = literal(typed.getLabel(), typed.getDatatype());

		assertThat(plain).isEqualTo(plain);
		assertThat(plain).isEqualTo(_plain);

		assertThat(tagged).isEqualTo(tagged);
		assertThat(tagged).isEqualTo(_tagged);

		assertThat(typed).isEqualTo(typed);
		assertThat(typed).isEqualTo(_typed);

		assertThat(plain).isNotEqualTo(null);
		assertThat(plain).isNotEqualTo(new Object());

		assertThat(plain).isNotEqualTo(tagged);
		assertThat(plain).isNotEqualTo(typed);
		assertThat(tagged).isNotEqualTo(typed);

		assertThat(plain).isNotEqualTo(literal("other"));
		assertThat(tagged).isNotEqualTo(literal(tagged.getLabel(), "other"));
		assertThat(typed).isNotEqualTo(literal(typed.getLabel(), datatype("http://example.org/other")));

		// hashCode() should return identical values for literals for which equals() is true

		assertThat(plain.hashCode()).isEqualTo(_plain.hashCode());
		assertThat(tagged.hashCode()).isEqualTo(_tagged.hashCode());
		assertThat(typed.hashCode()).isEqualTo(_typed.hashCode());

		assertThat(tagged.hashCode())
				.as("computed according to contract")
				.isEqualTo(tagged.getLabel().hashCode()); // !!! label >> label+language+datatype

	}

	@Test
	public final void testEqualsAndHashCodeCaseInsensitiveLanguage() {

		final Literal lowercase = literal("label", "en");
		final Literal uppercase = literal("label", "EN");

		assertThat(lowercase).isEqualTo(uppercase);
		assertThat(lowercase.hashCode()).isEqualTo(uppercase.hashCode());
	}

	@Test
	public final void testEqualsAndHashCodeXSDString() {

		// in RDF 1.1, there is no distinction between plain and string-typed literals

		final Literal plain = literal("label");
		final Literal typed = literal("label", datatype(XSD_STRING));

		assertThat(plain).isEqualTo(typed);
		assertThat(plain.hashCode()).isEqualTo(typed.hashCode());
	}

}
