package com.sap.olingo.jpa.metadata.core.edm.mapper.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.math.BigDecimal;
import java.sql.Blob;
import java.sql.Clob;
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.ZonedDateTime;
import java.util.Calendar;
import java.util.SimpleTimeZone;
import java.util.UUID;
import java.util.stream.Stream;

import jakarta.persistence.Lob;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import jakarta.persistence.metamodel.Attribute;

import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.geo.Geospatial.Dimension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;

import com.sap.olingo.jpa.metadata.core.edm.annotation.EdmGeospatial;
import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelException;

class JPATypeConverterTest {

  static Stream<Arguments> mappingJavaLobToOData() {

    return Stream.of(
        arguments(Blob.class, buildLobAttribute(), EdmPrimitiveTypeKind.Binary),
        arguments(Clob.class, buildLobAttribute(), EdmPrimitiveTypeKind.String));
  }

  static Stream<Arguments> mappingJavaTimeToOData() {
    return Stream.of(
        arguments(java.time.LocalTime.class, null, EdmPrimitiveTypeKind.TimeOfDay),
        arguments(java.sql.Time.class, null, EdmPrimitiveTypeKind.TimeOfDay),
        arguments(java.time.Duration.class, null, EdmPrimitiveTypeKind.Duration),
        arguments(java.time.LocalDate.class, null, EdmPrimitiveTypeKind.Date),
        arguments(java.sql.Date.class, null, EdmPrimitiveTypeKind.Date),
        arguments(Calendar.class, null, EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(Calendar.class, buildTimeAttribute(TemporalType.TIME), EdmPrimitiveTypeKind.TimeOfDay),
        arguments(Calendar.class, buildTimeAttribute(TemporalType.DATE), EdmPrimitiveTypeKind.Date),
        arguments(Calendar.class, buildTimeAttribute(TemporalType.TIMESTAMP), EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(Calendar.class, buildTimeAttribute(null), EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(Timestamp.class, null, EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(Timestamp.class, buildTimeAttribute(TemporalType.TIME), EdmPrimitiveTypeKind.TimeOfDay),
        arguments(Timestamp.class, buildTimeAttribute(TemporalType.DATE), EdmPrimitiveTypeKind.Date),
        arguments(Timestamp.class, buildTimeAttribute(TemporalType.TIMESTAMP), EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(Timestamp.class, buildTimeAttribute(null), EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(java.util.Date.class, null, EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(java.util.Date.class, buildTimeAttribute(TemporalType.TIME), EdmPrimitiveTypeKind.TimeOfDay),
        arguments(java.util.Date.class, buildTimeAttribute(TemporalType.DATE), EdmPrimitiveTypeKind.Date),
        arguments(java.util.Date.class, buildTimeAttribute(TemporalType.TIMESTAMP),
            EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(java.time.OffsetDateTime.class, buildTimeAttribute(null), EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(java.time.LocalDateTime.class, buildTimeAttribute(null), EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(java.time.ZonedDateTime.class, buildTimeAttribute(null), EdmPrimitiveTypeKind.DateTimeOffset),
        arguments(java.time.Instant.class, buildTimeAttribute(null), EdmPrimitiveTypeKind.DateTimeOffset));
  }

  static Stream<Arguments> mappingSimpleJavaToOData() {
    return Stream.of(
        arguments(Long.class, EdmPrimitiveTypeKind.Int64),
        arguments(long.class, EdmPrimitiveTypeKind.Int64),
        arguments(Integer.class, EdmPrimitiveTypeKind.Int32),
        arguments(int.class, EdmPrimitiveTypeKind.Int32),
        arguments(Short.class, EdmPrimitiveTypeKind.Int16),
        arguments(short.class, EdmPrimitiveTypeKind.Int16),
        arguments(String.class, EdmPrimitiveTypeKind.String),
        arguments(Character.class, EdmPrimitiveTypeKind.String),
        arguments(char.class, EdmPrimitiveTypeKind.String),
        arguments(char[].class, EdmPrimitiveTypeKind.String),
        arguments(Character[].class, EdmPrimitiveTypeKind.String),
        arguments(Double.class, EdmPrimitiveTypeKind.Double),
        arguments(double.class, EdmPrimitiveTypeKind.Double),
        arguments(Float.class, EdmPrimitiveTypeKind.Single),
        arguments(float.class, EdmPrimitiveTypeKind.Single),
        arguments(BigDecimal.class, EdmPrimitiveTypeKind.Decimal),
        arguments(Byte.class, EdmPrimitiveTypeKind.SByte),
        arguments(byte.class, EdmPrimitiveTypeKind.SByte),
        arguments(byte[].class, EdmPrimitiveTypeKind.Binary),
        arguments(Boolean.class, EdmPrimitiveTypeKind.Boolean),
        arguments(boolean.class, EdmPrimitiveTypeKind.Boolean),
        arguments(UUID.class, EdmPrimitiveTypeKind.Guid));
  }

  static Stream<Arguments> mappingJavaGeographyToOData() {
    return Stream.of(
        arguments(org.apache.olingo.commons.api.edm.geo.Point.class, buildDimensionAttribute(Dimension.GEOGRAPHY),
            EdmPrimitiveTypeKind.GeographyPoint),
        arguments(org.apache.olingo.commons.api.edm.geo.MultiPoint.class, buildDimensionAttribute(Dimension.GEOGRAPHY),
            EdmPrimitiveTypeKind.GeographyMultiPoint),
        arguments(org.apache.olingo.commons.api.edm.geo.LineString.class, buildDimensionAttribute(Dimension.GEOGRAPHY),
            EdmPrimitiveTypeKind.GeographyLineString),
        arguments(org.apache.olingo.commons.api.edm.geo.MultiLineString.class, buildDimensionAttribute(
            Dimension.GEOGRAPHY),
            EdmPrimitiveTypeKind.GeographyMultiLineString),
        arguments(org.apache.olingo.commons.api.edm.geo.Polygon.class, buildDimensionAttribute(Dimension.GEOGRAPHY),
            EdmPrimitiveTypeKind.GeographyPolygon),
        arguments(org.apache.olingo.commons.api.edm.geo.MultiPolygon.class, buildDimensionAttribute(
            Dimension.GEOGRAPHY),
            EdmPrimitiveTypeKind.GeographyMultiPolygon),
        arguments(org.apache.olingo.commons.api.edm.geo.GeospatialCollection.class, buildDimensionAttribute(
            Dimension.GEOGRAPHY),
            EdmPrimitiveTypeKind.GeographyCollection)

    );
  }

  static Stream<Arguments> mappingJavaGeometryToOData() {
    return Stream.of(
        arguments(org.apache.olingo.commons.api.edm.geo.Point.class, buildDimensionAttribute(Dimension.GEOMETRY),
            EdmPrimitiveTypeKind.GeometryPoint),
        arguments(org.apache.olingo.commons.api.edm.geo.MultiPoint.class, buildDimensionAttribute(Dimension.GEOMETRY),
            EdmPrimitiveTypeKind.GeometryMultiPoint),
        arguments(org.apache.olingo.commons.api.edm.geo.LineString.class, buildDimensionAttribute(Dimension.GEOMETRY),
            EdmPrimitiveTypeKind.GeometryLineString),
        arguments(org.apache.olingo.commons.api.edm.geo.MultiLineString.class, buildDimensionAttribute(
            Dimension.GEOMETRY),
            EdmPrimitiveTypeKind.GeometryMultiLineString),
        arguments(org.apache.olingo.commons.api.edm.geo.Polygon.class, buildDimensionAttribute(Dimension.GEOMETRY),
            EdmPrimitiveTypeKind.GeometryPolygon),
        arguments(org.apache.olingo.commons.api.edm.geo.MultiPolygon.class, buildDimensionAttribute(
            Dimension.GEOMETRY),
            EdmPrimitiveTypeKind.GeometryMultiPolygon),
        arguments(org.apache.olingo.commons.api.edm.geo.GeospatialCollection.class, buildDimensionAttribute(
            Dimension.GEOMETRY),
            EdmPrimitiveTypeKind.GeometryCollection)

    );
  }

  static Stream<Arguments> scalarJavaTypes() {
    return Stream.of(
        arguments(Integer.class, null, true),
        arguments(String.class, null, true),
        arguments(Character.class, null, true),
        arguments(Long.class, null, true),
        arguments(Short.class, null, true),
        arguments(Integer.class, null, true),
        arguments(Double.class, null, true),
        arguments(Float.class, null, true),
        arguments(BigDecimal.class, null, true),
        arguments(Byte.class, null, true),
        arguments(Boolean.class, null, true),
        arguments(java.time.LocalTime.class, null, true),
        arguments(java.sql.Time.class, null, true),
        arguments(java.time.Duration.class, null, true),
        arguments(java.time.LocalDate.class, null, true),
        arguments(java.time.Instant.class, null, true),
        arguments(java.sql.Date.class, null, true),
        arguments(Calendar.class, null, true),
        arguments(Timestamp.class, null, true),
        arguments(java.util.Date.class, null, true),
        arguments(UUID.class, null, true),
        arguments(Attribute.class, null, false));
  }

  static Stream<Arguments> supportedByOlingo() {

    return Stream.of(
        arguments(Boolean.class, true),
        arguments(Byte.class, true),
        arguments(Byte[].class, true),
        arguments(byte[].class, true),
        arguments(Double.class, true),
        arguments(Float.class, true),
        arguments(Integer.class, true),
        arguments(java.math.BigDecimal.class, true),
        arguments(java.math.BigInteger.class, true),
        arguments(java.sql.Time.class, true),
        arguments(java.sql.Timestamp.class, true),
        arguments(java.util.Calendar.class, true),
        arguments(java.util.Date.class, true),
        arguments(java.util.UUID.class, true),
        arguments(Long.class, true),
        arguments(Short.class, true),
        arguments(String.class, true),
        arguments(LocalDate.class, true),
        arguments(LocalTime.class, true),
        arguments(ZonedDateTime.class, true),
        arguments(LocalDateTime.class, false),
        arguments(OffsetDateTime.class, false),
        arguments(Instant.class, true));
  }

  static Stream<Arguments> simpleJavaTypes() {
    return Stream.concat(scalarJavaTypes(), Stream.of(
        arguments(Integer.class, null, true)));
  }

  private static Attribute<?, ?> buildLobAttribute() {
    final Attribute<?, ?> a = mock(Attribute.class);
    final AnnotatedElement e = mock(AnnotatedElement.class, Mockito.withSettings().extraInterfaces(Member.class));
    final Lob l = mock(Lob.class);

    when(a.getJavaMember()).thenReturn((Member) e);
    when(e.getAnnotation(Lob.class)).thenReturn(l);
    when(a.getName()).thenReturn("Lob");
    return a;
  }

  private static Attribute<?, ?> buildTimeAttribute(final TemporalType time) {
    final Attribute<?, ?> a = mock(Attribute.class);
    final AnnotatedElement e = mock(AnnotatedElement.class, Mockito.withSettings().extraInterfaces(Member.class));
    final Temporal t = mock(Temporal.class);

    when(a.getJavaMember()).thenReturn((Member) e);
    when(e.getAnnotation(Temporal.class)).thenReturn(t);
    when(t.value()).thenReturn(time);

    return a;
  }

  private static Attribute<?, ?> buildDimensionAttribute(final Dimension dimension) {
    final Attribute<?, ?> a = mock(Attribute.class);
    final AnnotatedElement e = mock(AnnotatedElement.class, Mockito.withSettings().extraInterfaces(Member.class));
    final EdmGeospatial g = mock(EdmGeospatial.class);

    when(a.getJavaMember()).thenReturn((Member) e);
    when(e.getAnnotation(EdmGeospatial.class)).thenReturn(g);
    when(g.dimension()).thenReturn(dimension);

    return a;
  }

  @ParameterizedTest
  @MethodSource({ "mappingJavaGeographyToOData", "mappingJavaGeometryToOData" })
  void checkConvertJavaGeographyToOData(final Class<?> javaType, final Attribute<?, ?> jpaAttribute,
      final EdmPrimitiveTypeKind odataType) throws ODataJPAModelException {

    assertEquals(odataType, JPATypeConverter.convertToEdmSimpleType(javaType, jpaAttribute));
    final ODataJPAModelException act = assertThrows(ODataJPAModelException.class,
        () -> JPATypeConverter.convertToEdmSimpleType(javaType, buildTimeAttribute(TemporalType.TIME)));
    assertEquals(ODataJPAModelException.MessageKeys.TYPE_NOT_SUPPORTED.getKey(), act.getId());
  }

  @ParameterizedTest
  @MethodSource("mappingJavaTimeToOData")
  void checkConvertJavaDateTimeToOData(final Class<?> javaType, final Attribute<?, ?> jpaAttribute,
      final EdmPrimitiveTypeKind odataType) throws ODataJPAModelException {

    assertEquals(odataType, JPATypeConverter.convertToEdmSimpleType(javaType, jpaAttribute));
  }

  @ParameterizedTest
  @MethodSource("mappingSimpleJavaToOData")
  void checkConvertSimpleJavaToOData(final Class<?> javaType, final EdmPrimitiveTypeKind odataType)
      throws ODataJPAModelException {

    assertEquals(odataType, JPATypeConverter.convertToEdmSimpleType(javaType));
  }

  @ParameterizedTest
  @MethodSource("scalarJavaTypes")
  void checkIsScalarJavaType(final Class<?> javaType, final Attribute<?, ?> jpaAttribute,
      final boolean isScalar) {

    assertEquals(isScalar, JPATypeConverter.isScalarType(javaType));
  }

  @ParameterizedTest
  @MethodSource("simpleJavaTypes")
  void checkIsSimpleJavaType(final Class<?> javaType, final Attribute<?, ?> jpaAttribute,
      final boolean isSimple) {

    assertEquals(isSimple, JPATypeConverter.isSimpleType(javaType, jpaAttribute));
  }

  @ParameterizedTest
  @MethodSource("supportedByOlingo")
  void checkIsSupportedByOlingo(final Class<?> javaType, final boolean isSupported) {

    assertEquals(isSupported, JPATypeConverter.isSupportedByOlingo(javaType));
  }

  @Test
  void checkReturnsNullOnUnknownTypeWithoutAnnotation() throws ODataJPAModelException {

    assertNull(JPATypeConverter.convertToEdmSimpleType(SimpleTimeZone.class));
  }

  @Test
  void checkThrowsExceptionOnUnknownTypeWithAnnotation() {

    assertThrows(ODataJPAModelException.class,
        () -> JPATypeConverter.convertToEdmSimpleType(SimpleTimeZone.class, buildTimeAttribute(TemporalType.TIME)));
  }

  @Test
  void checkThrowsExceptionOnUnknownGeographyType() {

    assertThrows(ODataJPAModelException.class,
        () -> JPATypeConverter.convertToEdmSimpleType(SimpleTimeZone.class, buildDimensionAttribute(
            Dimension.GEOGRAPHY)));
  }

  @Test
  void checkThrowsExceptionOnUnknownGeometryType() {

    assertThrows(ODataJPAModelException.class,
        () -> JPATypeConverter.convertToEdmSimpleType(SimpleTimeZone.class, buildDimensionAttribute(
            Dimension.GEOMETRY)));
  }
}
