package org.dromara.app.handler;

import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.common.redis.utils.RedisUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.util.ReflectionTestUtils;

import java.time.Duration;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@Tag("dev")
class MqttDeviceStatusServiceTest {

    private static GenericApplicationContext applicationContext;
    @Mock
    private AppDeviceMapper appDeviceMapper;
    @Mock
    private IAppWateringLogService wateringLogService;
    @Mock
    private RedissonClient redissonClient;
    @Mock
    private RLock lock;

    @BeforeAll
    static void initializeRedisUtils() {
        if (TableInfoHelper.getTableInfo(AppDevice.class) == null) {
            TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""), AppDevice.class);
        }
        applicationContext = new GenericApplicationContext();
        applicationContext.registerBean(RedissonClient.class, () -> org.mockito.Mockito.mock(RedissonClient.class));
        applicationContext.refresh();
        new SpringUtil().setApplicationContext(applicationContext);
    }

    @AfterAll
    static void closeApplicationContext() {
        applicationContext.close();
    }

    @Test
    void markOnlineCachesOnlineStatusWithTtlAndUpdatesDatabase() throws Exception {
        MqttDeviceStatusService service = newService();
        when(redissonClient.getLock("lock:mqtt:device:status:D01")).thenReturn(lock);
        when(lock.tryLock(0, 10, TimeUnit.SECONDS)).thenReturn(true);
        when(lock.isHeldByCurrentThread()).thenReturn(true);

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(RedisUtils::getClient).thenReturn(redissonClient);

            service.markOnline("D01");

            redis.verify(() -> RedisUtils.setCacheObject(
                eq("mqtt:device:status:D01"),
                any(Map.class),
                eq(Duration.ofSeconds(600))
            ));
            verify(appDeviceMapper).update(eq(null), any(LambdaUpdateWrapper.class));
            verify(lock).unlock();
        }
    }

    @Test
    void markOfflineDeletesOnlineCacheUpdatesDatabaseAndFinishesRunningLogs() throws Exception {
        MqttDeviceStatusService service = newService();
        when(redissonClient.getLock("lock:mqtt:device:status:D01")).thenReturn(lock);
        when(lock.tryLock(0, 10, TimeUnit.SECONDS)).thenReturn(true);
        when(lock.isHeldByCurrentThread()).thenReturn(true);
        when(appDeviceMapper.update(eq(null), any(LambdaUpdateWrapper.class))).thenReturn(1);

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(RedisUtils::getClient).thenReturn(redissonClient);

            service.markOffline("D01", "设备 MQTT 状态离线");

            redis.verify(() -> RedisUtils.deleteObject("mqtt:device:status:D01"));
            verify(appDeviceMapper).update(eq(null), any(LambdaUpdateWrapper.class));
            verify(wateringLogService).finishRunningLogsByDevice(eq("D01"), any(Date.class), eq("设备 MQTT 状态离线"));
            verify(lock).unlock();
        }
    }

    @Test
    void markOfflineIfCacheMissingSkipsWhenDeviceCameBackOnline() throws Exception {
        MqttDeviceStatusService service = newService();
        when(redissonClient.getLock("lock:mqtt:device:status:D01")).thenReturn(lock);
        when(lock.tryLock(0, 10, TimeUnit.SECONDS)).thenReturn(true);
        when(lock.isHeldByCurrentThread()).thenReturn(true);

        try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
            redis.when(RedisUtils::getClient).thenReturn(redissonClient);
            redis.when(() -> RedisUtils.hasKey("mqtt:device:status:D01")).thenReturn(true);

            service.markOfflineIfCacheMissing("D01", "设备掉线");

            redis.verify(RedisUtils::getClient);
            redis.verify(() -> RedisUtils.hasKey("mqtt:device:status:D01"));
            redis.verify(() -> RedisUtils.deleteObject("mqtt:device:status:D01"), never());
            verify(appDeviceMapper, never()).update(eq(null), any(LambdaUpdateWrapper.class));
            verify(wateringLogService, never()).finishRunningLogsByDevice(eq("D01"), any(Date.class), eq("设备掉线"));
            verify(lock).unlock();
        }
    }

    private MqttDeviceStatusService newService() {
        MqttDeviceStatusService service = new MqttDeviceStatusService(appDeviceMapper, wateringLogService);
        ReflectionTestUtils.setField(service, "deviceStatusCachePrefix", "mqtt:device:status:");
        ReflectionTestUtils.setField(service, "deviceStatusCacheTtlSeconds", 600L);
        return service;
    }
}
