package com.starry.admin.api;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;

class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {

    @Autowired
    private IPlayOrderEvaluateInfoService playOrderEvaluateInfoService;

    @Autowired
    private IPlayOrderComplaintInfoService playOrderComplaintInfoService;

    private final List<String> orderIdsToCleanup = new ArrayList<>();
    private final List<String> evalIdsToCleanup = new ArrayList<>();
    private final List<String> complaintIdsToCleanup = new ArrayList<>();

    @AfterEach
    void cleanUpOrders() {
        if (!orderIdsToCleanup.isEmpty()) {
            playOrderInfoService.removeByIds(orderIdsToCleanup);
            orderIdsToCleanup.clear();
        }
        if (!evalIdsToCleanup.isEmpty()) {
            playOrderEvaluateInfoService.removeByIds(evalIdsToCleanup);
            evalIdsToCleanup.clear();
        }
        if (!complaintIdsToCleanup.isEmpty()) {
            playOrderComplaintInfoService.removeByIds(complaintIdsToCleanup);
            complaintIdsToCleanup.clear();
        }
    }

    @Test
    void queryByPageFiltersRevokedOrdersAndDetailShowsReason() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            resetCustomerBalance();
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);

            String remark = "revoked-flow-" + IdUtils.getUuid();
            placeRandomOrder(remark, customerToken);

            ensureTenantContext();
            PlayOrderInfoEntity createdOrder = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getRemark, remark)
                    .orderByDesc(PlayOrderInfoEntity::getCreatedTime)
                    .last("limit 1")
                    .one();
            assertThat(createdOrder).as("Order with remark %s should exist", remark).isNotNull();

            String orderId = createdOrder.getId();
            ensureTenantContext();
            playOrderInfoService.lambdaUpdate()
                    .set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.NORMAL.getCode())
                    .eq(PlayOrderInfoEntity::getId, orderId)
                    .update();
            ensureTenantContext();
            playOrderInfoService.updateStateTo1(
                    OrderConstant.OperatorType.CLERK.getCode(),
                    ApiTestDataSeeder.DEFAULT_CLERK_ID,
                    ApiTestDataSeeder.DEFAULT_CLERK_ID,
                    orderId);
            ensureTenantContext();
            playOrderInfoService.updateStateTo23(
                    OrderConstant.OperatorType.CLERK.getCode(),
                    ApiTestDataSeeder.DEFAULT_CLERK_ID,
                    OrderConstant.OrderStatus.IN_PROGRESS.getCode(),
                    orderId);
            ensureTenantContext();
            playOrderInfoService.updateStateTo23(
                    OrderConstant.OperatorType.ADMIN.getCode(),
                    ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
                    OrderConstant.OrderStatus.COMPLETED.getCode(),
                    orderId);

            String revokeReason = "auto-revoke-" + IdUtils.getUuid();
            String revokePayload = "{" +
                    "\"orderId\":\"" + orderId + "\"," +
                    "\"refundToCustomer\":false," +
                    "\"refundReason\":\"" + revokeReason + "\"," +
                    "\"deductClerkEarnings\":false" +
                    "}";

            mockMvc.perform(post("/order/order/revokeCompleted")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(revokePayload))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200));

            ObjectNode filterPayload = basePayload(1, 10);
            filterPayload.put("orderStatus", OrderConstant.OrderStatus.REVOKED.getCode());
            JsonNode listRoot = executeOrderQuery(customerToken, filterPayload);
            JsonNode dataNode = listRoot.path("data");
            JsonNode records = dataNode.isArray() ? dataNode : dataNode.path("records");
            assertThat(records.isArray()).as("List response should contain records array").isTrue();
            assertThat(records.size()).as("Should return at least one revoked order").isGreaterThan(0);
            boolean found = false;
            for (JsonNode node : records) {
                assertThat(node.path("orderStatus").asText()).isEqualTo("5");
                if (orderId.equals(node.path("id").asText())) {
                    found = true;
                }
            }
            assertThat(found).as("Revoked order should be present in filter result").isTrue();

            MvcResult detailResult = mockMvc.perform(get("/wx/custom/order/queryById")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                            .param("id", orderId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andReturn();

            JsonNode detailRoot = objectMapper.readTree(detailResult.getResponse().getContentAsString());
            JsonNode detail = detailRoot.path("data");
            assertThat(detail.path("orderStatus").asText()).isEqualTo("5");
            assertThat(detail.path("refundReason").asText()).isEqualTo(revokeReason);
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void revokeCompletedOrderRejectsNonNormalOrderTypes() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            resetCustomerBalance();
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);

            String remark = "non-normal-" + IdUtils.getUuid();
            placeRandomOrder(remark, customerToken);

            ensureTenantContext();
            PlayOrderInfoEntity createdOrder = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getRemark, remark)
                    .orderByDesc(PlayOrderInfoEntity::getCreatedTime)
                    .last("limit 1")
                    .one();
            assertThat(createdOrder).isNotNull();

            String orderId = createdOrder.getId();
            ensureTenantContext();
            playOrderInfoService.lambdaUpdate()
                    .set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.GIFT.getCode())
                    .eq(PlayOrderInfoEntity::getId, orderId)
                    .update();

            ensureTenantContext();
            playOrderInfoService.updateStateTo1(
                    OrderConstant.OperatorType.CLERK.getCode(),
                    ApiTestDataSeeder.DEFAULT_CLERK_ID,
                    ApiTestDataSeeder.DEFAULT_CLERK_ID,
                    orderId);
            ensureTenantContext();
            playOrderInfoService.updateStateTo23(
                    OrderConstant.OperatorType.CLERK.getCode(),
                    ApiTestDataSeeder.DEFAULT_CLERK_ID,
                    OrderConstant.OrderStatus.IN_PROGRESS.getCode(),
                    orderId);
            ensureTenantContext();
            playOrderInfoService.updateStateTo23(
                    OrderConstant.OperatorType.ADMIN.getCode(),
                    ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
                    OrderConstant.OrderStatus.COMPLETED.getCode(),
                    orderId);

            ObjectNode revokePayload = objectMapper.createObjectNode();
            revokePayload.put("orderId", orderId);
            revokePayload.put("refundToCustomer", false);
            revokePayload.put("refundReason", "non-normal-type");
            revokePayload.put("deductClerkEarnings", false);

            mockMvc.perform(post("/order/order/revokeCompleted")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(revokePayload.toString()))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(500))
                    .andExpect(jsonPath("$.message").value("仅支持撤销普通服务订单"));
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void queryByPageSkipsRechargeOrdersByDefaultButAllowsExplicitFilter() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            resetCustomerBalance();
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);

            String rechargeRemark = "recharge-like-" + IdUtils.getUuid();
            LocalDateTime now = LocalDateTime.now().minusMinutes(20);
            PlayOrderInfoEntity rechargeOrder = persistOrder(now, order -> {
                order.setRemark(rechargeRemark);
                order.setOrderType(OrderConstant.OrderType.RECHARGE.getCode());
            });
            String giftRemark = "gift-like-" + IdUtils.getUuid();
            PlayOrderInfoEntity giftOrder = persistOrder(now.plusMinutes(5), order -> {
                order.setRemark(giftRemark);
                order.setOrderType(OrderConstant.OrderType.GIFT.getCode());
            });

            ObjectNode defaultPayload = basePayload(1, 20);
            JsonNode defaultRecords = queryOrders(customerToken, defaultPayload);
            assertThat(defaultRecords.size()).isGreaterThan(0);
            assertThat(defaultRecords).noneMatch(node -> rechargeOrder.getId().equals(node.path("id").asText()));

            ObjectNode explicitPayload = basePayload(1, 20);
            explicitPayload.put("orderType", OrderConstant.OrderType.GIFT.getCode());
            JsonNode filteredRecords = queryOrders(customerToken, explicitPayload);
            assertThat(filteredRecords)
                    .anyMatch(node -> giftOrder.getId().equals(node.path("id").asText()));
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void queryByPageReturnsOnlyOrdersBelongingToCurrentCustomer() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            String token = ensureCustomerToken();
            LocalDateTime base = LocalDateTime.now().minusMinutes(30);
            PlayOrderInfoEntity own = persistOrder(base, order -> order.setOrderNo("OWN-" + IdUtils.getUuid().substring(0, 6)));
            PlayOrderInfoEntity foreign = persistOrder(base.plusMinutes(5), order -> {
                order.setPurchaserBy("other-customer");
                order.setOrderNo("FOREIGN-" + IdUtils.getUuid().substring(0, 6));
            });

            ObjectNode payload = basePayload(1, 20);
            JsonNode records = queryOrders(token, payload);
            List<String> ids = new ArrayList<>();
            records.forEach(node -> ids.add(node.path("id").asText()));
            assertThat(ids).contains(own.getId());
            assertThat(ids).doesNotContain(foreign.getId());
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void queryByPageSupportsPagingMeta() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            String token = ensureCustomerToken();
            LocalDateTime base = LocalDateTime.now().plusHours(2);
            String pageMarker = "PAGE-" + IdUtils.getUuid().substring(0, 4);
            String pageGroup = "group-" + pageMarker;
            PlayOrderInfoEntity first = persistOrder(base, order -> {
                order.setOrderNo(pageMarker + "A");
                order.setGroupId(pageGroup);
            });
            PlayOrderInfoEntity second = persistOrder(base.plusMinutes(2), order -> {
                order.setOrderNo(pageMarker + "B");
                order.setGroupId(pageGroup);
            });
            PlayOrderInfoEntity third = persistOrder(base.plusMinutes(4), order -> {
                order.setOrderNo(pageMarker + "C");
                order.setGroupId(pageGroup);
            });
            ArrayNode purchaserWindow = range(base.minusMinutes(1), base.plusMinutes(5));

            ObjectNode pageOne = basePayload(1, 2);
            pageOne.set("purchaserTime", purchaserWindow);
            pageOne.put("orderNo", pageMarker);
            pageOne.put("groupId", pageGroup);
            JsonNode rootPageOne = executeOrderQuery(token, pageOne);
            JsonNode recordsOne = recordsFromRoot(rootPageOne);
            assertThat(recordsOne.size()).isEqualTo(2);
            assertThat(rootPageOne.path("pageInfo").path("pageSize").asInt()).isEqualTo(2);
            assertThat(rootPageOne.path("pageInfo").path("currentPage").asInt()).isEqualTo(1);

            ObjectNode pageTwo = basePayload(2, 2);
            pageTwo.set("purchaserTime", purchaserWindow);
            pageTwo.put("orderNo", pageMarker);
            pageTwo.put("groupId", pageGroup);
            JsonNode rootPageTwo = executeOrderQuery(token, pageTwo);
            JsonNode recordsTwo = recordsFromRoot(rootPageTwo);
            assertThat(recordsTwo.size()).isGreaterThan(0);
            assertThat(rootPageTwo.path("pageInfo").path("pageSize").asInt()).isEqualTo(2);
            assertThat(rootPageTwo.path("pageInfo").path("currentPage").asInt()).isEqualTo(2);
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void queryByPageFiltersByOrderStatus() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            String token = ensureCustomerToken();
            PlayOrderInfoEntity pending = persistOrder(LocalDateTime.now().minusMinutes(50),
                    order -> order.setOrderStatus(OrderConstant.OrderStatus.PENDING.getCode()));
            PlayOrderInfoEntity completed = persistOrder(LocalDateTime.now().minusMinutes(40),
                    order -> order.setOrderStatus(OrderConstant.OrderStatus.COMPLETED.getCode()));
            PlayOrderInfoEntity revoked = persistOrder(LocalDateTime.now().minusMinutes(30),
                    order -> order.setOrderStatus(OrderConstant.OrderStatus.REVOKED.getCode()));

            ObjectNode payload = basePayload(1, 10);
            payload.put("orderStatus", OrderConstant.OrderStatus.REVOKED.getCode());
            JsonNode records = queryOrders(token, payload);
            assertThat(records.size()).isGreaterThan(0);
            records.forEach(node -> assertThat(node.path("orderStatus").asText())
                    .isEqualTo(OrderConstant.OrderStatus.REVOKED.getCode()));
            assertThat(findById(records, revoked.getId())).isNotNull();
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void queryByPageFiltersByPlaceTypeAndCompositeCriteria() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            String token = ensureCustomerToken();
            LocalDateTime base = LocalDateTime.now().minusMinutes(90);
            PlayOrderInfoEntity target = persistOrder(base, order -> {
                order.setOrderStatus(OrderConstant.OrderStatus.IN_PROGRESS.getCode());
                order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
                order.setOrderNo("FOCUS-" + IdUtils.getUuid().substring(0, 4));
                order.setUseCoupon("1");
                order.setBackendEntry("1");
                order.setFirstOrder("1");
                order.setGroupId("group-focus");
                order.setSex("1");
                order.setPurchaserTime(base.plusMinutes(5));
                order.setAcceptTime(base.plusMinutes(10));
                order.setOrderEndTime(base.plusMinutes(50));
            });
            persistOrder(base.plusMinutes(5), order -> {
                order.setOrderStatus(OrderConstant.OrderStatus.IN_PROGRESS.getCode());
                order.setPlaceType(OrderConstant.PlaceType.RANDOM.getCode());
                order.setUseCoupon("0");
                order.setBackendEntry("0");
                order.setFirstOrder("0");
                order.setGroupId("group-noise");
                order.setSex("2");
            });

            ObjectNode payload = basePayload(1, 10);
            payload.put("placeType", OrderConstant.PlaceType.SPECIFIED.getCode());
            payload.put("orderNo", target.getOrderNo().substring(0, 6));
            payload.put("useCoupon", "1");
            payload.put("backendEntry", "1");
            payload.put("firstOrder", "1");
            payload.put("groupId", "group-focus");
            payload.put("sex", "1");
            payload.set("purchaserTime", range(target.getPurchaserTime().minusMinutes(1), target.getPurchaserTime().plusMinutes(1)));
            payload.set("acceptTime", range(target.getAcceptTime().minusMinutes(1), target.getAcceptTime().plusMinutes(1)));
            payload.set("endOrderTime", range(target.getOrderEndTime().minusMinutes(1), target.getOrderEndTime().plusMinutes(1)));

            JsonNode records = queryOrders(token, payload);
            assertThat(records.size()).isGreaterThan(0);
            JsonNode targetNode = findById(records, target.getId());
            assertThat(targetNode).isNotNull();
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void queryByPageMarksEvaluateAndComplaintFlags() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            String token = ensureCustomerToken();
            PlayOrderInfoEntity evaluated = persistOrder(LocalDateTime.now().minusMinutes(10),
                    order -> order.setOrderNo("EVAL-" + IdUtils.getUuid().substring(0, 4)));
            PlayOrderInfoEntity complained = persistOrder(LocalDateTime.now().minusMinutes(8),
                    order -> order.setOrderNo("COMP-" + IdUtils.getUuid().substring(0, 4)));
            markEvaluated(evaluated.getId());
            markComplained(complained.getId());

            ObjectNode payload = basePayload(1, 20);
            JsonNode records = queryOrders(token, payload);
            String evalFlag = findById(records, evaluated.getId()).path("evaluate").asText();
            String complaintFlag = findById(records, complained.getId()).path("complaint").asText();
            assertThat(evalFlag).isEqualTo("1");
            assertThat(complaintFlag).isEqualTo("1");
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    private void placeRandomOrder(String remark, String customerToken) throws Exception {
        String payload = "{" +
                "\"sex\":\"2\"," +
                "\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
                "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                "\"commodityQuantity\":1," +
                "\"weiChatCode\":\"apitest-customer-wx\"," +
                "\"excludeHistory\":\"0\"," +
                "\"couponIds\":[]," +
                "\"remark\":\"" + remark + "\"" +
                "}";

        mockMvc.perform(post("/wx/custom/order/random")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                        .contentType(MediaType.APPLICATION_JSON)
                    .content(payload))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data").value("下单成功"));
    }

    private PlayOrderInfoEntity persistOrder(LocalDateTime baseTime, java.util.function.Consumer<PlayOrderInfoEntity> customizer) {
        PlayOrderInfoEntity order = new PlayOrderInfoEntity();
        order.setId("order-" + IdUtils.getUuid());
        order.setOrderNo("WXQ-" + IdUtils.getUuid().substring(0, 8));
        order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        order.setOrderStatus(OrderConstant.OrderStatus.PENDING.getCode());
        order.setOrderType(OrderConstant.OrderType.NORMAL.getCode());
        order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
        order.setRewardType("0");
        order.setFirstOrder("0");
        order.setRefundType("0");
        order.setRefundAmount(BigDecimal.ZERO);
        order.setOrderMoney(new BigDecimal("99.00"));
        order.setFinalAmount(new BigDecimal("99.00"));
        order.setDiscountAmount(BigDecimal.ZERO);
        order.setEstimatedRevenue(new BigDecimal("40.00"));
        order.setEstimatedRevenueRatio(40);
        order.setUseCoupon("0");
        order.setBackendEntry("0");
        order.setCouponIds(java.util.Collections.emptyList());
        order.setPaymentSource("balance");
        order.setPayMethod("0");
        order.setPayState("1");
        order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        order.setPurchaserTime(baseTime);
        order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        order.setAcceptTime(baseTime.plusMinutes(5));
        order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
        order.setOrderStartTime(baseTime.plusMinutes(10));
        order.setOrderEndTime(baseTime.plusMinutes(40));
        order.setOrdersExpiredState("0");
        order.setOrderSettlementState("0");
        order.setSex("2");
        order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
        order.setCommodityType("1");
        order.setCommodityPrice(new BigDecimal("99.00"));
        order.setCommodityName("Weixin Order");
        order.setServiceDuration("60min");
        order.setCommodityNumber("1");
        order.setRemark("auto");
        order.setBackendRemark("auto");
        Date createdDate = toDate(baseTime);
        order.setCreatedTime(createdDate);
        order.setUpdatedTime(createdDate);
        order.setCreatedBy("wx-test");
        order.setUpdatedBy("wx-test");
        customizer.accept(order);
        ensureTenantContext();
        playOrderInfoService.save(order);
        orderIdsToCleanup.add(order.getId());
        return order;
    }

    private ObjectNode basePayload(int pageNum, int pageSize) {
        ObjectNode node = objectMapper.createObjectNode();
        node.put("pageNum", pageNum);
        node.put("pageSize", pageSize);
        return node;
    }

    private ArrayNode range(LocalDateTime start, LocalDateTime end) {
        ArrayNode node = objectMapper.createArrayNode();
        node.add(DATE_TIME_FORMATTER.format(start));
        node.add(DATE_TIME_FORMATTER.format(end));
        return node;
    }

    private Date toDate(LocalDateTime time) {
        return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
    }

    private String ensureCustomerToken() {
        resetCustomerBalance();
        String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
        return customerToken;
    }

    private void markEvaluated(String orderId) {
        PlayOrderEvaluateInfoEntity entity = new PlayOrderEvaluateInfoEntity();
        entity.setId("eval-" + IdUtils.getUuid());
        entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        entity.setOrderId(orderId);
        entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        entity.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
        entity.setAnonymous("0");
        entity.setEvaluateType("0");
        entity.setEvaluateLevel(5);
        entity.setEvaluateCon("Great job");
        entity.setEvaluateTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
        entity.setHidden("0");
        ensureTenantContext();
        playOrderEvaluateInfoService.save(entity);
        evalIdsToCleanup.add(entity.getId());
    }

    private void markComplained(String orderId) {
        PlayOrderComplaintInfoEntity entity = new PlayOrderComplaintInfoEntity();
        entity.setId("complaint-" + IdUtils.getUuid());
        entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        entity.setOrderId(orderId);
        entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        entity.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
        entity.setComplaintCon("Need assistance");
        entity.setComplaintTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
        entity.setHidden("0");
        ensureTenantContext();
        playOrderComplaintInfoService.save(entity);
        complaintIdsToCleanup.add(entity.getId());
    }

    private JsonNode findById(JsonNode records, String id) {
        for (JsonNode node : records) {
            if (id.equals(node.path("id").asText())) {
                return node;
            }
        }
        throw new AssertionError("Record with id " + id + " not found in response");
    }

    private JsonNode queryOrders(String customerToken, ObjectNode payload) throws Exception {
        JsonNode root = executeOrderQuery(customerToken, payload);
        JsonNode dataNode = root.path("data");
        return dataNode.isArray() ? dataNode : dataNode.path("records");
    }

    private JsonNode recordsFromRoot(JsonNode root) {
        JsonNode dataNode = root.path("data");
        return dataNode.isArray() ? dataNode : dataNode.path("records");
    }

    private JsonNode executeOrderQuery(String customerToken, ObjectNode payload) throws Exception {
        MvcResult result = mockMvc.perform(post("/wx/custom/order/queryByPage")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(payload.toString()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andReturn();
        return objectMapper.readTree(result.getResponse().getContentAsString());
    }
}
