package org.dromara.app.controller;

import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.vo.AppScheduleDetailVo;
import org.dromara.app.domain.vo.AppScheduleVo;
import org.dromara.app.domain.vo.AppSchedulingDeviceVo;
import org.dromara.app.service.*;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.vo.SysOssUploadVo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.ISysUserService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.springframework.context.support.GenericApplicationContext;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

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

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

    private static final int WORKER_COUNT = 32;
    private static final int GROUP_TIMEOUT_SECONDS = 30;
    private static final Long USER_ID = 99L;
    private static final DateTimeFormatter STRICT_SECOND_FORMATTER =
        DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT);

    @Mock private IAppDeviceService appDeviceService;
    @Mock private IAppScheduleService appScheduleService;
    @Mock private IAppScheduleDetailService appScheduleDetailService;
    @Mock private IAppSchedulingDeviceService appSchedulingDeviceService;
    @Mock private ISysUserService userService;
    @Mock private IAppWateringLogService appWateringLogService;
    @Mock private IDeviceCommandService deviceCommandService;
    @Mock private ISysOssService ossService;
    @Mock private IAppVersionService appVersionService;

    private GenericApplicationContext applicationContext;

    @BeforeEach
    void setUp() {
        applicationContext = new GenericApplicationContext();
        Supplier<ObjectMapper> objectMapperSupplier = ObjectMapper::new;
        applicationContext.registerBean(ObjectMapper.class, objectMapperSupplier);
        applicationContext.refresh();
        new SpringUtil().setApplicationContext(applicationContext);
    }

    @AfterEach
    void tearDown() {
        applicationContext.close();
    }

    @Test
    void switchDevice_formatsAllStartTimesCorrectlyUnderConcurrency() throws Exception {
        AppController controller = newController();
        int iterationsPerWorker = 100;
        int expectedCalls = WORKER_COUNT * iterationsPerWorker;
        ConcurrentLinkedQueue<String> startTimes = new ConcurrentLinkedQueue<>();
        AtomicInteger successfulResponses = new AtomicInteger();
        when(appDeviceService.switchDevice(eq("D01"), eq("1"), anyString(), eq(10)))
            .thenAnswer(invocation -> {
                startTimes.add(invocation.getArgument(2, String.class));
                return true;
            });

        runConcurrently(iterationsPerWorker, (worker, iteration) -> {
            R<Void> result = controller.switchDevice(
                "{\"deviceNo\":\"D01\",\"workStatus\":\"1\",\"durationMin\":\"10\"}");
            assertThat(result.getCode()).isEqualTo(R.SUCCESS);
            successfulResponses.incrementAndGet();
        });

        assertThat(successfulResponses.get()).isEqualTo(expectedCalls);
        assertThat(startTimes).hasSize(expectedCalls).allSatisfy(startTime ->
            assertThatCode(() -> LocalDateTime.parse(startTime, STRICT_SECOND_FORMATTER))
                .doesNotThrowAnyException());
        verify(appDeviceService, times(expectedCalls))
            .switchDevice(eq("D01"), eq("1"), anyString(), eq(10));
    }

    @Test
    void uploadImage_validatesAllSupportedFormatsUnderConcurrency() throws Exception {
        AppController controller = newController();
        int iterationsPerWorker = 50;
        int expectedCalls = WORKER_COUNT * iterationsPerWorker;
        List<ImageFixture> fixtures = supportedImages();
        SysOssVo oss = new SysOssVo();
        oss.setOssId(123L);
        oss.setOriginalName("plant-image");
        oss.setUrl("https://bucket.oss-cn-hangzhou.aliyuncs.com/plant-image");
        when(ossService.upload(any(MultipartFile.class))).thenReturn(oss);

        runConcurrently(iterationsPerWorker, (worker, iteration) -> {
            ImageFixture fixture = fixtures.get((worker + iteration) % fixtures.size());
            MockMultipartFile file = new MockMultipartFile(
                "file", fixture.fileName(), fixture.contentType(), fixture.content());

            R<SysOssUploadVo> result = controller.uploadImage(file);

            assertThat(result.getCode()).isEqualTo(R.SUCCESS);
            assertThat(result.getData().getOssId()).isEqualTo("123");
            assertThat(result.getData().getUrl()).isEqualTo(oss.getUrl());
        });

        verify(ossService, times(expectedCalls)).upload(any(MultipartFile.class));
    }

    @Test
    void editScheduleStatus_keepsMqttPayloadsIsolatedUnderConcurrency() throws Exception {
        AppController controller = newController();
        int iterationsPerWorker = 20;
        int expectedCalls = WORKER_COUNT * iterationsPerWorker;
        ConcurrentHashMap<String, Map<String, Object>> payloadsByDevice = new ConcurrentHashMap<>();
        when(appScheduleService.queryById(anyLong())).thenAnswer(invocation -> {
            Long scheduleId = invocation.getArgument(0, Long.class);
            AppScheduleVo schedule = new AppScheduleVo();
            schedule.setId(scheduleId);
            schedule.setUserId(USER_ID);
            schedule.setStatus("1");
            return schedule;
        });
        when(appScheduleService.updateStatusByBo(any(AppScheduleBo.class))).thenReturn(true);
        when(appSchedulingDeviceService.queryList(any(AppSchedulingDeviceBo.class))).thenAnswer(invocation -> {
            AppSchedulingDeviceBo bo = invocation.getArgument(0, AppSchedulingDeviceBo.class);
            AppSchedulingDeviceVo binding = new AppSchedulingDeviceVo();
            binding.setScheduleId(bo.getScheduleId());
            binding.setDeviceNo(deviceNo(bo.getScheduleId()));
            return List.of(binding);
        });
        when(appScheduleDetailService.queryByScheduleIdByStatus(anyLong())).thenAnswer(invocation -> {
            Long scheduleId = invocation.getArgument(0, Long.class);
            AppScheduleDetailVo detail = new AppScheduleDetailVo();
            detail.setScheduleId(scheduleId);
            detail.setWeekday((int) ((scheduleId - 1) % 7) + 1);
            detail.setTimeData("[{\"startTime\":\"08:00\",\"durationMin\":15}]");
            detail.setTriggerType(detailMarker(scheduleId));
            detail.setStatus("1");
            return List.of(detail);
        });
        when(deviceCommandService.sendScheduleBindCommand(anyString(), any())).thenAnswer(invocation -> {
            String deviceNo = invocation.getArgument(0, String.class);
            Map<String, Object> payload = invocation.getArgument(1);
            assertThat(payloadsByDevice.putIfAbsent(deviceNo, payload)).isNull();
            return "cmd-" + deviceNo;
        });

        runConcurrently(iterationsPerWorker, (worker, iteration) -> {
            try (MockedStatic<LoginHelper> loginHelper = mockStatic(LoginHelper.class)) {
                loginHelper.when(LoginHelper::getUserId).thenReturn(USER_ID);
                Long scheduleId = (long) worker * iterationsPerWorker + iteration + 1;
                AppScheduleBo bo = new AppScheduleBo();
                bo.setId(scheduleId);
                bo.setStatus("1");

                R<Void> result = controller.editScheduleStatus(bo);

                assertThat(result.getCode()).isEqualTo(R.SUCCESS);
            }
        });

        assertThat(payloadsByDevice).hasSize(expectedCalls);
        for (long scheduleId = 1; scheduleId <= expectedCalls; scheduleId++) {
            String deviceNo = deviceNo(scheduleId);
            Map<String, Object> payload = payloadsByDevice.get(deviceNo);
            assertThat(payload).as(deviceNo).isNotNull();
            assertThat(payload.get("deviceNo")).isEqualTo(deviceNo);
            List<?> details = asList(payload.get("details"));
            assertThat(details).hasSize(1);
            Map<?, ?> detail = asMap(details.get(0));
            assertThat(detail.get("triggerType")).isEqualTo(detailMarker(scheduleId));
            assertThat(detail.get("weekday")).isEqualTo((int) ((scheduleId - 1) % 7) + 1);
        }
        verify(deviceCommandService, times(expectedCalls)).sendScheduleBindCommand(anyString(), any());
        verify(appScheduleDetailService, times(expectedCalls)).queryByScheduleIdByStatus(anyLong());
    }

    private void runConcurrently(int iterationsPerWorker, ConcurrentOperation operation) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(WORKER_COUNT);
        CountDownLatch ready = new CountDownLatch(WORKER_COUNT);
        CountDownLatch start = new CountDownLatch(1);
        List<Future<Void>> futures = new ArrayList<>(WORKER_COUNT);
        try {
            for (int worker = 0; worker < WORKER_COUNT; worker++) {
                int workerIndex = worker;
                futures.add(executor.submit(() -> {
                    ready.countDown();
                    if (!start.await(10, TimeUnit.SECONDS)) {
                        throw new TimeoutException("并发工作线程等待启动超时");
                    }
                    for (int iteration = 0; iteration < iterationsPerWorker; iteration++) {
                        operation.execute(workerIndex, iteration);
                    }
                    return null;
                }));
            }
            assertThat(ready.await(10, TimeUnit.SECONDS)).as("32个工作线程应全部就绪").isTrue();
            start.countDown();
            awaitAll(futures);
        } finally {
            start.countDown();
            executor.shutdownNow();
            assertThat(executor.awaitTermination(10, TimeUnit.SECONDS))
                .as("并发测试线程池应按时结束")
                .isTrue();
        }
    }

    private void awaitAll(List<Future<Void>> futures) throws Exception {
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(GROUP_TIMEOUT_SECONDS);
        for (Future<Void> future : futures) {
            long remainingNanos = deadline - System.nanoTime();
            if (remainingNanos <= 0) {
                throw new TimeoutException("并发测试组执行超过" + GROUP_TIMEOUT_SECONDS + "秒");
            }
            future.get(remainingNanos, TimeUnit.NANOSECONDS);
        }
    }

    private AppController newController() {
        return new AppController(
            appDeviceService,
            appScheduleService,
            appScheduleDetailService,
            appSchedulingDeviceService,
            userService,
            appWateringLogService,
            deviceCommandService,
            ossService,
            appVersionService
        );
    }

    private List<ImageFixture> supportedImages() {
        return List.of(
            new ImageFixture("plant.jpg", "image/jpeg", new byte[]{
                (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0,
                0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00
            }),
            new ImageFixture("plant.png", "image/png", new byte[]{
                (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
                0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52
            }),
            new ImageFixture("plant.gif", "image/gif",
                new byte[]{0x47, 0x49, 0x46, 0x38, 0x39, 0x61}),
            new ImageFixture("plant.webp", "image/webp", new byte[]{
                0x52, 0x49, 0x46, 0x46, 0x18, 0x00, 0x00, 0x00,
                0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x20
            }),
            new ImageFixture("plant.bmp", "image/bmp",
                new byte[]{0x42, 0x4D, 0x00, 0x00, 0x00, 0x00})
        );
    }

    private String deviceNo(Long scheduleId) {
        return "D" + scheduleId;
    }

    private String detailMarker(Long scheduleId) {
        return "schedule-" + scheduleId;
    }

    private List<?> asList(Object value) {
        assertThat(value).isInstanceOf(List.class);
        return (List<?>) value;
    }

    private Map<?, ?> asMap(Object value) {
        assertThat(value).isInstanceOf(Map.class);
        return (Map<?, ?>) value;
    }

    @FunctionalInterface
    private interface ConcurrentOperation {
        void execute(int worker, int iteration) throws Exception;
    }

    private record ImageFixture(String fileName, String contentType, byte[] content) {
    }
}
