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.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
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 PlayClerkUserInfoApiTest extends AbstractApiTest {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private IPlayClerkLevelInfoService clerkLevelInfoService;

    @Autowired
    private IPlayClerkUserInfoService clerkUserInfoService;

    private final List<String> levelIdsToCleanup = new ArrayList<>();
    private final List<String> clerkIdsToCleanup = new ArrayList<>();
    private int scenarioSequence = 0;
    private static final Comparator<ClerkScenario> BACKEND_ORDERING = Comparator
            .comparing(ClerkScenario::isOnline, Comparator.reverseOrder())
            .thenComparing(ClerkScenario::isPinned, Comparator.reverseOrder())
            .thenComparingLong(ClerkScenario::getLevelOrder)
            .thenComparingInt(ClerkScenario::getSequence)
            .thenComparing(ClerkScenario::getId);

    @AfterEach
    void tearDown() {
        ensureTenantContext();
        if (!clerkIdsToCleanup.isEmpty()) {
            clerkUserInfoService.removeByIds(clerkIdsToCleanup);
            clerkIdsToCleanup.clear();
        }
        if (!levelIdsToCleanup.isEmpty()) {
            clerkLevelInfoService.removeByIds(levelIdsToCleanup);
            levelIdsToCleanup.clear();
        }
    }

    @Test
    // 场景：不同等级的排序号不同，接口应按照排序号升序返回，验证等级排序字段生效。
    void listOrdersByLevelOrderNumberAscending() throws Exception {
        ensureTenantContext();
        PlayClerkLevelInfoEntity lowOrderLevel = createClerkLevel("low", 1L, 50);
        PlayClerkLevelInfoEntity highOrderLevel = createClerkLevel("high", 5L, 60);

        String filterToken = "order-sort-" + IdUtils.getUuid().substring(0, 8);
        String lowOrderClerkId = createClerk(filterToken + "-low", lowOrderLevel.getId(), "0");
        String highOrderClerkId = createClerk(filterToken + "-high", highOrderLevel.getId(), "1");

        MvcResult result = mockMvc.perform(get("/clerk/user/list")
                        .param("pageNum", "1")
                        .param("pageSize", "20")
                        .param("nickname", filterToken)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        JsonNode root = objectMapper.readTree(responseBody);
        assertThat(root.get("code").asInt()).isEqualTo(200);
        JsonNode records = root.path("data");
        assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();

        List<String> orderedIds = new ArrayList<>();
        for (JsonNode record : records) {
            orderedIds.add(record.path("id").asText());
        }

        assertThat(orderedIds).contains(lowOrderClerkId, highOrderClerkId);
        assertThat(orderedIds.indexOf(highOrderClerkId))
                .withFailMessage("Online clerk should appear before offline regardless of level. token=%s list=%s",
                        filterToken, orderedIds)
                .isLessThan(orderedIds.indexOf(lowOrderClerkId));
    }

    @Test
    // 场景：相同等级排序号相同，接口应按在线状态优先展示在线店员，验证排序二级规则。
    void listOrdersByOnlineStateWhenOrderNumberMatches() throws Exception {
        ensureTenantContext();
        PlayClerkLevelInfoEntity level = createClerkLevel("tie", 3L, 70);

        String filterToken = "online-priority-" + IdUtils.getUuid().substring(0, 8);
        String onlineClerkId = createClerk(filterToken + "-online", level.getId(), "1");
        String offlineClerkId = createClerk(filterToken + "-offline", level.getId(), "0");

        MvcResult result = mockMvc.perform(get("/clerk/user/list")
                        .param("pageNum", "1")
                        .param("pageSize", "20")
                        .param("nickname", filterToken)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        JsonNode root = objectMapper.readTree(responseBody);
        assertThat(root.get("code").asInt()).isEqualTo(200);
        JsonNode records = root.path("data");
        assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();

        List<String> orderedIds = new ArrayList<>();
        for (JsonNode record : records) {
            orderedIds.add(record.path("id").asText());
        }

        assertThat(orderedIds).contains(onlineClerkId, offlineClerkId);
        assertThat(orderedIds.indexOf(onlineClerkId))
                .withFailMessage("Unexpected order for token %s: %s | response: %s",
                        filterToken, orderedIds, responseBody)
                .isLessThan(orderedIds.indexOf(offlineClerkId));
    }

    @Test
    // 场景：两个等级都未配置排序号时，接口仍可返回且在线客服优先排序。
    void listOrdersByOnlineStateWhenOrderNumberMissing() throws Exception {
        ensureTenantContext();
        PlayClerkLevelInfoEntity onlineLevel = createClerkLevel("null-online", null, 20);
        PlayClerkLevelInfoEntity offlineLevel = createClerkLevel("null-offline", null, 30);

        String filterToken = "null-priority-" + IdUtils.getUuid().substring(0, 8);
        String onlineClerkId = createClerk(filterToken + "-online", onlineLevel.getId(), "1");
        String offlineClerkId = createClerk(filterToken + "-offline", offlineLevel.getId(), "0");

        MvcResult result = mockMvc.perform(get("/clerk/user/list")
                        .param("pageNum", "1")
                        .param("pageSize", "20")
                        .param("nickname", filterToken)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        JsonNode root = objectMapper.readTree(responseBody);
        assertThat(root.get("code").asInt()).isEqualTo(200);
        JsonNode records = root.path("data");
        assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();

        List<String> orderedIds = new ArrayList<>();
        for (JsonNode record : records) {
            orderedIds.add(record.path("id").asText());
        }

        assertThat(orderedIds).contains(onlineClerkId, offlineClerkId);
        assertThat(orderedIds.indexOf(onlineClerkId))
                .withFailMessage("Online clerk should remain prioritized even without orderNumber. token=%s order=%s response=%s",
                        filterToken, orderedIds, responseBody)
                .isLessThan(orderedIds.indexOf(offlineClerkId));
    }

    @Test
    // 场景：存在未填写排序号的等级时，接口仍能返回，并默认将该等级排在有序的等级之后。
    void listHandlesNullOrderNumberGracefully() throws Exception {
        ensureTenantContext();
        PlayClerkLevelInfoEntity orderedLevel = createClerkLevel("ordered", 2L, 40);
        PlayClerkLevelInfoEntity nullLevel = createClerkLevel("null", null, 50);

        String filterToken = "null-order-" + IdUtils.getUuid().substring(0, 8);
        String orderedClerkId = createClerk(filterToken + "-ordered", orderedLevel.getId(), "1");
        String nullOrderClerkId = createClerk(filterToken + "-null", nullLevel.getId(), "1");

        MvcResult result = mockMvc.perform(get("/clerk/user/list")
                        .param("pageNum", "1")
                        .param("pageSize", "20")
                        .param("nickname", filterToken)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER))
                .andExpect(status().isOk())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        JsonNode root = objectMapper.readTree(responseBody);
        assertThat(root.get("code").asInt()).isEqualTo(200);
        JsonNode records = root.path("data");
        assertThat(records.isArray()).as("Response payload: %s", responseBody).isTrue();

        List<String> orderedIds = new ArrayList<>();
        for (JsonNode record : records) {
            orderedIds.add(record.path("id").asText());
        }

        assertThat(orderedIds).contains(orderedClerkId, nullOrderClerkId);
        assertThat(orderedIds.indexOf(orderedClerkId))
                .withFailMessage("Null orderNumber should fall back after populated ones. token=%s, order=%s, response=%s",
                        filterToken, orderedIds, responseBody)
                .isLessThan(orderedIds.indexOf(nullOrderClerkId));
    }

    @Test
    // 场景：更新店员等级时传入排序号，接口应成功持久化该值，验证新增字段的写入能力。
    void updateLevelPersistsOrderNumber() throws Exception {
        ensureTenantContext();
        PlayClerkLevelInfoEntity level = createClerkLevel("update", 8L, 80);

        long updatedOrderNumber = 42L;
        ObjectNode payload = objectMapper.createObjectNode();
        payload.put("id", level.getId());
        payload.put("name", level.getName());
        payload.put("firstRegularRatio", level.getFirstRegularRatio());
        payload.put("notFirstRegularRatio", level.getNotFirstRegularRatio());
        payload.put("firstRewardRatio", level.getFirstRewardRatio());
        payload.put("notFirstRewardRatio", level.getNotFirstRewardRatio());
        payload.put("firstRandomRadio", level.getFirstRandomRadio());
        payload.put("notFirstRandomRadio", level.getNotFirstRandomRadio());
        payload.put("styleType", level.getStyleType());
        payload.put("orderNumber", updatedOrderNumber);

        mockMvc.perform(post("/clerk/level/update")
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(payload.toString()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200));

        ensureTenantContext();
        PlayClerkLevelInfoEntity reloaded = clerkLevelInfoService.getById(level.getId());
        assertThat(reloaded.getOrderNumber()).isEqualTo(updatedOrderNumber);
    }

    @Test
    // 场景：更新时清空排序号，接口应允许写入null并持久化。
    void updateLevelAllowsClearingOrderNumber() throws Exception {
        ensureTenantContext();
        PlayClerkLevelInfoEntity level = createClerkLevel("clear", 5L, 85);

        ObjectNode payload = objectMapper.createObjectNode();
        payload.put("id", level.getId());
        payload.put("name", level.getName());
        payload.put("firstRegularRatio", level.getFirstRegularRatio());
        payload.put("notFirstRegularRatio", level.getNotFirstRegularRatio());
        payload.put("firstRewardRatio", level.getFirstRewardRatio());
        payload.put("notFirstRewardRatio", level.getNotFirstRewardRatio());
        payload.put("firstRandomRadio", level.getFirstRandomRadio());
        payload.put("notFirstRandomRadio", level.getNotFirstRandomRadio());
        payload.put("styleType", level.getStyleType());
        payload.putNull("orderNumber");

        mockMvc.perform(post("/clerk/level/update")
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(payload.toString()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200));

        ensureTenantContext();
        PlayClerkLevelInfoEntity reloaded = clerkLevelInfoService.getById(level.getId());
        assertThat(reloaded.getOrderNumber()).isNull();
    }

    private void ensureTenantContext() {
        SecurityUtils.setTenantId(DEFAULT_TENANT);
    }

    private PlayClerkLevelInfoEntity createClerkLevel(String suffix, Long orderNumber, int levelValue) {
        ensureTenantContext();
        String levelId = IdUtils.getUuid();
        PlayClerkLevelInfoEntity level = new PlayClerkLevelInfoEntity();
        level.setId(levelId);
        level.setTenantId(DEFAULT_TENANT);
        level.setName("API测试等级-" + suffix);
        level.setLevel(levelValue);
        level.setFirstRegularRatio(60);
        level.setNotFirstRegularRatio(50);
        level.setFirstRewardRatio(45);
        level.setNotFirstRewardRatio(35);
        level.setFirstRandomRadio(55);
        level.setNotFirstRandomRadio(40);
        level.setStyleType(levelValue);
        level.setOrderNumber(orderNumber);
        clerkLevelInfoService.save(level);
        levelIdsToCleanup.add(levelId);
        return level;
    }

    private String createClerk(String suffix, String levelId, String onlineState) {
        ensureTenantContext();
        String clerkId = IdUtils.getUuid();
        PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity();
        clerk.setId(clerkId);
        clerk.setTenantId(DEFAULT_TENANT);
        clerk.setNickname("API测试店员-" + suffix);
        clerk.setLevelId(levelId);
        clerk.setClerkState("1");
        clerk.setOnboardingState("1");
        clerk.setListingState("1");
        clerk.setDisplayState("1");
        clerk.setRecommendationState("0");
        clerk.setPinToTopState("0");
        clerk.setRandomOrderState("1");
        clerk.setFixingLevel("0");
        clerk.setOnlineState(onlineState);
        clerk.setPhone("138" + String.format("%08d", ThreadLocalRandom.current().nextInt(0, 100_000_000)));
        clerk.setOpenid("openid-" + suffix + "-" + IdUtils.getUuid());
        clerk.setWeiChatCode("wx-code-" + suffix);
        clerk.setWeiChatAvatar("https://example.com/avatar/" + suffix);
        clerk.setTypeId("api-type");
        clerk.setProvince("API省");
        clerk.setCity("API市");
        clerk.setEntryTime(LocalDateTime.now());
        clerk.setAddTime(LocalDateTime.now());
        clerkUserInfoService.save(clerk);
        clerkIdsToCleanup.add(clerkId);
        return clerkId;
    }
    @Test
    void listOrderingStableWithMultipleCriteria() throws Exception {
        ensureTenantContext();
        PlayClerkLevelInfoEntity level = createClerkLevel("stable", 10L, 80);

        String filterToken = "stable-" + IdUtils.getUuid().substring(0, 6);
        String pinnedOnline = createClerk(filterToken + "-pinned-online", level.getId(), "1");
        togglePin(pinnedOnline, "1");
        String pinnedOffline = createClerk(filterToken + "-pinned-offline", level.getId(), "0");
        togglePin(pinnedOffline, "1");
        String online1 = createClerk(filterToken + "-online-one", level.getId(), "1");
        pause(50);
        String online2 = createClerk(filterToken + "-online-two", level.getId(), "1");
        String offline = createClerk(filterToken + "-offline", level.getId(), "0");

        MvcResult result = mockMvc.perform(get("/clerk/user/list")
                        .param("pageNum", "1")
                        .param("pageSize", "20")
                        .param("nickname", filterToken)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER))
                .andExpect(status().isOk())
                .andReturn();

        JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
        JsonNode records = root.path("data");
        assertThat(records.isArray()).isTrue();
        List<String> orderedIds = new ArrayList<>();
        for (JsonNode record : records) {
            orderedIds.add(record.path("id").asText());
        }

        Map<String, ClerkScenario> expectedScenarios = new HashMap<>();
        expectedScenarios.put(pinnedOnline, new ClerkScenario(pinnedOnline, 1L, true, true, 0));
        expectedScenarios.put(online1, new ClerkScenario(online1, 1L, true, false, 1));
        expectedScenarios.put(online2, new ClerkScenario(online2, 1L, true, false, 2));
        expectedScenarios.put(pinnedOffline, new ClerkScenario(pinnedOffline, 1L, false, true, 3));
        expectedScenarios.put(offline, new ClerkScenario(offline, 1L, false, false, 4));

        List<ClerkScenario> actualScenarios = orderedIds.stream()
                .map(expectedScenarios::get)
                .collect(Collectors.toList());

        for (int i = 1; i < actualScenarios.size(); i++) {
            ClerkScenario previous = actualScenarios.get(i - 1);
            ClerkScenario current = actualScenarios.get(i);
            assertThat(previous).isNotNull();
            assertThat(current).isNotNull();
            assertThat(BACKEND_ORDERING.compare(previous, current))
                    .withFailMessage("Ordering violation between %s and %s, list=%s", previous.getId(), current.getId(),
                            orderedIds)
                    .isLessThanOrEqualTo(0);
        }
    }

    @Test
    void listOrderingHandlesBulkDataset() throws Exception {
        ensureTenantContext();
        String token = "bulk-" + IdUtils.getUuid().substring(0, 6);
        PlayClerkLevelInfoEntity gold = createClerkLevel(token + "-gold", 1L, 90);
        PlayClerkLevelInfoEntity silver = createClerkLevel(token + "-silver", 2L, 80);
        PlayClerkLevelInfoEntity iron = createClerkLevel(token + "-iron", 3L, 70);

        List<ClerkScenario> scenarios = new ArrayList<>();
        scenarios.add(buildScenario(token, "G-Pin-ON", gold, true, true));
        scenarios.add(buildScenario(token, "G-UnPin-ON", gold, true, false));
        scenarios.add(buildScenario(token, "G-Pin-Off", gold, false, true));
        scenarios.add(buildScenario(token, "G-UnPin-Off", gold, false, false));
        scenarios.add(buildScenario(token, "S-Pin-ON", silver, true, true));
        scenarios.add(buildScenario(token, "S-UnPin-ON", silver, true, false));
        scenarios.add(buildScenario(token, "S-Pin-Off", silver, false, true));
        scenarios.add(buildScenario(token, "S-UnPin-Off", silver, false, false));
        scenarios.add(buildScenario(token, "I-Pin-ON", iron, true, true));
        scenarios.add(buildScenario(token, "I-UnPin-ON", iron, true, false));
        scenarios.add(buildScenario(token, "I-Pin-Off", iron, false, true));
        scenarios.add(buildScenario(token, "I-UnPin-Off", iron, false, false));

        MvcResult result = mockMvc.perform(get("/clerk/user/list")
                        .param("pageNum", "1")
                        .param("pageSize", "80")
                        .param("nickname", token)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(USER_HEADER, DEFAULT_USER))
                .andExpect(status().isOk())
                .andReturn();

        JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
        JsonNode records = root.path("data");
        List<String> orderedIds = new ArrayList<>();
        for (JsonNode record : records) {
            orderedIds.add(record.path("id").asText());
        }

        Map<String, ClerkScenario> scenarioById = scenarios.stream()
                .collect(Collectors.toMap(ClerkScenario::getId, scenario -> scenario));

        assertThat(orderedIds).containsExactlyInAnyOrderElementsOf(scenarioById.keySet());

        List<ClerkScenario> orderedScenarios = orderedIds.stream()
                .map(scenarioById::get)
                .collect(Collectors.toList());
        for (int i = 1; i < orderedScenarios.size(); i++) {
            ClerkScenario previous = orderedScenarios.get(i - 1);
            ClerkScenario current = orderedScenarios.get(i);
            assertThat(BACKEND_ORDERING.compare(previous, current))
                    .withFailMessage("Ordering violation between %s and %s, list=%s",
                            previous.getId(), current.getId(), orderedIds)
                    .isLessThanOrEqualTo(0);
        }
    }


    private ClerkScenario buildScenario(String token, String suffix, PlayClerkLevelInfoEntity level, boolean online, boolean pinned) {
        String id = createClerk(token + "-" + suffix, level.getId(), online ? "1" : "0");
        if (pinned) {
            togglePin(id, "1");
        }
        long levelOrder = level.getOrderNumber() == null ? Long.MAX_VALUE : level.getOrderNumber();
        ClerkScenario scenario = new ClerkScenario(id, levelOrder, online, pinned, scenarioSequence++);
        pause(15);
        return scenario;
    }

    private void togglePin(String clerkId, String pinState) {
        ensureTenantContext();
        PlayClerkUserInfoEntity update = new PlayClerkUserInfoEntity();
        update.setId(clerkId);
        update.setPinToTopState(pinState);
        clerkUserInfoService.update(update);
    }

    private void pause(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
    }

    private static class ClerkScenario {
        private final String id;
        private final long levelOrder;
        private final boolean online;
        private final boolean pinned;
        private final int sequence;

        ClerkScenario(String id, long levelOrder, boolean online, boolean pinned, int sequence) {
            this.id = id;
            this.levelOrder = levelOrder;
            this.online = online;
            this.pinned = pinned;
            this.sequence = sequence;
        }

        String getId() {
            return id;
        }

        long getLevelOrder() {
            return levelOrder;
        }

        boolean isOnline() {
            return online;
        }

        boolean isPinned() {
            return pinned;
        }

        int getSequence() {
            return sequence;
        }
    }
}
