/*
 * Copyright 2013 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.olingo.odata2.annotation.processor.core.edm;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.apache.olingo.odata2.annotation.processor.core.model.Building;
import org.apache.olingo.odata2.annotation.processor.core.model.City;
import org.apache.olingo.odata2.annotation.processor.core.model.Employee;
import org.apache.olingo.odata2.annotation.processor.core.model.Location;
import org.apache.olingo.odata2.annotation.processor.core.model.Manager;
import org.apache.olingo.odata2.annotation.processor.core.model.ModelSharedConstants;
import org.apache.olingo.odata2.annotation.processor.core.model.Photo;
import org.apache.olingo.odata2.annotation.processor.core.model.RefBase;
import org.apache.olingo.odata2.annotation.processor.core.model.Room;
import org.apache.olingo.odata2.annotation.processor.core.model.Team;
import org.apache.olingo.odata2.api.annotation.edm.EdmComplexType;
import org.apache.olingo.odata2.api.annotation.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.annotation.edm.EdmEntityType;
import org.apache.olingo.odata2.api.annotation.edm.EdmProperty;
import org.apache.olingo.odata2.api.edm.EdmConcurrencyMode;
import org.apache.olingo.odata2.api.edm.EdmMultiplicity;
import org.apache.olingo.odata2.api.edm.FullQualifiedName;
import org.apache.olingo.odata2.api.edm.provider.Association;
import org.apache.olingo.odata2.api.edm.provider.AssociationEnd;
import org.apache.olingo.odata2.api.edm.provider.AssociationSet;
import org.apache.olingo.odata2.api.edm.provider.ComplexProperty;
import org.apache.olingo.odata2.api.edm.provider.ComplexType;
import org.apache.olingo.odata2.api.edm.provider.EntityContainer;
import org.apache.olingo.odata2.api.edm.provider.EntityContainerInfo;
import org.apache.olingo.odata2.api.edm.provider.EntitySet;
import org.apache.olingo.odata2.api.edm.provider.EntityType;
import org.apache.olingo.odata2.api.edm.provider.FunctionImport;
import org.apache.olingo.odata2.api.edm.provider.Key;
import org.apache.olingo.odata2.api.edm.provider.NavigationProperty;
import org.apache.olingo.odata2.api.edm.provider.Property;
import org.apache.olingo.odata2.api.edm.provider.PropertyRef;
import org.apache.olingo.odata2.api.edm.provider.Schema;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.junit.Test;

/**
 *
 */
public class AnnotationEdmProviderTest {

  private static final String TEST_MODEL_PACKAGE = "org.apache.olingo.odata2.annotation.processor.core.model";

  @EdmEntityType
  @EdmEntitySet
  private static final class GeneratedNamesTestClass {
    @EdmProperty GeneratedNamesComplexTestClass myComplexProperty;
  }

  @EdmComplexType
  private static final class GeneratedNamesComplexTestClass {}

  @EdmEntityType(namespace = "MyTestNamespace")
  @EdmEntitySet(container = "MyTestContainer")
  private static final class DefinedNamesTestClass {}

  private final AnnotationEdmProvider aep;
  private final Collection<Class<?>> annotatedClasses = new ArrayList<Class<?>>();

  public AnnotationEdmProviderTest() throws ODataException {
    annotatedClasses.add(RefBase.class);
    annotatedClasses.add(Building.class);
    annotatedClasses.add(City.class);
    annotatedClasses.add(Employee.class);
    annotatedClasses.add(Location.class);
    annotatedClasses.add(Manager.class);
    annotatedClasses.add(Photo.class);
    annotatedClasses.add(Room.class);
    annotatedClasses.add(Team.class);

    aep = new AnnotationEdmProvider(annotatedClasses);
  }

  @Test
  public void defaultNameAndNamespaceGeneration() throws ODataException {
    Collection<Class<?>> localAnnotatedClasses = new ArrayList<Class<?>>();
    localAnnotatedClasses.add(GeneratedNamesTestClass.class);
    localAnnotatedClasses.add(GeneratedNamesComplexTestClass.class);
    AnnotationEdmProvider localAep = new AnnotationEdmProvider(localAnnotatedClasses);
    // validate
    EntityType testType = localAep.getEntityType(new FullQualifiedName(
        GeneratedNamesTestClass.class.getPackage().getName(),
        GeneratedNamesTestClass.class.getSimpleName()));
    assertNotNull("Requested entity not found.", testType);
    assertEquals("GeneratedNamesTestClass", testType.getName());
    assertNull("This should not have a base type", testType.getBaseType());
    List<Property> properties = testType.getProperties();
    assertEquals(1, properties.size());
    ComplexProperty propComplex = (ComplexProperty) properties.get(0);
    assertEquals("MyComplexProperty", propComplex.getName());
    assertEquals(GeneratedNamesComplexTestClass.class.getPackage().getName(), propComplex.getType().getNamespace());
    assertEquals(GeneratedNamesComplexTestClass.class.getSimpleName(), propComplex.getType().getName());


    ComplexType testComplexType = localAep.getComplexType(
        new FullQualifiedName(GeneratedNamesComplexTestClass.class.getPackage().getName(),
            GeneratedNamesComplexTestClass.class.getSimpleName()));
    assertNotNull("Requested entity not found.", testComplexType);
    assertEquals("GeneratedNamesComplexTestClass", testComplexType.getName());
    assertNull("This should not have a base type", testComplexType.getBaseType());
  }

  @Test
  public void defaultNamespaceGenerationComplexType() throws ODataException {
    Collection<Class<?>> localAnnotatedClasses = new ArrayList<Class<?>>();
    localAnnotatedClasses.add(GeneratedNamesComplexTestClass.class);
    AnnotationEdmProvider localAep = new AnnotationEdmProvider(localAnnotatedClasses);
    // validate
    ComplexType testType = localAep.getComplexType(new FullQualifiedName(
        GeneratedNamesComplexTestClass.class.getPackage().getName(),
        GeneratedNamesComplexTestClass.class.getSimpleName()));
    assertNotNull("Requested entity not found.", testType);
    assertEquals("GeneratedNamesComplexTestClass", testType.getName());
    assertNull("This should not have a base type", testType.getBaseType());
  }

  @Test
  public void defaultContainerNameGeneration() throws ODataException {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    AnnotationEdmProvider localAep =
        new AnnotationEdmProvider((Collection) Arrays.asList(GeneratedNamesTestClass.class));

    EntityContainerInfo containerInfo = localAep.getEntityContainerInfo(null);
    assertNotNull(containerInfo);
    assertEquals("DefaultContainer", containerInfo.getName());
  }

  @Test
  public void defaultNamespaceDefined() throws ODataException {
    Collection<Class<?>> localAnnotatedClasses = new ArrayList<Class<?>>();
    localAnnotatedClasses.add(DefinedNamesTestClass.class);
    AnnotationEdmProvider localAep = new AnnotationEdmProvider(localAnnotatedClasses);
    // validate
    EntityType testClass = localAep.getEntityType(new FullQualifiedName("MyTestNamespace",
        DefinedNamesTestClass.class.getSimpleName()));
    assertNotNull("Requested entity not found.", testClass);
    assertEquals("DefinedNamesTestClass", testClass.getName());
    assertNull("This should not have a base type", testClass.getBaseType());
  }

  @Test
  public void defaultContainerNameDefined() throws ODataException {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    AnnotationEdmProvider localAep = new AnnotationEdmProvider((Collection) Arrays.asList(DefinedNamesTestClass.class));

    EntityContainerInfo containerInfo = localAep.getEntityContainerInfo(null);
    assertNotNull(containerInfo);
    assertEquals("MyTestContainer", containerInfo.getName());
  }

  @Test
  public void loadAnnotatedClassesFromPackage() throws Exception {
    AnnotationEdmProvider localAep = new AnnotationEdmProvider(TEST_MODEL_PACKAGE);

    // validate employee
    EntityType employee = localAep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Employee"));
    assertEquals("Employee", employee.getName());
    final List<PropertyRef> employeeKeys = employee.getKey().getKeys();
    assertEquals(1, employeeKeys.size());
    assertEquals("EmployeeId", employeeKeys.get(0).getName());
    assertEquals(6, employee.getProperties().size());
    assertEquals(3, employee.getNavigationProperties().size());

    List<Schema> schemas = localAep.getSchemas();
    assertEquals(1, schemas.size());
    EntityContainerInfo info = localAep.getEntityContainerInfo(ModelSharedConstants.CONTAINER_1);
    assertTrue(info.isDefaultEntityContainer());
  }

  @Test
  public void annotationProviderBasic() throws Exception {
    assertNotNull(aep);

    List<Schema> schemas = aep.getSchemas();
    assertEquals(1, schemas.size());
    EntityContainerInfo info = aep.getEntityContainerInfo(ModelSharedConstants.CONTAINER_1);
    assertTrue(info.isDefaultEntityContainer());

    FunctionImport funImp = aep.getFunctionImport(ModelSharedConstants.CONTAINER_1, "NoImport");
    assertNull(funImp);

    final FullQualifiedName associationFqn = new FullQualifiedName(
        ModelSharedConstants.NAMESPACE_1, "NoAssociation");
    Association noAssociation = aep.getAssociation(associationFqn);
    assertNull(noAssociation);

    AssociationSet noAssociationSet = aep.getAssociationSet(
        ModelSharedConstants.CONTAINER_1, associationFqn, "NoSrc", "NoSrcEntity");
    assertNull(noAssociationSet);

    AssociationSet asBuildingRooms = aep.getAssociationSet(
        ModelSharedConstants.CONTAINER_1, defaultFqn("BuildingRooms"), "Buildings", "r_Building");
    assertNotNull(asBuildingRooms);
    assertEquals("Buildings", asBuildingRooms.getEnd1().getEntitySet());
    assertEquals("r_Building", asBuildingRooms.getEnd1().getRole());
    assertEquals("Rooms", asBuildingRooms.getEnd2().getEntitySet());
    assertEquals("r_Rooms", asBuildingRooms.getEnd2().getRole());
  }

  @Test
  public void annotationProviderGetDefaultContainer() throws Exception {
    assertNotNull(aep);

    List<Schema> schemas = aep.getSchemas();
    assertEquals(1, schemas.size());
    EntityContainerInfo info = aep.getEntityContainerInfo(null);
    assertTrue(info.isDefaultEntityContainer());
    assertEquals(ModelSharedConstants.CONTAINER_1, info.getName());
  }

  @Test
  public void schemaBasic() throws Exception {
    assertNotNull(aep);

    List<Schema> schemas = aep.getSchemas();
    assertEquals(1, schemas.size());

    Schema schema = schemas.get(0);
    List<EntityContainer> containers = schema.getEntityContainers();
    assertEquals(1, containers.size());
    EntityContainer container = containers.get(0);
    assertEquals(ModelSharedConstants.CONTAINER_1, container.getName());
    final List<EntitySet> entitySets = container.getEntitySets();
    assertEquals(6, entitySets.size());

    List<Association> associations = schema.getAssociations();
    assertEquals(5, associations.size());
    for (Association association : associations) {
      assertNotNull(association.getName());
      validateAssociation(association);
    }
  }

  private FullQualifiedName defaultFqn(final String name) {
    return new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, name);
  }

  private void validateAssociation(final Association association) {
    String name = association.getName();
    if (name.equals("r_Employees_2_r_Room")) {
      validateAssociation(association,
          "r_Room", EdmMultiplicity.ONE, defaultFqn("Room"),
          "r_Employees", EdmMultiplicity.MANY, defaultFqn("Employee"));
    } else if (name.equals("BuildingRooms")) {
      validateAssociation(association,
          "r_Building", EdmMultiplicity.ONE, defaultFqn("Building"),
          "r_Rooms", EdmMultiplicity.MANY, defaultFqn("Room"));
    } else if (name.equals("ManagerEmployees")) {
      validateAssociation(association,
          "r_Manager", EdmMultiplicity.ONE, defaultFqn("Manager"),
          "r_Employees", EdmMultiplicity.MANY, defaultFqn("Employee"));
    } else if (name.equals("TeamEmployees")) {
      validateAssociation(association,
          "r_Team", EdmMultiplicity.ONE, defaultFqn("Team"),
          "r_Employees", EdmMultiplicity.MANY, defaultFqn("Employee"));
    } else if (name.equals("Team_2_r_SubTeam")) {
      validateAssociation(association,
          "Team", EdmMultiplicity.ONE, defaultFqn("Team"),
          "r_SubTeam", EdmMultiplicity.ONE, defaultFqn("Team"));
    } else {
      fail("Got unknown association to validate with name '" + name + "'.");
    }
  }

  private void validateAssociation(final Association association,
      final String fromRole, final EdmMultiplicity fromMulti, final FullQualifiedName fromType,
      final String toRole, final EdmMultiplicity toMulti, final FullQualifiedName toType) {

    AssociationEnd[] ends = new AssociationEnd[] { association.getEnd1(), association.getEnd2() };
    for (AssociationEnd associationEnd : ends) {
      if (associationEnd.getRole().equals(fromRole)) {
        validateAssociationEnd(associationEnd, fromRole, fromMulti, fromType);
      } else if (associationEnd.getRole().equals(toRole)) {
        validateAssociationEnd(associationEnd, toRole, toMulti, toType);
      } else {
        fail("Unexpected navigation end '" + associationEnd.getRole()
            + "' for association with name '" + association.getName() + "'.");
      }
    }
  }

  private void validateAssociationEnd(final AssociationEnd associationEnd,
      final String role, final EdmMultiplicity multiplicity, final FullQualifiedName type) {
    assertEquals(role, associationEnd.getRole());
    assertEquals(multiplicity, associationEnd.getMultiplicity());
    assertEquals(type, associationEnd.getType());
  }

  @Test
  public void entitySetTeams() throws Exception {
    // validate teams
    EntitySet teams = aep.getEntitySet(ModelSharedConstants.CONTAINER_1, "Teams");
    assertEquals(ModelSharedConstants.NAMESPACE_1, teams.getEntityType().getNamespace());
    assertEquals("Team", teams.getEntityType().getName());
  }

  @Test
  public void entityTypeEmployee() throws Exception {
    // validate employee
    EntityType employee = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Employee"));
    assertEquals("Employee", employee.getName());
    final List<PropertyRef> employeeKeys = employee.getKey().getKeys();
    assertEquals(1, employeeKeys.size());
    assertEquals("EmployeeId", employeeKeys.get(0).getName());
    assertEquals(6, employee.getProperties().size());
    assertEquals(3, employee.getNavigationProperties().size());
    Property name = getProperty(employee, "EmployeeName");
    assertEquals(Integer.valueOf(20), name.getFacets().getMaxLength());

    for (NavigationProperty navigationProperty : employee.getNavigationProperties()) {
      if (navigationProperty.getName().equals("ne_Manager")) {
        validateNavProperty(navigationProperty, "ManagerEmployees", "r_Employees", "r_Manager");
      } else if (navigationProperty.getName().equals("ne_Team")) {
        validateNavProperty(navigationProperty, "TeamEmployees", "r_Employees", "r_Team");
      } else if (navigationProperty.getName().equals("ne_Room")) {
        validateNavProperty(navigationProperty, "r_Employees_2_r_Room", "r_Employees", "r_Room");
      } else {
        fail("Got unexpected navigation property with name '" + navigationProperty.getName() + "'.");
      }
    }
  }


  @Test
  public void facetsTest() throws Exception {
    EntityType employee = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Employee"));
    assertEquals("Employee", employee.getName());
    Property name = getProperty(employee, "EmployeeName");
    assertEquals(Integer.valueOf(20), name.getFacets().getMaxLength());
    assertNull(name.getFacets().getConcurrencyMode());
    assertTrue(name.getFacets().isNullable());
    Property id = getProperty(employee, "EmployeeId");
    assertFalse(id.getFacets().isNullable());

    ComplexType city = aep.getComplexType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "c_City"));
    Property postalCode = getProperty(city.getProperties(), "PostalCode");
    assertEquals(Integer.valueOf(5), postalCode.getFacets().getMaxLength());

    EntityType room = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Room"));
    Property version = getProperty(room, "Version");
    assertEquals(Integer.valueOf(0), version.getFacets().getScale());
    assertEquals(Integer.valueOf(0), version.getFacets().getPrecision());
    assertEquals(EdmConcurrencyMode.Fixed, version.getFacets().getConcurrencyMode());
  }

  @Test
  public void entityTypeTeam() throws Exception {
    // validate team
    EntityType team = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Team"));
    assertEquals("Team", team.getName());
    assertEquals("Base", team.getBaseType().getName());
    assertEquals(ModelSharedConstants.NAMESPACE_1, team.getBaseType().getNamespace());

    assertEquals(1, team.getProperties().size());
    assertEquals(2, team.getNavigationProperties().size());
    NavigationProperty navPropTeamEmployess = team.getNavigationProperties().get(0);
    validateNavProperty(navPropTeamEmployess, "TeamEmployees", "r_Team", "r_Employees");
    NavigationProperty navPropTeamTeam = team.getNavigationProperties().get(1);
    validateNavProperty(navPropTeamTeam, "Team_2_r_SubTeam", "Team", "r_SubTeam");
  }

  @Test
  public void entityTypePhotoWithTwoKeyProperties() throws Exception {
    // validate team
    EntityType photo = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Photo"));
    assertEquals("Photo", photo.getName());
    final List<Property> properties = photo.getProperties();
    assertEquals(5, properties.size());
    assertTrue(containsProperty(properties, "Name"));
    assertTrue(containsProperty(properties, "ImageFormat"));
    assertTrue(containsProperty(properties, "MimeType"));
    assertTrue(containsProperty(properties, "ImageUrl"));
    assertTrue(containsProperty(properties, "Image"));
    assertFalse(photo.isAbstract());
    assertTrue(photo.isHasStream());
    assertEquals("MimeType",photo.getMapping().getMediaResourceMimeTypeKey());
    assertEquals("ImageUrl",photo.getMapping().getMediaResourceSourceKey());

    Key photoKey = photo.getKey();
    List<PropertyRef> keyReferences = photoKey.getKeys();
    assertEquals(2, keyReferences.size());
    PropertyRef name = getPropertyRef(keyReferences, "Name");
    assertEquals("Name", name.getName());
    PropertyRef imageFormat = getPropertyRef(keyReferences, "ImageFormat");
    assertEquals("ImageFormat", imageFormat.getName());

    // assertEquals(0, photo.getNavigationProperties().size());
    assertNull(photo.getNavigationProperties());
  }

  @Test
  public void entityTypeAbstractBaseType() throws Exception {
    // validate employee
    EntityType baseType = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Base"));
    assertEquals("Base", baseType.getName());
    final List<PropertyRef> keys = baseType.getKey().getKeys();
    assertEquals(1, keys.size());
    assertEquals("Id", keys.get(0).getName());
    assertEquals(2, baseType.getProperties().size());
    assertTrue(baseType.isAbstract());

    // validate base for team
    EntityType team = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Team"));
    assertEquals("Team", team.getName());
    assertEquals("Base", team.getBaseType().getName());
    assertEquals(ModelSharedConstants.NAMESPACE_1, team.getBaseType().getNamespace());
  }

  @Test
  public void complexTypeLocation() throws Exception {
    // validate employee
    EntityType employee = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Employee"));
    final List<Property> properties = employee.getProperties();
    Property location = null;
    for (Property property : properties) {
      if (property.getName().equals("Location")) {
        location = property;
      }
    }
    assertNotNull(location);
    assertEquals("Location", location.getName());

    // validate location complex type
    ComplexType locationType = aep.getComplexType(
        new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "c_Location"));
    assertEquals("c_Location", locationType.getName());
    assertEquals(2, locationType.getProperties().size());
    assertEquals(false, location.getFacets().isNullable());
  }

  @Test
  public void entityTypeRoomWithNavigation() throws Exception {
    // validate employee
    EntityType room = aep.getEntityType(new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, "Room"));
    assertEquals("Room", room.getName());
    assertEquals("Base", room.getBaseType().getName());
    assertEquals(2, room.getProperties().size());
    final List<NavigationProperty> navigationProperties = room.getNavigationProperties();
    assertEquals(2, navigationProperties.size());

    for (NavigationProperty navigationProperty : navigationProperties) {
      if (navigationProperty.getName().equals("nr_Employees")) {
        validateNavProperty(navigationProperty, "r_Employees_2_r_Room", "r_Room", "r_Employees");
      } else if (navigationProperty.getName().equals("nr_Building")) {
        validateNavProperty(navigationProperty, "BuildingRooms", "r_Rooms", "r_Building");
      } else {
        fail("Got unexpected navigation property with name '" + navigationProperty.getName() + "'.");
      }
    }
  }

  private void validateNavProperty(final NavigationProperty navigationProperty, final String name,
      final String relationship, final String fromRole, final String toRole) {
    if (name != null) {
      assertEquals(name, navigationProperty.getName());
    }
    FullQualifiedName fqn = new FullQualifiedName(ModelSharedConstants.NAMESPACE_1, relationship);
    assertEquals("Wrong relationship for navigation property.", fqn, navigationProperty.getRelationship());
    assertEquals("Wrong fromRole for navigation property.", fromRole, navigationProperty.getFromRole());
    assertEquals("Wrong toRole for navigation property.", toRole, navigationProperty.getToRole());
  }

  private void validateNavProperty(final NavigationProperty navigationProperty,
      final String relationship, final String fromRole, final String toRole) {
    validateNavProperty(navigationProperty, null, relationship, fromRole, toRole);
  }

  private boolean containsProperty(final List<Property> properties, final String propertyName) {
    return getProperty(properties, propertyName) != null;
  }

  private Property getProperty(EntityType entity, String propertyName) {
    return getProperty(entity.getProperties(), propertyName);
  }

  private Property getProperty(final List<Property> properties, final String name) {
    for (Property property : properties) {
      if (name.equals(property.getName())) {
        return property;
      }
    }
    fail("Requested property with name '" + name + "' not available.");
    return null;
  }

  private PropertyRef getPropertyRef(final List<PropertyRef> properties, final String name) {
    for (PropertyRef property : properties) {
      if (name.equals(property.getName())) {
        return property;
      }
    }
    return null;
  }
}
