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

import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.contains;
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 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.repository.ReservationAiWorkflowRepository;
import java.time.Instant;
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)
@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;

    @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);

        mockMvc.perform(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));

        mockMvc.perform(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);

        mockMvc.perform(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 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);

        mockMvc.perform(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());

        mockMvc.perform(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);

        mockMvc.perform(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));

        mockMvc.perform(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 captureService.capture(new CaptureSourceMessageCommand(
                HOTEL_ID,
                "AGENTBUS",
                "EMAIL",
                externalMessageId,
                "thread-" + externalMessageId,
                "frame-" + externalMessageId,
                "session-frontend-query",
                Instant.parse("2026-07-08T08:00:00Z"),
                "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 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, created_at, updated_at
                )
                VALUES (?, ?, 'GROUP_CODE', ?, ?, ?, ?, 'AI_CANDIDATE', ?, ?, 0,
                    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);
    }
}
