package cn.nianxx.thhotel.workflows.reservation.control;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.matchesPattern;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderVisibility;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest(
        classes = ThHotelApplication.class,
        properties = {
                "spring.datasource.url=jdbc:h2:mem:reservation_frontend_query_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
                "auth.bootstrap.admin.username=frontend-query-admin",
                "auth.bootstrap.admin.password=Admin@123456",
                "auth.bootstrap.admin.display-name=前端查询管理员",
                "auth.bootstrap.default-hotel-id=HOTEL-TEST",
                "auth.bootstrap.default-hotel-name=测试酒店",
                "auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
                "auth.session.ttl-minutes=720"
        })
@AutoConfigureMockMvc
@ActiveProfiles("test")
class ReservationFrontendQueryControllerTest {

    private static final String HOTEL_ID = "HOTEL-TEST";
    private static final String UTC_INSTANT_PATTERN = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$";

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private SourceMessageCaptureService captureService;

    @MockitoSpyBean
    private SourceMessageQueryService sourceMessageQueryService;

    @MockitoSpyBean
    private ReservationAiWorkflowRepository workflowRepository;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private String adminToken;

    /**
     * 获取前端查询管理员 token，测试通过真实登录接口覆盖认证链路。
     */
    private String adminToken() throws Exception {
        if (adminToken == null) {
            adminToken = loginToken(mockMvc, "frontend-query-admin", "Admin@123456");
        }
        return adminToken;
    }

    @Test
    void shouldReturnTaskWorkbenchListWithRealtimeAvailability() throws Exception {
        SourceMessageCaptureResult source = captureSourceMessage(
                "mail-frontend-task-list-001",
                "Frontend Query List Smoke");
        Long orderId = 930000000000000101L;
        Long firstTaskId = 930000000000000301L;
        Long secondTaskId = 930000000000000302L;
        insertActiveGroupOrder(orderId, source.inboxId(), "GRP-FRONTEND-LIST-001");
        insertTransition(930000000000000201L, source.inboxId(), 1, "GRP-FRONTEND-LIST-001",
                "New Booking", "NEW_BOOKING", "NEW_BOOKING");
        insertTransition(930000000000000202L, source.inboxId(), 2, "GRP-FRONTEND-LIST-001",
                "Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
        insertTask(firstTaskId, orderId, source.inboxId(), 930000000000000201L, "New Booking",
                "NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
        insertTask(secondTaskId, orderId, source.inboxId(), 930000000000000202L, "Update Booking",
                "UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 2);
        reset(sourceMessageQueryService);
        reset(workflowRepository);

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
                        .param("hotel_id", HOTEL_ID)
                        .param("order_id", orderId.toString())
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].task_id").value(firstTaskId.toString()))
                .andExpect(jsonPath("$.items[0].order_id").value(orderId.toString()))
                .andExpect(jsonPath("$.items[0].hotel_id").value(HOTEL_ID))
                .andExpect(jsonPath("$.items[0].display_order_key").value("GRP-FRONTEND-LIST-001"))
                .andExpect(jsonPath("$.items[0].task_type").value("NEW_BOOKING"))
                .andExpect(jsonPath("$.items[0].task_status").value("PENDING_CONFIRM"))
                .andExpect(jsonPath("$.items[0].queue_sequence").value(1))
                .andExpect(jsonPath("$.items[0].queue_participation").value(true))
                .andExpect(jsonPath("$.items[0].can_process").value(true))
                .andExpect(jsonPath("$.items[0].readonly_reason_code").value("PROCESSABLE"))
                .andExpect(jsonPath("$.items[0].source_message_id").value(source.inboxId().toString()))
                .andExpect(jsonPath("$.items[0].source_subject").value("Frontend Query List Smoke"))
                .andExpect(jsonPath("$.items[0].source_sender_summary").value("guest@example.test"))
                .andExpect(jsonPath("$.items[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.items[0].external_conversation_id")
                        .value("thread-mail-frontend-task-list-001"))
                .andExpect(jsonPath("$.items[0].conversation_message_count").value(1))
                .andExpect(jsonPath("$.items[0].created_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.items[0].updated_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.items[1].task_id").value(secondTaskId.toString()))
                .andExpect(jsonPath("$.items[1].task_type").value("UPDATE_BOOKING"))
                .andExpect(jsonPath("$.items[1].external_conversation_id")
                        .value("thread-mail-frontend-task-list-001"))
                .andExpect(jsonPath("$.items[1].conversation_message_count").value(1))
                .andExpect(jsonPath("$.items[1].can_process").value(false))
                .andExpect(jsonPath("$.items[1].readonly_reason_code").value("PREVIOUS_TASK_NOT_FINISHED"))
                .andExpect(jsonPath("$.page.page_num").value(1))
                .andExpect(jsonPath("$.page.page_size").value(20))
                .andExpect(jsonPath("$.page.total").value(2));

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
                        .param("hotel_id", HOTEL_ID)
                        .param("keyword", "Frontend Query List Smoke")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].source_subject").value("Frontend Query List Smoke"))
                .andExpect(jsonPath("$.items[1].source_subject").value("Frontend Query List Smoke"))
                .andExpect(jsonPath("$.page.total").value(2));
        verify(sourceMessageQueryService, never()).getSummary(anyLong());
        verify(workflowRepository, never()).findQueueTasksBefore(anyString(), anyLong(), any());
    }

    @Test
    void shouldFilterTaskWorkbenchListByOrderStatus() throws Exception {
        SourceMessageCaptureResult source = captureSourceMessage(
                "mail-frontend-task-order-status-001",
                "Frontend Query Task Order Status Smoke");
        Long activeOrderId = 930000000000001101L;
        Long endedOrderId = 930000000000001102L;
        Long activeTaskId = 930000000000001301L;
        Long endedTaskId = 930000000000001302L;
        insertGroupOrder(activeOrderId, source.inboxId(), "GRP-FRONTEND-TASK-ACTIVE-001", "ACTIVE");
        insertGroupOrder(endedOrderId, source.inboxId(), "GRP-FRONTEND-TASK-ENDED-001", "ENDED");
        insertTransition(930000000000001201L, source.inboxId(), 1, "GRP-FRONTEND-TASK-ACTIVE-001",
                "New Booking", "NEW_BOOKING", "NEW_BOOKING");
        insertTransition(930000000000001202L, source.inboxId(), 2, "GRP-FRONTEND-TASK-ENDED-001",
                "Cancel Booking", "CANCEL_BOOKING", "CANCEL_BOOKING");
        insertTask(activeTaskId, activeOrderId, source.inboxId(), 930000000000001201L, "New Booking",
                "NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
        insertTask(endedTaskId, endedOrderId, source.inboxId(), 930000000000001202L, "Cancel Booking",
                "CANCEL_BOOKING", "CANCEL_BOOKING", "PENDING_CONFIRM", 1);

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
                        .param("hotel_id", HOTEL_ID)
                        .param("order_status", "ENDED")
                        .param("keyword", "GRP-FRONTEND-TASK-")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items.length()").value(1))
                .andExpect(jsonPath("$.items[0].task_id").value(endedTaskId.toString()))
                .andExpect(jsonPath("$.items[0].order_id").value(endedOrderId.toString()))
                .andExpect(jsonPath("$.items[0].display_order_key").value("GRP-FRONTEND-TASK-ENDED-001"))
                .andExpect(jsonPath("$.page.total").value(1));
    }

    @Test
    void shouldReturnTaskWorkbenchListWithLatestSourceMessageFirst() throws Exception {
        SourceMessageCaptureResult oldSource = captureSourceMessage(
                "mail-frontend-task-sort-old-001",
                "Frontend Query Task Sort Old",
                Instant.parse("2026-07-08T01:00:00Z"));
        SourceMessageCaptureResult latestSource = captureSourceMessage(
                "mail-frontend-task-sort-latest-001",
                "Frontend Query Task Sort Latest",
                Instant.parse("2026-07-10T09:30:00Z"));
        Long oldOrderId = 930000000000001401L;
        Long latestOrderId = 930000000000001402L;
        Long oldTaskId = 930000000000001601L;
        Long latestTaskId = 930000000000001602L;
        insertGroupOrder(oldOrderId, oldSource.inboxId(), "GRP-FRONTEND-TASK-SORT-OLD", "ACTIVE");
        insertGroupOrder(latestOrderId, latestSource.inboxId(), "GRP-FRONTEND-TASK-SORT-LATEST", "ACTIVE");
        insertTransition(930000000000001501L, oldSource.inboxId(), 1, "GRP-FRONTEND-TASK-SORT-OLD",
                "New Booking", "NEW_BOOKING", "NEW_BOOKING");
        insertTransition(930000000000001502L, latestSource.inboxId(), 1, "GRP-FRONTEND-TASK-SORT-LATEST",
                "Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
        insertTask(oldTaskId, oldOrderId, oldSource.inboxId(), 930000000000001501L, "New Booking",
                "NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
        insertTask(latestTaskId, latestOrderId, latestSource.inboxId(), 930000000000001502L, "Update Booking",
                "UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 1);

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
                        .param("hotel_id", HOTEL_ID)
                        .param("keyword", "GRP-FRONTEND-TASK-SORT-")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].task_id").value(latestTaskId.toString()))
                .andExpect(jsonPath("$.items[0].source_subject").value("Frontend Query Task Sort Latest"))
                .andExpect(jsonPath("$.items[1].task_id").value(oldTaskId.toString()))
                .andExpect(jsonPath("$.items[1].source_subject").value("Frontend Query Task Sort Old"))
                .andExpect(jsonPath("$.page.total").value(2));
    }

    @Test
    void shouldReturnOrderListWithLatestTaskActivityFirst() throws Exception {
        SourceMessageCaptureResult oldSource = captureSourceMessage(
                "mail-frontend-order-sort-old-001",
                "Frontend Query Order Sort Old",
                Instant.parse("2026-07-08T01:00:00Z"));
        SourceMessageCaptureResult latestSource = captureSourceMessage(
                "mail-frontend-order-sort-latest-001",
                "Frontend Query Order Sort Latest",
                Instant.parse("2026-07-10T09:30:00Z"));
        Long staleButRecentlyUpdatedOrderId = 930000000000001701L;
        Long latestActivityOrderId = 930000000000001702L;
        Long staleTaskId = 930000000000001901L;
        Long latestTaskId = 930000000000001902L;
        insertGroupOrder(staleButRecentlyUpdatedOrderId, oldSource.inboxId(),
                "GRP-FRONTEND-ORDER-SORT-STALE", "ACTIVE");
        insertGroupOrder(latestActivityOrderId, latestSource.inboxId(),
                "GRP-FRONTEND-ORDER-SORT-LATEST", "ACTIVE");
        updateOrderUpdatedAt(staleButRecentlyUpdatedOrderId, Instant.parse("2026-07-09T00:00:00Z"));
        updateOrderUpdatedAt(latestActivityOrderId, Instant.parse("2026-07-08T02:00:00Z"));
        insertTransition(930000000000001801L, oldSource.inboxId(), 1, "GRP-FRONTEND-ORDER-SORT-STALE",
                "New Booking", "NEW_BOOKING", "NEW_BOOKING");
        insertTransition(930000000000001802L, latestSource.inboxId(), 1, "GRP-FRONTEND-ORDER-SORT-LATEST",
                "Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
        insertTask(staleTaskId, staleButRecentlyUpdatedOrderId, oldSource.inboxId(), 930000000000001801L,
                "New Booking", "NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
        insertTask(latestTaskId, latestActivityOrderId, latestSource.inboxId(), 930000000000001802L,
                "Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 1);
        updateOrderLatestActivityAt(staleButRecentlyUpdatedOrderId, Instant.parse("2026-07-09T00:00:00Z"));
        updateOrderLatestActivityAt(latestActivityOrderId, Instant.parse("2026-07-10T09:30:00Z"));

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
                        .param("hotel_id", HOTEL_ID)
                        .param("keyword", "GRP-FRONTEND-ORDER-SORT-")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].order_id").value(latestActivityOrderId.toString()))
                .andExpect(jsonPath("$.items[0].group_code").value("GRP-FRONTEND-ORDER-SORT-LATEST"))
                .andExpect(jsonPath("$.items[1].order_id").value(staleButRecentlyUpdatedOrderId.toString()))
                .andExpect(jsonPath("$.items[1].group_code").value("GRP-FRONTEND-ORDER-SORT-STALE"))
                .andExpect(jsonPath("$.page.total").value(2));
    }

    @Test
    void shouldKeepOrderLatestActivityAtRequiredForIndexedSorting() {
        String isNullable = jdbcTemplate.queryForObject("""
                SELECT IS_NULLABLE
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_NAME = 'WORKFLOW_RESERVATION_ORDER'
                  AND COLUMN_NAME = 'LATEST_ACTIVITY_AT'
                """, String.class);

        assertThat(isNullable).isEqualTo("NO");
    }

    @Test
    void shouldOnlyReturnVisibleOrdersInFrontendOrderList() throws Exception {
        SourceMessageCaptureResult visibleSource = captureSourceMessage(
                "mail-frontend-order-visible-filter-001",
                "Frontend Query Visible Filter");
        SourceMessageCaptureResult archivedSource = captureSourceMessage(
                "mail-frontend-order-visible-filter-archived-001",
                "Frontend Query Visible Filter Archived");
        Long visibleOrderId = 930000000000002201L;
        Long archivedOrderId = 930000000000002202L;
        insertGroupOrder(visibleOrderId, visibleSource.inboxId(),
                "GRP-FRONTEND-VISIBLE-FILTER-001", "ACTIVE");
        insertGroupOrder(archivedOrderId, archivedSource.inboxId(),
                "GRP-FRONTEND-VISIBLE-FILTER-ARCHIVED", "ACTIVE");
        updateOrderVisibility(archivedOrderId, "ARCHIVED_SYSTEM");

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
                        .param("hotel_id", HOTEL_ID)
                        .param("keyword", "GRP-FRONTEND-VISIBLE-FILTER-")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items[0].order_id").value(visibleOrderId.toString()))
                .andExpect(jsonPath("$.page.total").value(1));
    }

    @Test
    void shouldTouchOrderLatestActivityWhenRepositoryInsertsTask() {
        SourceMessageCaptureResult source = captureSourceMessage(
                "mail-frontend-order-activity-touch-001",
                "Frontend Query Order Activity Touch",
                Instant.parse("2026-07-08T01:00:00Z"));
        LocalDateTime orderCreatedAt = LocalDateTime.parse("2026-07-08T01:00:00");
        LocalDateTime taskCreatedAt = LocalDateTime.parse("2026-07-10T09:30:00");
        ReservationOrderSnapshot order = workflowRepository.insertOrder(new ReservationOrderDraft(
                HOTEL_ID,
                ReservationOrderKeyType.GROUP_CODE.name(),
                "GRP-FRONTEND-ORDER-ACTIVITY-TOUCH",
                "GRP-FRONTEND-ORDER-ACTIVITY-TOUCH",
                "TMP-FRONTEND-ORDER-ACTIVITY-TOUCH",
                ReservationOrderStatus.ACTIVE.name(),
                ReservationOrderVisibility.VISIBLE.name(),
                "AI_CANDIDATE",
                null,
                "GRP-FRONTEND-ORDER-ACTIVITY-TOUCH",
                source.inboxId(),
                orderCreatedAt));

        workflowRepository.insertTask(new ReservationTaskDraft(
                HOTEL_ID,
                order.id(),
                source.inboxId(),
                930000000000002001L,
                "normal_task",
                "Update Booking",
                "UPDATE_BOOKING",
                "UPDATE_BOOKING",
                "frontend_query",
                ReservationTaskStatus.PENDING_CONFIRM.name(),
                true,
                1,
                null,
                null,
                false,
                null,
                taskCreatedAt));

        Timestamp latestActivityAt = jdbcTemplate.queryForObject("""
                SELECT latest_activity_at
                FROM workflow_reservation_order
                WHERE id = ?
                """, Timestamp.class, order.id());
        assertThat(latestActivityAt).isNotNull();
        assertThat(latestActivityAt.toLocalDateTime()).isEqualTo(taskCreatedAt);
    }

    @Test
    void shouldReturnOrderDetailWithTaskTimeline() throws Exception {
        SourceMessageCaptureResult source = captureSourceMessage(
                "mail-frontend-order-detail-001",
                "Frontend Query Detail Smoke");
        Long orderId = 930000000000000401L;
        Long firstTaskId = 930000000000000601L;
        Long secondTaskId = 930000000000000602L;
        insertActiveGroupOrder(orderId, source.inboxId(), "GRP-FRONTEND-DETAIL-001");
        insertTransition(930000000000000501L, source.inboxId(), 1, "GRP-FRONTEND-DETAIL-001",
                "New Booking", "NEW_BOOKING", "NEW_BOOKING");
        insertTransition(930000000000000502L, source.inboxId(), 2, "GRP-FRONTEND-DETAIL-001",
                "Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
        insertTask(firstTaskId, orderId, source.inboxId(), 930000000000000501L, "New Booking",
                "NEW_BOOKING", "NEW_BOOKING", "COMPLETED", 1);
        insertTask(secondTaskId, orderId, source.inboxId(), 930000000000000502L, "Update Booking",
                "UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 2);
        reset(workflowRepository);

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
                        .param("hotel_id", HOTEL_ID)
                        .param("include_tasks", "true")
                        .param("include_source_summary", "true"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.order.order_id").value(orderId.toString()))
                .andExpect(jsonPath("$.order.hotel_id").value(HOTEL_ID))
                .andExpect(jsonPath("$.order.order_status").value("ACTIVE"))
                .andExpect(jsonPath("$.order.temporary_order_no").value("TMP-" + orderId))
                .andExpect(jsonPath("$.order.group_code").value("GRP-FRONTEND-DETAIL-001"))
                .andExpect(jsonPath("$.order.confirmation_number").value(nullValue()))
                .andExpect(jsonPath("$.order.display_name").value("GRP-FRONTEND-DETAIL-001"))
                .andExpect(jsonPath("$.order.created_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.order.updated_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.tasks[0].task_id").value(firstTaskId.toString()))
                .andExpect(jsonPath("$.tasks[0].task_status").value("COMPLETED"))
                .andExpect(jsonPath("$.tasks[0].source_message_id").value(source.inboxId().toString()))
                .andExpect(jsonPath("$.tasks[0].source_subject").value("Frontend Query Detail Smoke"))
                .andExpect(jsonPath("$.tasks[0].source_sender_summary").value("guest@example.test"))
                .andExpect(jsonPath("$.tasks[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.tasks[0].external_conversation_id")
                        .value("thread-mail-frontend-order-detail-001"))
                .andExpect(jsonPath("$.tasks[0].conversation_message_count").value(1))
                .andExpect(jsonPath("$.tasks[0].readonly_reason_code").value("TASK_FINISHED"))
                .andExpect(jsonPath("$.tasks[0].created_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
                .andExpect(jsonPath("$.tasks[1].task_id").value(secondTaskId.toString()))
                .andExpect(jsonPath("$.tasks[1].task_status").value("PENDING_CONFIRM"))
                .andExpect(jsonPath("$.tasks[1].can_process").value(true))
                .andExpect(jsonPath("$.tasks[1].readonly_reason_code").value("PROCESSABLE"))
                .andExpect(jsonPath("$.warnings.length()").value(0));
        verify(workflowRepository, never()).findQueueTasksBefore(anyString(), anyLong(), any());

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
                        .param("hotel_id", HOTEL_ID)
                        .param("include_tasks", "false"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.order.order_id").value(orderId.toString()))
                .andExpect(jsonPath("$.tasks.length()").value(0));
    }

    @Test
    void shouldReturnOrderListWithOpenTaskCountAndNextProcessableTask() throws Exception {
        SourceMessageCaptureResult source = captureSourceMessage(
                "mail-frontend-order-list-001",
                "Frontend Query Order List Smoke");
        Long activeOrderId = 930000000000000701L;
        Long endedOrderId = 930000000000000702L;
        Long pendingTaskId = 930000000000000901L;
        Long completedTaskId = 930000000000000902L;
        Long failedTaskId = 930000000000000903L;
        insertGroupOrder(activeOrderId, source.inboxId(), "GRP-FRONTEND-ORDERS-ACTIVE-001", "ACTIVE");
        insertGroupOrder(endedOrderId, source.inboxId(), "GRP-FRONTEND-ORDERS-ENDED-001", "ENDED");
        insertTransition(930000000000000801L, source.inboxId(), 1, "GRP-FRONTEND-ORDERS-ACTIVE-001",
                "New Booking", "NEW_BOOKING", "NEW_BOOKING");
        insertTransition(930000000000000802L, source.inboxId(), 2, "GRP-FRONTEND-ORDERS-ACTIVE-001",
                "Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
        insertTransition(930000000000000803L, source.inboxId(), 3, "GRP-FRONTEND-ORDERS-ACTIVE-001",
                "Cancel Booking", "CANCEL_BOOKING", "CANCEL_BOOKING");
        insertTask(pendingTaskId, activeOrderId, source.inboxId(), 930000000000000801L, "New Booking",
                "NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
        insertTask(completedTaskId, activeOrderId, source.inboxId(), 930000000000000802L, "Update Booking",
                "UPDATE_BOOKING", "UPDATE_BOOKING", "COMPLETED", 2);
        insertTask(failedTaskId, activeOrderId, source.inboxId(), 930000000000000803L, "Cancel Booking",
                "CANCEL_BOOKING", "CANCEL_BOOKING", "FAILED", 3);

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
                        .param("hotel_id", HOTEL_ID)
                        .param("keyword", "GRP-FRONTEND-ORDERS-")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items.length()").value(2))
                .andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].order_status")
                        .value(contains("ACTIVE")))
                .andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].open_task_count")
                        .value(contains(1)))
                .andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].next_processable_task_id")
                        .value(contains(pendingTaskId.toString())))
                .andExpect(jsonPath("$.items[?(@.order_id=='" + endedOrderId + "')].order_status")
                        .value(contains("ENDED")))
                .andExpect(jsonPath("$.items[?(@.order_id=='" + endedOrderId + "')].open_task_count")
                        .value(contains(0)))
                .andExpect(jsonPath("$.page.total").value(2));

        performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
                        .param("hotel_id", HOTEL_ID)
                        .param("order_status", "ENDED")
                        .param("keyword", "GRP-FRONTEND-ORDERS-")
                        .param("page_num", "1")
                        .param("page_size", "20"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.items.length()").value(1))
                .andExpect(jsonPath("$.items[0].order_id").value(endedOrderId.toString()))
                .andExpect(jsonPath("$.items[0].order_status").value("ENDED"));
    }

    private SourceMessageCaptureResult captureSourceMessage(String externalMessageId, String subject) {
        return captureSourceMessage(externalMessageId, subject, Instant.parse("2026-07-08T08:00:00Z"));
    }

    private SourceMessageCaptureResult captureSourceMessage(
            String externalMessageId,
            String subject,
            Instant receivedAt) {
        return captureService.capture(new CaptureSourceMessageCommand(
                HOTEL_ID,
                "AGENTBUS",
                "EMAIL",
                externalMessageId,
                "thread-" + externalMessageId,
                "frame-" + externalMessageId,
                "session-frontend-query",
                receivedAt,
                null,
                "guest@example.test",
                subject,
                "Please handle booking message.",
                "<html><body>Please handle booking message.</body></html>",
                "{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}",
                "agentbus-outlook-v1",
                List.of()
        ));
    }

    private void updateOrderUpdatedAt(Long orderId, Instant updatedAt) {
        jdbcTemplate.update("""
                UPDATE workflow_reservation_order
                SET updated_at = ?
                WHERE id = ?
                """, Timestamp.from(updatedAt), orderId);
    }

    private void updateOrderLatestActivityAt(Long orderId, Instant latestActivityAt) {
        jdbcTemplate.update("""
                UPDATE workflow_reservation_order
                SET latest_activity_at = ?
                WHERE id = ?
                """, Timestamp.from(latestActivityAt), orderId);
    }

    private void updateOrderVisibility(Long orderId, String orderVisibility) {
        jdbcTemplate.update("""
                UPDATE workflow_reservation_order
                SET order_visibility = ?
                WHERE id = ?
                """, orderVisibility, orderId);
    }

    private void insertActiveGroupOrder(Long orderId, Long sourceMessageId, String groupCode) {
        insertGroupOrder(orderId, sourceMessageId, groupCode, "ACTIVE");
    }

    private void insertGroupOrder(Long orderId, Long sourceMessageId, String groupCode, String orderStatus) {
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_order (
                    id, hotel_id, order_key_type, order_business_key, active_business_key,
                    temporary_order_code, order_status, business_key_source, display_name,
                    source_message_id, version, latest_activity_at, created_at, updated_at
                )
                VALUES (?, ?, 'GROUP_CODE', ?, ?, ?, ?, 'AI_CANDIDATE', ?, ?, 0,
                    CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, orderId, HOTEL_ID, groupCode, groupCode, "TMP-" + orderId, orderStatus, groupCode, sourceMessageId);
    }

    private void insertTransition(
            Long transitionId,
            Long sourceMessageId,
            Integer sourceEventIndex,
            String groupCode,
            String aiTaskType,
            String systemTaskType,
            String taskCardType) {
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_ai_transition (
                    id, hotel_id, batch_id, source_message_id, source_event_index, array_index,
                    execution_order, catalog_code, skill_id, result_type, ai_task_type,
                    system_task_type, task_card_type, task_subtype, current_or_history,
                    group_code, item_payload_sha256, item_idempotency_key, blocked_until_parent_completed,
                    ai_payload_json, case_keys_json, extracted_fields_json, created_at, updated_at
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, 'S02', 'frontend_query_skill',
                    'normal_task', ?, ?, ?, 'frontend_query', 'current', ?, ?, ?, 0,
                    '{}', '{}', '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, transitionId, HOTEL_ID, transitionId - 1, sourceMessageId, sourceEventIndex,
                sourceEventIndex, sourceEventIndex, aiTaskType, systemTaskType, taskCardType, groupCode,
                "0".repeat(64), transitionId.toString());
    }

    private void insertTask(
            Long taskId,
            Long orderId,
            Long sourceMessageId,
            Long transitionId,
            String aiTaskType,
            String systemTaskType,
            String taskCardType,
            String taskStatus,
            Integer executionOrder) {
        jdbcTemplate.update("""
                INSERT INTO workflow_reservation_task (
                    id, hotel_id, order_id, source_message_id, ai_transition_id,
                    result_type, ai_task_type, system_task_type, task_card_type, task_subtype,
                    task_status, queue_participation, execution_order, blocked_until_parent_completed,
                    version, created_at, updated_at
                )
                VALUES (?, ?, ?, ?, ?, 'normal_task', ?, ?, ?, 'frontend_query',
                    ?, 1, ?, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """, taskId, HOTEL_ID, orderId, sourceMessageId, transitionId, aiTaskType,
                systemTaskType, taskCardType, taskStatus, executionOrder);
    }
}
