import { afterAll, afterEach, beforeAll, expect, test } from 'vitest'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'

import { Template } from '../../src'
import { TEST_API_KEY, apiUrl } from '../setup'

// Hold the request open until the caller aborts. If the signal is already
// aborted by the time the handler runs, `addEventListener('abort', …)` would
// never fire — so check `aborted` first to avoid hanging.
function holdUntilAborted(signal: AbortSignal): Promise<never> {
  return new Promise<never>((_, reject) => {
    const abort = () => reject(new DOMException('aborted', 'AbortError'))
    if (signal.aborted) {
      abort()
      return
    }
    signal.addEventListener('abort', abort, { once: true })
  })
}

const restHandlers = [
  http.post(apiUrl('/v3/templates'), async ({ request }) => {
    await holdUntilAborted(request.signal)
    return HttpResponse.json({})
  }),
  http.get(apiUrl('/templates/aliases/:alias'), async ({ request }) => {
    await holdUntilAborted(request.signal)
    return HttpResponse.json({})
  }),
  http.get(
    apiUrl('/templates/:templateID/builds/:buildID/status'),
    async ({ request }) => {
      await holdUntilAborted(request.signal)
      return HttpResponse.json({})
    }
  ),
  http.post(apiUrl('/templates/tags'), async ({ request }) => {
    await holdUntilAborted(request.signal)
    return HttpResponse.json({})
  }),
  http.delete(apiUrl('/templates/tags'), async ({ request }) => {
    await holdUntilAborted(request.signal)
    return HttpResponse.json({})
  }),
  http.get(apiUrl('/templates/:templateID/tags'), async ({ request }) => {
    await holdUntilAborted(request.signal)
    return HttpResponse.json([])
  }),
]

const server = setupServer(...restHandlers)

beforeAll(() =>
  server.listen({
    onUnhandledRequest: 'bypass',
  })
)

afterAll(() => server.close())

afterEach(() => server.resetHandlers())

// Resolves once MSW has dispatched the next request, so tests can abort
// deterministically instead of guessing with `setTimeout`.
function nextRequestStart(): Promise<void> {
  return new Promise<void>((resolve) => {
    const listener = () => {
      server.events.removeListener('request:start', listener)
      resolve()
    }
    server.events.on('request:start', listener)
  })
}

test('Template.build rejects when AbortSignal is aborted', async () => {
  const controller = new AbortController()
  const requestStarted = nextRequestStart()

  const template = Template().fromBaseImage()
  const promise = Template.build(template, 'test-template', {
    apiKey: TEST_API_KEY,
    signal: controller.signal,
  })

  await requestStarted
  controller.abort()

  await expect(promise).rejects.toThrow()
})

test('Template.build rejects immediately when signal is already aborted', async () => {
  const controller = new AbortController()
  controller.abort()

  const template = Template().fromBaseImage()
  await expect(
    Template.build(template, 'test-template', {
      apiKey: TEST_API_KEY,
      signal: controller.signal,
    })
  ).rejects.toThrow()
})

test('Template.buildInBackground rejects when AbortSignal is aborted', async () => {
  const controller = new AbortController()
  const requestStarted = nextRequestStart()

  const template = Template().fromBaseImage()
  const promise = Template.buildInBackground(template, 'test-template', {
    apiKey: TEST_API_KEY,
    signal: controller.signal,
  })

  await requestStarted
  controller.abort()

  await expect(promise).rejects.toThrow()
})

test('Template.exists rejects when AbortSignal is aborted', async () => {
  const controller = new AbortController()
  const requestStarted = nextRequestStart()

  const promise = Template.exists('some-template', {
    apiKey: TEST_API_KEY,
    signal: controller.signal,
  })

  await requestStarted
  controller.abort()

  await expect(promise).rejects.toThrow()
})

test('Template.getBuildStatus rejects when AbortSignal is aborted', async () => {
  const controller = new AbortController()
  const requestStarted = nextRequestStart()

  const promise = Template.getBuildStatus(
    { templateId: 'tpl-1', buildId: 'build-1' },
    {
      apiKey: TEST_API_KEY,
      signal: controller.signal,
    }
  )

  await requestStarted
  controller.abort()

  await expect(promise).rejects.toThrow()
})

test('Template.assignTags rejects when AbortSignal is aborted', async () => {
  const controller = new AbortController()
  const requestStarted = nextRequestStart()

  const promise = Template.assignTags('some-template:v1', 'stable', {
    apiKey: TEST_API_KEY,
    signal: controller.signal,
  })

  await requestStarted
  controller.abort()

  await expect(promise).rejects.toThrow()
})

test('Template.removeTags rejects when AbortSignal is aborted', async () => {
  const controller = new AbortController()
  const requestStarted = nextRequestStart()

  const promise = Template.removeTags('some-template', 'stable', {
    apiKey: TEST_API_KEY,
    signal: controller.signal,
  })

  await requestStarted
  controller.abort()

  await expect(promise).rejects.toThrow()
})

test('Template.getTags rejects when AbortSignal is aborted', async () => {
  const controller = new AbortController()
  const requestStarted = nextRequestStart()

  const promise = Template.getTags('some-template', {
    apiKey: TEST_API_KEY,
    signal: controller.signal,
  })

  await requestStarted
  controller.abort()

  await expect(promise).rejects.toThrow()
})
