package eu.reply.lea.services.yms.task.queue;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import javax.persistence.EntityManager;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import eu.reply.lea.services.masterdata.reference.LocationReference;
import eu.reply.lea.services.masterdata.reference.SiteReference;
import eu.reply.lea.services.yms.task.queue.event.ElementDequeuedEvent;
import eu.reply.lea.services.yms.task.queue.event.ElementEnqueuedEvent;
import eu.reply.sce.backend.core.entity.EntityReference;
import eu.reply.sce.backend.core.jee.event.Producer;

public class QueueTest {

    private ConcreteTestQueue testQueue;

    @Mock
    private EntityManager entityManager;

    @Mock
    private Producer<ElementEnqueuedEvent> elementEnqueuedEventProducer;
    @Mock
    private Producer<ElementDequeuedEvent> elementDequeuedEventProducer;

    private EntityReference externalEntityRef;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        externalEntityRef = new EntityReference("yms", "transit", "01");
        EntityReference executionArea = new EntityReference("yms", "ExecutionArea", "01");
        EntityReference waitingArea = new EntityReference("yms", "WaitingArea", "01");
        LocationReference locationRef = new LocationReference(new EntityReference("yms", "Location", "01"));
        SiteReference siteReference = new SiteReference("001", "Site01");
        testQueue = new ConcreteTestQueue(executionArea, waitingArea, locationRef, siteReference);
    }


    @Test
    public void testQueueSuccessfully() {
        boolean scheduled = false;
        QueuedElement queuedElementMock = new QueuedElementMock(791281L, externalEntityRef, scheduled, testQueue.toEntityReference(),
                                                                    testQueue.getExecutionLocationReference());
        testQueue= spy(testQueue);
        when(testQueue.createQueuedElement(any(), anyBoolean()))
            .thenReturn(queuedElementMock);
        QueuedElement result = testQueue.queue(externalEntityRef, entityManager, scheduled, elementEnqueuedEventProducer);
        assertNotNull(result);
        assertEquals(1, testQueue.getQueuedElementsCount());
        assertEquals(result, testQueue.getQueuedElements().stream().findAny().orElseThrow());
        verify(entityManager).persist(any(QueuedElement.class));
        verify(elementEnqueuedEventProducer).fire(any(ElementEnqueuedEvent.class));
    }

    @Test
    public void testDequeueSuccessfully() {
        boolean scheduled = false;
        // Preparing the queue with an element
        QueuedElement queuedElementMock = new QueuedElementMock(791281L, externalEntityRef, scheduled, testQueue.toEntityReference(),
                                                                testQueue.getExecutionLocationReference());
        testQueue= spy(testQueue);
        when(testQueue.createQueuedElement(any(), anyBoolean()))
            .thenReturn(queuedElementMock);
        QueuedElement element = testQueue.queue(externalEntityRef, entityManager, scheduled,elementEnqueuedEventProducer);
        assertEquals(element, queuedElementMock);

        reset(entityManager); // Resetting the entityManager mock to clear interactions
        assertEquals(1, testQueue.getQueuedElementsCount());
        assertTrue(queuedElementMock.getIsInsideTheQueue());

        boolean result = testQueue.dequeue(queuedElementMock, elementDequeuedEventProducer);

        assertTrue(result);
        assertFalse(queuedElementMock.getIsInsideTheQueue());
        assertEquals(0, testQueue.getQueuedElementsCount());
        verify(elementDequeuedEventProducer).fire(any(ElementDequeuedEvent.class));
    }

    @Test
    public void testQueueWithDuplicateElement() {
        boolean scheduled = false;
        QueuedElement queuedElementMock = new QueuedElementMock(791281L, externalEntityRef, scheduled, testQueue.toEntityReference(),
                                                                testQueue.getExecutionLocationReference());
        testQueue= spy(testQueue);
        when(testQueue.createQueuedElement(externalEntityRef, scheduled))
            .thenReturn(queuedElementMock);
        // Queue the same element twice
        QueuedElement element = testQueue.queue(externalEntityRef, entityManager, scheduled, elementEnqueuedEventProducer);
        QueuedElement element2 = testQueue.queue(externalEntityRef, entityManager, scheduled, elementEnqueuedEventProducer);

        assertNull(element2);
        assertEquals(1, testQueue.getQueuedElementsCount()); // Ensure it didn't increase
        verify(entityManager, times(1)).persist(any(QueuedElement.class)); // Only persisted once
        verify(elementEnqueuedEventProducer, times(1)).fire(any(ElementEnqueuedEvent.class)); // Only fired once
    }

    @Test
    public void testDequeueNonexistentElement() {
        boolean scheduled = false;
        EntityReference externalEntityReference2 = new EntityReference("yms", "transit", "02");
        // Preparing the queue with an element
        QueuedElement queuedElementMock = new QueuedElementMock(791281L, externalEntityRef, scheduled, testQueue.toEntityReference(),
                                                                testQueue.getExecutionLocationReference());
        QueuedElement queuedElementMock2 = new QueuedElementMock(791281L, externalEntityReference2, scheduled, testQueue.toEntityReference(),
                                                                testQueue.getExecutionLocationReference());
        testQueue= spy(testQueue);
        when(testQueue.createQueuedElement(eq(externalEntityRef), anyBoolean()))
            .thenReturn(queuedElementMock);

        when(testQueue.createQueuedElement(eq(externalEntityReference2), anyBoolean()))
            .thenReturn(queuedElementMock2);

        QueuedElement element = testQueue.queue(externalEntityRef, entityManager, scheduled, elementEnqueuedEventProducer);

        assertNotNull(element);

        boolean result = testQueue.dequeue(queuedElementMock2, elementDequeuedEventProducer);

        assertFalse(result);
        assertEquals(1, testQueue.getQueuedElementsCount());
        verify(elementDequeuedEventProducer, never()).fire(any(ElementDequeuedEvent.class));
    }
}
package eu.reply.lea.services.yms.task.queue;

import static eu.reply.lea.services.yms.ModuleConfiguration.MODULE_NAME;

import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;

import org.hibernate.annotations.SortNatural;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import eu.reply.lea.services.masterdata.reference.LocationReference;
import eu.reply.lea.services.masterdata.reference.SiteReference;
import eu.reply.lea.services.yms.task.queue.event.ElementDequeuedEvent;
import eu.reply.lea.services.yms.task.queue.event.ElementEnqueuedEvent;
import eu.reply.sce.backend.core.entity.EntityReference;
import eu.reply.sce.backend.core.entity.StandardEntity;
import eu.reply.sce.backend.core.entity.configuration.BusinessKey;
import eu.reply.sce.backend.core.jee.event.Producer;

@Getter
@Entity
@DiscriminatorColumn(name = "LOC_TYPE")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@NamedQueries(value = {
    @NamedQuery(name = Queue.Q_FIND_BY_LOCATION_REFERENCE, query =
        "select q from Queue q where q.executionLocationReference.entityId = :locationReferenceId "
        + "and q.executionLocationReference.entityName = :locationReferenceEntityName "
        + "and q.executionLocationReference.module = :locationReferenceModule"),
    @NamedQuery(name = Queue.Q_FIND_BY_WAITING_AREA, query =
        "select q from Queue q where q.waitingArea.entityId = :waitingAreaReferenceId "
        + "and q.waitingArea.entityName = :waitingAreaReferenceEntityName "
        + "and q.waitingArea.module = :waitingAreaReferenceModule"),
})
@Table(indexes = {
    @Index(columnList = "waitingArea_entityId,waitingArea_entityName,waitingArea_module"),
    @Index(columnList = "executionArea_entityId,executionArea_entityName,executionArea_module"),
    @Index(columnList = "site_entityId,site_entityName,site_module"),
    },
    uniqueConstraints = @UniqueConstraint(
        columnNames = {"executionArea_entityId", "executionArea_entityName", "executionArea_module"}))
@BusinessKey(attributes = {"executionLocationReference.businessKey"})
public abstract class Queue extends StandardEntity {

    public static final String Q_FIND_BY_LOCATION_REFERENCE = "AbstractQueue.Q_FIND_BY_LOCATION_REFERENCE";
    public static final String Q_FIND_BY_WAITING_AREA = "AbstractQueue.Q_FIND_BY_WAITING_AREA";
    private static final long serialVersionUID = 2213504911524892516L;


    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "entityId", column = @Column(name = "waitingArea_entityId")),
        @AttributeOverride(name = "entityName", column = @Column(name = "waitingArea_entityName")),
        @AttributeOverride(name = "module", column = @Column(name = "waitingArea_module")),
        @AttributeOverride(name = "usageType", column = @Column(name = "waitingArea_usageType")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "waitingArea_businessKey"))
    })
    @Getter
    @Setter(AccessLevel.PRIVATE)
    private EntityReference waitingArea;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "entityId", column = @Column(name = "executionArea_entityId")),
        @AttributeOverride(name = "entityName", column = @Column(name = "executionArea_entityName")),
        @AttributeOverride(name = "module", column = @Column(name = "executionArea_module")),
        @AttributeOverride(name = "usageType", column = @Column(name = "executionArea_usageType")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "executionArea_businessKey"))
    })
    @Getter
    @Setter(AccessLevel.PRIVATE)
    private EntityReference executionArea;


    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "entityId", column = @Column(name = "execution_location_entityId")),
        @AttributeOverride(name = "entityName", column = @Column(name = "execution_location_entityName")),
        @AttributeOverride(name = "module", column = @Column(name = "execution_location_module")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "execution_location_businessKey"))
    })
    @Getter
    @Setter
    private LocationReference executionLocationReference;

    @NotNull
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "entityName", column = @Column(name = "site_entityName")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "site_businesskey")),
        @AttributeOverride(name = "module", column = @Column(name = "site_module", length = 20)),
        @AttributeOverride(name = "entityId", column = @Column(name = "site_entityId", length = 50)) })
    @Getter
    @Setter(AccessLevel.PRIVATE)
    private SiteReference site;

    private int queuedElementsCount = 0;


    @OneToMany(fetch = FetchType.LAZY)
    @SortNatural
    private final SortedSet<QueuedElement> queuedElements = new TreeSet<>();


    protected Queue() {
        super();

    }

    protected Queue(final EntityReference executionArea, final EntityReference waitingArea, final LocationReference locationRef,
                    final SiteReference siteReference) {
        super();
        this.setExecutionArea(executionArea);
        this.setWaitingArea(waitingArea);
        this.setExecutionLocationReference(locationRef);
        this.setSite(siteReference);
    }

    public int getQueuedElementsCount() {
        return queuedElementsCount;
    }

    private void setQueuedElementsCount(final int queuedElementsCount) {
        this.queuedElementsCount = queuedElementsCount;
    }

    /**
     * Returns an unmodifiable and sorted view of the set of queued elements.
     * <p>
     * This method provides access to the current sorted set of {@link QueuedElement} instances,
     * ensuring that the returned set cannot be modified by the caller. Any attempt to
     * modify the returned set will result in an {@link UnsupportedOperationException}.
     * </p>
     *
     * @return an unmodifiable and sorted set of {@link QueuedElement} instances
     */
    public SortedSet<QueuedElement> getQueuedElements() {
        return Collections.unmodifiableSortedSet(queuedElements);
    }

    @JsonIgnore
    public QueuedElement queue(final EntityReference externalEntityReference, EntityManager entityManager, boolean scheduled,
                         final Producer<ElementEnqueuedEvent> elementEnqueuedEventProducer) {
        QueuedElement queuedElement = this.createQueuedElement(externalEntityReference, scheduled);
        if (this.queuedElements.add(queuedElement)) {
            entityManager.persist(queuedElement);
            this.setQueuedElementsCount(this.getQueuedElements().size());
            elementEnqueuedEventProducer.fire(new ElementEnqueuedEvent(
                this.toEntityReference(),
                this.getWaitingArea(),
                this.getExecutionArea(),
                new QueuedElementReference(queuedElement)
            ));
            return queuedElement;
        }
        return null;
    }

    //This method is useful for test purposes
    QueuedElement createQueuedElement(final EntityReference externalEntityReference, boolean scheduled){
        return new QueuedElement(externalEntityReference, scheduled, this.toEntityReference(),
                          this.getExecutionLocationReference());
    }

    @JsonIgnore
    public boolean dequeue(final QueuedElement queuedElement,
                           final Producer<ElementDequeuedEvent> elementDequeuedEventProducer) {
        if (this.queuedElements.remove(queuedElement)) {
            this.setQueuedElementsCount(this.getQueuedElements().size());
            elementDequeuedEventProducer.fire(new ElementDequeuedEvent(
                this.toEntityReference(),
                this.getWaitingArea(),
                this.getExecutionArea(),
                this.getExecutionLocationReference(),
                new QueuedElementReference(queuedElement),
                queuedElement.getExternalEntityRef()));
                queuedElement.removeFromQueue();
            return true;
        }
        return false;
    }

    public boolean isInQueue(final EntityReference externalEntityReference){
        QueuedElement queuedElement = this.createQueuedElement(externalEntityReference,false);
        return this.queuedElements.contains(queuedElement);
    }

    public EntityReference toEntityReference(){
        return new EntityReference(MODULE_NAME, getClass().getSimpleName(), String.valueOf(this.getId()), null, this.getExecutionLocationReference().getBusinessKey());
    }
}
package eu.reply.lea.services.yms.task.queue;



import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.NotNull;
import java.util.Calendar;

import eu.reply.lea.services.masterdata.reference.LocationReference;
import eu.reply.sce.backend.core.entity.EntityReference;
import eu.reply.sce.backend.core.entity.StandardEntity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@NoArgsConstructor
@NamedQuery(name = QueuedElement.Q_FIND_BY_EXTERNAL_ENTITY_AND_EXECUTION_LOCATION_AND_IS_INSIDE_THE_QUEUE,
    query = "select queuedElement from QueuedElement queuedElement where "
            + "queuedElement.externalEntityRef.entityId = :externalEntityId "
            + "and queuedElement.externalEntityRef.entityName = :externalEntityName "
            + "and queuedElement.externalEntityRef.module = :externalEntityModule "
            + "and queuedElement.executionLocationReference.entityId = :locationReferenceId "
            + "and queuedElement.executionLocationReference.entityName = :locationReferenceEntityName "
            + "and queuedElement.executionLocationReference.module = :locationReferenceModule "
            + "and queuedElement.isInsideTheQueue = :isInsideTheQueue")
@Table(indexes = {
    @Index(name = "idx_queued_element_external_entity", columnList =
        "externalEntityRef_entity_id, externalEntityRef_entity_name, externalEntityRef_module"),
    @Index(name = "idx_queued_element_execution_location", columnList =
        "locationReference_entity_id, locationReference_entity_name, locationReference_module"),
    @Index(name = "idx_queued_element_is_inside_queue", columnList =
        "isInsideTheQueue")

})
public class QueuedElement extends StandardEntity implements Comparable<QueuedElement> {

    public static final String Q_FIND_BY_EXTERNAL_ENTITY_AND_EXECUTION_LOCATION_AND_IS_INSIDE_THE_QUEUE = "QueuedElement.Q_FIND_BY_EXTERNAL_ENTITY_AND_EXECUTION_LOCATION_AND_IS_INSIDE_THE_QUEUE";

    @Getter
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "module", column = @Column(name = "externalEntityRef_module")),
        @AttributeOverride(name = "entityName", column = @Column(name = "externalEntityRef_entity_name")),
        @AttributeOverride(name = "entityId", column = @Column(name = "externalEntityRef_entity_id")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "externalEntityRef_business_key")),
        @AttributeOverride(name = "usageType", column = @Column(name = "externalEntityRef_usage_type"))
    })
    private EntityReference externalEntityRef;

    @Setter(AccessLevel.PRIVATE)
    @Getter
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "module", column = @Column(name = "locationReference_module")),
        @AttributeOverride(name = "entityName", column = @Column(name = "locationReference_entity_name")),
        @AttributeOverride(name = "entityId", column = @Column(name = "locationReference_entity_id")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "locationReference_business_key")),
    })
    private LocationReference lastLocationReference;

    @Setter(AccessLevel.PRIVATE)
    @Getter
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "module", column = @Column(name = "executionLocationReference_module")),
        @AttributeOverride(name = "entityName", column = @Column(name = "executionLocationReference_entity_name")),
        @AttributeOverride(name = "entityId", column = @Column(name = "executionLocationReference_entity_id")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "executionLocationReference_business_key")),
    })
    private LocationReference executionLocationReference;
    @Getter
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "module", column = @Column(name = "queue_module")),
        @AttributeOverride(name = "entityName", column = @Column(name = "queue_entity_name")),
        @AttributeOverride(name = "entityId", column = @Column(name = "queue_entity_id")),
        @AttributeOverride(name = "businessKey", column = @Column(name = "queue_business_key")),
        @AttributeOverride(name = "usageType", column = @Column(name = "queue_usage_type"))
    })
    private EntityReference queue;

    @Getter
    private AreaType areaType=AreaType.BOOKING_AREA;
    @Setter
    @Getter
    private int priority=1;
    @Getter
    private boolean scheduled;
    @Setter
    @Getter
    @Temporal(TemporalType.TIMESTAMP)
    @NotNull
    private Calendar enqueuedDate;

    @Getter
    @Setter(AccessLevel.PRIVATE)
    private Boolean isInsideTheQueue;

    public QueuedElement(final EntityReference externalEntityRef, final boolean scheduled, final EntityReference queue, final LocationReference executionLocationReference){
        this.externalEntityRef = externalEntityRef;
        this.scheduled = scheduled;
        this.setQueue(queue, executionLocationReference);
        this.isInsideTheQueue = true;
    }

    private void setQueue(final EntityReference queue, final LocationReference executionLocationReference) {
        if (queue == null){
            throw new IllegalArgumentException("Queue can't be null");
        }
        this.queue = queue;
        this.executionLocationReference = executionLocationReference;
        setEnqueuedDate(Calendar.getInstance());
    }


    public void setInExecutionArea(final ExecutionArea executionArea) {
        LocationReference locationReference = executionArea.getLocationReference();
        if(locationReference.equals(this.getLastLocationReference())){
            return;
        }
        this.setAreaType(AreaType.EXECUTION_AREA);
        this.setLastLocationReference(locationReference);
    }

    public void setInWaitingArea(final WaitingArea waitingArea) {
        LocationReference locationReference = waitingArea.getLocationReference();
        if(locationReference.equals(this.getLastLocationReference())){
            return;
        }
        this.setAreaType(AreaType.WAITING_AREA);
        this.setLastLocationReference(locationReference);
    }

    private void setAreaType(final AreaType areaType) {
        this.areaType = areaType;
    }


    @JsonIgnore
    public boolean isInExecutionArea() {
        return this.getAreaType().isExecutionArea();
    }

    @JsonIgnore
    public boolean isInWaitingArea() {
        return this.getAreaType().isWaitingArea();
    }

    @Override
    public int compareTo(QueuedElement other) {
        // Ensure "other" is not null to prevent NullPointerException
        if (other == null) throw new NullPointerException("Cannot compare to null");
        if (this.equals(other)){
            return 0;
        }

        // Compare priorities; here, lower numbers are considered "higher priority"
        int priorityComparison = Integer.compare(this.priority, other.priority);
        if (priorityComparison != 0) {
            return priorityComparison; // Return the opposite of the natural order to treat lower numbers as higher priority
        }
        if (this.scheduled != other.scheduled) {
            return this.scheduled ? -1 : 1; // scheduled (true) comes before non-scheduled (false)
        }

        int dateComparison = this.enqueuedDate.compareTo(other.enqueuedDate);
        if (dateComparison!=0){
            return dateComparison;
        }
        return Integer.compare(this.hashCode(), other.hashCode());
    }


    public QueuedElementReference toEntityReference(){
        return new QueuedElementReference(this);
    }

    @Override
    public boolean equals(Object anObject) {
        boolean equalObjects = false;

        if (anObject != null && this.getClass() == anObject.getClass()) {
            QueuedElement typedObject = (QueuedElement) anObject;
            equalObjects =
                this.getExternalEntityRef().equals(typedObject.getExternalEntityRef()) &&
                this.getQueue().equals(typedObject.getQueue()) &&
                this.getIsInsideTheQueue().equals(typedObject.getIsInsideTheQueue());
        }

        return equalObjects;
    }

    @Override
    public int hashCode() {
        int hashCodeValue =
            + (63815 * 59)
            + this.getExternalEntityRef().hashCode()
            + this.getQueue().hashCode()
            + this.getIsInsideTheQueue().hashCode();

        return hashCodeValue;
    }

    public void removeFromQueue(){
        // This method changes the 'active' field, which is used in both the 'equals' and 'hashCode' methods.
        // If this object is part of a Set or used as a key in a Map, ensure to update the collection accordingly
        // to avoid inconsistencies due to the change in hash code or equality status.
        this.isInsideTheQueue = false;
    }


}

Add test for the method isInQueue