import { afterEach, describe, expect, it, vi } from 'vitest'
import { subscribeRecoveredRun } from '@/lib/query/hooks/run-stream/recovered-run-subscription'

function jsonResponse(payload: unknown, status = 200) {
  return {
    ok: status >= 200 && status < 300,
    json: async () => payload,
  }
}

async function waitForCondition(condition: () => boolean, timeoutMs = 1000) {
  const startedAt = Date.now()
  while (Date.now() - startedAt < timeoutMs) {
    if (condition()) return
    await new Promise((resolve) => setTimeout(resolve, 10))
  }
  throw new Error('condition not met before timeout')
}

describe('recovered run subscription', () => {
  const originalFetch = globalThis.fetch

  afterEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
    vi.useRealTimers()
    if (originalFetch) {
      globalThis.fetch = originalFetch
    } else {
      Reflect.deleteProperty(globalThis, 'fetch')
    }
  })

  it('replays run events and keeps recovering when no terminal event is present', async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      jsonResponse({
        events: [
          {
            seq: 1,
            eventType: 'step.start',
            stepKey: 'clip_1_phase1',
            attempt: 1,
            payload: {
              stepTitle: '分镜规划',
              stepIndex: 1,
              stepTotal: 4,
              message: 'running',
            },
            createdAt: '2026-02-28T00:00:01.000Z',
          },
        ],
      }),
    )
    globalThis.fetch = fetchMock as unknown as typeof fetch

    const applyAndCapture = vi.fn()
    const onSettled = vi.fn()

    const cleanup = subscribeRecoveredRun({
      runId: 'run-1',
      taskStreamTimeoutMs: 10_000,
      applyAndCapture,
      onSettled,
    })

    await waitForCondition(() => fetchMock.mock.calls.length > 0 && applyAndCapture.mock.calls.length > 0)
    expect(fetchMock).toHaveBeenCalledWith(
      '/api/runs/run-1/events?afterSeq=0&limit=500',
      expect.objectContaining({ method: 'GET', cache: 'no-store' }),
    )
    expect(applyAndCapture).toHaveBeenCalledWith(expect.objectContaining({
      event: 'step.start',
      runId: 'run-1',
      stepId: 'clip_1_phase1',
    }))
    expect(onSettled).not.toHaveBeenCalled()
    cleanup()
  })

  it('settles recovery when replay hits terminal run event', async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      jsonResponse({
        events: [
          {
            seq: 1,
            eventType: 'run.error',
            payload: {
              message: 'exception TypeError: fetch failed sending request',
            },
            createdAt: '2026-02-28T00:00:02.000Z',
          },
        ],
      }),
    )
    globalThis.fetch = fetchMock as unknown as typeof fetch

    const applyAndCapture = vi.fn()
    const onSettled = vi.fn()

    subscribeRecoveredRun({
      runId: 'run-1',
      taskStreamTimeoutMs: 10_000,
      applyAndCapture,
      onSettled,
    })

    await waitForCondition(() => onSettled.mock.calls.length === 1 && applyAndCapture.mock.calls.length > 0)
    expect(onSettled).toHaveBeenCalledTimes(1)
    expect(applyAndCapture).toHaveBeenCalledWith(expect.objectContaining({
      event: 'run.error',
      runId: 'run-1',
    }))
  })

  it('replays step.chunk output so refresh keeps prior text', async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      jsonResponse({
        events: [
          {
            seq: 1,
            eventType: 'step.chunk',
            stepKey: 'clip_1_phase1',
            payload: {
              stream: {
                kind: 'text',
                lane: 'main',
                seq: 1,
                delta: '旧输出',
              },
            },
            createdAt: '2026-02-28T00:00:03.000Z',
          },
        ],
      }),
    )
    globalThis.fetch = fetchMock as unknown as typeof fetch

    const applyAndCapture = vi.fn()
    const onSettled = vi.fn()

    const cleanup = subscribeRecoveredRun({
      runId: 'run-1',
      taskStreamTimeoutMs: 10_000,
      applyAndCapture,
      onSettled,
    })

    await waitForCondition(() => applyAndCapture.mock.calls.some((call) => call[0]?.event === 'step.chunk'))
    expect(applyAndCapture).toHaveBeenCalledWith(expect.objectContaining({
      event: 'step.chunk',
      runId: 'run-1',
      stepId: 'clip_1_phase1',
      textDelta: '旧输出',
    }))
    cleanup()
  })

  it('emits run.error and settles when idle timeout is reached', async () => {
    vi.useFakeTimers()
    const fetchMock = vi.fn().mockResolvedValue(
      jsonResponse({
        events: [],
      }),
    )
    globalThis.fetch = fetchMock as unknown as typeof fetch

    const applyAndCapture = vi.fn()
    const onSettled = vi.fn()

    subscribeRecoveredRun({
      runId: 'run-timeout',
      taskStreamTimeoutMs: 3_000,
      applyAndCapture,
      onSettled,
    })

    await vi.advanceTimersByTimeAsync(3_200)

    expect(onSettled).toHaveBeenCalledTimes(1)
    expect(applyAndCapture).toHaveBeenCalledWith(expect.objectContaining({
      event: 'run.error',
      runId: 'run-timeout',
      message: 'run stream timeout: run-timeout',
    }))

    vi.useRealTimers()
  })

  it('resets idle timeout when a new event arrives during recovery', async () => {
    vi.useFakeTimers()
    let eventFetchCount = 0
    const fetchMock = vi.fn().mockImplementation(async () => {
      eventFetchCount += 1
      if (eventFetchCount === 2) {
        return jsonResponse({
          events: [
            {
              seq: 1,
              eventType: 'run.start',
              payload: { message: 'resumed' },
              createdAt: '2026-02-28T00:00:01.500Z',
            },
          ],
        })
      }
      return jsonResponse({ events: [] })
    })
    globalThis.fetch = fetchMock as unknown as typeof fetch

    const applyAndCapture = vi.fn()
    const onSettled = vi.fn()

    subscribeRecoveredRun({
      runId: 'run-recover',
      taskStreamTimeoutMs: 3_000,
      applyAndCapture,
      onSettled,
    })

    await vi.advanceTimersByTimeAsync(3_200)
    expect(onSettled).not.toHaveBeenCalled()

    await vi.advanceTimersByTimeAsync(2_000)
    expect(onSettled).toHaveBeenCalledTimes(1)
    expect(applyAndCapture).toHaveBeenCalledWith(expect.objectContaining({
      event: 'run.start',
      runId: 'run-recover',
    }))

    vi.useRealTimers()
  })

  it('reconciles run snapshot to failed when event polling stays empty', async () => {
    vi.useFakeTimers()
    const fetchMock = vi.fn().mockImplementation(async (input: RequestInfo | URL) => {
      const url = String(input)
      if (url.includes('/api/runs/run-reconcile/events')) {
        return jsonResponse({ events: [] })
      }
      if (url === '/api/runs/run-reconcile') {
        return jsonResponse({
          run: {
            id: 'run-reconcile',
            status: 'failed',
            errorMessage: 'Ark Responses 调用失败',
          },
        })
      }
      return jsonResponse({ events: [] })
    })
    globalThis.fetch = fetchMock as unknown as typeof fetch

    const applyAndCapture = vi.fn()
    const onSettled = vi.fn()

    subscribeRecoveredRun({
      runId: 'run-reconcile',
      taskStreamTimeoutMs: 20_000,
      applyAndCapture,
      onSettled,
    })

    await vi.advanceTimersByTimeAsync(3_500)

    expect(onSettled).toHaveBeenCalledTimes(1)
    expect(fetchMock).toHaveBeenCalledWith(
      '/api/runs/run-reconcile',
      expect.objectContaining({ method: 'GET', cache: 'no-store' }),
    )
    expect(applyAndCapture).toHaveBeenCalledWith(expect.objectContaining({
      event: 'run.error',
      runId: 'run-reconcile',
      message: 'Ark Responses 调用失败',
    }))

    vi.useRealTimers()
  })
})
