import { afterEach, describe, expect, it, vi } from 'vitest'

const cleanupFns: Array<() => void> = []

vi.mock('vue', async (importOriginal) => {
  const actual = await importOriginal<typeof import('vue')>()
  return {
    ...actual,
    onBeforeUnmount: (fn: () => void) => {
      cleanupFns.push(fn)
    },
  }
})

const { computed, nextTick, reactive, ref } = await import('vue')
const {
  defaultSessionUiMetadata,
  mergeSessionUiMetadata,
} = await import('../src/dimsdk/core/session-meta.ts')
const {
  useChatQueue,
} = await import('../src/dimsdk/pages/ChatPage/logic/queue.ts')

type StoreMock = ReturnType<typeof createStoreMock>

function assignSessionMeta(target: any, next: any): void {
  target.queue = [...next.queue]
  target.queueLength = next.queueLength
  target.status = next.status
  target.lastUpdatedAt = next.lastUpdatedAt
  target.lastErrorCode = next.lastErrorCode
  target.pinnedMessageIds = [...next.pinnedMessageIds]
  target.titleSuggestedAt = next.titleSuggestedAt
}

function createStoreMock() {
  const state = reactive({
    approval: {
      active: null as any,
      decisions: {} as Record<string, unknown>,
      mode: 'normal' as const,
      pendingAutoRun: null as any,
      queue: [] as any[],
    },
    config: {
      interleaved_thinking: false,
      model: 'model-a',
      temperature: 0,
    },
    context: {
      env: {} as Record<string, string | undefined>,
    },
    messages: [] as any[],
    paused: null as any,
    provider: {
      activeId: 'cp_test',
      connections: {} as Record<string, unknown>,
      customProviders: [
        {
          adapter: 'openai',
          apiKey: '',
          baseUrl: '',
          id: 'cp_test',
          models: [{ id: 'model-a' }],
          name: 'Test Provider',
        },
      ],
    },
    runningAgentRound: false,
    sessionId: 'session_queue_test',
    sessionMeta: reactive(defaultSessionUiMetadata()),
  })

  return {
    clearRevertedMessages: vi.fn(),
    interrupt: vi.fn(),
    send: vi.fn(async (_text: string) => {}),
    setEnvVar: vi.fn((key: string, value: string | undefined) => {
      if (value == null || value === '') delete state.context.env[key]
      else state.context.env[key] = value
    }),
    state,
    steer: vi.fn(async (_text: string) => true),
  }
}

function createQueueHarness(store: StoreMock) {
  const input = ref('')
  const focusFiles = ref<string[]>([])
  const historyIndex = ref(0)
  const queueConfirmVisible = ref(false)
  const queueConfirmReason = ref<any>(null)
  const showSessions = ref(false)
  const toasts: string[] = []

  const updateSessionMeta = (patch: any, opts?: { bumpUpdatedAt?: boolean }) => {
    const bump = opts?.bumpUpdatedAt !== false ? { lastUpdatedAt: Date.now() } : {}
    const next = mergeSessionUiMetadata(store.state.sessionMeta, {
      ...patch,
      ...bump,
    })
    assignSessionMeta(store.state.sessionMeta, next)
  }

  const queue = useChatQueue({
    anyDialogOpen: computed(() => false),
    currentSessionId: computed(() => String(store.state.sessionId ?? '').trim()),
    focusFiles,
    history: computed(() => [] as string[]),
    historyIndex,
    input,
    queueConfirmReason,
    queueConfirmVisible,
    queueErrorCode: computed(() => store.state.sessionMeta.lastErrorCode ?? null),
    queueItems: computed(() => store.state.sessionMeta.queue),
    queueStatus: computed(() => store.state.sessionMeta.status),
    sessionMeta: computed(() => store.state.sessionMeta),
    setStickToBottom: () => {},
    showCopyToast: (text: string) => {
      toasts.push(text)
    },
    showSessions,
    store: store as any,
    updateSessionMeta,
  })

  return {
    ...queue,
    queueConfirmReason,
    queueConfirmVisible,
    queueItems: computed(() => store.state.sessionMeta.queue),
    queueStatus: computed(() => store.state.sessionMeta.status),
    toasts,
  }
}

function createDeferred() {
  let resolve!: () => void
  const promise = new Promise<void>((res) => {
    resolve = res
  })
  return { promise, resolve }
}

async function flushQueue(turns = 6): Promise<void> {
  for (let index = 0; index < turns; index += 1) {
    await Promise.resolve()
    await nextTick()
  }
}

async function waitForActiveSend(
  activeSends: Array<{ finish: () => Promise<void>; text: string }>,
  expected: string,
): Promise<{ finish: () => Promise<void>; text: string }> {
  for (let index = 0; index < 12; index += 1) {
    const current = activeSends[0]
    if (current?.text === expected) return activeSends.shift()!
    await flushQueue()
  }
  throw new Error(`Timed out waiting for queued send "${expected}"`)
}

afterEach(() => {
  while (cleanupFns.length > 0) cleanupFns.pop()?.()
  vi.restoreAllMocks()
})

describe('dimsdk chat queue', () => {
  it('falls back to the current config when the saved queue config is unavailable', async () => {
    const store = createStoreMock()
    const queue = createQueueHarness(store)
    const sent: string[] = []
    const deferred = createDeferred()

    store.send = vi.fn(async (text: string) => {
      sent.push(text)
      store.state.runningAgentRound = true
      await deferred.promise
    })

    store.state.runningAgentRound = true
    await flushQueue()
    expect(queue.enqueueMessage('1')).toBe(true)

    store.state.provider.customProviders[0]!.models = [{ id: 'model-b' }]
    store.state.config.model = 'model-b'
    store.state.runningAgentRound = false
    await flushQueue()

    expect(sent).toEqual(['1'])
    expect(queue.queueItems.value).toHaveLength(0)
    expect(queue.queueConfirmVisible.value).toBe(false)

    deferred.resolve()
    store.state.runningAgentRound = false
    await flushQueue()

    expect(queue.queueStatus.value).toBe('idle')
  })

  it('drains five queued messages in order after each llm done', async () => {
    const store = createStoreMock()
    const activeSends: Array<{ finish: () => Promise<void>; text: string }> = []
    const sent: string[] = []
    const expected = ['1', '2', '3', '4', '5']

    store.send = vi.fn((text: string) => {
      sent.push(text)
      store.state.messages.push({
        content: text,
        createdAtMs: Date.now(),
        id: `user_${text}`,
        role: 'user',
      })
      store.state.runningAgentRound = true
      const deferred = createDeferred()
      activeSends.push({
        finish: async () => {
          store.state.runningAgentRound = false
          deferred.resolve()
          await flushQueue()
        },
        text,
      })
      return deferred.promise
    })

    const queue = createQueueHarness(store)

    store.state.runningAgentRound = true
    await flushQueue()
    for (const text of expected) {
      expect(queue.enqueueMessage(text)).toBe(true)
    }
    expect(queue.queueItems.value.map((item) => item.text)).toEqual(expected)

    store.state.runningAgentRound = false
    await flushQueue()

    for (let index = 0; index < expected.length; index += 1) {
      const text = expected[index]!
      const current = await waitForActiveSend(activeSends, text)
      expect(sent).toEqual(expected.slice(0, index + 1))
      expect(queue.queueItems.value.map((item) => item.text)).toEqual(
        expected.slice(index + 1),
      )
      await current.finish()
    }

    expect(queue.queueItems.value).toHaveLength(0)
    expect(queue.queueStatus.value).toBe('idle')
    expect(
      store.state.messages
        .filter((message: any) => message.role === 'user')
        .map((message: any) => message.content),
    ).toEqual(expected)
  })

  it('continues draining queued messages after an Esc interrupt cancels the active round', async () => {
    const store = createStoreMock()
    const activeSends: Array<{ finish: () => Promise<void>; text: string }> = []
    const sent: string[] = []

    store.send = vi.fn((text: string) => {
      sent.push(text)
      store.state.messages.push({
        content: text,
        createdAtMs: Date.now(),
        id: `user_${text}`,
        role: 'user',
      })
      store.state.runningAgentRound = true
      const deferred = createDeferred()
      activeSends.push({
        finish: async () => {
          store.state.runningAgentRound = false
          deferred.resolve()
          await flushQueue()
        },
        text,
      })
      return deferred.promise
    })

    const queue = createQueueHarness(store)

    store.state.runningAgentRound = true
    await flushQueue()
    expect(queue.enqueueMessage('1')).toBe(true)
    expect(queue.enqueueMessage('2')).toBe(true)

    queue.requestInterrupt()
    expect(store.interrupt).toHaveBeenCalledTimes(1)

    store.state.runningAgentRound = false
    await flushQueue()

    const first = await waitForActiveSend(activeSends, '1')
    expect(sent).toEqual(['1'])
    expect(queue.queueItems.value.map((item) => item.text)).toEqual(['2'])

    await first.finish()

    const second = await waitForActiveSend(activeSends, '2')
    expect(sent).toEqual(['1', '2'])
    expect(queue.queueItems.value).toHaveLength(0)

    await second.finish()

    expect(queue.queueStatus.value).toBe('idle')
  })

  it('injects promoted steer messages individually before draining the remaining queue', async () => {
    const store = createStoreMock()
    const activeSends: Array<{ finish: () => Promise<void>; text: string }> = []
    const sent: string[] = []
    const queue = createQueueHarness(store)
    const expected = ['1', '2', '3', '4', '5']
    let nowMs = 10_000
    vi.spyOn(Date, 'now').mockImplementation(() => nowMs++)

    store.send = vi.fn((text: string) => {
      sent.push(text)
      store.state.messages.push({
        content: text,
        createdAtMs: Date.now(),
        id: `user_${text}`,
        role: 'user',
      })
      store.state.runningAgentRound = true
      const deferred = createDeferred()
      activeSends.push({
        finish: async () => {
          store.state.runningAgentRound = false
          deferred.resolve()
          await flushQueue()
        },
        text,
      })
      return deferred.promise
    })

    store.steer = vi.fn(async (text: string) => {
      queueMicrotask(() => {
        store.state.messages.push({
          content: text,
          createdAtMs: Date.now(),
          id: `steer_${text}`,
          metadata: {
            originalEventType: 'steer_injected',
            steer: true,
          },
          role: 'user',
        })
      })
      return true
    })

    store.state.runningAgentRound = true
    await flushQueue()
    for (const text of expected) {
      expect(queue.enqueueMessage(text)).toBe(true)
    }

    const idByText = new Map(
      queue.queueItems.value.map((item) => [item.text, item.id] as const),
    )
    await queue.upgradeQueueItemToSteer(idByText.get('1')!)
    await queue.upgradeQueueItemToSteer(idByText.get('2')!)
    await queue.upgradeQueueItemToSteer(idByText.get('3')!)
    await flushQueue()

    expect(store.steer).toHaveBeenCalledTimes(3)
    expect(
      store.state.messages
        .filter(
          (message: any) =>
            message.role === 'user'
            && message.metadata?.originalEventType === 'steer_injected',
        )
        .map((message: any) => message.content),
    ).toEqual(['1', '2', '3'])
    expect(queue.queueItems.value.map((item) => item.text)).toEqual(['4', '5'])

    store.state.runningAgentRound = false
    await flushQueue()

    for (const text of ['4', '5']) {
      const current = await waitForActiveSend(activeSends, text)
      await current.finish()
    }

    expect(sent).toEqual(['4', '5'])
    expect(
      store.state.messages
        .filter((message: any) => message.role === 'user')
        .map((message: any) => message.content),
    ).toEqual(expected)
    expect(queue.queueItems.value).toHaveLength(0)
    expect(queue.queueStatus.value).toBe('idle')
  })
})
