/*******************************************************************************
 * 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 java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.SECOND_OF_DAY;
import static java.time.temporal.ChronoUnit.HALF_DAYS;
import static java.time.temporal.ChronoUnit.HOURS;
import static java.time.temporal.ChronoUnit.YEARS;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_BOOLEAN;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_BYTE;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_DATE;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_DATETIME;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_DECIMAL;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_DOUBLE;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_DURATION;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_FLOAT;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_GDAY;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_GMONTH;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_GMONTHDAY;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_GYEAR;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_GYEARMONTH;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_INT;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_INTEGER;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_LONG;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_SHORT;
import static org.eclipse.rdf4j.model.LiteralTest.XSD_TIME;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
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.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalUnit;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.util.Date;
import java.util.List;

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

import org.junit.Test;

/**
 * Abstract {@link ValueFactory} test suite.
 *
 * @author Alessandro Bollini
 * @author jeen
 * @since 3.5.0
 */
public abstract class ValueFactoryTest {

	/**
	 * Creates a test value factory instance.
	 *
	 * @return a new instance of the concrete value factory class under test
	 */
	protected abstract ValueFactory factory();

	@Test
	public void testCreateBNode() {

		final BNode bnode = factory().createBNode();

		assertThat(bnode).isNotNull();
		assertThat(bnode.getID()).isNotNull();
	}

	@Test
	public void testCreateLiteralBoolean() {

		final Literal _true = factory().createLiteral(true);

		assertThat(_true).isNotNull();
		assertThat(_true.booleanValue()).isTrue();
		assertThat(_true.getLabel()).isEqualTo("true");
		assertThat(_true.getDatatype().stringValue()).isEqualTo(XSD_BOOLEAN);

		final Literal _false = factory().createLiteral(false);

		assertThat(_false).isNotNull();
		assertThat(_false.booleanValue()).isFalse();
		assertThat(_false.getLabel()).isEqualTo("false");
		assertThat(_false.getDatatype().stringValue()).isEqualTo(XSD_BOOLEAN);
	}

	@Test
	public void testCreateLiteralByte() {

		final byte value = 42;

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.byteValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_BYTE);
	}

	@Test
	public void testCreateLiteralShort() {

		final short value = 42;

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.shortValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_SHORT);
	}

	@Test
	public void testCreateLiteralInt() {

		final int value = 42;

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.intValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_INT);
	}

	@Test
	public void testCreateLiteralLong() {

		final long value = 42L;

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.longValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_LONG);
	}

	@Test
	public void testCreateLiteralFloat() {

		final float value = 42.0f;
		final ValueFactory factory = factory();

		final Literal literal = factory.createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.floatValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42.0");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_FLOAT);

		assertThat(factory.createLiteral(Float.POSITIVE_INFINITY).getLabel()).isEqualTo("INF");
		assertThat(factory.createLiteral(Float.NEGATIVE_INFINITY).getLabel()).isEqualTo("-INF");
		assertThat(factory.createLiteral(Float.NaN).getLabel()).isEqualTo("NaN");

	}

	@Test
	public void testCreateLiteralDouble() {

		final double value = 42.0d;
		final ValueFactory factory = factory();

		final Literal literal = factory.createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.doubleValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42.0");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DOUBLE);

		assertThat(factory.createLiteral(Double.POSITIVE_INFINITY).getLabel()).isEqualTo("INF");
		assertThat(factory.createLiteral(Double.NEGATIVE_INFINITY).getLabel()).isEqualTo("-INF");
		assertThat(factory.createLiteral(Double.NaN).getLabel()).isEqualTo("NaN");

	}

	@Test
	public void testCreateLiteralInteger() {

		final BigInteger value = new BigInteger("42");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.integerValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_INTEGER);
	}

	@Test
	public void testCreateLiteralDecimal() {

		final BigDecimal value = new BigDecimal("42.0");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.decimalValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("42.0");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DECIMAL);
	}

	@Test
	public void testCreateLiteralTemporalNull() {
		assertThatNullPointerException().isThrownBy(() -> factory().createLiteral((TemporalAccessor) null));
	}

	@Test
	public void testCreateLiteralTemporalLocalDateTime() {

		final LocalDateTime value = LocalDateTime.parse("2020-09-30T01:02:03.004");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DATETIME);

	}

	@Test
	public void testCreateLiteralTemporalOffsetDateTime() {

		final OffsetDateTime value = OffsetDateTime.parse("2020-09-30T01:02:03.004Z");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DATETIME);

	}

	@Test
	public void testCreateLiteralTemporalLocalTime() {

		final LocalTime value = LocalTime.parse("01:02:03.004");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_TIME);

	}

	@Test
	public void testCreateLiteralTemporalOffsetTime() {

		final OffsetTime value = OffsetTime.parse("01:02:03.004Z");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_TIME);

	}

	@Test
	public void testCreateLiteralTemporalLocalDate() {

		final LocalDate value = LocalDate.parse("2020-11-14");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DATE);

	}

	@Test
	public void testCreateLiteralTemporalOffsetDate() {

		final String label = "2020-09-30Z";

		final TemporalAccessor value = new DateTimeFormatterBuilder() // OffsetDate not supported by java.time
				.append(DateTimeFormatter.ISO_LOCAL_DATE)
				.appendOffsetId()
				.toFormatter()
				.parse(label);

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DATE);

	}

	@Test
	public void testCreateLiteralTemporalYearMonth() {

		final YearMonth value = YearMonth.parse("2020-09");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_GYEARMONTH);

	}

	@Test
	public void testCreateLiteralTemporalYear() {

		final Year value = Year.parse("2020");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_GYEAR);

	}

	@Test
	public void testCreateLiteralTemporalMonthDay() {

		final MonthDay value = MonthDay.parse("--11-14");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_GMONTHDAY);

	}

	@Test
	public void testCreateLiteralTemporalDay() {

		final TemporalAccessor value = new TemporalAccessor() {

			@Override
			public boolean isSupported(TemporalField field) {
				return field.equals(DAY_OF_MONTH);
			}

			@Override
			public long getLong(TemporalField field) {
				if (field == DAY_OF_MONTH) {
					return 14;
				} else {
					throw new UnsupportedTemporalTypeException(field.toString());
				}
			}

		};

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("---14");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_GDAY);

	}

	@Test
	public void testCreateLiteralTemporalMonth() {

		final Month value = Month.NOVEMBER;

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAccessorValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo("--11");
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_GMONTH);

	}

	@Test
	public void testCreateLiteralTemporalAccessorUnsupported() {

		final TemporalAccessor accessor = new TemporalAccessor() {

			@Override
			public boolean isSupported(TemporalField field) {
				return field.equals(SECOND_OF_DAY);
			}

			@Override
			public long getLong(TemporalField field) {
				if (field == SECOND_OF_DAY) {
					return 123;
				} else {
					throw new UnsupportedTemporalTypeException(field.toString());
				}
			}

		};

		assertThatIllegalArgumentException().isThrownBy(() -> factory().createLiteral(accessor));

	}

	@Test
	public void testCreateLiteralTemporalPeriod() {

		final Period value = Period.parse("P1Y");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAmountValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DURATION);

	}

	@Test
	public void testCreateLiteralTemporalDuration() {

		final Duration value = Duration.ofSeconds(7);

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.temporalAmountValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DURATION);

	}

	@Test
	public void testCreateLiteralTemporalAmountUnsupported() {

		class TestAmount implements TemporalAmount {

			private final List<TemporalUnit> units;

			private TestAmount(ChronoUnit... units) {
				this.units = unmodifiableList(asList(units));
			}

			@Override
			public long get(TemporalUnit unit) {
				return unit.isDateBased() ? 1 : -1;
			}

			@Override
			public List<TemporalUnit> getUnits() {
				return units;
			}

			@Override
			public Temporal addTo(Temporal temporal) {
				throw new UnsupportedOperationException();
			}

			@Override
			public Temporal subtractFrom(Temporal temporal) {
				throw new UnsupportedOperationException();
			}

		}

		assertThatIllegalArgumentException().as("unsupported components")
				.isThrownBy(() -> factory().createLiteral(new TestAmount(HALF_DAYS)));

		assertThatIllegalArgumentException().as("mixed sign components")
				.isThrownBy(() -> factory().createLiteral(new TestAmount(YEARS, HOURS)));

	}

	@Test
	public void testCreateLiteralXMLGregorianCalendar() throws DatatypeConfigurationException {

		final XMLGregorianCalendar value = DatatypeFactory.newInstance()
				.newXMLGregorianCalendar("2020-09-30T01:02:03.004Z");

		final Literal literal = factory().createLiteral(value);

		assertThat(literal).isNotNull();
		assertThat(literal.calendarValue()).isEqualTo(value);
		assertThat(literal.getLabel()).isEqualTo(value.toString());
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DATETIME);

	}

	@Test
	public void testCreateLiteralDate() throws DatatypeConfigurationException {

		final Date date = new Date(2020, 9, 30, 1, 2, 3);
		final String string = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").format(date);

		final Literal literal = factory().createLiteral(date);

		assertThat(literal).isNotNull();
		assertThat(literal.calendarValue()).isEqualTo(DatatypeFactory.newInstance().newXMLGregorianCalendar(string));
		assertThat(literal.getLabel()).isEqualTo(string);
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_DATETIME);

	}

}
