package cn.iocoder.yudao.module.iot.service.rule.data;

import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO;
import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataRuleMapper;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.rule.data.action.IotDataRuleAction;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;

import javax.annotation.Resource;
import java.util.Map;

import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static java.util.Collections.singletonList;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

/**
 * {@link IotDataRuleServiceImpl} 的单元测试类
 *
 * @author 芋道源码
 */
@Import(IotDataRuleServiceImpl.class)
class IotDataRuleServiceImplTest extends BaseDbUnitTest {

    @Resource
    private IotDataRuleServiceImpl dataRuleService;

    @Resource
    private IotDataRuleMapper dataRuleMapper;

    @MockBean
    private IotDataSinkService dataSinkService;
    @MockBean
    private IotDataRuleAction dataRuleAction;
    @MockBean
    private IotProductService productService;
    @MockBean
    private IotDeviceService deviceService;
    @MockBean
    private IotThingModelService thingModelService;

    @Test
    public void testExecuteDataRule_propertyPost_singleIdentifierMatched() {
        // 准备参数
        Long deviceId = randomLongId();
        String identifier = "temperature";
        IotDeviceMessage message = createPropertyPostMessage(deviceId,
                MapUtil.<String, Object>builder().put(identifier, 25.5).build());
        // mock 数据：插入一条限定 identifier=temperature 的规则
        Long sinkId = randomLongId();
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), identifier, sinkId);
        // mock 方法
        IotDataSinkDO sink = mockEnabledSink(sinkId);

        // 调用
        dataRuleService.executeDataRule(message);

        // 断言：sink action 被调用一次
        verify(dataRuleAction).execute(eq(message), eq(sink));
    }

    @Test
    public void testExecuteDataRule_propertyPost_multiIdentifierOneMatched() {
        // 准备参数：上报 {temperature, humidity}，规则只限定 humidity
        Long deviceId = randomLongId();
        IotDeviceMessage message = createPropertyPostMessage(deviceId,
                MapUtil.<String, Object>builder().put("temperature", 25.5).put("humidity", 60).build());
        // mock 数据
        Long sinkId = randomLongId();
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), "humidity", sinkId);
        // mock 方法
        IotDataSinkDO sink = mockEnabledSink(sinkId);

        // 调用
        dataRuleService.executeDataRule(message);

        // 断言
        verify(dataRuleAction).execute(eq(message), eq(sink));
    }

    @Test
    public void testExecuteDataRule_propertyPost_multiIdentifierDeduped() {
        // 准备参数：上报 {temperature, humidity}，规则 identifier=null 不限定属性
        Long deviceId = randomLongId();
        IotDeviceMessage message = createPropertyPostMessage(deviceId,
                MapUtil.<String, Object>builder().put("temperature", 25.5).put("humidity", 60).build());
        // mock 数据：identifier=null 时两个属性 key 都会命中同一条规则，需在 sink 调用前去重
        Long sinkId = randomLongId();
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), null, sinkId);
        // mock 方法
        IotDataSinkDO sink = mockEnabledSink(sinkId);

        // 调用
        dataRuleService.executeDataRule(message);

        // 断言：去重后只触发一次，而不是 2 次
        verify(dataRuleAction).execute(eq(message), eq(sink));
    }

    @Test
    public void testExecuteDataRule_propertyPost_multiRuleSameSinkDeduped() {
        // 准备参数：上报 {temperature, humidity}，两条规则分别命中不同 identifier，但都指向同一 sink
        Long deviceId = randomLongId();
        IotDeviceMessage message = createPropertyPostMessage(deviceId,
                MapUtil.<String, Object>builder().put("temperature", 25.5).put("humidity", 60).build());
        // mock 数据：插入两条规则，identifier 分别为 temperature 与 humidity，sinkId 相同
        Long sinkId = randomLongId();
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), "temperature", sinkId);
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), "humidity", sinkId);
        // mock 方法
        IotDataSinkDO sink = mockEnabledSink(sinkId);

        // 调用
        dataRuleService.executeDataRule(message);

        // 断言：跨规则去重后，sink action 只触发一次，而不是 2 次
        verify(dataRuleAction).execute(eq(message), eq(sink));
    }

    @Test
    public void testExecuteDataRule_propertyPost_emptyParamsMatchesWildcardRule() {
        // 准备参数：上报空属性，规则 identifier=null 不限定属性，按"任意 property report 都同步"语义应命中
        Long deviceId = randomLongId();
        IotDeviceMessage message = createPropertyPostMessage(deviceId,
                MapUtil.<String, Object>builder().build());
        // mock 数据
        Long sinkId = randomLongId();
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), null, sinkId);
        // mock 方法
        IotDataSinkDO sink = mockEnabledSink(sinkId);

        // 调用
        dataRuleService.executeDataRule(message);

        // 断言
        verify(dataRuleAction).execute(eq(message), eq(sink));
    }

    @Test
    public void testExecuteDataRule_propertyPost_noIdentifierMatched() {
        // 准备参数：上报 {temperature}，规则限定 humidity
        Long deviceId = randomLongId();
        IotDeviceMessage message = createPropertyPostMessage(deviceId,
                MapUtil.<String, Object>builder().put("temperature", 25.5).build());
        // mock 数据
        Long sinkId = randomLongId();
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), "humidity", sinkId);

        // 调用
        dataRuleService.executeDataRule(message);

        // 断言：sink action 不应被调用
        verify(dataRuleAction, never()).execute(any(), any());
    }

    @Test
    public void testExecuteDataRule_eventPost_singleIdentifierMatched() {
        // 准备参数：事件触发器走单 identifier 路径（与改动前行为保持一致）
        Long deviceId = randomLongId();
        String identifier = "alarm";
        IotDeviceMessage message = randomPojo(IotDeviceMessage.class, o -> {
            o.setDeviceId(deviceId);
            o.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod());
            o.setParams(MapUtil.<String, Object>builder()
                    .put("identifier", identifier).put("value", "fired").build());
        });
        // mock 数据
        Long sinkId = randomLongId();
        insertEnabledRule(deviceId, IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), identifier, sinkId);
        // mock 方法
        IotDataSinkDO sink = mockEnabledSink(sinkId);

        // 调用
        dataRuleService.executeDataRule(message);

        // 断言
        verify(dataRuleAction).execute(eq(message), eq(sink));
    }

    // ========== 辅助方法 ==========

    private IotDeviceMessage createPropertyPostMessage(Long deviceId, Map<String, Object> params) {
        return randomPojo(IotDeviceMessage.class, o -> {
            o.setDeviceId(deviceId);
            o.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
            o.setParams(params);
        });
    }

    /**
     * 向 H2 中插入一条启用状态的数据流转规则，命中后会路由到 {@code sinkId}
     */
    private void insertEnabledRule(Long deviceId, String method, String identifier, Long sinkId) {
        IotDataRuleDO.SourceConfig config = randomPojo(IotDataRuleDO.SourceConfig.class, o -> {
            o.setDeviceId(deviceId);
            o.setMethod(method);
            o.setIdentifier(identifier);
        });
        IotDataRuleDO rule = randomPojo(IotDataRuleDO.class, o -> {
            o.setId(null);
            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
            o.setSourceConfigs(singletonList(config));
            o.setSinkIds(singletonList(sinkId));
        });
        dataRuleMapper.insert(rule);
    }

    /**
     * 构造一个启用状态的数据流转目的并塞入对应 mock；返回 sink 用于断言
     */
    private IotDataSinkDO mockEnabledSink(Long sinkId) {
        IotDataSinkDO sink = randomPojo(IotDataSinkDO.class, o -> {
            o.setId(sinkId);
            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
        });
        when(dataSinkService.getDataSinkFromCache(sinkId)).thenReturn(sink);
        when(dataRuleAction.getType()).thenReturn(sink.getType());
        return sink;
    }

}
