From f2ba5dbdc859e73afc780108a54cd2e8fde88fad Mon Sep 17 00:00:00 2001 From: Ujjawal Prabhat Date: Mon, 25 May 2026 15:31:25 +0800 Subject: [PATCH 1/3] O3-5692: Add lightweight queue-entry-summary REST endpoint --- .../module/queue/api/QueueEntryService.java | 16 +++ .../module/queue/api/dao/QueueEntryDao.java | 16 +++ .../queue/api/dao/impl/QueueEntryDaoImpl.java | 116 ++++++++++++++++++ .../queue/api/impl/QueueEntryServiceImpl.java | 14 +++ .../queue/api/dao/QueueEntryDaoTest.java | 50 ++++++++ .../web/QueueEntrySummaryRestController.java | 97 +++++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java diff --git a/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java b/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java index b28a818..90164e9 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java +++ b/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java @@ -11,8 +11,10 @@ import javax.validation.constraints.NotNull; +import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Optional; import org.openmrs.Location; @@ -129,6 +131,20 @@ public interface QueueEntryService { @Authorized({ PrivilegeConstants.GET_QUEUE_ENTRIES }) List getQueueEntries(@NotNull QueueEntrySearchCriteria searchCriteria); + /** + * Paginated variant of {@link #getQueueEntries(QueueEntrySearchCriteria)}. Nulls disable the + * corresponding pagination bound. + */ + @Authorized({ PrivilegeConstants.GET_QUEUE_ENTRIES }) + List getQueueEntries(@NotNull QueueEntrySearchCriteria searchCriteria, Integer startIndex, Integer limit); + + /** + * Batch-resolves the uuid of the previous queue entry for each input entry that has a non-null + * {@code queueComingFrom}. Entries without a resolvable predecessor are absent from the result. + */ + @Authorized({ PrivilegeConstants.GET_QUEUE_ENTRIES }) + Map getPreviousQueueEntryUuids(Collection entries); + /** * @return {@link Long} count of queue entries that match the given * %{@link QueueEntrySearchCriteria} diff --git a/api/src/main/java/org/openmrs/module/queue/api/dao/QueueEntryDao.java b/api/src/main/java/org/openmrs/module/queue/api/dao/QueueEntryDao.java index 9188fc6..04253b9 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/dao/QueueEntryDao.java +++ b/api/src/main/java/org/openmrs/module/queue/api/dao/QueueEntryDao.java @@ -11,8 +11,10 @@ import javax.validation.constraints.NotNull; +import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Map; import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria; import org.openmrs.module.queue.model.QueueEntry; @@ -24,6 +26,20 @@ public interface QueueEntryDao extends BaseQueueDao { */ List getQueueEntries(@NotNull QueueEntrySearchCriteria searchCriteria); + /** + * Paginated variant of {@link #getQueueEntries(QueueEntrySearchCriteria)}. Nulls disable the + * corresponding pagination bound. + */ + List getQueueEntries(@NotNull QueueEntrySearchCriteria searchCriteria, Integer startIndex, Integer limit); + + /** + * Batch-resolves the uuid of the previous queue entry for each of the given entries (i.e. the entry + * the patient was transitioned from, identified by matching patient + visit, queue = + * input.queueComingFrom, endedAt = input.startedAt). Entries with no {@code queueComingFrom} are + * absent from the returned map. + */ + Map getPreviousQueueEntryUuids(Collection entries); + /** * @return {@link Long} of the number of queue entries that match the given * %{@link QueueEntrySearchCriteria} diff --git a/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java b/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java index 0b35577..d852088 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java +++ b/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java @@ -16,8 +16,14 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import org.hibernate.Criteria; import org.hibernate.Session; @@ -26,6 +32,7 @@ import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.openmrs.Patient; +import org.openmrs.Visit; import org.openmrs.module.queue.api.dao.QueueEntryDao; import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria; import org.openmrs.module.queue.model.Queue; @@ -41,14 +48,87 @@ public QueueEntryDaoImpl(@Qualifier("sessionFactory") SessionFactory sessionFact @Override public List getQueueEntries(QueueEntrySearchCriteria searchCriteria) { + return getQueueEntries(searchCriteria, null, null); + } + + @Override + public List getQueueEntries(QueueEntrySearchCriteria searchCriteria, Integer startIndex, Integer limit) { Criteria c = createCriteriaFromSearchCriteria(searchCriteria); c.addOrder(Order.desc("qe.sortWeight")); c.addOrder(Order.asc("qe.startedAt")); c.addOrder(Order.asc("qe.dateCreated")); c.addOrder(Order.asc("qe.queueEntryId")); + if (startIndex != null) { + c.setFirstResult(startIndex); + } + if (limit != null) { + c.setMaxResults(limit); + } return c.list(); } + @Override + public Map getPreviousQueueEntryUuids(Collection entries) { + if (entries == null || entries.isEmpty()) { + return Collections.emptyMap(); + } + + List candidates = new ArrayList<>(); + Set queuesComingFrom = new HashSet<>(); + Set patients = new HashSet<>(); + Set visits = new HashSet<>(); + Set startedAts = new HashSet<>(); + for (QueueEntry e : entries) { + if (e.getQueueComingFrom() == null || e.getPatient() == null || e.getStartedAt() == null) { + continue; + } + candidates.add(e); + queuesComingFrom.add(e.getQueueComingFrom()); + patients.add(e.getPatient()); + startedAts.add(e.getStartedAt()); + if (e.getVisit() != null) { + visits.add(e.getVisit()); + } + } + if (candidates.isEmpty()) { + return Collections.emptyMap(); + } + + // Filter via IN clauses; post-match the composite tuple in Java to avoid an OR-AND explosion. + StringBuilder hql = new StringBuilder(); + hql.append("FROM QueueEntry prev WHERE prev.voided = false "); + hql.append("AND prev.endedAt IN (:startedAts) "); + hql.append("AND prev.patient IN (:patients) "); + hql.append("AND prev.queue IN (:queuesComingFrom)"); + if (!visits.isEmpty()) { + hql.append(" AND (prev.visit IS NULL OR prev.visit IN (:visits))"); + } + + javax.persistence.Query query = getCurrentSession().createQuery(hql.toString()); + query.setParameter("startedAts", startedAts); + query.setParameter("patients", patients); + query.setParameter("queuesComingFrom", queuesComingFrom); + if (!visits.isEmpty()) { + query.setParameter("visits", visits); + } + List candidatesFromDb = query.getResultList(); + + Map index = new HashMap<>(); + for (QueueEntry prev : candidatesFromDb) { + index.put(new PrevKey(prev.getPatient(), prev.getVisit(), prev.getQueue(), prev.getEndedAt()), prev); + } + + Map result = new LinkedHashMap<>(); + for (QueueEntry curr : candidates) { + QueueEntry prev = index + .get(new PrevKey(curr.getPatient(), curr.getVisit(), curr.getQueueComingFrom(), curr.getStartedAt())); + if (prev != null) { + result.put(curr, prev.getUuid()); + } + } + return result; + } + @Override public Long getCountOfQueueEntries(QueueEntrySearchCriteria searchCriteria) { Criteria criteria = createCriteriaFromSearchCriteria(searchCriteria); @@ -176,4 +256,40 @@ private Criteria createCriteriaFromSearchCriteria(QueueEntrySearchCriteria searc } return c; } + + private static final class PrevKey { + + private final Patient patient; + + private final Visit visit; + + private final Queue queue; + + private final Date endedAt; + + PrevKey(Patient patient, Visit visit, Queue queue, Date endedAt) { + this.patient = patient; + this.visit = visit; + this.queue = queue; + this.endedAt = endedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PrevKey)) { + return false; + } + PrevKey k = (PrevKey) o; + return java.util.Objects.equals(patient, k.patient) && java.util.Objects.equals(visit, k.visit) + && java.util.Objects.equals(queue, k.queue) && java.util.Objects.equals(endedAt, k.endedAt); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(patient, visit, queue, endedAt); + } + } } diff --git a/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java b/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java index 15c1c47..abb326a 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java @@ -17,9 +17,11 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Optional; import lombok.Setter; @@ -215,6 +217,18 @@ public List getQueueEntries(QueueEntrySearchCriteria searchCriteria) return dao.getQueueEntries(searchCriteria); } + @Override + @Transactional(readOnly = true) + public List getQueueEntries(QueueEntrySearchCriteria searchCriteria, Integer startIndex, Integer limit) { + return dao.getQueueEntries(searchCriteria, startIndex, limit); + } + + @Override + @Transactional(readOnly = true) + public Map getPreviousQueueEntryUuids(Collection entries) { + return dao.getPreviousQueueEntryUuids(entries); + } + @Override @Transactional(readOnly = true) public Long getCountOfQueueEntries(QueueEntrySearchCriteria searchCriteria) { diff --git a/integration-tests/src/test/java/org/openmrs/module/queue/api/dao/QueueEntryDaoTest.java b/integration-tests/src/test/java/org/openmrs/module/queue/api/dao/QueueEntryDaoTest.java index 0672298..7f55af6 100644 --- a/integration-tests/src/test/java/org/openmrs/module/queue/api/dao/QueueEntryDaoTest.java +++ b/integration-tests/src/test/java/org/openmrs/module/queue/api/dao/QueueEntryDaoTest.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.Before; @@ -486,6 +487,55 @@ public void getOverlappingQueueEntries_shouldReturnEntriesOverlappingWithGivenRa assertThat(noOverlapResults, hasSize(0)); } + @Test + public void getQueueEntries_shouldRespectStartIndexAndLimit() { + // Full result set (active only) is 4 entries, sorted by sortWeight DESC then startedAt ASC: + // → ids [3 (sw=20), 2 (sw=10), 1 (sw=0, started earlier), 4 (sw=0, started later)] + List all = dao.getQueueEntries(criteria); + assertThat(all, hasSize(4)); + + List firstPage = dao.getQueueEntries(criteria, 0, 2); + assertThat(firstPage, hasSize(2)); + assertThat(firstPage.get(0).getQueueEntryId(), is(all.get(0).getQueueEntryId())); + assertThat(firstPage.get(1).getQueueEntryId(), is(all.get(1).getQueueEntryId())); + + List secondPage = dao.getQueueEntries(criteria, 2, 2); + assertThat(secondPage, hasSize(2)); + assertThat(secondPage.get(0).getQueueEntryId(), is(all.get(2).getQueueEntryId())); + assertThat(secondPage.get(1).getQueueEntryId(), is(all.get(3).getQueueEntryId())); + + // Past the end → empty + assertThat(dao.getQueueEntries(criteria, 10, 5), hasSize(0)); + + // Nulls disable bounds — equivalent to the unpaginated call + assertThat(dao.getQueueEntries(criteria, null, null), hasSize(4)); + } + + @Test + public void getPreviousQueueEntryUuids_shouldReturnEmptyForNullOrEmptyInput() { + assertThat(dao.getPreviousQueueEntryUuids(null).isEmpty(), is(true)); + assertThat(dao.getPreviousQueueEntryUuids(Collections.emptyList()).isEmpty(), is(true)); + } + + @Test + public void getPreviousQueueEntryUuids_shouldReturnUuidForEntriesWithMatchingPredecessorOnly() { + // In the dataset, entry 2 was transitioned from queue 1 — entry 1 is its predecessor + // (same patient=100, same visit=101, prev.endedAt = entry2.startedAt). + QueueEntry entry1 = dao.get(1).orElseThrow(IllegalStateException::new); + QueueEntry entry2 = dao.get(2).orElseThrow(IllegalStateException::new); + QueueEntry entry3 = dao.get(3).orElseThrow(IllegalStateException::new); + QueueEntry entry4 = dao.get(4).orElseThrow(IllegalStateException::new); + + Map result = dao.getPreviousQueueEntryUuids(Arrays.asList(entry1, entry2, entry3, entry4)); + + // Only entry 2 has a queueComingFrom that resolves to a real predecessor. + assertThat(result.size(), is(1)); + assertThat(result.get(entry2), is(entry1.getUuid())); + assertThat(result.containsKey(entry1), is(false)); + assertThat(result.containsKey(entry3), is(false)); + assertThat(result.containsKey(entry4), is(false)); + } + /** * Utility method that tests criteria against both DAO methods to getQueueEntries and * getCountOfQueueEntries diff --git a/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java new file mode 100644 index 0000000..c7c5cf3 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java @@ -0,0 +1,97 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.queue.web; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.openmrs.module.queue.api.QueueEntryService; +import org.openmrs.module.queue.api.QueueServicesWrapper; +import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria; +import org.openmrs.module.queue.model.QueueEntry; +import org.openmrs.module.queue.web.resources.parser.QueueEntrySearchCriteriaParser; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.module.webservices.rest.web.ConversionUtil; +import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.RestUtil; +import org.openmrs.module.webservices.rest.web.representation.CustomRepresentation; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; +import org.openmrs.module.webservices.rest.web.v1_0.controller.BaseRestController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Lightweight, flat-shape, server-paginated read endpoint for the Service Queues table. Returns + * only the fields the queue table renders; avoids the deep nested encounter/obs/diagnosis graph + * that the default {@code QueueEntryResource} representation pulls in. Three-layer batch-load + * pattern mirrors openmrs-module-emrapi PR #250. + */ +@Controller +public class QueueEntrySummaryRestController extends BaseRestController { + + private static final String SUMMARY_REPRESENTATION = "uuid,patient:(uuid,display),queue:(uuid,display)," + + "status:(uuid,display),priority:(uuid,display),priorityComment,startedAt,visit:(uuid,startDatetime)"; + + private final QueueServicesWrapper services; + + private final QueueEntrySearchCriteriaParser parser; + + @Autowired + public QueueEntrySummaryRestController(QueueServicesWrapper services, QueueEntrySearchCriteriaParser parser) { + this.services = services; + this.parser = parser; + } + + @RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/queue-entry-summary", method = RequestMethod.GET) + public ResponseEntity getQueueEntrySummaries(HttpServletRequest request, HttpServletResponse response) { + @SuppressWarnings("unchecked") + Map parameterMap = request.getParameterMap(); + QueueEntrySearchCriteria criteria = parser.constructFromRequest(parameterMap); + RequestContext context = RestUtil.getRequestContext(request, response, Representation.DEFAULT); + + QueueEntryService queueEntryService = services.getQueueEntryService(); + List entries = queueEntryService.getQueueEntries(criteria, context.getStartIndex(), context.getLimit()); + Map previousUuids = queueEntryService.getPreviousQueueEntryUuids(entries); + + CustomRepresentation entryRep = new CustomRepresentation(SUMMARY_REPRESENTATION); + List results = new ArrayList<>(entries.size()); + for (QueueEntry entry : entries) { + SimpleObject row = (SimpleObject) ConversionUtil.convertToRepresentation(entry, entryRep); + SimpleObject visit = (SimpleObject) row.remove("visit"); + row.add("visitUuid", visit == null ? null : visit.get("uuid")); + row.add("visitStartDatetime", visit == null ? null : visit.get("startDatetime")); + row.add("previousQueueEntryUuid", previousUuids.get(entry)); + results.add(row); + } + + boolean totalCountRequested = Boolean.valueOf(context.getParameter("totalCount")); + boolean hasMore; + AlreadyPaged paged; + if (totalCountRequested) { + Long totalCount = queueEntryService.getCountOfQueueEntries(criteria); + hasMore = context.getStartIndex() + entries.size() < totalCount; + paged = new AlreadyPaged<>(context, results, hasMore, totalCount); + } else { + hasMore = entries.size() == context.getLimit(); + paged = new AlreadyPaged<>(context, results, hasMore); + } + return new ResponseEntity<>(paged, HttpStatus.OK); + } +} From 0a56195444619b6532eccfcf4ea3c8a9c44cda89 Mon Sep 17 00:00:00 2001 From: Ujjawal Prabhat Date: Mon, 25 May 2026 15:49:34 +0800 Subject: [PATCH 2/3] follow-up improvement --- .../module/queue/web/QueueEntrySummaryRestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java index c7c5cf3..1ce119e 100644 --- a/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java +++ b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java @@ -81,7 +81,7 @@ public ResponseEntity getQueueEntrySummaries(HttpServletRequest request, Http results.add(row); } - boolean totalCountRequested = Boolean.valueOf(context.getParameter("totalCount")); + boolean totalCountRequested = Boolean.parseBoolean(context.getParameter("totalCount")); boolean hasMore; AlreadyPaged paged; if (totalCountRequested) { From 8ca390ef10839b0bee232fafe564f2fcc1fa3383 Mon Sep 17 00:00:00 2001 From: Ujjawal Prabhat Date: Mon, 25 May 2026 19:25:29 +0800 Subject: [PATCH 3/3] fixup --- .../web/QueueEntrySummaryRestController.java | 6 +- .../QueueEntrySummaryRestControllerTest.java | 192 ++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 omod/src/test/java/org/openmrs/module/queue/web/QueueEntrySummaryRestControllerTest.java diff --git a/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java index 1ce119e..1203644 100644 --- a/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java +++ b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntrySummaryRestController.java @@ -46,7 +46,7 @@ @Controller public class QueueEntrySummaryRestController extends BaseRestController { - private static final String SUMMARY_REPRESENTATION = "uuid,patient:(uuid,display),queue:(uuid,display)," + private static final String SUMMARY_REPRESENTATION = "uuid,patient:(uuid,display,patientIdentifier),queue:(uuid,display)," + "status:(uuid,display),priority:(uuid,display),priorityComment,startedAt,visit:(uuid,startDatetime)"; private final QueueServicesWrapper services; @@ -74,6 +74,10 @@ public ResponseEntity getQueueEntrySummaries(HttpServletRequest request, Http List results = new ArrayList<>(entries.size()); for (QueueEntry entry : entries) { SimpleObject row = (SimpleObject) ConversionUtil.convertToRepresentation(entry, entryRep); + SimpleObject patient = (SimpleObject) row.get("patient"); + if (patient != null) { + patient.add("identifier", patient.remove("patientIdentifier")); + } SimpleObject visit = (SimpleObject) row.remove("visit"); row.add("visitUuid", visit == null ? null : visit.get("uuid")); row.add("visitStartDatetime", visit == null ? null : visit.get("startDatetime")); diff --git a/omod/src/test/java/org/openmrs/module/queue/web/QueueEntrySummaryRestControllerTest.java b/omod/src/test/java/org/openmrs/module/queue/web/QueueEntrySummaryRestControllerTest.java new file mode 100644 index 0000000..7a95684 --- /dev/null +++ b/omod/src/test/java/org/openmrs/module/queue/web/QueueEntrySummaryRestControllerTest.java @@ -0,0 +1,192 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.queue.web; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openmrs.api.context.Context; +import org.openmrs.module.queue.api.QueueEntryService; +import org.openmrs.module.queue.api.QueueServicesWrapper; +import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria; +import org.openmrs.module.queue.model.QueueEntry; +import org.openmrs.module.queue.web.resources.parser.QueueEntrySearchCriteriaParser; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.module.webservices.rest.web.ConversionUtil; +import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.RestUtil; +import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; +import org.springframework.http.ResponseEntity; + +@ExtendWith(MockitoExtension.class) +public class QueueEntrySummaryRestControllerTest { + + private QueueEntrySummaryRestController controller; + + @Mock + private QueueServicesWrapper queueServicesWrapper; + + @Mock + private QueueEntryService queueEntryService; + + @Mock + private QueueEntrySearchCriteriaParser parser; + + private MockedStatic restUtil; + + private MockedStatic context; + + private MockedStatic conversionUtil; + + private HttpServletRequest request; + + private HttpServletResponse response; + + private RequestContext requestContext; + + @BeforeEach + public void prepareMocks() { + restUtil = mockStatic(RestUtil.class); + context = mockStatic(Context.class); + conversionUtil = mockStatic(ConversionUtil.class); + + lenient().when(queueServicesWrapper.getQueueEntryService()).thenReturn(queueEntryService); + context.when(Context::isAuthenticated).thenReturn(true); + + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + Map parameterMap = new HashMap<>(); + when(request.getParameterMap()).thenReturn(parameterMap); + when(parser.constructFromRequest(parameterMap)).thenReturn(new QueueEntrySearchCriteria()); + + requestContext = mock(RequestContext.class); + lenient().when(requestContext.getStartIndex()).thenReturn(0); + lenient().when(requestContext.getLimit()).thenReturn(50); + lenient().when(requestContext.getRequest()).thenReturn(request); + restUtil.when(() -> RestUtil.getRequestContext(any(), any(), any())).thenReturn(requestContext); + + controller = new QueueEntrySummaryRestController(queueServicesWrapper, parser); + } + + @AfterEach + public void cleanup() { + restUtil.close(); + context.close(); + conversionUtil.close(); + } + + @Test + public void getQueueEntrySummaries_shouldReturnPaginatedSummariesWithIdentifier() { + QueueEntry entry1 = new QueueEntry(); + QueueEntry entry2 = new QueueEntry(); + when(queueEntryService.getQueueEntries(any(), anyInt(), anyInt())).thenReturn(Arrays.asList(entry1, entry2)); + Map previousUuids = new HashMap<>(); + previousUuids.put(entry2, "prev-uuid"); + when(queueEntryService.getPreviousQueueEntryUuids(any(Collection.class))).thenReturn(previousUuids); + + conversionUtil.when(() -> ConversionUtil.convertToRepresentation(any(QueueEntry.class), any())) + .thenAnswer(invocation -> { + SimpleObject row = new SimpleObject(); + row.add("uuid", "qe-uuid"); + SimpleObject patient = new SimpleObject(); + patient.add("uuid", "patient-uuid"); + patient.add("display", "100GEJ - John Doe"); + patient.add("patientIdentifier", "100GEJ"); + row.add("patient", patient); + SimpleObject visit = new SimpleObject(); + visit.add("uuid", "visit-uuid"); + visit.add("startDatetime", "2026-05-25T08:55:00.000+0000"); + row.add("visit", visit); + return row; + }); + + ResponseEntity resp = controller.getQueueEntrySummaries(request, response); + + AlreadyPaged paged = (AlreadyPaged) resp.getBody(); + assertThat(paged, notNullValue()); + List results = paged.getPageOfResults(); + assertThat(results, hasSize(2)); + + SimpleObject firstRow = (SimpleObject) results.get(0); + assertThat(firstRow.get("uuid"), is("qe-uuid")); + assertThat(firstRow.get("visitUuid"), is("visit-uuid")); + assertThat(firstRow.get("visitStartDatetime"), is("2026-05-25T08:55:00.000+0000")); + assertThat(firstRow.get("visit"), is(nullValue())); + assertThat(firstRow.get("previousQueueEntryUuid"), is(nullValue())); + SimpleObject firstPatient = (SimpleObject) firstRow.get("patient"); + assertThat(firstPatient.get("identifier"), is("100GEJ")); + assertThat(firstPatient.get("patientIdentifier"), is(nullValue())); + assertThat(firstPatient.get("display"), is("100GEJ - John Doe")); + + SimpleObject secondRow = (SimpleObject) results.get(1); + assertThat(secondRow.get("previousQueueEntryUuid"), is("prev-uuid")); + } + + @Test + public void getQueueEntrySummaries_shouldHandleTotalCountRequest() { + when(queueEntryService.getQueueEntries(any(), anyInt(), anyInt())) + .thenReturn(Collections.singletonList(new QueueEntry())); + when(queueEntryService.getPreviousQueueEntryUuids(any(Collection.class))).thenReturn(Collections.emptyMap()); + when(queueEntryService.getCountOfQueueEntries(any())).thenReturn(137L); + when(requestContext.getParameter("totalCount")).thenReturn("true"); + conversionUtil.when(() -> ConversionUtil.convertToRepresentation(any(QueueEntry.class), any())) + .thenReturn(new SimpleObject()); + + ResponseEntity resp = controller.getQueueEntrySummaries(request, response); + + AlreadyPaged paged = (AlreadyPaged) resp.getBody(); + assertThat(paged.getTotalCount(), equalTo(137L)); + assertThat(paged.hasMoreResults(), is(true)); + verify(queueEntryService).getCountOfQueueEntries(any()); + } + + @Test + public void getQueueEntrySummaries_shouldReturnEmptyListWhenNoResults() { + when(queueEntryService.getQueueEntries(any(), anyInt(), anyInt())).thenReturn(Collections.emptyList()); + when(queueEntryService.getPreviousQueueEntryUuids(any(Collection.class))).thenReturn(Collections.emptyMap()); + + ResponseEntity resp = controller.getQueueEntrySummaries(request, response); + + AlreadyPaged paged = (AlreadyPaged) resp.getBody(); + assertThat(paged.getPageOfResults(), is(empty())); + assertThat(paged.hasMoreResults(), is(false)); + conversionUtil.verify(() -> ConversionUtil.convertToRepresentation(any(QueueEntry.class), any()), never()); + } +}