package com.starry.admin.api;

import static org.hamcrest.Matchers.nullValue;
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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
import com.starry.admin.modules.withdraw.enums.EarningsType;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MvcResult;

class AdminWithdrawalControllerApiTest extends AbstractApiTest {

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Autowired
    private IEarningsService earningsService;
    @Autowired
    private IWithdrawalService withdrawalService;
    @Autowired
    private IPlayOrderInfoService orderInfoService;
    @Autowired
    private IPlayClerkUserInfoService clerkUserInfoService;
    @Autowired
    private IPlayCustomUserInfoService customUserInfoService;

    private final ObjectMapper objectMapper = new ObjectMapper();
    private final List<String> earningsToCleanup = new ArrayList<>();
    private final List<String> withdrawalsToCleanup = new ArrayList<>();
    private final List<String> ordersToCleanup = new ArrayList<>();

    @AfterEach
    void tearDown() {
        if (!earningsToCleanup.isEmpty()) {
            earningsService.removeByIds(earningsToCleanup);
            earningsToCleanup.clear();
        }
        if (!withdrawalsToCleanup.isEmpty()) {
            withdrawalService.removeByIds(withdrawalsToCleanup);
            withdrawalsToCleanup.clear();
        }
        if (!ordersToCleanup.isEmpty()) {
            orderInfoService.removeByIds(ordersToCleanup);
            ordersToCleanup.clear();
        }
    }

    @Test
    void auditReturnsEarningLinesWithOrderDetails() throws Exception {
        ensureTenantContext();
        ensureProfileFixtures();

        PlayClerkUserInfoEntity expectedClerk = clerkUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        PlayCustomUserInfoEntity expectedCustomer = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        Assertions.assertThat(expectedClerk).as("default clerk fixture missing").isNotNull();
        Assertions.assertThat(expectedCustomer).as("default customer fixture missing").isNotNull();
        String expectedClerkNickname = expectedClerk.getNickname();
        String expectedCustomerNickname = expectedCustomer.getNickname();

        PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(2));
        WithdrawalRequestEntity withdrawal = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("88.60"));

        LocalDateTime now = LocalDateTime.now();
        seedEarningLine(withdrawal.getId(), order.getId(), new BigDecimal("50.30"), "withdrawn", now.minusMinutes(30), EarningsType.ORDER);
        seedEarningLine(withdrawal.getId(), null, new BigDecimal("38.30"), "withdrawing", now.minusMinutes(10), EarningsType.ORDER);

        MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + withdrawal.getId() + "/audit")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data").isArray())
                .andExpect(jsonPath("$.data.length()").value(2))
                .andReturn();

        JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");

        boolean foundOrder = false;
        boolean foundMissing = false;
        for (JsonNode node : data) {
            String orderNo = node.path("orderNo").isMissingNode() ? null : node.path("orderNo").asText(null);
            if (order.getOrderNo().equals(orderNo)) {
                Assertions.assertThat(node.path("orderStatus").asText()).isEqualTo(order.getOrderStatus());
                Assertions.assertThat(node.path("earningType").asText()).isEqualTo(EarningsType.ORDER.name());
                Assertions.assertThat(node.path("orderClerkId").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
                Assertions.assertThat(normalizeUtf8(node.path("orderClerkNickname").asText())).isEqualTo(expectedClerkNickname);
                Assertions.assertThat(node.path("orderCustomerId").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
                Assertions.assertThat(normalizeUtf8(node.path("orderCustomerNickname").asText())).isEqualTo(expectedCustomerNickname);
                foundOrder = true;
            }
            if (node.path("orderNo").isNull()) {
                foundMissing = true;
            }
        }
        Assertions.assertThat(foundOrder).isTrue();
        Assertions.assertThat(foundMissing).isTrue();
    }

    @Test
    void auditRejectsMissingRequest() throws Exception {
        ensureTenantContext();

        mockMvc.perform(get("/admin/withdraw/requests/missing-request/audit")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(500))
                .andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
    }

    @Test
    void auditRejectsCrossTenantAccess() throws Exception {
        ensureTenantContext();
        WithdrawalRequestEntity outsider = seedWithdrawal("tenant-other", new BigDecimal("30.00"));

        mockMvc.perform(get("/admin/withdraw/requests/" + outsider.getId() + "/audit")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(500))
                .andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
    }

    @Test
    void auditReturnsEmptyListWhenNoEarningsFound() throws Exception {
        ensureTenantContext();
        WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("22.00"));

        mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.length()").value(0));
    }

    @Test
    void auditHandlesMissingOrderRecordsGracefully() throws Exception {
        ensureTenantContext();
        WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("40.00"));
        String orphanOrderId = "order-orphan-" + IdUtils.getUuid();
        seedEarningLine(request.getId(), orphanOrderId, new BigDecimal("15.00"), "withdrawn", LocalDateTime.now().minusMinutes(5), EarningsType.ORDER);

        mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data[0].orderNo").value(nullValue()))
                .andExpect(jsonPath("$.data[0].orderId").value(orphanOrderId))
                .andExpect(jsonPath("$.data[0].orderClerkId").value(nullValue()))
                .andExpect(jsonPath("$.data[0].orderClerkNickname").value(nullValue()))
                .andExpect(jsonPath("$.data[0].orderCustomerId").value(nullValue()))
                .andExpect(jsonPath("$.data[0].orderCustomerNickname").value(nullValue()));
    }

    @Test
    void auditReturnsLinesSortedByCreatedTime() throws Exception {
        ensureTenantContext();
        PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(1));
        WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("90.00"));
        LocalDateTime now = LocalDateTime.now();
        String firstId = seedEarningLine(request.getId(), order.getId(), new BigDecimal("10"), "withdrawn", now.minusMinutes(20), EarningsType.ORDER);
        Thread.sleep(5L);
        String secondId = seedEarningLine(request.getId(), null, new BigDecimal("20"), "withdrawn", now.minusMinutes(10), EarningsType.ORDER);
        Thread.sleep(5L);
        String thirdId = seedEarningLine(request.getId(), null, new BigDecimal("30"), "withdrawn", now.minusMinutes(5), EarningsType.ORDER);

        MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.length()").value(3))
                .andReturn();

        JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");
        List<String> ids = new ArrayList<>();
        LocalDateTime previous = null;
        for (JsonNode node : data) {
            ids.add(node.get("id").asText());
            String createdText = node.path("createdTime").asText();
            if (createdText != null && !createdText.isEmpty()) {
                LocalDateTime created = LocalDateTime.parse(createdText, DATE_TIME_FORMATTER);
                if (previous != null) {
                    Assertions.assertThat(created.isBefore(previous)).isFalse();
                }
                previous = created;
            }
        }
        Assertions.assertThat(ids).containsExactlyInAnyOrder(firstId, secondId, thirdId);
    }

    @Test
    void auditSupportsCommissionEarnings() throws Exception {
        ensureTenantContext();
        WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("55.00"));
        seedEarningLine(request.getId(), null, new BigDecimal("55.00"), "withdrawn", LocalDateTime.now().minusMinutes(2), EarningsType.COMMISSION);

        mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data[0].earningType").value(EarningsType.COMMISSION.name()));
    }

    @SuppressWarnings("deprecation")
    private PlayOrderInfoEntity seedOrder(LocalDateTime endTime) {
        PlayOrderInfoEntity order = new PlayOrderInfoEntity();
        String id = "order-audit-" + IdUtils.getUuid();
        order.setId(id);
        order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        order.setOrderNo("ORD-" + System.currentTimeMillis());
        order.setOrderStatus("3");
        order.setOrderType("2");
        order.setPlaceType("0");
        order.setRewardType("0");
        order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
        order.setOrderMoney(new BigDecimal("120.50"));
        order.setFinalAmount(order.getOrderMoney());
        order.setEstimatedRevenue(new BigDecimal("80.25"));
        order.setOrderSettlementState("1");
        order.setOrderEndTime(endTime);
        order.setOrderSettlementTime(endTime);
        Date nowDate = toDate(LocalDateTime.now());
        order.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        order.setCreatedTime(nowDate);
        order.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        order.setUpdatedTime(nowDate);
        order.setDeleted(false);
        orderInfoService.save(order);
        ordersToCleanup.add(id);
        return order;
    }

    private WithdrawalRequestEntity seedWithdrawal(String tenantId, BigDecimal amount) {
        WithdrawalRequestEntity entity = new WithdrawalRequestEntity();
        String id = "withdraw-audit-" + IdUtils.getUuid();
        entity.setId(id);
        entity.setTenantId(tenantId);
        entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        entity.setAmount(amount);
        entity.setFee(BigDecimal.ZERO);
        entity.setNetAmount(amount);
        entity.setDestAccount("alipay:audit@test.com");
        entity.setStatus("processing");
        entity.setPayeeSnapshot("{\"displayName\":\"审计专用\"}");
        Date nowDate = toDate(LocalDateTime.now());
        entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        entity.setCreatedTime(nowDate);
        entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        entity.setUpdatedTime(nowDate);
        withdrawalService.save(entity);
        withdrawalsToCleanup.add(id);
        return entity;
    }

    private String seedEarningLine(String withdrawalId, String orderId, BigDecimal amount, String status, LocalDateTime createdAt, EarningsType earningType) {
        EarningsLineEntity entity = new EarningsLineEntity();
        String id = "earn-audit-" + IdUtils.getUuid();
        entity.setId(id);
        entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        String resolvedOrderId = orderId != null ? orderId : "order-missing-" + IdUtils.getUuid();
        entity.setOrderId(resolvedOrderId);
        entity.setAmount(amount);
        entity.setStatus(status);
        entity.setEarningType(earningType);
        entity.setUnlockTime(createdAt.minusHours(1));
        entity.setWithdrawalId(withdrawalId);
        Date createdDate = toDate(createdAt);
        entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        entity.setCreatedTime(createdDate);
        entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        entity.setUpdatedTime(createdDate);
        entity.setDeleted(false);
        earningsService.save(entity);
        earningsToCleanup.add(id);
        return id;
    }

    private void ensureProfileFixtures() {
        ensureTenantContext();
        ensureClerkFixture();
        ensureCustomerFixture();
    }

    private void ensureClerkFixture() {
        PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        if (clerk != null) {
            return;
        }
        PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
        entity.setId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
        entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        entity.setSysUserId(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        entity.setOpenid(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID);
        entity.setNickname("小测官");
        entity.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
        entity.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
        entity.setFixingLevel("1");
        entity.setSex("2");
        entity.setPhone("13900000001");
        entity.setWeiChatCode("apitest-clerk");
        entity.setAvatar("https://example.com/avatar.png");
        entity.setAccountBalance(BigDecimal.ZERO);
        entity.setOnboardingState("1");
        entity.setListingState("1");
        entity.setDisplayState("1");
        entity.setOnlineState("1");
        entity.setRandomOrderState("1");
        entity.setClerkState("1");
        entity.setEntryTime(LocalDateTime.now());
        entity.setToken("apitest-clerk-token");
        entity.setDeleted(false);
        clerkUserInfoService.save(entity);
    }

    private void ensureCustomerFixture() {
        PlayCustomUserInfoEntity customer = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        if (customer != null) {
            return;
        }
        PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
        entity.setId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        entity.setOpenid("openid-customer-apitest");
        entity.setUnionid("unionid-customer-apitest");
        entity.setNickname("测试顾客");
        entity.setSex(1);
        entity.setPhone("13700000002");
        entity.setWeiChatCode("apitest-customer");
        entity.setAccountBalance(new BigDecimal("200.00"));
        entity.setAccumulatedRechargeAmount(new BigDecimal("200.00"));
        entity.setAccumulatedConsumptionAmount(BigDecimal.ZERO);
        entity.setAccountState("1");
        entity.setSubscribeState("1");
        entity.setPurchaseState("1");
        entity.setMobilePhoneState("1");
        Date now = new Date();
        entity.setRegistrationTime(now);
        entity.setLastLoginTime(now);
        entity.setToken("apitest-customer-token");
        entity.setDeleted(false);
        customUserInfoService.save(entity);
    }

    private void ensureTenantContext() {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
    }

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

    private String normalizeUtf8(String value) {
        if (value == null) {
            return null;
        }
        boolean convertible = value.chars().allMatch(ch -> ch <= 0xFF);
        if (!convertible) {
            return value;
        }
        byte[] bytes = value.getBytes(StandardCharsets.ISO_8859_1);
        return new String(bytes, StandardCharsets.UTF_8);
    }
}
