package com.ruoyi.web.modules.goods.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.mockito.ArgumentCaptor;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
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 com.ruoyi.common.exception.ServiceException;
import com.ruoyi.web.modules.category.constant.CategoryConstants;
import com.ruoyi.web.modules.category.domain.BizGoodsCategory;
import com.ruoyi.web.modules.category.facade.ICategoryFacade;
import com.ruoyi.web.modules.category.mapper.BizGoodsCategoryMapper;
import com.ruoyi.web.modules.goodsservice.facade.IGoodsServiceFacade;
import com.ruoyi.web.modules.goodsservice.mapper.BizGoodsServiceMapper;
import com.ruoyi.web.modules.goods.constant.GoodsConstants;
import com.ruoyi.web.modules.goods.domain.BizGoods;
import com.ruoyi.web.modules.goods.domain.BizGoodsPic;
import com.ruoyi.web.modules.goods.dto.GoodsAuditDTO;
import com.ruoyi.web.modules.goods.dto.GoodsAttrItemDTO;
import com.ruoyi.web.modules.goods.dto.GoodsBatchIdsDTO;
import com.ruoyi.web.modules.goods.dto.GoodsOffShelfDTO;
import com.ruoyi.web.modules.goods.dto.GoodsSaveDTO;
import com.ruoyi.web.modules.goods.exception.GoodsBatchOperationException;
import com.ruoyi.web.modules.goods.facade.IGoodsOrderFacade;
import com.ruoyi.web.modules.freight.constant.FreightTemplateConstants;
import com.ruoyi.web.modules.freight.domain.BizShopFreightTemplate;
import com.ruoyi.web.modules.freight.facade.IFreightTemplateFacade;
import com.ruoyi.web.modules.freight.mapper.BizShopFreightTemplateMapper;
import com.ruoyi.web.modules.goods.domain.BizGoodsFreight;
import com.ruoyi.web.modules.goods.mapper.BizGoodsAttrMapper;
import com.ruoyi.web.modules.goods.mapper.BizGoodsFreightMapper;
import com.ruoyi.web.modules.goods.mapper.BizGoodsMapper;
import com.ruoyi.web.modules.goods.mapper.BizGoodsPicMapper;
import com.ruoyi.web.modules.goods.mapper.BizGoodsServiceSnapshotMapper;
import com.ruoyi.web.modules.goods.vo.GoodsDetailVO;
import com.ruoyi.web.modules.goods.service.impl.GoodsServiceImpl;
import com.ruoyi.web.modules.goods.support.GoodsSnGenerator;
import com.ruoyi.web.modules.stock.service.IStockInboundFacade;
import com.ruoyi.web.modules.stock.service.IStockOutboundFacade;
import com.ruoyi.web.modules.stock.support.StockSupport;
import com.ruoyi.web.modules.merchant.mapper.BizMerchantMapper;
import com.ruoyi.web.modules.store.constant.ShopConstants;
import com.ruoyi.web.modules.store.domain.BizShop;
import com.ruoyi.web.modules.store.facade.IShopGlobalConfigFacade;
import com.ruoyi.web.modules.store.mapper.BizShopMapper;
import com.ruoyi.web.modules.template.domain.BizGoodsAttrTemplate;
import com.ruoyi.web.modules.template.mapper.BizGoodsAttrTemplateMapper;

@ExtendWith(MockitoExtension.class)
class GoodsServiceImplTest
{
    @Mock
    private BizGoodsMapper goodsMapper;

    @Mock
    private BizGoodsServiceSnapshotMapper snapshotMapper;

    @Mock
    private BizGoodsCategoryMapper categoryMapper;

    @Mock
    private BizShopMapper shopMapper;

    @Mock
    private BizGoodsServiceMapper goodsServiceMapper;

    @Mock
    private BizMerchantMapper merchantMapper;

    @Mock
    private ICategoryFacade categoryFacade;

    @Mock
    private IGoodsServiceFacade goodsServiceFacade;

    @Mock
    private GoodsSnGenerator goodsSnGenerator;

    @Mock
    private IShopGlobalConfigFacade shopGlobalConfigFacade;

    @Mock
    private IGoodsOrderFacade goodsOrderFacade;

    @Mock
    private BizGoodsAttrTemplateMapper attrTemplateMapper;

    @Mock
    private BizGoodsAttrMapper goodsAttrMapper;

    @Mock
    private BizGoodsPicMapper goodsPicMapper;

    @Mock
    private BizGoodsFreightMapper goodsFreightMapper;

    @Mock
    private BizShopFreightTemplateMapper freightTemplateMapper;

    @Mock
    private IFreightTemplateFacade freightTemplateFacade;

    @Mock
    private IStockInboundFacade stockInboundFacade;

    @Mock
    private IStockOutboundFacade stockOutboundFacade;

    @Mock
    private StockSupport stockSupport;

    @InjectMocks
    private GoodsServiceImpl goodsService;

    @Test
    void submit_fromDraft_needAudit_becomesPending()
    {
        BizGoods goods = draftGoods(1L, 10L);
        when(goodsMapper.selectByIdAndShop(1L, 10L)).thenReturn(goods);
        BizShop shop = new BizShop();
        when(shopMapper.selectById(10L)).thenReturn(shop);
        when(shopGlobalConfigFacade.getDefaultAuditPass()).thenReturn(ShopConstants.AUDIT_PASS_NO);

        Map<String, Object> data = goodsService.submitGoods(10L, 1L, "seller");

        assertEquals(GoodsConstants.STATUS_PENDING, data.get("goodsStatus"));
        verify(goodsMapper).updateSubmitStatus(eq(1L), eq(GoodsConstants.STATUS_PENDING), any(Date.class),
                eq(true), eq("seller"));
    }

    @Test
    void submit_fromDraft_autoPass_becomesOnSale()
    {
        BizGoods goods = draftGoods(2L, 10L);
        when(goodsMapper.selectByIdAndShop(2L, 10L)).thenReturn(goods);
        BizShop shop = new BizShop();
        when(shopMapper.selectById(10L)).thenReturn(shop);
        when(shopGlobalConfigFacade.getDefaultAuditPass()).thenReturn(ShopConstants.AUDIT_PASS_YES);

        Map<String, Object> data = goodsService.submitGoods(10L, 2L, "seller");

        assertEquals(GoodsConstants.STATUS_ON_SALE, data.get("goodsStatus"));
    }

    @Test
    void submit_fromOnSale_rejected()
    {
        BizGoods goods = draftGoods(3L, 10L);
        goods.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);
        when(goodsMapper.selectByIdAndShop(3L, 10L)).thenReturn(goods);

        assertThrows(ServiceException.class, () -> goodsService.submitGoods(10L, 3L, "seller"));
    }

    @Test
    void submit_resubmit_keepsSubmitTime()
    {
        Date t1 = new Date(1_000_000L);
        BizGoods goods = draftGoods(4L, 10L);
        goods.setGoodsStatus(GoodsConstants.STATUS_OFF_SHELF);
        goods.setSubmitTime(t1);
        when(goodsMapper.selectByIdAndShop(4L, 10L)).thenReturn(goods);
        BizShop shop = new BizShop();
        when(shopMapper.selectById(10L)).thenReturn(shop);
        when(shopGlobalConfigFacade.getDefaultAuditPass()).thenReturn(ShopConstants.AUDIT_PASS_NO);

        goodsService.submitGoods(10L, 4L, "seller");

        verify(goodsMapper).updateSubmitStatus(eq(4L), eq(GoodsConstants.STATUS_PENDING), eq(t1), eq(false),
                eq("seller"));
    }

    @Test
    void audit_pass_updatesOnSale()
    {
        BizGoods pending = draftGoods(11L, 10L);
        pending.setGoodsStatus(GoodsConstants.STATUS_PENDING);
        when(goodsMapper.selectById(11L)).thenReturn(pending);
        when(goodsMapper.updateAuditPass(11L, "admin")).thenReturn(1);

        GoodsAuditDTO dto = new GoodsAuditDTO();
        dto.setGoodsIds(Collections.singletonList(11L));
        dto.setPass(true);
        goodsService.auditGoods(dto, "admin");

        verify(goodsMapper).updateAuditPass(11L, "admin");
    }

    @Test
    void audit_reject_writesReason()
    {
        BizGoods pending = draftGoods(12L, 10L);
        pending.setGoodsStatus(GoodsConstants.STATUS_PENDING);
        when(goodsMapper.selectById(12L)).thenReturn(pending);
        when(goodsMapper.updateAuditReject(12L, "资料不全", "admin")).thenReturn(1);

        GoodsAuditDTO dto = new GoodsAuditDTO();
        dto.setGoodsIds(Collections.singletonList(12L));
        dto.setPass(false);
        dto.setRejectReason("资料不全");
        goodsService.auditGoods(dto, "admin");

        verify(goodsMapper).updateAuditReject(12L, "资料不全", "admin");
    }

    @Test
    void audit_emptyGoodsIds_fails()
    {
        GoodsAuditDTO dto = new GoodsAuditDTO();
        dto.setGoodsIds(Collections.emptyList());
        dto.setPass(true);

        assertThrows(ServiceException.class, () -> goodsService.auditGoods(dto, "admin"));
    }

    @Test
    void audit_goodsNotFound_batchFail()
    {
        when(goodsMapper.selectById(99L)).thenReturn(null);

        GoodsAuditDTO dto = new GoodsAuditDTO();
        dto.setGoodsIds(Collections.singletonList(99L));
        dto.setPass(true);

        GoodsBatchOperationException ex = assertThrows(GoodsBatchOperationException.class,
                () -> goodsService.auditGoods(dto, "admin"));
        assertTrue(ex.getReasons().get(0).contains("商品不存在"));
    }

    @Test
    void offShelf_pending_fails()
    {
        BizGoods pending = draftGoods(13L, 10L);
        pending.setGoodsStatus(GoodsConstants.STATUS_PENDING);
        when(goodsMapper.selectById(13L)).thenReturn(pending);

        GoodsOffShelfDTO dto = new GoodsOffShelfDTO();
        dto.setGoodsIds(Collections.singletonList(13L));

        assertThrows(GoodsBatchOperationException.class, () -> goodsService.offShelfPlatform(dto, "admin"));
        verify(goodsMapper, never()).updateOffShelf(any(), any());
    }

    @Test
    void offShelf_batchMixed_fails()
    {
        BizGoods onSale = draftGoods(14L, 10L);
        onSale.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);
        BizGoods off = draftGoods(15L, 10L);
        off.setGoodsStatus(GoodsConstants.STATUS_OFF_SHELF);
        when(goodsMapper.selectById(14L)).thenReturn(onSale);
        when(goodsMapper.selectById(15L)).thenReturn(off);

        GoodsOffShelfDTO dto = new GoodsOffShelfDTO();
        dto.setGoodsIds(Arrays.asList(14L, 15L));

        assertThrows(GoodsBatchOperationException.class, () -> goodsService.offShelfPlatform(dto, "admin"));
        verify(goodsMapper, never()).updateOffShelf(any(), any());
    }

    @Test
    void countPlatformPending_delegatesMapper()
    {
        when(goodsMapper.countPlatformPending()).thenReturn(7);
        assertEquals(7, goodsService.countPlatformPending());
    }

    @Test
    void selectPlatformDetail_draft_notFound()
    {
        BizGoods draft = draftGoods(16L, 10L);
        draft.setGoodsStatus(GoodsConstants.STATUS_DRAFT);
        when(goodsMapper.selectById(16L)).thenReturn(draft);

        assertThrows(ServiceException.class, () -> goodsService.selectPlatformDetail(16L));
    }

    @Test
    void audit_rejectWithoutReason_fails()
    {
        GoodsAuditDTO dto = new GoodsAuditDTO();
        dto.setGoodsIds(Collections.singletonList(1L));
        dto.setPass(false);

        assertThrows(ServiceException.class, () -> goodsService.auditGoods(dto, "admin"));
    }

    @Test
    void audit_batchWithNonPending_fails()
    {
        BizGoods pending = draftGoods(1L, 10L);
        pending.setGoodsStatus(GoodsConstants.STATUS_PENDING);
        BizGoods onSale = draftGoods(2L, 10L);
        onSale.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);
        when(goodsMapper.selectById(1L)).thenReturn(pending);
        when(goodsMapper.selectById(2L)).thenReturn(onSale);

        GoodsAuditDTO dto = new GoodsAuditDTO();
        dto.setGoodsIds(Arrays.asList(1L, 2L));
        dto.setPass(true);

        assertThrows(GoodsBatchOperationException.class, () -> goodsService.auditGoods(dto, "admin"));
        verify(goodsMapper, never()).updateAuditPass(any(), any());
    }

    @Test
    void offShelf_onlyOnSale()
    {
        BizGoods goods = draftGoods(5L, 10L);
        goods.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);
        when(goodsMapper.selectById(5L)).thenReturn(goods);
        when(goodsMapper.updateOffShelf(5L, "admin")).thenReturn(1);

        GoodsOffShelfDTO dto = new GoodsOffShelfDTO();
        dto.setGoodsIds(Collections.singletonList(5L));
        goodsService.offShelfPlatform(dto, "admin");

        verify(goodsMapper).updateOffShelf(5L, "admin");
    }

    @Test
    void update_rejectsGoodsStatusInBody()
    {
        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setGoodsId(6L);
        dto.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);

        assertThrows(ServiceException.class,
                () -> goodsService.updateSellerGoods(10L, dto, "seller"));
    }

    @Test
    void insert_generatesSnAndDraftStatus()
    {
        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000001");
        BizGoodsCategory cat = new BizGoodsCategory();
        cat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(cat);
        when(goodsMapper.insert(any())).thenAnswer(inv -> {
            BizGoods g = inv.getArgument(0);
            g.setGoodsId(99L);
            return 1;
        });

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        Long id = goodsService.insertSellerGoods(10L, dto, "seller");

        assertEquals(99L, id);
        verify(goodsMapper).insert(any(BizGoods.class));
        verify(goodsFreightMapper).insert(any(BizGoodsFreight.class));
    }

    @Test
    void insert_withShopCategoryLevel2()
    {
        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000002");
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        BizGoodsCategory shopCat = new BizGoodsCategory();
        shopCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        when(categoryMapper.selectById(2005L, 10L)).thenReturn(shopCat);
        when(goodsMapper.insert(any())).thenAnswer(inv -> {
            BizGoods g = inv.getArgument(0);
            g.setGoodsId(100L);
            return 1;
        });

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setShopCategoryId(2005L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        goodsService.insertSellerGoods(10L, dto, "seller");

        verify(goodsMapper).insert(any(BizGoods.class));
    }

    @Test
    void insert_missingFreightType_fails()
    {
        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> goodsService.insertSellerGoods(10L, dto, "seller"));
        assertEquals(GoodsConstants.MSG_FREIGHT_TYPE_REQUIRED, ex.getMessage());
    }

    @Test
    void insert_withFreightTemplate_persistsFreight()
    {
        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000007");
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        BizShopFreightTemplate template = new BizShopFreightTemplate();
        template.setTemplateId(5L);
        when(freightTemplateMapper.selectById(5L, 10L)).thenReturn(template);
        when(goodsMapper.insert(any())).thenAnswer(inv -> {
            BizGoods g = inv.getArgument(0);
            g.setGoodsId(105L);
            return 1;
        });

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        dto.setFreightType(FreightTemplateConstants.FREIGHT_TYPE_TEMPLATE);
        dto.setFreightTemplateId(5L);

        goodsService.insertSellerGoods(10L, dto, "seller");

        ArgumentCaptor<BizGoodsFreight> captor = ArgumentCaptor.forClass(BizGoodsFreight.class);
        verify(goodsFreightMapper).insert(captor.capture());
        assertEquals(FreightTemplateConstants.FREIGHT_TYPE_TEMPLATE, captor.getValue().getFreightType());
        assertEquals(5L, captor.getValue().getFreightTemplateId());
    }

    @Test
    void insert_invalidFreightTemplate_fails()
    {
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        when(freightTemplateMapper.selectById(999L, 10L)).thenReturn(null);

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        dto.setFreightType(FreightTemplateConstants.FREIGHT_TYPE_TEMPLATE);
        dto.setFreightTemplateId(999L);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> goodsService.insertSellerGoods(10L, dto, "seller"));
        assertTrue(ex.getMessage().contains("运费模版"));
    }

    @Test
    void insert_invalidShopCategory_fails()
    {
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        when(categoryMapper.selectById(999L, 10L)).thenReturn(null);

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setShopCategoryId(999L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> goodsService.insertSellerGoods(10L, dto, "seller"));
        assertTrue(ex.getMessage().contains("店铺商品二级分类"));
    }

    @Test
    void insert_withAttrTemplate_persistsTemplateId()
    {
        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000003");
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        BizGoodsAttrTemplate template = new BizGoodsAttrTemplate();
        template.setTemplateId(4L);
        when(attrTemplateMapper.selectById(4L, 10L)).thenReturn(template);
        when(goodsMapper.insert(any())).thenAnswer(inv -> {
            BizGoods g = inv.getArgument(0);
            g.setGoodsId(101L);
            return 1;
        });

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setAttrTemplateId(4L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        goodsService.insertSellerGoods(10L, dto, "seller");

        ArgumentCaptor<BizGoods> captor = ArgumentCaptor.forClass(BizGoods.class);
        verify(goodsMapper).insert(captor.capture());
        assertEquals(4L, captor.getValue().getAttrTemplateId());
    }

    @Test
    void insert_invalidAttrTemplate_fails()
    {
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        when(attrTemplateMapper.selectById(999L, 10L)).thenReturn(null);

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setAttrTemplateId(999L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> goodsService.insertSellerGoods(10L, dto, "seller"));
        assertTrue(ex.getMessage().contains("属性模版"));
        verify(goodsMapper, never()).insert(any());
    }

    @Test
    void insert_withAttributes_persistsGoodsAttrRows()
    {
        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000004");
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        when(goodsMapper.insert(any())).thenAnswer(inv -> {
            BizGoods g = inv.getArgument(0);
            g.setGoodsId(102L);
            return 1;
        });

        GoodsAttrItemDTO attr = new GoodsAttrItemDTO();
        attr.setItemName("品牌");
        attr.setValues(Collections.singletonList("华为"));
        GoodsAttrItemDTO spec = new GoodsAttrItemDTO();
        spec.setItemName("内存");
        spec.setValues(Arrays.asList("8G", "16G"));

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("手机");
        dto.setMainPic("/a.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("1999"));
        dto.setStock(10);
        dto.setAttributes(Collections.singletonList(attr));
        dto.setSpecs(Collections.singletonList(spec));
        withFixedFreight(dto);

        goodsService.insertSellerGoods(10L, dto, "seller");

        verify(goodsAttrMapper).deleteByGoodsId(102L);
        verify(goodsAttrMapper).batchInsert(any());
    }

    @Test
    void delete_draft_success()
    {
        BizGoods goods = draftGoods(7L, 10L);
        goods.setStock(5);
        when(goodsMapper.selectByIdAndShop(7L, 10L)).thenReturn(goods);
        when(goodsMapper.logicDeleteByIds(any(), eq(10L), eq("seller"))).thenReturn(1);

        goodsService.deleteSellerGoods(10L, 7L, "seller");

        verify(goodsMapper).logicDeleteByIds(any(), eq(10L), eq("seller"));
        verify(snapshotMapper).deleteByGoodsId(7L);
        verify(goodsAttrMapper).deleteByGoodsId(7L);
        verify(goodsPicMapper).deleteByGoodsId(7L);
        verify(goodsFreightMapper).deleteByGoodsId(7L);
        verify(stockOutboundFacade).createDeleteSpecOutbound(any());
    }

    @Test
    void delete_onSale_fails()
    {
        BizGoods goods = draftGoods(8L, 10L);
        goods.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);
        when(goodsMapper.selectByIdAndShop(8L, 10L)).thenReturn(goods);

        GoodsBatchIdsDTO dto = new GoodsBatchIdsDTO();
        dto.setGoodsIds(Collections.singletonList(8L));

        assertThrows(GoodsBatchOperationException.class,
                () -> goodsService.deleteSellerGoodsBatch(10L, dto, "seller"));
        verify(goodsMapper, never()).logicDeleteByIds(any(), any(), any());
    }

    @Test
    void submitBatch_mixedStatus_fails()
    {
        BizGoods draft = draftGoods(1L, 10L);
        BizGoods onSale = draftGoods(2L, 10L);
        onSale.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);
        when(goodsMapper.selectByIdAndShop(1L, 10L)).thenReturn(draft);
        when(goodsMapper.selectByIdAndShop(2L, 10L)).thenReturn(onSale);

        GoodsBatchIdsDTO dto = new GoodsBatchIdsDTO();
        dto.setGoodsIds(Arrays.asList(1L, 2L));

        assertThrows(GoodsBatchOperationException.class,
                () -> goodsService.submitGoodsBatch(10L, dto, "seller"));
    }

    @Test
    void offShelf_unfinishedOrder_fails()
    {
        BizGoods goods = draftGoods(9L, 10L);
        goods.setGoodsStatus(GoodsConstants.STATUS_ON_SALE);
        when(goodsMapper.selectById(9L)).thenReturn(goods);
        when(goodsOrderFacade.hasUnfinishedOrderByGoodsId(9L)).thenReturn(true);

        GoodsOffShelfDTO dto = new GoodsOffShelfDTO();
        dto.setGoodsIds(Collections.singletonList(9L));

        assertThrows(GoodsBatchOperationException.class,
                () -> goodsService.offShelfSeller(10L, 9L, "seller"));
        verify(goodsMapper, never()).updateOffShelf(any(), any());
    }

    @Test
    void insert_withSubPics_persistsGallery()
    {
        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000005");
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        when(goodsMapper.insert(any())).thenAnswer(inv -> {
            BizGoods g = inv.getArgument(0);
            g.setGoodsId(103L);
            return 1;
        });

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/main.jpg");
        dto.setSubPics(Arrays.asList("/sub1.jpg", "/sub2.jpg"));
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        goodsService.insertSellerGoods(10L, dto, "seller");

        verify(goodsPicMapper).deleteByGoodsId(103L);
        ArgumentCaptor<List<BizGoodsPic>> captor = ArgumentCaptor.forClass(List.class);
        verify(goodsPicMapper).batchInsert(captor.capture());
        List<BizGoodsPic> rows = captor.getValue();
        assertEquals(2, rows.size());
        assertEquals("/sub1.jpg", rows.get(0).getPicUrl());
        assertEquals(0, rows.get(0).getSortNo());
        assertEquals("/sub2.jpg", rows.get(1).getPicUrl());
        assertEquals(1, rows.get(1).getSortNo());
    }

    @Test
    void insert_withoutSubPics_skipsBatchInsert()
    {
        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000006");
        BizGoodsCategory platformCat = new BizGoodsCategory();
        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
        when(goodsMapper.insert(any())).thenAnswer(inv -> {
            BizGoods g = inv.getArgument(0);
            g.setGoodsId(104L);
            return 1;
        });

        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/main.jpg");
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        goodsService.insertSellerGoods(10L, dto, "seller");

        verify(goodsPicMapper).deleteByGoodsId(104L);
        verify(goodsPicMapper, never()).batchInsert(any());
    }

    @Test
    void insert_subPicsExceedLimit_fails()
    {
        GoodsSaveDTO dto = new GoodsSaveDTO();
        dto.setCategoryId(100L);
        dto.setGoodsName("复合肥");
        dto.setMainPic("/main.jpg");
        dto.setSubPics(Arrays.asList(
                "/1.jpg", "/2.jpg", "/3.jpg", "/4.jpg", "/5.jpg",
                "/6.jpg", "/7.jpg", "/8.jpg", "/9.jpg", "/10.jpg", "/11.jpg"));
        dto.setDetailContent("detail");
        dto.setSalePrice(new BigDecimal("10"));
        dto.setStock(5);
        withFixedFreight(dto);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> goodsService.insertSellerGoods(10L, dto, "seller"));
        assertEquals(GoodsConstants.MSG_SUB_PIC_TOO_MANY, ex.getMessage());
        verify(goodsMapper, never()).insert(any());
    }

    @Test
    void selectSellerDetail_loadsSubPics()
    {
        BizGoods goods = draftGoods(20L, 10L);
        goods.setCategoryId(100L);
        goods.setGoodsName("测试");
        goods.setMainPic("/main.jpg");
        when(goodsMapper.selectByIdAndShop(20L, 10L)).thenReturn(goods);
        when(categoryFacade.getCategoryPath(100L)).thenReturn("兽药 > 抗生素");
        when(snapshotMapper.selectVisibleByGoodsId(20L)).thenReturn(Collections.emptyList());
        when(goodsAttrMapper.selectByGoodsId(20L)).thenReturn(Collections.emptyList());
        when(goodsPicMapper.selectPicUrlsByGoodsId(20L)).thenReturn(Arrays.asList("/sub1.jpg", "/sub2.jpg"));
        BizGoodsFreight freight = new BizGoodsFreight();
        freight.setFreightType(FreightTemplateConstants.FREIGHT_TYPE_FIXED);
        freight.setFixedAmount(new BigDecimal("8"));
        when(goodsFreightMapper.selectByGoodsId(20L)).thenReturn(freight);

        GoodsDetailVO vo = goodsService.selectSellerDetail(10L, 20L);

        assertEquals(2, vo.getSubPics().size());
        assertEquals("/sub1.jpg", vo.getSubPics().get(0));
        assertEquals(FreightTemplateConstants.FREIGHT_TYPE_FIXED, vo.getFreightType());
        assertEquals(0, new BigDecimal("8").compareTo(vo.getFixedFreightAmount()));
    }

    private void withFixedFreight(GoodsSaveDTO dto)
    {
        dto.setFreightType(FreightTemplateConstants.FREIGHT_TYPE_FIXED);
        dto.setFixedFreightAmount(new BigDecimal("10"));
    }

    private BizGoods draftGoods(Long goodsId, Long shopId)
    {
        BizGoods goods = new BizGoods();
        goods.setGoodsId(goodsId);
        goods.setShopId(shopId);
        goods.setGoodsStatus(GoodsConstants.STATUS_DRAFT);
        goods.setGoodsSn("G20260526000001");
        return goods;
    }
}
