package work.slhaf.partner.module.action.dispatcher.executor

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.ArgumentMatchers.any
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.jupiter.MockitoExtension
import org.slf4j.LoggerFactory
import work.slhaf.partner.core.action.ActionCapability
import work.slhaf.partner.core.action.entity.Action
import work.slhaf.partner.core.action.entity.ExecutableAction
import work.slhaf.partner.core.action.entity.Schedulable
import work.slhaf.partner.core.action.entity.SchedulableExecutableAction
import work.slhaf.partner.module.action.executor.ActionExecutor
import work.slhaf.partner.module.action.scheduler.ActionScheduler
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit

/**
 * ActionScheduler.execute(...) 测试矩阵（控制流入口：execute）。
 *
 * 场景编号与矩阵对应：
 * 1) null 入参早退（B1）
 * 2) PREPARE + ONCE 合法时间入轮（B2 → B2.3）
 * 3) 非 PREPARE 状态忽略（B2 → B2.1）
 * 4) ONCE 过期/跨日解析失败（B2 → B2.2）
 * 5) CYCLE cron 非法解析失败（B2 → B2.2）
 * 6) putAction 异常传播（B2 异常中断）
 * 7) 同小时调度触发 ACTIVE（B2.3 + 状态变更）
 * 15) 混合输入（成功/失败/忽略路径混合）
 *
 * 以下矩阵场景因并发/时间依赖难以稳定复现，仅在文档中标注，不在本类实现：
 * 8) withTimeout 超时导致协程取消
 * 9) tick 触发 onTrigger 并调用 ActionExecutor
 * 10) tick step<=0 空转延迟
 * 11) loadActions 跨小时修复
 * 13) actionExecutor 阻塞导致调度延迟
 * 14) schedule 与 tick 并发访问竞态
 */
@ExtendWith(MockitoExtension::class)
class ActionSchedulerTest {

    @Mock
    private lateinit var actionExecutor: ActionExecutor

    @Mock
    private lateinit var actionCapability: ActionCapability

    @InjectMocks
    private lateinit var actionScheduler: ActionScheduler

    companion object {
        val log = LoggerFactory.getLogger(ActionSchedulerTest::class.java)!!
    }

    @Test
    fun `running test`() {
        fun buildAction(time: ZonedDateTime): SchedulableExecutableAction {
            return SchedulableExecutableAction(
                "tendency",
                mutableMapOf(),
                "reason",
                "description",
                "source",
                Schedulable.ScheduleType.ONCE,
                time.toString()
            )
        }

        val now = ZonedDateTime.now()
        val actions = setOf(
            buildAction(now.plusMinutes(1)),
            buildAction(now.truncatedTo(ChronoUnit.HOURS).plusHours(1).minusSeconds(1)),
        )
        Mockito.`when`(actionCapability.listActions(null, null))
            .thenReturn(actions as Set<ExecutableAction>)
        Mockito.`when`(actionExecutor.execute(any()))
            .thenAnswer {
                val actionData = it.arguments[0] as ExecutableAction
                log.info("Executed action $actionData at ${ZonedDateTime.now()}")
                null
            }
        actionScheduler.init()


        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("ActionSchedulerTest"))
        scope.launch {
            actionScheduler.schedule(buildAction(now.plusSeconds(5)))
        }
        readlnOrNull()
    }

    @Test
    fun `execute should put action and schedule valid ONCE prepare action`() {
        // 场景编号：2；路径：B2 → B2.3；目的：验证正常入轮与副作用
        initTimeWheelWithPrimaryActions(emptySet())
        val action = buildScheduledAction(
            type = Schedulable.ScheduleType.ONCE,
            ZonedDateTime.now().plusHours(1).toString()
        )

        actionScheduler.schedule(action)

        verify(actionCapability, times(1)).putAction(action)
        val timeWheel = timeWheel()
        val bucket = actionsGroupByHour(timeWheel)[action.scheduleContentHour()]
        assertTrue(bucket.contains(action))
    }

    @Test
    fun `execute should ignore non-prepare action for scheduling`() {
        // 场景编号：3；路径：B2 → B2.1；目的：验证忽略非 PREPARE 状态
        initTimeWheelWithPrimaryActions(emptySet())
        val action = buildScheduledAction(
            type = Schedulable.ScheduleType.ONCE
        )

        actionScheduler.schedule(action)

        verify(actionCapability, times(1)).putAction(action)
        val allScheduled = allScheduledActions(timeWheel())
        assertFalse(allScheduled.contains(action))
    }

    @Test
    fun `execute should skip expired ONCE action`() {
        // 场景编号：4；路径：B2 → B2.2；目的：验证解析失败被跳过
        initTimeWheelWithPrimaryActions(emptySet())
        val action = buildScheduledAction(
            type = Schedulable.ScheduleType.ONCE
        )

        actionScheduler.schedule(action)

        val allScheduled = allScheduledActions(timeWheel())
        assertFalse(allScheduled.contains(action))
    }

    @Test
    fun `execute should skip invalid CYCLE cron`() {
        // 场景编号：5；路径：B2 → B2.2；目的：验证 cron 解析失败被跳过
        initTimeWheelWithPrimaryActions(emptySet())
        val action = buildScheduledAction(
            type = Schedulable.ScheduleType.CYCLE,
            scheduleContentOverride = "invalid-cron"
        )

        actionScheduler.schedule(action)

        val allScheduled = allScheduledActions(timeWheel())
        assertFalse(allScheduled.contains(action))
    }

    @Test
    fun `execute should propagate exception from putAction`() {
        // 场景编号：6；路径：B2 异常中断；目的：验证异常传播
        initTimeWheelWithPrimaryActions(emptySet())
        val action = buildScheduledAction(
            type = Schedulable.ScheduleType.ONCE
        )
        Mockito.doThrow(RuntimeException("boom"))
            .`when`(actionCapability)
            .putAction(action)

        assertThrows(RuntimeException::class.java) {
            actionScheduler.schedule(action)
        }
    }

    @Test
    fun `execute should activate wheel when scheduling current hour`() {
        // 场景编号：7；路径：B2.3；目的：验证同小时调度触发 ACTIVE
        initTimeWheelWithPrimaryActions(emptySet())
        val action = buildScheduledAction(
            type = Schedulable.ScheduleType.ONCE,
            scheduleContentOverride = ZonedDateTime.now().plusMinutes(2).toString()
        )

        val timeWheel = timeWheel()
        val actionHour = action.scheduleContentHour()
        setCurrentHour(timeWheel, actionHour)
        setWheelState(timeWheel, "SLEEPING")

        actionScheduler.schedule(action)

        assertEquals("ACTIVE", wheelStateName(timeWheel))
    }

    @Test
    fun `execute should handle mixed actions consistently`() {
        // 场景编号：15；路径：B2 + B2.1/B2.2/B2.3；目的：验证混合输入行为
        initTimeWheelWithPrimaryActions(emptySet())
        val ok = buildScheduledAction(
            type = Schedulable.ScheduleType.ONCE,
            scheduleContentOverride = ZonedDateTime.now().plusMinutes(2).toString()
        )
        val nonPrepare = buildScheduledAction(
            type = Schedulable.ScheduleType.ONCE,
            scheduleContentOverride = ZonedDateTime.now().plusMinutes(2).toString()
        )
        nonPrepare.status = Action.Status.FAILED
        val invalid = buildScheduledAction(
            type = Schedulable.ScheduleType.CYCLE,
            scheduleContentOverride = "invalid-cron"
        )

        actionScheduler.schedule(ok)
        actionScheduler.schedule(nonPrepare)
        actionScheduler.schedule(invalid)

        verify(actionCapability, times(1)).putAction(ok)
        verify(actionCapability, times(1)).putAction(nonPrepare)
        verify(actionCapability, times(1)).putAction(invalid)
        val allScheduled = allScheduledActions(timeWheel())
        assertTrue(allScheduled.contains(ok))
        assertFalse(allScheduled.contains(nonPrepare))
        assertFalse(allScheduled.contains(invalid))
    }

    private fun initTimeWheelWithPrimaryActions(actions: Set<SchedulableExecutableAction>) {
        @Suppress("UNCHECKED_CAST")
        Mockito.`when`(actionCapability.listActions(null, null))
            .thenReturn(actions as Set<ExecutableAction>)
        actionScheduler.init()
    }

    private fun buildScheduledAction(
        type: Schedulable.ScheduleType,
        scheduleContentOverride: String? = null
    ): SchedulableExecutableAction {
        val action = SchedulableExecutableAction(
            "test",
            mutableMapOf(),
            "reason",
            "description",
            "test",
            type,
            scheduleContentOverride ?: scheduleContentOverride.toString()
        )
        return action
    }

    private fun SchedulableExecutableAction.scheduleContentHour(): Int {
        return ZonedDateTime.parse(this.scheduleContent).hour
    }

    private fun timeWheel(): Any {
        val field = actionScheduler.javaClass.getDeclaredField("timeWheel")
        field.isAccessible = true
        return field.get(actionScheduler)
    }

    @Suppress("UNCHECKED_CAST")
    private fun actionsGroupByHour(timeWheel: Any): Array<MutableSet<SchedulableExecutableAction>> {
        val field = timeWheel.javaClass.getDeclaredField("actionsGroupByHour")
        field.isAccessible = true
        return field.get(timeWheel) as Array<MutableSet<SchedulableExecutableAction>>
    }

    private fun allScheduledActions(timeWheel: Any): Set<SchedulableExecutableAction> {
        val result = linkedSetOf<SchedulableExecutableAction>()
        for (bucket in actionsGroupByHour(timeWheel)) {
            result.addAll(bucket)
        }
        return result
    }

    private fun setCurrentHour(timeWheel: Any, hour: Int) {
        val field = timeWheel.javaClass.getDeclaredField("currentHour")
        field.isAccessible = true
        field.setInt(timeWheel, hour)
    }

    private fun setWheelState(timeWheel: Any, name: String) {
        val field = timeWheel.javaClass.getDeclaredField("state")
        field.isAccessible = true
        @Suppress("UNCHECKED_CAST")
        val state = field.get(timeWheel) as MutableStateFlow<Any>
        state.value = wheelStateEnum(name)
    }

    private fun wheelStateName(timeWheel: Any): String {
        val field = timeWheel.javaClass.getDeclaredField("state")
        field.isAccessible = true
        val state = field.get(timeWheel) as MutableStateFlow<*>
        val value = state.value as Enum<*>
        return value.name
    }

    private fun wheelStateEnum(name: String): Any {
        @Suppress("UNCHECKED_CAST")
        val clazz = Class.forName(
            $$"work.slhaf.partner.component.modules.action.dispatcher.scheduler.ActionScheduler$TimeWheel$WheelState"
        ) as Class<out Enum<*>>
        return java.lang.Enum.valueOf(clazz, name)
    }
}
