package tech.easyflow.ai.documentimport.task;

import org.apache.ibatis.annotations.Update;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.data.redis.RedisSystemException;
import tech.easyflow.ai.entity.DocumentImportBatch;
import tech.easyflow.ai.entity.DocumentImportTask;
import tech.easyflow.ai.enums.DocumentImportBatchStatus;
import tech.easyflow.ai.enums.DocumentImportMode;
import tech.easyflow.ai.enums.DocumentImportTaskPhase;
import tech.easyflow.ai.enums.DocumentImportTaskStatus;
import tech.easyflow.ai.mapper.DocumentImportBatchItemMapper;
import tech.easyflow.ai.mapper.DocumentImportBatchMapper;
import tech.easyflow.ai.mapper.DocumentImportTaskMapper;

import java.math.BigInteger;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.Date;

/**
 * {@link DocumentImportBatchCircuitBreaker} 批次熔断回归测试。
 */
public class DocumentImportBatchCircuitBreakerTest {

    /**
     * 验证 Redis 超时会强制中断批次、撤销任务并刷新真实计数。
     */
    @Test
    public void shouldInterruptAutomaticBatchOnRedisFailure() {
        BigInteger taskId = BigInteger.valueOf(11);
        BigInteger batchId = BigInteger.valueOf(12);
        DocumentImportTask task = new DocumentImportTask();
        task.setId(taskId);
        task.setBatchId(batchId);
        task.setPhase(DocumentImportTaskPhase.PARSE.name());
        task.setStatus(DocumentImportTaskStatus.PENDING.name());
        DocumentImportBatch batch = new DocumentImportBatch();
        batch.setId(batchId);
        batch.setImportMode(DocumentImportMode.AUTO.name());
        batch.setStatus(DocumentImportBatchStatus.RUNNING.name());
        DocumentImportBatchMapper batchMapper =
            Mockito.mock(DocumentImportBatchMapper.class);
        DocumentImportBatchItemMapper itemMapper =
            Mockito.mock(DocumentImportBatchItemMapper.class);
        DocumentImportTaskMapper taskMapper =
            Mockito.mock(DocumentImportTaskMapper.class);
        Mockito.when(taskMapper.selectOneById(taskId)).thenReturn(task);
        Mockito.when(batchMapper.selectOneById(batchId)).thenReturn(batch);
        Mockito.when(batchMapper.interruptRunningBatchForActiveTask(
            Mockito.eq(batchId),
            Mockito.eq(taskId),
            Mockito.eq("redis_unavailable"),
            Mockito.anyString(),
            Mockito.any(Date.class)
        )).thenReturn(1);
        Mockito.when(taskMapper.interruptActiveDocuments(
            Mockito.eq(batchId), Mockito.anyString(),
            Mockito.eq("redis_unavailable"), Mockito.any(Date.class),
            Mockito.eq(BigInteger.ZERO)
        )).thenReturn(1);
        Mockito.when(taskMapper.interruptActiveTasks(
            Mockito.eq(batchId), Mockito.anyString(),
            Mockito.eq("redis_unavailable"), Mockito.any(Date.class),
            Mockito.eq(BigInteger.ZERO)
        )).thenReturn(2);
        Mockito.when(itemMapper.interruptActiveItems(
            Mockito.eq(batchId), Mockito.anyString(),
            Mockito.eq("redis_unavailable"), Mockito.any(Date.class)
        )).thenReturn(2);
        DocumentImportBatchCircuitBreaker circuitBreaker =
            new DocumentImportBatchCircuitBreaker(
                batchMapper, itemMapper, taskMapper);

        boolean interrupted = circuitBreaker.interruptTaskBatch(
            taskId,
            new RedisSystemException(
                "Redis command timed out", new IllegalStateException("timeout"))
        );

        Assert.assertTrue(interrupted);
        Mockito.verify(batchMapper).refreshCountersFromItems(
            Mockito.eq(batchId), Mockito.any(Date.class));
    }

    /**
     * 验证无批次任务不会误触发批次熔断。
     */
    @Test
    public void shouldLeaveStandaloneTaskForDeferredRetry() {
        BigInteger taskId = BigInteger.valueOf(21);
        DocumentImportTask task = new DocumentImportTask();
        task.setId(taskId);
        DocumentImportBatchMapper batchMapper =
            Mockito.mock(DocumentImportBatchMapper.class);
        DocumentImportBatchItemMapper itemMapper =
            Mockito.mock(DocumentImportBatchItemMapper.class);
        DocumentImportTaskMapper taskMapper =
            Mockito.mock(DocumentImportTaskMapper.class);
        Mockito.when(taskMapper.selectOneById(taskId)).thenReturn(task);
        DocumentImportBatchCircuitBreaker circuitBreaker =
            new DocumentImportBatchCircuitBreaker(
                batchMapper, itemMapper, taskMapper);

        Assert.assertFalse(circuitBreaker.interruptTaskBatch(
            taskId, new IllegalStateException("temporary failure")));
        Mockito.verify(batchMapper, Mockito.never()).interruptRunningBatch(
            Mockito.any(), Mockito.anyString(), Mockito.anyString(),
            Mockito.any(Date.class));
    }

    /**
     * 验证旧任务已结束后，迟到消费者异常不会中断新一轮运行批次。
     */
    @Test
    public void shouldIgnoreLateFailureFromFinishedTask() {
        BigInteger taskId = BigInteger.valueOf(31);
        DocumentImportTask task = new DocumentImportTask();
        task.setId(taskId);
        task.setBatchId(BigInteger.valueOf(32));
        task.setStatus(DocumentImportTaskStatus.FAILED.name());
        DocumentImportBatchMapper batchMapper =
            Mockito.mock(DocumentImportBatchMapper.class);
        DocumentImportBatchItemMapper itemMapper =
            Mockito.mock(DocumentImportBatchItemMapper.class);
        DocumentImportTaskMapper taskMapper =
            Mockito.mock(DocumentImportTaskMapper.class);
        Mockito.when(taskMapper.selectOneById(taskId)).thenReturn(task);
        DocumentImportBatchCircuitBreaker circuitBreaker =
            new DocumentImportBatchCircuitBreaker(
                batchMapper, itemMapper, taskMapper);

        Assert.assertTrue(circuitBreaker.interruptTaskBatch(
            taskId, new IllegalStateException("迟到异常")));

        Mockito.verify(batchMapper, Mockito.never())
            .interruptRunningBatchForActiveTask(
                Mockito.any(), Mockito.any(), Mockito.anyString(),
                Mockito.anyString(), Mockito.any(Date.class));
        Mockito.verify(batchMapper, Mockito.never()).selectOneById(Mockito.any());
    }

    /**
     * 验证恢复令牌失效后旧持有者无法中断仍由新持有者运行的批次。
     */
    @Test
    public void shouldRejectInterruptionFromStaleRecoveryOwner() {
        BigInteger batchId = BigInteger.valueOf(41);
        DocumentImportBatch batch = new DocumentImportBatch();
        batch.setId(batchId);
        batch.setImportMode(DocumentImportMode.AUTO.name());
        batch.setStatus(DocumentImportBatchStatus.RUNNING.name());
        DocumentImportBatchMapper batchMapper =
            Mockito.mock(DocumentImportBatchMapper.class);
        DocumentImportBatchItemMapper itemMapper =
            Mockito.mock(DocumentImportBatchItemMapper.class);
        DocumentImportTaskMapper taskMapper =
            Mockito.mock(DocumentImportTaskMapper.class);
        Mockito.when(batchMapper.selectOneById(batchId)).thenReturn(batch);
        Mockito.when(batchMapper.interruptOwnedRecoveryBatch(
            Mockito.eq(batchId),
            Mockito.eq("stale-token"),
            Mockito.anyString(),
            Mockito.anyString(),
            Mockito.any(Date.class)
        )).thenReturn(0);
        DocumentImportBatchCircuitBreaker circuitBreaker =
            new DocumentImportBatchCircuitBreaker(
                batchMapper, itemMapper, taskMapper);

        Assert.assertFalse(circuitBreaker.interruptRecoveryBatch(
            batchId,
            "stale-token",
            new IllegalStateException("旧持有者异常")
        ));
        Mockito.verifyNoInteractions(itemMapper);
        Mockito.verify(taskMapper, Mockito.never()).interruptActiveTasks(
            Mockito.any(), Mockito.anyString(), Mockito.anyString(),
            Mockito.any(Date.class), Mockito.any());
    }

    /**
     * 验证 SQL 语法错误保留为通用系统异常，避免伪装成数据库不可用。
     */
    @Test
    public void shouldKeepBadSqlGrammarAsInfrastructureFailure() {
        BigInteger batchId = BigInteger.valueOf(51);
        DocumentImportBatch batch = runningAutoBatch(batchId);
        DocumentImportBatchMapper batchMapper =
            Mockito.mock(DocumentImportBatchMapper.class);
        Mockito.when(batchMapper.selectOneById(batchId)).thenReturn(batch);
        Mockito.when(batchMapper.interruptRunningBatch(
            Mockito.eq(batchId),
            Mockito.eq("document_import_infrastructure_failure"),
            Mockito.anyString(),
            Mockito.any(Date.class)
        )).thenReturn(1);
        DocumentImportBatchCircuitBreaker circuitBreaker =
            new DocumentImportBatchCircuitBreaker(
                batchMapper,
                Mockito.mock(DocumentImportBatchItemMapper.class),
                Mockito.mock(DocumentImportTaskMapper.class)
            );

        Assert.assertTrue(circuitBreaker.interruptBatch(
            batchId,
            new BadSqlGrammarException(
                "query", "SELECT broken", new SQLException("syntax", "42000"))
        ));
    }

    /**
     * 验证明确的数据库资源故障仍归类为数据库不可用。
     */
    @Test
    public void shouldClassifyDatabaseResourceFailureAsUnavailable() {
        BigInteger batchId = BigInteger.valueOf(61);
        DocumentImportBatch batch = runningAutoBatch(batchId);
        DocumentImportBatchMapper batchMapper =
            Mockito.mock(DocumentImportBatchMapper.class);
        Mockito.when(batchMapper.selectOneById(batchId)).thenReturn(batch);
        Mockito.when(batchMapper.interruptRunningBatch(
            Mockito.eq(batchId),
            Mockito.eq("database_unavailable"),
            Mockito.anyString(),
            Mockito.any(Date.class)
        )).thenReturn(1);
        DocumentImportBatchCircuitBreaker circuitBreaker =
            new DocumentImportBatchCircuitBreaker(
                batchMapper,
                Mockito.mock(DocumentImportBatchItemMapper.class),
                Mockito.mock(DocumentImportTaskMapper.class)
            );

        Assert.assertTrue(circuitBreaker.interruptBatch(
            batchId,
            new DataAccessResourceFailureException("connection unavailable")
        ));
    }

    /**
     * 验证恢复与任务熔断 SQL 都包含对应所有权围栏。
     *
     * @throws Exception Mapper 方法不存在时抛出
     */
    @Test
    public void interruptionSqlShouldFenceRecoveryAndActiveTask()
        throws Exception {
        Method recoveryMethod = DocumentImportBatchMapper.class.getMethod(
            "interruptOwnedRecoveryBatch",
            BigInteger.class,
            String.class,
            String.class,
            String.class,
            Date.class
        );
        String recoverySql = String.join(" ",
            recoveryMethod.getAnnotation(Update.class).value());
        Assert.assertTrue(recoverySql.contains("recovery_pending=1"));
        Assert.assertTrue(recoverySql.contains(
            "recovery_token=#{recoveryToken}"));
        Assert.assertTrue(recoverySql.contains(
            "recovery_lease_until > #{modified}"));

        Method taskMethod = DocumentImportBatchMapper.class.getMethod(
            "interruptRunningBatchForActiveTask",
            BigInteger.class,
            BigInteger.class,
            String.class,
            String.class,
            Date.class
        );
        String taskSql = String.join(" ",
            taskMethod.getAnnotation(Update.class).value());
        Assert.assertTrue(taskSql.contains("task.id=#{taskId}"));
        Assert.assertTrue(taskSql.contains(
            "task.status IN ('PENDING','RUNNING')"));
    }

    /**
     * 创建运行中的自动导入批次。
     *
     * @param batchId 批次 ID
     * @return 测试批次
     */
    private DocumentImportBatch runningAutoBatch(BigInteger batchId) {
        DocumentImportBatch batch = new DocumentImportBatch();
        batch.setId(batchId);
        batch.setImportMode(DocumentImportMode.AUTO.name());
        batch.setStatus(DocumentImportBatchStatus.RUNNING.name());
        return batch;
    }
}
