package org.dromara.app.service.impl;

import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import io.github.linpeilie.Converter;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.dromara.app.domain.AppWateringLog;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.mapper.AppWateringLogMapper;
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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.support.GenericApplicationContext;

import java.text.SimpleDateFormat;
import java.util.List;
import java.util.function.Supplier;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

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

    @Mock
    private AppWateringLogMapper baseMapper;

    @BeforeAll
    static void initMybatisPlusTableInfo() {
        if (TableInfoHelper.getTableInfo(AppWateringLog.class) == null) {
            TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""), AppWateringLog.class);
        }
    }

    @Test
    void confirmScheduleLog_updatesMatchingRunningScheduleLogBeforeInsert() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setDeviceNo("D01");
        bo.setScheduleId(10L);
        bo.setUserId(99L);
        bo.setStartTime(format.parse("2026-07-07 08:30:00"));
        bo.setDurationMin("10");
        bo.setEndTime(format.parse("2026-07-07 08:40:00"));
        bo.setTriggerType("0");
        bo.setStatus("0");
        bo.setRemark("排程浇水完成");

        AppWateringLog runningLog = new AppWateringLog();
        runningLog.setId(100L);
        runningLog.setDeviceNo("D01");
        runningLog.setScheduleId(10L);
        runningLog.setStartTime(bo.getStartTime());
        runningLog.setDurationMin("10");
        runningLog.setEndTime(bo.getEndTime());
        runningLog.setTriggerType("0");
        runningLog.setStatus("1");

        when(baseMapper.selectOne(any(Wrapper.class))).thenReturn(null, runningLog);
        when(baseMapper.updateById(any(AppWateringLog.class))).thenReturn(1);

        Boolean result = withConverterContext(() -> {
            AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
            return service.confirmScheduleLog(bo);
        });

        assertThat(result).isTrue();
        ArgumentCaptor<AppWateringLog> updateCaptor = ArgumentCaptor.forClass(AppWateringLog.class);
        verify(baseMapper).updateById(updateCaptor.capture());
        AppWateringLog update = updateCaptor.getValue();
        assertThat(update.getId()).isEqualTo(100L);
        assertThat(update.getStatus()).isEqualTo("0");
        assertThat(update.getRemark()).isEqualTo("排程浇水完成");
        verify(baseMapper, never()).insert(any(AppWateringLog.class));
    }

    @Test
    void confirmScheduleLog_updatesMatchingRunningManualLogBeforeInsert() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setDeviceNo("D01");
        bo.setScheduleId(0L);
        bo.setUserId(99L);
        bo.setStartTime(format.parse("2026-07-07 08:30:00"));
        bo.setDurationMin("10");
        bo.setEndTime(format.parse("2026-07-07 08:40:00"));
        bo.setTriggerType("1");
        bo.setStatus("0");
        bo.setRemark("设备按键完成浇水");

        AppWateringLog runningLog = new AppWateringLog();
        runningLog.setId(101L);
        runningLog.setDeviceNo("D01");
        runningLog.setScheduleId(0L);
        runningLog.setStartTime(bo.getStartTime());
        runningLog.setDurationMin("10");
        runningLog.setEndTime(bo.getEndTime());
        runningLog.setTriggerType("1");
        runningLog.setStatus("1");

        when(baseMapper.selectOne(any(Wrapper.class))).thenReturn(null, runningLog);
        when(baseMapper.updateById(any(AppWateringLog.class))).thenReturn(1);

        Boolean result = withConverterContext(() -> {
            AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
            return service.confirmScheduleLog(bo);
        });

        assertThat(result).isTrue();
        ArgumentCaptor<AppWateringLog> updateCaptor = ArgumentCaptor.forClass(AppWateringLog.class);
        verify(baseMapper).updateById(updateCaptor.capture());
        AppWateringLog update = updateCaptor.getValue();
        assertThat(update.getId()).isEqualTo(101L);
        assertThat(update.getTriggerType()).isEqualTo("1");
        assertThat(update.getStatus()).isEqualTo("0");
        assertThat(update.getRemark()).isEqualTo("设备按键完成浇水");
        verify(baseMapper, never()).insert(any(AppWateringLog.class));
    }

    @Test
    void confirmScheduleLog_matchesRunningLogByMinuteRange() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setDeviceNo("D01");
        bo.setScheduleId(10L);
        bo.setUserId(99L);
        bo.setStartTime(format.parse("2026-07-07 09:20:27"));
        bo.setDurationMin("10");
        bo.setEndTime(format.parse("2026-07-07 09:30:27"));
        bo.setTriggerType("0");
        bo.setStatus("0");
        bo.setRemark("排程浇水完成");

        AppWateringLog runningLog = new AppWateringLog();
        runningLog.setId(102L);
        runningLog.setDeviceNo("D01");
        runningLog.setStartTime(format.parse("2026-07-07 09:20:00"));
        runningLog.setDurationMin("10");
        runningLog.setEndTime(format.parse("2026-07-07 09:30:00"));
        runningLog.setStatus("1");

        when(baseMapper.selectOne(any(Wrapper.class))).thenReturn(null, runningLog);
        when(baseMapper.updateById(any(AppWateringLog.class))).thenReturn(1);

        Boolean result = withConverterContext(() -> {
            AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
            return service.confirmScheduleLog(bo);
        });

        assertThat(result).isTrue();
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(baseMapper, times(2)).selectOne(wrapperCaptor.capture());
        List<Wrapper> wrappers = wrapperCaptor.getAllValues();
        String runningQuery = wrappers.get(1).getCustomSqlSegment().toLowerCase();
        assertThat(runningQuery).contains("starttime between");
        assertThat(runningQuery).doesNotContain("starttime =");
    }

    @Test
    void confirmScheduleLog_matchesRunningManualLogWithoutDurationEquality() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setDeviceNo("D01");
        bo.setScheduleId(0L);
        bo.setUserId(99L);
        bo.setStartTime(format.parse("2026-07-07 09:20:15"));
        bo.setDurationMin("3");
        bo.setEndTime(format.parse("2026-07-07 09:23:45"));
        bo.setTriggerType("1");
        bo.setStatus("0");
        bo.setRemark("设备按键完成浇水");

        AppWateringLog runningLog = new AppWateringLog();
        runningLog.setId(103L);
        runningLog.setDeviceNo("D01");
        runningLog.setScheduleId(0L);
        runningLog.setStartTime(format.parse("2026-07-07 09:20:00"));
        runningLog.setDurationMin("10");
        runningLog.setEndTime(format.parse("2026-07-07 09:30:00"));
        runningLog.setTriggerType("1");
        runningLog.setStatus("1");

        when(baseMapper.selectOne(any(Wrapper.class))).thenReturn(null, runningLog);
        when(baseMapper.updateById(any(AppWateringLog.class))).thenReturn(1);

        Boolean result = withConverterContext(() -> {
            AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
            return service.confirmScheduleLog(bo);
        });

        assertThat(result).isTrue();
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(baseMapper, times(2)).selectOne(wrapperCaptor.capture());
        String runningQuery = wrapperCaptor.getAllValues().get(1).getCustomSqlSegment().toLowerCase();
        assertThat(runningQuery).contains("starttime between");
        assertThat(runningQuery).contains("triggertype =");
        assertThat(runningQuery).doesNotContain("durationmin =");

        ArgumentCaptor<AppWateringLog> updateCaptor = ArgumentCaptor.forClass(AppWateringLog.class);
        verify(baseMapper).updateById(updateCaptor.capture());
        assertThat(updateCaptor.getValue().getId()).isEqualTo(103L);
        verify(baseMapper, never()).insert(any(AppWateringLog.class));
    }

    @Test
    void confirmScheduleLog_matchesRunningScheduleLogWithoutDurationEqualityWhenFinishedEarly() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setDeviceNo("D01");
        bo.setScheduleId(10L);
        bo.setUserId(99L);
        bo.setStartTime(format.parse("2026-07-07 09:20:15"));
        bo.setDurationMin("3");
        bo.setEndTime(format.parse("2026-07-07 09:23:45"));
        bo.setTriggerType("0");
        bo.setStatus("0");
        bo.setRemark("设备上报排程浇水完成");

        AppWateringLog runningLog = new AppWateringLog();
        runningLog.setId(104L);
        runningLog.setDeviceNo("D01");
        runningLog.setScheduleId(10L);
        runningLog.setStartTime(format.parse("2026-07-07 09:20:00"));
        runningLog.setDurationMin("10");
        runningLog.setEndTime(format.parse("2026-07-07 09:30:00"));
        runningLog.setTriggerType("0");
        runningLog.setStatus("1");

        when(baseMapper.selectOne(any(Wrapper.class))).thenReturn(null, runningLog);
        when(baseMapper.updateById(any(AppWateringLog.class))).thenReturn(1);

        Boolean result = withConverterContext(() -> {
            AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
            return service.confirmScheduleLog(bo);
        });

        assertThat(result).isTrue();
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(baseMapper, times(2)).selectOne(wrapperCaptor.capture());
        String runningQuery = wrapperCaptor.getAllValues().get(1).getCustomSqlSegment().toLowerCase();
        assertThat(runningQuery).contains("starttime between");
        assertThat(runningQuery).contains("triggertype =");
        assertThat(runningQuery).doesNotContain("durationmin =");

        ArgumentCaptor<AppWateringLog> updateCaptor = ArgumentCaptor.forClass(AppWateringLog.class);
        verify(baseMapper).updateById(updateCaptor.capture());
        assertThat(updateCaptor.getValue().getId()).isEqualTo(104L);
        verify(baseMapper, never()).insert(any(AppWateringLog.class));
    }

    @Test
    void confirmScheduleLog_updatesExistingFinishedManualLogByMinuteBeforeInsert() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setDeviceNo("D01");
        bo.setScheduleId(0L);
        bo.setUserId(99L);
        bo.setStartTime(format.parse("2026-07-07 10:31:27"));
        bo.setDurationMin("3");
        bo.setEndTime(format.parse("2026-07-07 10:34:27"));
        bo.setTriggerType("1");
        bo.setStatus("0");
        bo.setRemark("设备按键完成浇水");

        AppWateringLog finishedLog = new AppWateringLog();
        finishedLog.setId(106L);
        finishedLog.setDeviceNo("D01");
        finishedLog.setScheduleId(0L);
        finishedLog.setStartTime(format.parse("2026-07-07 10:31:00"));
        finishedLog.setDurationMin("2");
        finishedLog.setEndTime(format.parse("2026-07-07 10:33:30"));
        finishedLog.setTriggerType("1");
        finishedLog.setStatus("0");

        when(baseMapper.selectOne(any(Wrapper.class))).thenReturn(null, null, finishedLog);
        when(baseMapper.updateById(any(AppWateringLog.class))).thenReturn(1);

        Boolean result = withConverterContext(() -> {
            AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
            return service.confirmScheduleLog(bo);
        });

        assertThat(result).isTrue();
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(baseMapper, times(3)).selectOne(wrapperCaptor.capture());
        String existingQuery = wrapperCaptor.getAllValues().get(2).getCustomSqlSegment().toLowerCase();
        assertThat(existingQuery).contains("starttime between");
        assertThat(existingQuery).contains("triggertype =");
        assertThat(existingQuery).contains("status =");

        ArgumentCaptor<AppWateringLog> updateCaptor = ArgumentCaptor.forClass(AppWateringLog.class);
        verify(baseMapper).updateById(updateCaptor.capture());
        assertThat(updateCaptor.getValue().getId()).isEqualTo(106L);
        assertThat(updateCaptor.getValue().getStatus()).isEqualTo("0");
        verify(baseMapper, never()).insert(any(AppWateringLog.class));
    }

    @Test
    void finishRunningLogsByDevice_finishesLatestStatusOneLogWithPlannedEndTime() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLog runningLog = new AppWateringLog();
        runningLog.setId(105L);
        runningLog.setDeviceNo("D01");
        runningLog.setStartTime(format.parse("2026-07-07 09:20:00"));
        runningLog.setEndTime(format.parse("2026-07-07 09:30:00"));
        runningLog.setDurationMin("10");
        runningLog.setTriggerType("0");
        runningLog.setStatus("1");

        when(baseMapper.selectOne(any(Wrapper.class))).thenReturn(runningLog);
        when(baseMapper.update(eq(null), any(Wrapper.class))).thenReturn(1);

        AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
        int result = service.finishRunningLogsByDevice(
            "D01",
            format.parse("2026-07-07 09:23:30"),
            "手动停止浇水"
        );

        assertThat(result).isEqualTo(1);
        ArgumentCaptor<Wrapper> queryCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(baseMapper).selectOne(queryCaptor.capture());
        String query = queryCaptor.getValue().getCustomSqlSegment().toLowerCase();
        assertThat(query).contains("status =");
        assertThat(query).contains("order by");
        assertThat(query).contains("limit 1");
        assertThat(query).doesNotContain("endtime is null");

        ArgumentCaptor<Wrapper> updateCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(baseMapper).update(eq(null), updateCaptor.capture());
        LambdaUpdateWrapper<AppWateringLog> updateWrapper = (LambdaUpdateWrapper<AppWateringLog>) updateCaptor.getValue();
        String sqlSet = updateWrapper.getSqlSet().toLowerCase();
        assertThat(sqlSet).contains("status");
        assertThat(sqlSet).contains("endtime");
        assertThat(updateWrapper.getParamNameValuePairs().values()).contains("0");
    }

    @Test
    void saveEstimatedScheduleLog_defaultsStatusZeroBeforeInsert() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setDeviceNo("D01");
        bo.setScheduleId(10L);
        bo.setUserId(99L);
        bo.setStartTime(format.parse("2026-07-07 09:20:00"));
        bo.setEndTime(format.parse("2026-07-07 09:30:00"));
        bo.setDurationMin("10");

        when(baseMapper.exists(any(Wrapper.class))).thenReturn(false);
        when(baseMapper.insert(any(AppWateringLog.class))).thenReturn(1);

        Boolean result = withConverterContext(() -> {
            AppWateringLogServiceImpl service = new AppWateringLogServiceImpl(baseMapper);
            return service.saveEstimatedScheduleLog(bo);
        });

        assertThat(result).isTrue();
        ArgumentCaptor<AppWateringLog> insertCaptor = ArgumentCaptor.forClass(AppWateringLog.class);
        verify(baseMapper).insert(insertCaptor.capture());
        AppWateringLog insert = insertCaptor.getValue();
        assertThat(insert.getTriggerType()).isEqualTo("0");
        assertThat(insert.getStatus()).isEqualTo("0");
        assertThat(insert.getRemark()).isEqualTo("设备离线，按排程自动补记");
    }

    private <T> T withConverterContext(Supplier<T> action) {
        try (GenericApplicationContext applicationContext = new GenericApplicationContext()) {
            Converter converter = mock(Converter.class);
            lenient().when(converter.convert(any(AppWateringLogBo.class), eq(AppWateringLog.class)))
                .thenAnswer(invocation -> toEntity(invocation.getArgument(0)));
            applicationContext.registerBean(Converter.class, () -> converter);
            applicationContext.refresh();
            new SpringUtil().setApplicationContext(applicationContext);
            return action.get();
        }
    }

    private AppWateringLog toEntity(AppWateringLogBo bo) {
        AppWateringLog entity = new AppWateringLog();
        entity.setId(bo.getId());
        entity.setUserId(bo.getUserId());
        entity.setDeviceNo(bo.getDeviceNo());
        entity.setCommandId(bo.getCommandId());
        entity.setScheduleId(bo.getScheduleId());
        entity.setStartTime(bo.getStartTime());
        entity.setEndTime(bo.getEndTime());
        entity.setDurationMin(bo.getDurationMin());
        entity.setZones(bo.getZones());
        entity.setTriggerType(bo.getTriggerType());
        entity.setRemark(bo.getRemark());
        entity.setStatus(bo.getStatus());
        entity.setCreateTime(bo.getCreateTime());
        return entity;
    }
}
