package com.example.demo.service;

import static org.junit.jupiter.api.Assertions.*;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.model.*;
import com.example.demo.model.enums.CommonTaskType;
import com.example.demo.repository.*;

/**
 * 积分回退逻辑集成测试
 * 
 * 测试目标：
 * 1. 验证完整的积分冻结-扣除/返还流程
 * 2. 验证分镜任务的双积分处理
 * 3. 检测已知的积分泄漏漏洞
 * 4. 验证并发场景下的积分安全性
 * 
 * 使用方法：
 * 1. 确保数据库已初始化（schema.sql已执行）
 * 2. 运行整个测试类：mvn test -Dtest=PointsRefundIntegrationTest
 * 3. 运行单个测试：mvn test -Dtest=PointsRefundIntegrationTest#testStoryboardTask_PartialFailure
 */
@SpringBootTest
@Transactional
@Rollback
public class PointsRefundIntegrationTest {

    @Autowired
    private UserService userService;
    
    @Autowired
    private TaskQueueService taskQueueService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PointsFreezeRecordRepository pointsFreezeRecordRepository;
    
    @Autowired
    private ImageToVideoTaskRepository imageToVideoTaskRepository;
    
    @Autowired
    private StoryboardVideoTaskRepository storyboardVideoTaskRepository;
    
    @Autowired
    private TaskQueueRepository taskQueueRepository;
    
    private static int testUserCounter = 0;
    
    /**
     * 创建测试用户
     */
    private User createTestUser(String username, int points, int frozenPoints) {
        User user = new User();
        user.setUsername(username);
        user.setEmail(username + "@test.com");
        user.setPasswordHash("test_password_hash");
        user.setRole("ROLE_USER");
        user.setPoints(points);
        user.setFrozenPoints(frozenPoints);
        user.setUserId("TEST_" + System.currentTimeMillis() + "_" + (testUserCounter++));
        return userRepository.save(user);
    }
    
    @BeforeEach
    public void setUp() {
        // 每个测试前清理数据
        pointsFreezeRecordRepository.deleteAll();
    }
    
    // ==================== 基础测试 ====================
    
    @Test
    @DisplayName("测试1: 正常冻结和扣除积分")
    public void test01_FreezeAndDeduct_Success() {
        // 准备数据
        String username = "test_user_" + System.currentTimeMillis();
        User user = createTestUser(username, 10000, 0);
        String taskId = "test_task_001";
        
        // 1. 冻结积分
        PointsFreezeRecord record = userService.freezePoints(
            username, taskId, CommonTaskType.IMAGE_TO_VIDEO, 500, "测试任务"
        );
        
        // 验证冻结状态
        User afterFreeze = userService.findByUsername(username);
        assertEquals(10000, afterFreeze.getPoints(), "总积分应保持不变");
        assertEquals(500, afterFreeze.getFrozenPoints(), "冻结积分应为500");
        assertEquals(9500, afterFreeze.getAvailablePoints(), "可用积分应为9500");
        assertEquals(PointsFreezeRecord.FreezeStatus.FROZEN, record.getStatus(), "记录状态应为FROZEN");
        
        // 2. 扣除积分
        userService.deductFrozenPoints(taskId);
        
        // 验证扣除结果
        User afterDeduct = userService.findByUsername(username);
        assertEquals(9500, afterDeduct.getPoints(), "总积分应减少500");
        assertEquals(0, afterDeduct.getFrozenPoints(), "冻结积分应归零");
        assertEquals(9500, afterDeduct.getAvailablePoints(), "可用积分应为9500");
        
        PointsFreezeRecord finalRecord = pointsFreezeRecordRepository.findByTaskId(taskId).get();
        assertEquals(PointsFreezeRecord.FreezeStatus.DEDUCTED, finalRecord.getStatus(), "记录状态应为DEDUCTED");
        
        System.out.println("✅ 测试1通过：正常冻结和扣除流程正确");
    }
    
    @Test
    @DisplayName("测试2: 正常冻结和返还积分")
    public void test02_FreezeAndReturn_Success() {
        String username = "test_user_" + System.currentTimeMillis();
        User user = createTestUser(username, 10000, 0);
        String taskId = "test_task_002";
        
        // 1. 冻结积分
        userService.freezePoints(username, taskId, CommonTaskType.IMAGE_TO_VIDEO, 500, "测试任务");
        
        // 2. 返还积分
        userService.returnFrozenPoints(taskId);
        
        // 验证返还结果
        User afterReturn = userService.findByUsername(username);
        assertEquals(10000, afterReturn.getPoints(), "总积分应保持不变");
        assertEquals(0, afterReturn.getFrozenPoints(), "冻结积分应归零");
        assertEquals(10000, afterReturn.getAvailablePoints(), "可用积分应恢复为10000");
        
        PointsFreezeRecord record = pointsFreezeRecordRepository.findByTaskId(taskId).get();
        assertEquals(PointsFreezeRecord.FreezeStatus.RETURNED, record.getStatus(), "记录状态应为RETURNED");
        
        System.out.println("✅ 测试2通过：正常冻结和返还流程正确");
    }
    
    // ==================== 幂等性测试 ====================
    
    @Test
    @DisplayName("测试3: 重复扣除积分的幂等性")
    public void test03_DeductPoints_Idempotent() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        String taskId = "test_task_003";
        
        // 冻结积分
        userService.freezePoints(username, taskId, CommonTaskType.IMAGE_TO_VIDEO, 500, "测试任务");
        
        // 第一次扣除
        userService.deductFrozenPoints(taskId);
        User afterFirst = userService.findByUsername(username);
        assertEquals(9500, afterFirst.getPoints());
        
        // 第二次扣除（幂等性测试）
        userService.deductFrozenPoints(taskId);
        User afterSecond = userService.findByUsername(username);
        assertEquals(9500, afterSecond.getPoints(), "重复扣除不应改变积分");
        
        System.out.println("✅ 测试3通过：扣除操作具有幂等性");
    }
    
    @Test
    @DisplayName("测试4: 重复返还积分的幂等性")
    public void test04_ReturnPoints_Idempotent() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        String taskId = "test_task_004";
        
        // 冻结积分
        userService.freezePoints(username, taskId, CommonTaskType.IMAGE_TO_VIDEO, 500, "测试任务");
        
        // 第一次返还
        userService.returnFrozenPoints(taskId);
        User afterFirst = userService.findByUsername(username);
        assertEquals(10000, afterFirst.getPoints());
        assertEquals(0, afterFirst.getFrozenPoints());
        
        // 第二次返还（幂等性测试）
        userService.returnFrozenPoints(taskId);
        User afterSecond = userService.findByUsername(username);
        assertEquals(10000, afterSecond.getPoints(), "重复返还不应改变积分");
        assertEquals(0, afterSecond.getFrozenPoints(), "冻结积分应仍为0");
        
        System.out.println("✅ 测试4通过：返还操作具有幂等性");
    }
    
    // ==================== 分镜任务测试 ====================
    
    @Test
    @DisplayName("测试5: 分镜任务双积分正常流程")
    public void test05_StoryboardTask_DualPoints_Success() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        String taskId = "sb_test_001";
        
        // 1. 冻结分镜图积分
        userService.freezePoints(username, taskId + "_img", 
            CommonTaskType.STORYBOARD_IMAGE, 300, "分镜图");
        
        // 2. 冻结视频积分
        userService.freezePoints(username, taskId + "_vid", 
            CommonTaskType.STORYBOARD_VIDEO, 700, "分镜视频");
        
        User afterFreeze = userService.findByUsername(username);
        assertEquals(10000, afterFreeze.getPoints(), "总积分应保持不变");
        assertEquals(1000, afterFreeze.getFrozenPoints(), "冻结积分应为1000");
        
        // 3. 分镜图成功，扣除 _img 积分
        userService.deductFrozenPoints(taskId + "_img");
        
        User afterImgDeduct = userService.findByUsername(username);
        assertEquals(9700, afterImgDeduct.getPoints(), "总积分应减少300");
        assertEquals(700, afterImgDeduct.getFrozenPoints(), "冻结积分应剩700");
        
        // 4. 视频生成成功，扣除 _vid 积分
        userService.deductFrozenPoints(taskId + "_vid");
        
        User finalUser = userService.findByUsername(username);
        assertEquals(9000, finalUser.getPoints(), "总积分应为9000");
        assertEquals(0, finalUser.getFrozenPoints(), "冻结积分应归零");
        
        System.out.println("✅ 测试5通过：分镜任务双积分正常流程正确");
    }
    
    @Test
    @DisplayName("测试6: 分镜任务部分失败 - 分镜图阶段失败")
    public void test06_StoryboardTask_PartialFailure_ImageStage() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        String taskId = "sb_test_002";
        
        // 1. 冻结双重积分
        userService.freezePoints(username, taskId + "_img", 
            CommonTaskType.STORYBOARD_IMAGE, 300, "分镜图");
        userService.freezePoints(username, taskId + "_vid", 
            CommonTaskType.STORYBOARD_VIDEO, 700, "分镜视频");
        
        // 2. 分镜图阶段失败，返还 _img 积分
        userService.returnFrozenPoints(taskId + "_img");
        
        User afterReturn = userService.findByUsername(username);
        assertEquals(10000, afterReturn.getPoints(), "总积分应保持不变");
        assertEquals(700, afterReturn.getFrozenPoints(), "应只剩视频积分");
        
        // 3. 视频阶段也失败，返还 _vid 积分
        userService.returnFrozenPoints(taskId + "_vid");
        
        User finalUser = userService.findByUsername(username);
        assertEquals(10000, finalUser.getPoints(), "总积分应保持不变");
        assertEquals(0, finalUser.getFrozenPoints(), "所有冻结积分应返还");
        
        System.out.println("✅ 测试6通过：分镜任务部分失败处理正确");
    }
    
    @Test
    @DisplayName("测试7: 【漏洞检测】分镜任务系统重启场景下的积分泄漏")
    public void test07_StoryboardTask_PartialFailure_BugScenario() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        String taskId = "sb_bug_test";
        
        // 1. 创建分镜任务
        StoryboardVideoTask task = new StoryboardVideoTask();
        task.setTaskId(taskId);
        task.setUsername(username);
        task.setStatus(StoryboardVideoTask.TaskStatus.PENDING);
        task.setCreatedAt(LocalDateTime.now().minusHours(2)); // 模拟超时
        storyboardVideoTaskRepository.save(task);
        
        // 2. 冻结双重积分（模拟业务逻辑预扣）
        userService.freezePoints(username, taskId + "_img", 
            CommonTaskType.STORYBOARD_IMAGE, 300, "分镜图");
        userService.freezePoints(username, taskId + "_vid", 
            CommonTaskType.STORYBOARD_VIDEO, 700, "分镜视频");
        
        User afterFreeze = userService.findByUsername(username);
        assertEquals(1000, afterFreeze.getFrozenPoints(), "初始冻结积分应为1000");
        
        // 3. 模拟分镜图生成失败（realTaskId为空）
        task.setRealTaskId(null); // 关键：没有外部任务ID
        storyboardVideoTaskRepository.save(task);
        
        // 4. 触发系统重启清理逻辑
        System.out.println("⚠️ 正在触发系统重启清理逻辑...");
        taskQueueService.checkAndHandleQueueTimeoutTasks();
        
        // 5. 验证积分状态（检测漏洞）
        User finalUser = userService.findByUsername(username);
        assertEquals(10000, finalUser.getPoints(), "总积分应保持不变");
        
        int expectedFrozenPoints = 0; // 期望值：所有积分都应返还
        int actualFrozenPoints = finalUser.getFrozenPoints();
        
        System.out.println("📊 积分状态：总积分=" + finalUser.getPoints() + 
                         ", 冻结积分=" + actualFrozenPoints + 
                         ", 可用积分=" + finalUser.getAvailablePoints());
        
        // ⚠️ 这里会检测到漏洞
        if (actualFrozenPoints != expectedFrozenPoints) {
            System.err.println("🔴 检测到积分泄漏漏洞！");
            System.err.println("期望冻结积分: " + expectedFrozenPoints);
            System.err.println("实际冻结积分: " + actualFrozenPoints);
            System.err.println("泄漏的积分: " + actualFrozenPoints);
            
            // 检查哪个积分记录未返还
            var imgRecord = pointsFreezeRecordRepository.findByTaskId(taskId + "_img");
            var vidRecord = pointsFreezeRecordRepository.findByTaskId(taskId + "_vid");
            
            System.err.println("_img 记录状态: " + (imgRecord.isPresent() ? imgRecord.get().getStatus() : "不存在"));
            System.err.println("_vid 记录状态: " + (vidRecord.isPresent() ? vidRecord.get().getStatus() : "不存在"));
            
            fail("检测到积分泄漏漏洞！期望冻结积分=" + expectedFrozenPoints + 
                 "，实际冻结积分=" + actualFrozenPoints + 
                 "\n这是由于 TaskQueueService.recoverOrCleanupStuckTasks() 中，" +
                 "noRealTaskId 分支只返还了 _img 积分，未返还 _vid 积分导致的。");
        }
        
        System.out.println("✅ 测试7通过：未检测到积分泄漏（漏洞已修复）");
    }
    
    // ==================== 异常场景测试 ====================
    
    @Test
    @DisplayName("测试8: 积分不足时冻结失败")
    public void test08_FreezePoints_InsufficientPoints() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 100, 0);
        String taskId = "test_task_insufficient";
        
        // 尝试冻结超过可用积分的积分
        RuntimeException exception = assertThrows(RuntimeException.class, () -> {
            userService.freezePoints(username, taskId, 
                CommonTaskType.IMAGE_TO_VIDEO, 500, "测试任务");
        });
        
        assertTrue(exception.getMessage().contains("积分不足"), 
                  "异常消息应包含'积分不足': " + exception.getMessage());
        
        System.out.println("✅ 测试8通过：积分不足时正确拒绝冻结");
    }
    
    @Test
    @DisplayName("测试9: 已完成任务不允许重复提交")
    public void test09_FreezePoints_AlreadyDeducted() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        String taskId = "test_task_duplicate";
        
        // 1. 冻结并扣除积分
        userService.freezePoints(username, taskId, 
            CommonTaskType.IMAGE_TO_VIDEO, 500, "测试任务");
        userService.deductFrozenPoints(taskId);
        
        // 2. 尝试重新冻结相同taskId（模拟重复提交）
        RuntimeException exception = assertThrows(RuntimeException.class, () -> {
            userService.freezePoints(username, taskId, 
                CommonTaskType.IMAGE_TO_VIDEO, 500, "重复任务");
        });
        
        assertTrue(exception.getMessage().contains("任务已完成") || 
                   exception.getMessage().contains("不允许重复提交"),
                  "异常消息应包含'任务已完成'或'不允许重复提交': " + exception.getMessage());
        
        System.out.println("✅ 测试9通过：已完成任务正确拒绝重复提交");
    }
    
    // ==================== 完整流程测试 ====================
    
    @Test
    @DisplayName("测试10: 图生视频任务完整失败流程")
    public void test10_ImageToVideoTask_CompleteFailureFlow() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        String taskId = "img2vid_failure_test";
        
        // 1. 创建任务
        ImageToVideoTask task = new ImageToVideoTask();
        task.setTaskId(taskId);
        task.setUsername(username);
        task.setStatus(ImageToVideoTask.TaskStatus.PENDING);
        imageToVideoTaskRepository.save(task);
        
        // 2. 冻结积分
        userService.freezePoints(username, taskId, 
            CommonTaskType.IMAGE_TO_VIDEO, 500, "图生视频");
        
        User afterFreeze = userService.findByUsername(username);
        assertEquals(500, afterFreeze.getFrozenPoints());
        
        // 3. 添加到队列
        TaskQueue taskQueue = taskQueueService.addImageToVideoTask(username, taskId);
        assertNotNull(taskQueue);
        
        // 4. 标记任务失败
        taskQueueService.markTaskAsFailed(taskId, "API调用失败");
        
        // 5. 验证最终状态
        User finalUser = userService.findByUsername(username);
        assertEquals(10000, finalUser.getPoints(), "积分应完全返还");
        assertEquals(0, finalUser.getFrozenPoints(), "冻结积分应归零");
        
        ImageToVideoTask finalTask = imageToVideoTaskRepository.findByTaskId(taskId).get();
        assertEquals(ImageToVideoTask.TaskStatus.FAILED, finalTask.getStatus());
        
        // 验证队列记录已删除
        assertFalse(taskQueueRepository.findByTaskId(taskId).isPresent(), 
                   "失败任务的队列记录应被删除");
        
        System.out.println("✅ 测试10通过：完整失败流程处理正确");
    }
    
    // ==================== 监控和统计测试 ====================
    
    @Test
    @DisplayName("测试11: 积分冻结记录状态统计")
    public void test11_PointsFreezeRecord_StatusStatistics() {
        String username = "test_user_" + System.currentTimeMillis();
        createTestUser(username, 10000, 0);
        
        // 创建不同状态的积分记录
        // FROZEN 状态
        userService.freezePoints(username, "task_frozen_1", 
            CommonTaskType.IMAGE_TO_VIDEO, 100, "测试1");
        userService.freezePoints(username, "task_frozen_2", 
            CommonTaskType.TEXT_TO_VIDEO, 200, "测试2");
        
        // DEDUCTED 状态
        userService.freezePoints(username, "task_deducted", 
            CommonTaskType.IMAGE_TO_VIDEO, 300, "测试3");
        userService.deductFrozenPoints("task_deducted");
        
        // RETURNED 状态
        userService.freezePoints(username, "task_returned", 
            CommonTaskType.IMAGE_TO_VIDEO, 400, "测试4");
        userService.returnFrozenPoints("task_returned");
        
        // 统计各状态数量
        long frozenCount = pointsFreezeRecordRepository.findByUsernameOrderByCreatedAtDesc(username)
            .stream().filter(r -> r.getStatus() == PointsFreezeRecord.FreezeStatus.FROZEN).count();
        
        long deductedCount = pointsFreezeRecordRepository.findByUsernameOrderByCreatedAtDesc(username)
            .stream().filter(r -> r.getStatus() == PointsFreezeRecord.FreezeStatus.DEDUCTED).count();
        
        long returnedCount = pointsFreezeRecordRepository.findByUsernameOrderByCreatedAtDesc(username)
            .stream().filter(r -> r.getStatus() == PointsFreezeRecord.FreezeStatus.RETURNED).count();
        
        System.out.println("📊 积分记录统计：");
        System.out.println("  FROZEN: " + frozenCount);
        System.out.println("  DEDUCTED: " + deductedCount);
        System.out.println("  RETURNED: " + returnedCount);
        
        assertEquals(2, frozenCount, "FROZEN状态应有2条");
        assertEquals(1, deductedCount, "DEDUCTED状态应有1条");
        assertEquals(1, returnedCount, "RETURNED状态应有1条");
        
        System.out.println("✅ 测试11通过：积分记录状态统计正确");
    }
    
    // ==================== 测试总结方法 ====================
    
    /**
     * 运行所有测试后输出总结
     */
    @Test
    @DisplayName("测试总结")
    public void testSummary() {
        System.out.println("\n" + "=".repeat(60));
        System.out.println("积分回退逻辑测试总结");
        System.out.println("=".repeat(60));
        System.out.println("✅ 基础功能测试：正常冻结、扣除、返还流程");
        System.out.println("✅ 幂等性测试：防止重复扣除和返还");
        System.out.println("✅ 分镜任务测试：双积分处理逻辑");
        System.out.println("⚠️  漏洞检测测试：分镜任务部分失败场景");
        System.out.println("✅ 异常场景测试：积分不足、重复提交");
        System.out.println("✅ 完整流程测试：任务失败的端到端流程");
        System.out.println("=".repeat(60));
        System.out.println("\n建议：");
        System.out.println("1. 如果测试7失败，请修复 TaskQueueService.recoverOrCleanupStuckTasks()");
        System.out.println("2. 在生产环境部署前，运行完整的回归测试");
        System.out.println("3. 添加积分异常监控告警（冻结超过4小时）");
        System.out.println("=".repeat(60) + "\n");
    }
}
