package io.leangen.graphql;

import graphql.language.IntValue;
import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import io.leangen.geantyref.TypeFactory;
import io.leangen.graphql.annotations.GraphQLArgument;
import io.leangen.graphql.annotations.GraphQLQuery;
import io.leangen.graphql.util.Scalars;
import org.junit.Test;

import java.math.BigInteger;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class TemporalScalarsTest {

    @Test
    public void testDate() {
        testTemporal(Date.class, "2017-06-24T23:22:34.120Z", 1498346554120L);
    }

    @Test
    public void testSqlDate() {
        testTemporal(java.sql.Date.class, "2017-06-24", 1498346554120L);
    }

    @Test
    public void testSqlTime() {
        testTemporal(Time.class, "23:15:44", 1498346144505L);
    }

    @Test
    public void testSqlTimestamp() {
        testTemporal(Timestamp.class, "2017-06-24T23:15:44.500Z", 1498346144500L);
    }

    @Test
    public void testSqlTypesAsDates() {
        Coercing dateCoercing = Scalars.toGraphQLScalarType(Date.class).getCoercing();
        Coercing sqlDateCoercing = Scalars.toGraphQLScalarType(java.sql.Date.class).getCoercing();
        Coercing sqlTimeCoercing = Scalars.toGraphQLScalarType(Time.class).getCoercing();
        Coercing sqlTimestampCoercing = Scalars.toGraphQLScalarType(Timestamp.class).getCoercing();
        java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.parse("2017-06-24"));
        Time sqlTime = Time.valueOf(LocalTime.parse("23:15:44"));
        Timestamp sqlTimestamp = Timestamp.valueOf(LocalDateTime.parse("2017-06-24T23:15:44.500"));
        assertEquals(dateCoercing.serialize(sqlDate), sqlDateCoercing.serialize(sqlDate));
        assertEquals(dateCoercing.serialize(sqlTime), sqlTimeCoercing.serialize(sqlTime));
        assertEquals(dateCoercing.serialize(sqlTimestamp), sqlTimestampCoercing.serialize(sqlTimestamp));
    }

    @Test
    public void testCalendar() {
        testTemporal(Calendar.class, "2017-06-24T23:22:34.120Z", 1498346554120L);
    }

    @Test
    public void testInstant() {
        testTemporal(Instant.class, "2017-06-24T23:22:34.120Z", 1498346554120L);
    }

    @Test
    public void testLocalDate() {
        testTemporal(LocalDate.class, "2017-06-24", 1498346144503L);
    }

    @Test
    public void testLocalTime() {
        testTemporal(LocalTime.class, "23:15:44.505", 1498346144505L);
    }

    @Test
    public void testLocalDateTime() {
        testTemporal(LocalDateTime.class, "2017-06-24T23:15:44.500", 1498346144500L);
    }

    @Test
    public void testZonedDateTime() {
        testTemporal(ZonedDateTime.class, "2017-06-24T23:15:44.510Z", 1498346144510L);
    }

    @Test
    public void testOffsetTime() {
        testTemporal(OffsetTime.class, "09:15:30Z", 33330000L);
    }

    @Test
    public void testOffsetDateTime() {
        testTemporal(OffsetDateTime.class, "2017-06-24T23:15:44.510Z", "2017-06-24T22:15:44.510-01:00", 1498346144510L);
    }

    private void testTemporal(Class type, String expected, long literal) {
        testTemporal(type, expected, expected, literal);
    }

    private void testTemporal(Class type, String expected, String stringLiteral, long literal) {
        GraphQLScalarType scalar = Scalars.toGraphQLScalarType(type);
        testStringTemporal(type, scalar.getCoercing(), stringLiteral, stringLiteral);
        testEpochMilliTemporal(type, scalar.getCoercing(), expected, literal);
        testTemporalMapping(type, scalar);
    }

    private void testStringTemporal(Class type, Coercing coercing, String expected, String stringLiteral) {
        Object parsed = coercing.parseLiteral(new StringValue(stringLiteral));
        assertTrue(type.isInstance(parsed));
        assertEquals(expected, coercing.serialize(parsed));

        parsed = coercing.parseValue(stringLiteral);
        assertTrue(type.isInstance(parsed));
        assertEquals(expected, coercing.serialize(parsed));
        
        Object same = coercing.parseValue(parsed);
        assertEquals(parsed, same);
    }
    
    private void testEpochMilliTemporal(Class type, Coercing coercing, String expected, long literal) {
        Object parsed = coercing.parseLiteral(new IntValue(new BigInteger(Long.toString(literal))));
        assertTrue(type.isInstance(parsed));
        assertEquals(expected, coercing.serialize(parsed));
        
        parsed = coercing.parseValue(literal);
        assertTrue(type.isInstance(parsed));
        assertEquals(expected, coercing.serialize(parsed));
    }
    
    private void testTemporalMapping(Class type, GraphQLScalarType scalar) {
        GraphQLSchema schema = new GraphQLSchemaGenerator()
                .withOperationsFromSingleton(new ScalarService(), TypeFactory.parameterizedClass(ScalarService.class, type))
                .generate();
        GraphQLFieldDefinition query = schema.getQueryType().getFieldDefinition("identity");
        assertEquals(scalar, query.getType());
        assertEquals(scalar, query.getArgument("input").getType());
    }
    
    public static class ScalarService<T> {
        
        @GraphQLQuery(name = "identity")
        public T identity(@GraphQLArgument(name = "input") T input) {
            return input;
        }
    }
}