package com.sap.olingo.jpa.processor.core.api.example;

import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import jakarta.persistence.EntityManager;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.SingularAttribute;

import org.apache.olingo.commons.api.ex.ODataException;
import org.apache.olingo.commons.api.http.HttpMethod;
import org.apache.olingo.commons.api.http.HttpStatusCode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.internal.creation.MockSettingsImpl;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAAssociationPath;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAAttribute;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAEntityType;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAPath;
import com.sap.olingo.jpa.metadata.core.edm.mapper.api.JPAProtectionInfo;
import com.sap.olingo.jpa.metadata.core.edm.mapper.exception.ODataJPAModelException;
import com.sap.olingo.jpa.processor.core.api.JPAClaimsPair;
import com.sap.olingo.jpa.processor.core.api.JPAODataClaimProvider;
import com.sap.olingo.jpa.processor.core.exception.ODataJPAProcessException;
import com.sap.olingo.jpa.processor.core.modify.JPAUpdateResult;
import com.sap.olingo.jpa.processor.core.processor.JPAModifyUtil;
import com.sap.olingo.jpa.processor.core.processor.JPARequestEntity;
import com.sap.olingo.jpa.processor.core.processor.JPARequestLink;
import com.sap.olingo.jpa.processor.core.testmodel.AdministrativeDivision;
import com.sap.olingo.jpa.processor.core.testmodel.AdministrativeDivisionKey;
import com.sap.olingo.jpa.processor.core.testmodel.Collection;
import com.sap.olingo.jpa.processor.core.testmodel.DeepProtectedExample;
import com.sap.olingo.jpa.processor.core.testmodel.InhouseAddress;
import com.sap.olingo.jpa.processor.core.testmodel.Organization;
import com.sap.olingo.jpa.processor.core.testmodel.Person;
import com.sap.olingo.jpa.processor.core.testmodel.PersonDeepProtected;
import com.sap.olingo.jpa.processor.core.testmodel.PostalAddressData;
import com.sap.olingo.jpa.processor.core.testobjects.OrganizationWithAudit;
import com.sap.olingo.jpa.processor.core.util.TestBase;
import com.sap.olingo.jpa.processor.core.util.TestHelper;

class JPAExampleCUDRequestHandlerTest extends TestBase {
  private JPAExampleCUDRequestHandler cut;
  private EntityManager em;
  private Metamodel metamodel;
  private JPARequestEntity requestEntity;
  private Map<String, Object> data;
  private Map<String, Object> keys;
  private Map<JPAAssociationPath, List<JPARequestLink>> relationLinks;

  @BeforeEach
  void setup() throws ODataException {
    helper = new TestHelper(emf, PUNIT_NAME);
    em = mock(EntityManager.class);
    requestEntity = mock(JPARequestEntity.class);
    metamodel = mock(Metamodel.class);
    data = new HashMap<>();
    keys = new HashMap<>();
    relationLinks = new HashMap<>();

    doReturn(new JPAModifyUtil()).when(requestEntity).getModifyUtil();
    doReturn(data).when(requestEntity).getData();
    doReturn(keys).when(requestEntity).getKeys();
    doReturn(metamodel).when(em).getMetamodel();
    doReturn(emptySet()).when(metamodel).getEntities();
    doReturn(relationLinks).when(requestEntity).getRelationLinks();
    cut = new JPAExampleCUDRequestHandler();
  }

  @Test
  void checkCreateEntity() throws ODataJPAProcessException, ODataJPAModelException {

    final Object act = createAdminDiv();
    assertNotNull(act);
    assertEquals("NUTS2", ((AdministrativeDivision) act).getCodeID());
    verify(em).persist(act);
  }

  @Test
  void checkCreateEntityWithPrimitiveCollection() throws ODataJPAProcessException, ODataJPAModelException {

    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();

    final List<String> comments = new ArrayList<>(2);
    comments.add("This is just test");
    comments.add("YAT");

    data.put("iD", "504");
    data.put("comment", comments);

    final Organization act = (Organization) cut.createEntity(requestEntity, em);
    assertNotNull(act);
    assertEquals("504", act.getID());
    assertFalse(act.getComment().isEmpty());
    verify(em).persist(act);
  }

  @Test
  void checkCreateEntityWithComplexCollection() throws ODataJPAProcessException, ODataJPAModelException {

    doReturn(helper.getJPAEntityType("Persons")).when(requestEntity).getEntityType();

    final List<Map<String, Object>> inhouseAddrs = new ArrayList<>(2);
    final Map<String, Object> addr1 = new HashMap<>(4);
    addr1.put("roomNumber", 32);
    addr1.put("floor", 2);
    addr1.put("building", "7");
    addr1.put("taskID", "MAIN");
    inhouseAddrs.add(addr1);

    final Map<String, Object> addr2 = new HashMap<>(4);
    addr2.put("roomNumber", 245);
    addr2.put("floor", -3);
    addr2.put("building", "1");
    addr2.put("taskID", "DEV");
    inhouseAddrs.add(addr2);

    data.put("iD", "707");
    data.put("inhouseAddress", inhouseAddrs);

    final Person act = (Person) cut.createEntity(requestEntity, em);
    assertNotNull(act);
    assertEquals("707", act.getID());
    assertEquals(2, act.getInhouseAddress().size());
    assertNotNull(act.getInhouseAddress().get(0).getTaskID());
    verify(em).persist(act);
  }

  @Test
  void checkCreateEntityWithComplexCollcetionInitialyNull() throws ODataJPAProcessException,
      ODataJPAModelException {
    assertNull(new Collection().getNested());

    doReturn(helper.getJPAEntityType("Collections")).when(requestEntity).getEntityType();

    final Map<String, Object> complex = new HashMap<>();
    final List<Map<String, Object>> nested = new ArrayList<>();
    final Map<String, Object> nestedItem = new HashMap<>();
    final Map<String, Object> inner = new HashMap<>();
    final List<String> comment = new ArrayList<>();
    inner.put("figure1", 100L);

    nestedItem.put("inner", inner);
    nestedItem.put("number", 50L);
    nested.add(nestedItem);

    comment.add("How about this");

    complex.put("number", 25L);
    complex.put("comment", comment);
    data.put("iD", "707");
    data.put("complex", complex);
    data.put("nested", nested);

    final Collection act = (Collection) cut.createEntity(requestEntity, em);
    assertNotNull(act);

    assertEquals("707", act.getID());
    assertNotNull(act.getNested());
    assertEquals(1, act.getNested().size());
    assertEquals(50L, act.getNested().get(0).getNumber());
    assertEquals(100L, act.getNested().get(0).getInner().getFigure1());
    assertNotNull(act.getComplex());
    assertNotNull(act.getComplex().getComment());
    assertEquals(1, act.getComplex().getComment().size());
  }

  @Test
  void checkCreateDeepEntity() throws ODataJPAProcessException, ODataJPAModelException {
    // http://localhost:8080/tutorial/v1/AdministrativeDivisions(DivisionCode='DE5',CodeID='NUTS1',CodePublisher='Eurostat')/Children
    // key : "divisionCode=DE5, codeID=NUTS1, codePublisher=Eurostat"
    // jpaDeepEntities = JPAAssPath ; JPARequestEntity

    final JPAEntityType et = helper.getJPAEntityType("AdministrativeDivisions");
    final JPAAssociationPath path = et.getAssociationPath("Children");
    final Map<JPAAssociationPath, List<JPARequestEntity>> deepEntities = new HashMap<>();
    final JPARequestEntity deepEntity = mock(JPARequestEntity.class);
    final Map<String, Object> deepData = new HashMap<>();
    final AdministrativeDivision parent = new AdministrativeDivision(new AdministrativeDivisionKey("Eurostat", "NUTS1",
        "DE5"));

    doReturn(et).when(requestEntity).getEntityType();
    keys.put("divisionCode", "DE5");
    keys.put("codeID", "NUTS1");
    keys.put("codePublisher", "Eurostat");
    doReturn(deepEntities).when(requestEntity).getRelatedEntities();
    deepEntities.put(path, Arrays.asList(deepEntity));

    doReturn(et).when(deepEntity).getEntityType();
    doReturn(deepData).when(deepEntity).getData();
    doReturn(new JPAModifyUtil()).when(deepEntity).getModifyUtil();

    deepData.put("DivisionCode", "DE52");
    deepData.put("CodeID", "NUTS2");
    deepData.put("CodePublisher", "Eurostat");
    deepData.put("CountryCode", "DEU");

    doReturn(parent).when(em).getReference(eq(et.getTypeClass()), any());

    final AdministrativeDivision act = (AdministrativeDivision) cut.createEntity(requestEntity, em);
    cut.validateChanges(em);
    assertNotNull(act.getChildren());
    assertEquals(1, act.getChildren().size());

  }

  @Test
  void checkCreateLinkedThrowsExceptionTargetNotFound() throws ODataJPAProcessException, ODataJPAModelException {
    // http://localhost:8080/tutorial/v1/AdministrativeDivisions(DivisionCode='DE5',CodeID='NUTS1',CodePublisher='Eurostat')/Children
    // key : "divisionCode=DE5, codeID=NUTS1, codePublisher=Eurostat"
    // jpaDeepEntities = JPAAssPath ; JPARequestEntity

    final JPAEntityType et = helper.getJPAEntityType("AdministrativeDivisions");
    final JPAAssociationPath path = et.getAssociationPath("Parent");
    final List<JPARequestLink> links = new ArrayList<>();
    final JPARequestLink link = mock(JPARequestLink.class);
    final AdministrativeDivisionKey parentKey = new AdministrativeDivisionKey("Eurostat", "NUTS1", "DE5");

    doReturn(et).when(requestEntity).getEntityType();
    data.put("divisionCode", "DE52");
    data.put("codeID", "NUTS2");
    data.put("codePublisher", "Eurostat");
    doReturn(Collections.emptyMap()).when(requestEntity).getRelatedEntities();

    final Map<String, Object> relatedKey = new HashMap<>();
    relatedKey.put("divisionCode", "DE5");
    relatedKey.put("codeID", "NUTS1");
    relatedKey.put("codePublisher", "Eurostat");
    doReturn(null).when(em).find(et.getTypeClass(), parentKey);

    relationLinks.put(path, links);
    doReturn(et).when(link).getEntityType();
    doReturn(relatedKey).when(link).getRelatedKeys();
    links.add(link);

    cut.createEntity(requestEntity, em);
    final ODataJPAProcessException act = assertThrows(ODataJPAProcessException.class, () -> cut.validateChanges(em));
    assertEquals(HttpStatusCode.BAD_REQUEST.getStatusCode(), act.getStatusCode());
  }

  @Test
  void checkCreateLinkedEntityOneToMany() throws ODataJPAProcessException, ODataJPAModelException {
    // http://localhost:8080/tutorial/v1/AdministrativeDivisions(DivisionCode='DE5',CodeID='NUTS1',CodePublisher='Eurostat')/Children
    // key : "divisionCode=DE5, codeID=NUTS1, codePublisher=Eurostat"
    // jpaDeepEntities = JPAAssPath ; JPARequestEntity

    final JPAEntityType et = helper.getJPAEntityType("AdministrativeDivisions");
    final JPAAssociationPath path = et.getAssociationPath("Parent");
    final List<JPARequestLink> links = new ArrayList<>();
    final JPARequestLink link = mock(JPARequestLink.class);
    final AdministrativeDivisionKey parentKey = new AdministrativeDivisionKey("Eurostat", "NUTS1", "DE5");
    final AdministrativeDivision parent = new AdministrativeDivision(parentKey);

    doReturn(et).when(requestEntity).getEntityType();
    data.put("divisionCode", "DE52");
    data.put("codeID", "NUTS2");
    data.put("codePublisher", "Eurostat");
    doReturn(Collections.emptyMap()).when(requestEntity).getRelatedEntities();

    final Map<String, Object> relatedKey = new HashMap<>();
    relatedKey.put("divisionCode", "DE5");
    relatedKey.put("codeID", "NUTS1");
    relatedKey.put("codePublisher", "Eurostat");
    doReturn(parent).when(em).find(et.getTypeClass(), parentKey);

    relationLinks.put(path, links);
    doReturn(et).when(link).getEntityType();
    doReturn(relatedKey).when(link).getRelatedKeys();
    links.add(link);

    final AdministrativeDivision act = (AdministrativeDivision) cut.createEntity(requestEntity, em);
    cut.validateChanges(em);
    assertNotNull(act.getParent());
    assertEquals(parentKey.getCodeID(), act.getParentCodeID());
    assertEquals(parentKey.getDivisionCode(), act.getParentDivisionCode());
    assertEquals(parentKey.getCodePublisher(), act.getCodePublisher());
  }

  @Test
  void checkCreateLinkedEntityManyToOne() throws ODataJPAProcessException, ODataJPAModelException {
    // http://localhost:8080/tutorial/v1/AdministrativeDivisions(DivisionCode='DE5',CodeID='NUTS1',CodePublisher='Eurostat')/Children
    // key : "divisionCode=DE5, codeID=NUTS1, codePublisher=Eurostat"
    // jpaDeepEntities = JPAAssPath ; JPARequestEntity

    final JPAEntityType et = helper.getJPAEntityType("AdministrativeDivisions");
    final JPAAssociationPath path = et.getAssociationPath("Children");
    final List<JPARequestLink> links = new ArrayList<>();
    final JPARequestLink link = mock(JPARequestLink.class);
    final AdministrativeDivisionKey key = new AdministrativeDivisionKey("Eurostat", "NUTS1", "DE5");
    final AdministrativeDivision target = new AdministrativeDivision(key);
    final AdministrativeDivisionKey childKey = new AdministrativeDivisionKey("Eurostat", "NUTS2", "DE52");
    final AdministrativeDivision child = new AdministrativeDivision(childKey);

    doReturn(et).when(requestEntity).getEntityType();
    data.put("divisionCode", "DE5");
    data.put("codeID", "NUTS1");
    data.put("codePublisher", "Eurostat");
    doReturn(Collections.emptyMap()).when(requestEntity).getRelatedEntities();
    doReturn(null, target).when(em).find(et.getTypeClass(), key);

    final Map<String, Object> relatedKey = new HashMap<>();
    relatedKey.put("divisionCode", "DE52");
    relatedKey.put("codeID", "NUTS2");
    relatedKey.put("codePublisher", "Eurostat");
    doReturn(child).when(em).find(et.getTypeClass(), childKey);

    relationLinks.put(path, links);
    doReturn(et).when(link).getEntityType();
    doReturn(relatedKey).when(link).getRelatedKeys();
    links.add(link);

    final AdministrativeDivision act = (AdministrativeDivision) cut.createEntity(requestEntity, em);
    cut.validateChanges(em);
    assertEquals(act.getCodeID(), child.getParentCodeID());
    assertEquals(act.getDivisionCode(), child.getParentDivisionCode());
    assertEquals(act.getCodePublisher(), child.getCodePublisher());
    assertNotNull(act.getChildren());
    // assertEquals(1, act.getChildren().size()); //NOSONAR
  }

  @Test
  void checkCreateEntityAutoId() throws ODataJPAProcessException, ODataJPAModelException {
    final EntityType<?> jpaEt = mock(EntityType.class);
    final Set<EntityType<?>> jpaEts = new HashSet<>();
    final SingularAttribute<?, ?> at = mock(SingularAttribute.class);
    final Member atAsMember = mock(Member.class, new MockSettingsImpl<>().extraInterfaces(AnnotatedElement.class));
    final GeneratedValue generatedValue = mock(GeneratedValue.class);
    jpaEts.add(jpaEt);
    doReturn(true).when(jpaEt).hasSingleIdAttribute();
    doReturn(jpaEts).when(metamodel).getEntities();
    doReturn("AdministrativeDivision").when(jpaEt).getName();
    doReturn(at).when(jpaEt).getId(any());
    doReturn(atAsMember).when(at).getJavaMember();
    doReturn(generatedValue).when((AnnotatedElement) atAsMember).getAnnotation(GeneratedValue.class);

    final Object act = createAdminDiv();

    assertNotNull(act);
    assertEquals("NUTS2", ((AdministrativeDivision) act).getCodeID());
    verify(em, times(2)).persist(act);
    verify(em, never()).find(any(), any());
  }

  @Test
  void checkCreateEntityWithoutAutoId() throws ODataJPAProcessException, ODataJPAModelException {
    final EntityType<?> jpaEt = mock(EntityType.class);
    final Set<EntityType<?>> jpaEts = new HashSet<>();
    final SingularAttribute<?, ?> at = mock(SingularAttribute.class);
    final Member atAsMember = mock(Member.class, new MockSettingsImpl<>().extraInterfaces(AnnotatedElement.class));
    jpaEts.add(jpaEt);
    doReturn(jpaEts).when(metamodel).getEntities();
    doReturn("AdministrativeDivision").when(jpaEt).getName();
    doReturn(at).when(jpaEt).getId(any());
    doReturn(atAsMember).when(at).getJavaMember();
    doReturn(null).when((AnnotatedElement) atAsMember).getAnnotation(GeneratedValue.class);

    final Object act = createAdminDiv();

    assertNotNull(act);
    assertEquals("NUTS2", ((AdministrativeDivision) act).getCodeID());
    verify(em).persist(act);
    verify(em).find(any(), any());
  }

  @Test
  void checkCreateEntityAutoIdIdNotFound() throws ODataJPAProcessException, ODataJPAModelException {
    final EntityType<?> jpaEt = mock(EntityType.class);
    final Set<EntityType<?>> jpaEts = new HashSet<>();
    jpaEts.add(jpaEt);
    doReturn(jpaEts).when(metamodel).getEntities();
    doReturn("AdministrativeDivision").when(jpaEt).getName();
    doReturn(null).when(jpaEt).getId(any());

    final Object act = createAdminDiv();

    assertNotNull(act);
    assertEquals("NUTS2", ((AdministrativeDivision) act).getCodeID());
    verify(em).persist(act);
    verify(em).find(any(), any());
  }

  @Test
  void checkCreateEntityAutoIdNoJpaEntityType() throws ODataJPAProcessException, ODataJPAModelException {
    final Set<EntityType<?>> jpaEts = new HashSet<>();
    doReturn(jpaEts).when(metamodel).getEntities();

    final Object act = createAdminDiv();

    assertNotNull(act);
    assertEquals("NUTS2", ((AdministrativeDivision) act).getCodeID());
    verify(em).persist(act);
    verify(em).find(any(), any());
  }

  @Test
  void checkUpdateEntityNotFoundUpsertsEntity() throws ODataJPAModelException, ODataJPAProcessException {
    final String id = "100";
    final Organization beforeImage = new Organization(id);
    final JPAEntityType et = helper.getJPAEntityType("Organizations");
    beforeImage.setName1("Example Ltd");

    doReturn(et).when(requestEntity).getEntityType();
    doReturn(null).when(em).find(eq(et.getTypeClass()), any());
    data.put("name1", "Example SE");
    keys.put("iD", id);

    final var act = cut.updateEntity(requestEntity, em, HttpMethod.PATCH);

    assertNotNull(act);
    assertTrue(act.wasCreate());

    final Organization org = (Organization) act.modifiedEntity();
    assertEquals("100", org.getID());

  }

  @Test
  void checkDeleteSimplePrimitiveProperty() throws ODataJPAProcessException, ODataJPAModelException {
    final String id = "1";
    final Organization beforeImage = new Organization(id);
    beforeImage.setName1("Example Ltd");

    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Organization.class, id);

    data.put("name1", null);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNull(((Organization) act.modifiedEntity()).getName1());
  }

  @Test
  void checkDeletePrimitiveCollectionProperty() throws ODataJPAProcessException, ODataJPAModelException {
    final String id = "1";
    final Organization beforeImage = new Organization(id);
    beforeImage.getComment().add("YAC");

    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Organization.class, id);

    data.put("comment", null);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNull(((Organization) act.modifiedEntity()).getComment());
  }

  @Test
  void checkDeleteSimpleComplexProperty() throws ODataJPAProcessException, ODataJPAModelException {
    final String id = "1";
    final Organization beforeImage = new Organization(id);
    beforeImage.setAddress(new PostalAddressData());

    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Organization.class, id);

    data.put("address", null);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNull(((Organization) act.modifiedEntity()).getAddress());
  }

  @Test
  void checkDeleteComplexCollectionProperty() throws ODataJPAProcessException, ODataJPAModelException {

    final String id = "2";
    final Person beforeImage = new Person();
    beforeImage.setID(id);
    beforeImage.getInhouseAddress().add(new InhouseAddress("DEV", "D-2"));

    doReturn(helper.getJPAEntityType("Persons")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Person.class, id);

    data.put("inhouseAddress", null);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNull(((Person) act.modifiedEntity()).getInhouseAddress());
  }

  @Test
  void checkDeleteSimplePrimitivePropertyDeep() throws ODataJPAProcessException, ODataJPAModelException {
    final String id = "1";
    final Organization beforeImage = new Organization(id);
    final PostalAddressData addr = new PostalAddressData();
    final Map<String, Object> addrData = new HashMap<>();
    addr.setPOBox("23145-1235");
    addr.setCityName("Hamburg");
    beforeImage.setAddress(addr);
    beforeImage.setName1("Example Ltd");

    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Organization.class, id);

    data.put("address", addrData);
    addrData.put("pOBox", null);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNull(((Organization) act.modifiedEntity()).getAddress().getPOBox());
    assertEquals("Hamburg", ((Organization) act.modifiedEntity()).getAddress().getCityName());
  }

  @Test
  void checkDeleteEntity() throws ODataJPAProcessException, ODataJPAModelException {
    final String id = "1";
    final Organization beforeImage = new Organization(id);

    doReturn(beforeImage).when(em).find(Organization.class, id);
    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    keys.put("iD", id);
    cut.deleteEntity(requestEntity, em);
    verify(em).remove(beforeImage);
  }

  @Test
  void checkDeleteNoErrorIfEntityDoesNotExists() throws ODataJPAProcessException, ODataJPAModelException {
    final String id = "1";
    final Organization beforeImage = new Organization(id);

    doReturn(null).when(em).find(Organization.class, id);
    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    keys.put("iD", id);
    cut.deleteEntity(requestEntity, em);
    verify(em, times(0)).remove(beforeImage);
  }

  @Test
  void checkPatchOneSimplePrimitiveValue() throws ODataJPAModelException, ODataJPAProcessException {
    final JPAUpdateResult act = updateSimplePrimitiveValue();

    assertFalse(act.wasCreate());
    assertEquals("Example SE", ((Organization) act.modifiedEntity()).getName1());
  }

  @Test
  void checkPatchOneSimpleComplexValue() throws ODataJPAModelException, ODataJPAProcessException {

    final String id = "1";
    final Organization beforeImage = new Organization(id);
    final PostalAddressData beforeAddr = new PostalAddressData();
    final Map<String, Object> changedAddr = new HashMap<>();

    beforeImage.setAddress(beforeAddr);
    beforeAddr.setHouseNumber("45A");
    beforeAddr.setCityName("Test");
    beforeAddr.setPostalCode("12345");

    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Organization.class, id);

    changedAddr.put("houseNumber", "45");
    changedAddr.put("streetName", "Example Street");

    data.put("address", changedAddr);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNotNull(((Organization) act.modifiedEntity()).getAddress());
    final PostalAddressData afterImage = ((Organization) act.modifiedEntity()).getAddress();

    assertEquals("45", afterImage.getHouseNumber());
    assertEquals("Test", afterImage.getCityName());
    assertEquals("12345", afterImage.getPostalCode());
    assertEquals("Example Street", afterImage.getStreetName());
  }

  @Test
  void checkPatchEmptyComplexCollectionProperty() throws ODataJPAProcessException, ODataJPAModelException {

    final String id = "2";
    final Person beforeImage = new Person();
    final List<Map<String, Object>> newInhouseAddresses = new ArrayList<>(2);
    beforeImage.setID(id);
    beforeImage.getInhouseAddress().add(new InhouseAddress("DEV", "D-2"));

    doReturn(helper.getJPAEntityType("Persons")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Person.class, id);

    data.put("inhouseAddress", newInhouseAddresses);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNotNull(((Person) act.modifiedEntity()).getInhouseAddress());
    assertTrue(((Person) act.modifiedEntity()).getInhouseAddress().isEmpty());
  }

  @Test
  void checkPatchComplexCollectionProperty() throws ODataJPAProcessException, ODataJPAModelException {

    final String id = "2";
    final Person beforeImage = new Person();
    final List<Map<String, Object>> newInhouseAddresses = new ArrayList<>(2);
    beforeImage.setID(id);
    beforeImage.getInhouseAddress().add(new InhouseAddress("DEV", "D-2"));

    doReturn(helper.getJPAEntityType("Persons")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Person.class, id);

    final Map<String, Object> addr1 = new HashMap<>();
    addr1.put("taskID", "MAIN");
    addr1.put("floor", "U1");
    newInhouseAddresses.add(addr1);
    final Map<String, Object> addr2 = new HashMap<>();
    addr2.put("taskID", "EDU");
    addr2.put("floor", "E");
    newInhouseAddresses.add(addr2);

    data.put("inhouseAddress", newInhouseAddresses);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.PATCH);

    assertFalse(act.wasCreate());
    assertNotNull(((Person) act.modifiedEntity()).getInhouseAddress());
    final List<InhouseAddress> actInhouseAddrs = ((Person) act.modifiedEntity()).getInhouseAddress();
    assertEquals(2, actInhouseAddrs.size());
    assertTrue(actInhouseAddrs.get(0).getBuilding() == null || actInhouseAddrs.get(0).getBuilding().isEmpty());
    assertTrue(actInhouseAddrs.get(1).getBuilding() == null || actInhouseAddrs.get(1).getBuilding().isEmpty());
  }

  @Test
  void checkPatchOnePrimitiveCollectionValue() throws ODataJPAModelException, ODataJPAProcessException {
    final String id = "1";
    final Organization beforeImage = new Organization(id);
    final List<String> newComments = Arrays.asList("This is a test", "YAT");
    beforeImage.getComment().add("YAC");

    doReturn(helper.getJPAEntityType("Organizations")).when(requestEntity).getEntityType();
    doReturn(beforeImage).when(em).find(Organization.class, id);

    data.put("comment", newComments);
    keys.put("iD", id);

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertFalse(act.wasCreate());
    assertNotNull(((Organization) act.modifiedEntity()).getComment());
    final List<String> actComments = ((Organization) act.modifiedEntity()).getComment();
    assertEquals(2, actComments.size());
    assertTrue(actComments.contains("YAT"));
    assertTrue(actComments.contains("This is a test"));
  }

  @Test
  void checkPatchCreateBindingLinkBetweenTwoEntities() throws ODataJPAModelException,
      ODataJPAProcessException {
    /*
     * URL: ../AdministrativeDivisions(DivisionCode='DE51',CodeID='NUTS2',CodePublisher='Eurostat')
     * Body:
     * "Parent@odata.bind": ["AdministrativeDivisions(DivisionCode='DE5',CodeID='NUTS1',CodePublisher='Eurostat')" ]
     */
    final JPAEntityType et = helper.getJPAEntityType("AdministrativeDivisions");
    final JPAAssociationPath path = et.getAssociationPath("Children");
    final JPARequestLink link = mock(JPARequestLink.class);
    final Map<String, Object> childKeys = new HashMap<>();
    final AdministrativeDivision parent = new AdministrativeDivision(new AdministrativeDivisionKey("Eurostat", "NUTS1",
        "DE5"));
    final AdministrativeDivision child = new AdministrativeDivision(new AdministrativeDivisionKey("Eurostat", "NUTS2",
        "DE51"));

    doReturn(et).when(requestEntity).getEntityType();
    keys.put("divisionCode", "DE5");
    keys.put("codeID", "NUTS1");
    keys.put("codePublisher", "Eurostat");

    relationLinks.put(path, Arrays.asList(link));
    doReturn(et).when(link).getEntityType();
    doReturn(childKeys).when(link).getRelatedKeys();
    childKeys.put("divisionCode", "DE51");
    childKeys.put("codeID", "NUTS2");
    childKeys.put("codePublisher", "Eurostat");
    doReturn(childKeys).when(link).getValues();

    doReturn(parent).when(em).find(et.getTypeClass(), parent.getKey());
    doReturn(child).when(em).find(et.getTypeClass(), child.getKey());

    final JPAUpdateResult act = cut.updateEntity(requestEntity, em, HttpMethod.DELETE);

    assertNotNull(act);
    assertEquals(parent, act.modifiedEntity());
    assertEquals(1, parent.getChildren().size());
    assertEquals("DE51", parent.getChildren().get(0).getDivisionCode());
  }

  @Test
  void checkAuditFieldsSetOnCreate() throws ODataJPAModelException, ODataJPAProcessException {
    final OrganizationWithAudit act = createOrganization();
    cut.validateChanges(em);
    assertNotNull(act.getCreatedBy());
    assertNotNull(act.getCreatedAt());
    assertEquals(act.getCreatedBy(), act.getUpdatedBy());
    assertEquals(act.getCreatedAt(), act.getUpdatedAt());
  }

  @Test
  void checkAuditFieldsSetOnUpdate() throws ODataJPAModelException, ODataJPAProcessException {
    final OrganizationWithAudit act = (OrganizationWithAudit) updateOrganization().modifiedEntity();
    cut.validateChanges(em);
    assertNotNull(act.getUpdatedBy());
    assertNotNull(act.getUpdatedAt());
    assertNotEquals(act.getCreatedBy(), act.getUpdatedBy());
    assertNotEquals(act.getCreatedAt(), act.getUpdatedAt());
  }

  @Test
  void checkAuthorizationsCreateRejectsNotClaimsNotAllowed() {
    final JPAExampleModifyException act = assertThrows(JPAExampleModifyException.class,
        () -> createPersonProtected(null));
    assertEquals(HttpStatusCode.FORBIDDEN.getStatusCode(), act.getStatusCode());
  }

  @Test
  void checkAuthorizationsCreateRejectsAttributeNotPresent() {
    final JPAODataClaimProvider claims = mock(JPAODataClaimProvider.class);
    final JPAExampleModifyException act = assertThrows(JPAExampleModifyException.class, () -> createPersonProtected(
        claims));
    assertEquals(HttpStatusCode.FORBIDDEN.getStatusCode(), act.getStatusCode());
  }

  @Test
  void checkAuthorizationsCreateRejectsAttributeNotMatch() {
    final JPAClaimsPair<String> claim = new JPAClaimsPair<>("DOT01");
    final JPAODataClaimProvider claims = mock(JPAODataClaimProvider.class);
    when(claims.get("BuildingNumber")).thenReturn(singletonList(claim));
    final JPAExampleModifyException act = assertThrows(JPAExampleModifyException.class,
        () -> createPersonProtected(claims));
    assertEquals(HttpStatusCode.FORBIDDEN.getStatusCode(), act.getStatusCode());
  }

  @Test
  void checkAuthorizationsCreateRejectedOnlyOneProvided() {
    final JPAClaimsPair<String> claim = new JPAClaimsPair<>("MID*");
