package com.ruoyi.web.modules.home.controller;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
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 java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.util.NestedServletException;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.web.modules.home.constant.SearchAppConstants;
import com.ruoyi.web.modules.home.service.ISearchAppService;
import com.ruoyi.web.modules.home.vo.HomeHotGoodsVO;
import com.ruoyi.web.modules.home.vo.SearchGoodsPageVO;
import com.ruoyi.web.modules.home.vo.SearchResultAppVO;
import com.ruoyi.web.modules.home.vo.SearchShopAppVO;
import com.ruoyi.web.modules.home.vo.SearchShopPageVO;

/**
 * C 端全站搜索接口测试（CSR-API-001～026 核心）
 */
@ExtendWith(MockitoExtension.class)
class SearchAppControllerTest
{
    @Mock
    private ISearchAppService searchAppService;

    @InjectMocks
    private SearchAppController controller;

    private MockMvc mockMvc;

    @BeforeEach
    void setUp()
    {
        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    }

    @Test
    void search_returns200WithoutToken() throws Exception
    {
        when(searchAppService.search(any())).thenReturn(sampleResult());

        mockMvc.perform(get("/api/search").param("keyword", "阿莫"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.goods.total").value(1))
                .andExpect(jsonPath("$.data.goods.rows[0].goodsId").value(1))
                .andExpect(jsonPath("$.data.shops.total").value(1))
                .andExpect(jsonPath("$.data.shops.rows[0].shopId").value(101));
    }

    @Test
    void search_responseContainsGoodsAndShops() throws Exception
    {
        when(searchAppService.search(any())).thenReturn(sampleResult());

        mockMvc.perform(get("/api/search").param("keyword", "阿莫"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.goods.rows[0].goodsName").value("阿莫西林"))
                .andExpect(jsonPath("$.data.goods.rows[0].mainPic").value("/pic/g1.jpg"))
                .andExpect(jsonPath("$.data.goods.rows[0].salePrice").value(88.00))
                .andExpect(jsonPath("$.data.goods.rows[0].shopName").value("阿莫兽药店"))
                .andExpect(jsonPath("$.data.shops.rows[0].shopName").value("阿莫兽药店"))
                .andExpect(jsonPath("$.data.shops.rows[0].shopAvatar").value("/avatar/s.jpg"));
    }

    @Test
    void search_missingKeywordRejected()
    {
        when(searchAppService.search(any()))
                .thenThrow(new ServiceException(SearchAppConstants.MSG_KEYWORD_REQUIRED));

        NestedServletException ex = assertThrows(NestedServletException.class,
                () -> mockMvc.perform(get("/api/search")).andReturn());
        assertInstanceOf(ServiceException.class, ex.getCause());
    }

    @Test
    void search_blankKeywordRejected()
    {
        when(searchAppService.search(any()))
                .thenThrow(new ServiceException(SearchAppConstants.MSG_KEYWORD_REQUIRED));

        NestedServletException ex = assertThrows(NestedServletException.class,
                () -> mockMvc.perform(get("/api/search").param("keyword", "   ")).andReturn());
        assertInstanceOf(ServiceException.class, ex.getCause());
    }

    @Test
    void search_invalidSortRejected()
    {
        when(searchAppService.search(any()))
                .thenThrow(new ServiceException(SearchAppConstants.MSG_SORT_INVALID));

        NestedServletException ex = assertThrows(NestedServletException.class,
                () -> mockMvc.perform(get("/api/search")
                        .param("keyword", "阿莫")
                        .param("sortBy", "foo")).andReturn());
        assertInstanceOf(ServiceException.class, ex.getCause());
    }

    @Test
    void search_emptyGoodsAndShops() throws Exception
    {
        SearchResultAppVO result = new SearchResultAppVO();
        result.getGoods().setTotal(0);
        result.getGoods().setRows(Collections.emptyList());
        result.getShops().setTotal(0);
        result.getShops().setRows(Collections.emptyList());
        when(searchAppService.search(any())).thenReturn(result);

        mockMvc.perform(get("/api/search").param("keyword", "不存在xyz999"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.goods.total").value(0))
                .andExpect(jsonPath("$.data.goods.rows").isEmpty())
                .andExpect(jsonPath("$.data.shops.total").value(0))
                .andExpect(jsonPath("$.data.shops.rows").isEmpty());
    }

    @Test
    void search_goodsEmptyShopsPresent() throws Exception
    {
        SearchResultAppVO result = new SearchResultAppVO();
        result.getGoods().setTotal(0);
        result.getGoods().setRows(Collections.emptyList());
        SearchShopPageVO shops = new SearchShopPageVO();
        shops.setTotal(1);
        shops.setRows(Collections.singletonList(shop(101L)));
        result.setShops(shops);
        when(searchAppService.search(any())).thenReturn(result);

        mockMvc.perform(get("/api/search").param("keyword", "阿莫"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.goods.total").value(0))
                .andExpect(jsonPath("$.data.shops.total").value(1));
    }

    @Test
    void search_multipleGoodsPreservedOrder() throws Exception
    {
        SearchResultAppVO result = new SearchResultAppVO();
        SearchGoodsPageVO goods = new SearchGoodsPageVO();
        goods.setTotal(2);
        goods.setRows(Arrays.asList(goods(1L), goods(2L)));
        result.setGoods(goods);
        result.getShops().setTotal(0);
        when(searchAppService.search(any())).thenReturn(result);

        mockMvc.perform(get("/api/search").param("keyword", "阿莫"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.goods.rows.length()").value(2))
                .andExpect(jsonPath("$.data.goods.rows[0].goodsId").value(1))
                .andExpect(jsonPath("$.data.goods.rows[1].goodsId").value(2));
    }

    @Test
    void search_passesPaginationParams() throws Exception
    {
        when(searchAppService.search(any())).thenReturn(sampleResult());

        mockMvc.perform(get("/api/search")
                .param("keyword", "阿莫")
                .param("sortBy", "price_asc")
                .param("goodsPageNum", "2")
                .param("goodsPageSize", "5")
                .param("shopPageNum", "1")
                .param("shopPageSize", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200));
    }

    private static SearchResultAppVO sampleResult()
    {
        SearchResultAppVO result = new SearchResultAppVO();
        SearchGoodsPageVO goods = new SearchGoodsPageVO();
        goods.setTotal(1);
        goods.setRows(Collections.singletonList(goods(1L)));
        result.setGoods(goods);
        SearchShopPageVO shops = new SearchShopPageVO();
        shops.setTotal(1);
        shops.setRows(Collections.singletonList(shop(101L)));
        result.setShops(shops);
        return result;
    }

    private static HomeHotGoodsVO goods(Long goodsId)
    {
        HomeHotGoodsVO vo = new HomeHotGoodsVO();
        vo.setGoodsId(goodsId);
        vo.setGoodsSn("G00" + goodsId);
        vo.setGoodsName("阿莫西林");
        vo.setMainPic("/pic/g1.jpg");
        vo.setSalePrice(new BigDecimal("88.00"));
        vo.setShopId(101L);
        vo.setShopName("阿莫兽药店");
        return vo;
    }

    private static SearchShopAppVO shop(Long shopId)
    {
        SearchShopAppVO vo = new SearchShopAppVO();
        vo.setShopId(shopId);
        vo.setShopName("阿莫兽药店");
        vo.setShopAvatar("/avatar/s.jpg");
        vo.setShopStatus("0");
        return vo;
    }
}
