import { chai, expect, test, vi } from 'vitest'

test('simple usage', async () => {
  await expect.poll(() => false).toBe(false)
  await expect.poll(() => false).not.toBe(true)
  // .resolves allowed after .poll
  await expect(Promise.resolve(1)).resolves.toBe(1)

  await expect(async () => {
    await expect.poll(() => Promise.resolve(1)).resolves.toBe(1)
  }).rejects.toThrow('expect.poll() is not supported in combination with .resolves')
  await expect(async () => {
    await expect.poll(() => Promise.reject(new Error('empty'))).rejects.toThrow('empty')
  }).rejects.toThrow('expect.poll() is not supported in combination with .rejects')

  const unsupported = [
    'matchSnapshot',
    'toMatchSnapshot',
    'toMatchInlineSnapshot',
    'throws',
    'Throw',
    'throw',
    'toThrow',
    'toThrowError',
    'toThrowErrorMatchingSnapshot',
    'toThrowErrorMatchingInlineSnapshot',
  ] as const

  for (const key of unsupported) {
    await expect(async () => {
      await expect.poll(() => Promise.resolve(1))[key as 'matchSnapshot']()
    }).rejects.toThrow(`expect.poll() is not supported in combination with .${key}(). Use vi.waitFor() if your assertion condition is unstable.`)
  }
})

test('timeout', async () => {
  await expect(async () => {
    await expect.poll(() => false, { timeout: 100, interval: 10 }).toBe(true)
  }).rejects.toThrow(expect.objectContaining({
    message: 'expected false to be true // Object.is equality',
    stack: expect.stringContaining('expect-poll.test.ts:38:68'),
    cause: expect.objectContaining({
      message: 'Matcher did not succeed in time.',
    }),
  }))
})

test('interval', async () => {
  const fn = vi.fn(() => true)
  await expect(async () => {
    // using big values because CI can be slow
    await expect.poll(fn, { interval: 100, timeout: 500 }).toBe(false)
  }).rejects.toThrow()
  // CI can be unstable, but there should be always at least 5 calls
  expect(fn.mock.calls.length >= 4).toBe(true)
})

test('fake timers don\'t break it', async () => {
  const now = Date.now()
  vi.useFakeTimers()
  await expect(async () => {
    await expect.poll(() => false, { timeout: 100 }).toBe(true)
  }).rejects.toThrow('expected false to be true // Object.is equality')
  vi.useRealTimers()
  const diff = Date.now() - now
  expect(diff >= 100).toBe(true)
})

test('fake timers are advanced on each poll interval', async ({ onTestFinished }) => {
  vi.useFakeTimers()
  onTestFinished(() => {
    vi.useRealTimers()
  })

  let didAdvance = false

  setTimeout(() => {
    didAdvance = true
  }, 50)

  await expect.poll(() => didAdvance, { interval: 100 }).toBe(true)
})

test('custom matcher works correctly', async () => {
  const fn = vi.fn()
  let idx = 0
  expect.extend({
    toBeJestCompatible() {
      idx++
      fn({ poll: this.poll })
      return {
        pass: idx > 2,
        message: () => 'ok',
      }
    },
  })
  await expect.poll(() => 1, { interval: 10 }).toBeJestCompatible()
  expect(fn).toHaveBeenCalledTimes(3)
  expect(fn).toHaveBeenCalledWith({ poll: true })
})

test('toBeDefined', async () => {
  await expect.poll(() => 1).toBeDefined()
  await expect.poll(() => undefined).not.toBeDefined()

  await expect(() =>
    expect.poll(() => 1, { timeout: 100, interval: 10 }).not.toBeDefined(),
  ).rejects.toThrow(expect.objectContaining({
    message: 'expected 1 to be undefined',
    cause: expect.objectContaining({
      message: 'Matcher did not succeed in time.',
    }),
  }))

  await expect(() =>
    expect.poll(() => undefined, { timeout: 100, interval: 10 }).toBeDefined(),
  ).rejects.toThrow(expect.objectContaining({
    message: 'expected undefined to be defined',
    cause: expect.objectContaining({
      message: 'Matcher did not succeed in time.',
    }),
  }))
})

test('custom message', async () => {
  await expect(() =>
    expect.poll(() => 1, { timeout: 100, interval: 10, message: 'custom' }).toBe(2),
  ).rejects.toMatchInlineSnapshot(`[AssertionError: custom: expected 1 to be 2 // Object.is equality]`)
})

test('should set _isLastPollAttempt flag on last call', async () => {
  const fn = vi.fn(function (this: object) {
    return chai.util.flag(this, '_isLastPollAttempt')
  })
  await expect(async () => {
    await expect.poll(fn, { interval: 100, timeout: 500 }).toBe(false)
  }).rejects.toThrow()
  fn.mock.results.forEach((result, index) => {
    const isLastCall = index === fn.mock.results.length - 1
    expect(result.value).toBe(isLastCall ? true : undefined)
  })
})

test('should handle success on last attempt', async () => {
  const fn = vi.fn(function (this: object) {
    if (chai.util.flag(this, '_isLastPollAttempt')) {
      return 1
    }
    return undefined
  })
  await expect.poll(fn, { interval: 100, timeout: 500 }).toBe(1)
})

test('should handle failure on last attempt', async () => {
  const fn = vi.fn(function (this: object) {
    if (chai.util.flag(this, '_isLastPollAttempt')) {
      return 3
    }
    return 2
  })
  await expect(async () => {
    await expect.poll(fn, { interval: 10, timeout: 100 }).toBe(1)
  }).rejects.toThrow(expect.objectContaining({
    // makes sure cause message reflects the last attempt value
    message: 'expected 3 to be 1 // Object.is equality',
    cause: expect.objectContaining({
      message: 'Matcher did not succeed in time.',
    }),
  }))
})
