import { FetchRequest } from '@mswjs/interceptors'
import { http } from '#http/http'
import { graphql } from '../../../graphql'
import { ws } from '../../../ws'
import { bypass } from '#utils/bypass'
import type { HttpNetworkFrameEventMap } from './http-frame'
import { HttpNetworkFrame } from './http-frame'
import { InMemoryHandlersController } from '../../experimental/handlers-controller'
import { getSiblingHandlers } from '../../utils/internal/attach-sibling-handlers'

const gql = graphql.link('*')

beforeAll(() => {
  vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterEach(() => {
  vi.clearAllMocks()
})

afterAll(() => {
  vi.restoreAllMocks()
})

function spyOnNetworkFrame(frame: HttpNetworkFrame) {
  const events: Array<
    HttpNetworkFrameEventMap[keyof HttpNetworkFrameEventMap]
  > = []

  frame.events.on('*', (event) => events.push(event))

  return {
    events,
  }
}

test('filters only request type handlers', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: new Request('http://localhost/api'),
  })

  const httpHandlers = [http.post('http://localhost/api/user', () => {})]
  const graphqlHandlers = [gql.query('GetUser', () => {})]
  const webSocketHandlers = [
    ws.link('ws://localhost').addEventListener('connection', () => {}),
  ]
  const webSocketSiblingHandlers = webSocketHandlers.flatMap((handler) =>
    getSiblingHandlers(handler),
  )

  const controller = new InMemoryHandlersController([
    ...httpHandlers,
    ...webSocketHandlers,
    ...graphqlHandlers,
  ])

  expect(frame.getHandlers(controller)).toEqual([
    ...httpHandlers,
    ...webSocketSiblingHandlers,
    ...graphqlHandlers,
  ])
  expect(frame.getHandlers(new InMemoryHandlersController([]))).toEqual([])
})

test('resolves a matching request', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: new Request('http://localhost/api'),
  })
  const { events } = spyOnNetworkFrame(frame)
  const unhandledFrameCallback = vi.fn()

  const matches = await frame.resolve(
    [
      http.get('http://localhost/api', () => {
        return new Response(null, { status: 204 })
      }),
    ],
    unhandledFrameCallback,
    { quiet: true },
  )

  expect.soft(matches).toBe(true)
  expect.soft(frame.respondWith).toHaveBeenCalledExactlyOnceWith(
    expect.objectContaining({
      status: 204,
    }),
  )
  expect.soft(frame.passthrough).not.toHaveBeenCalled()
  expect.soft(frame.errorWith).not.toHaveBeenCalled()
  expect.soft(unhandledFrameCallback).not.toHaveBeenCalled()
  expect.soft(events).toEqual([
    expect.objectContaining({
      type: 'request:start',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:match',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:end',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  ])
})

test('resolves a non-matching request', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: new Request('http://localhost/api'),
  })
  const { events } = spyOnNetworkFrame(frame)
  const unhandledFrameCallback = vi.fn()

  const matches = await frame.resolve(
    [
      http.get('http://example.com/resource', () => {
        return new Response(null, { status: 204 })
      }),
    ],
    unhandledFrameCallback,
    { quiet: true },
  )

  expect.soft(matches).toBe(false)
  expect.soft(frame.passthrough).toHaveBeenCalledOnce()
  expect.soft(frame.respondWith).not.toHaveBeenCalled()
  expect.soft(frame.errorWith).not.toHaveBeenCalled()
  expect.soft(unhandledFrameCallback).toHaveBeenCalledExactlyOnceWith(
    expect.objectContaining({
      frame,
    }),
  )
  expect.soft(events).toEqual([
    expect.objectContaining({
      type: 'request:start',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:unhandled',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:end',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  ])
})

/**
 * @todo Revisit the `http.all()` handler below once `http.connect()`
 * is supported: a matching CONNECT handler must take precedence.
 */
test('passes through an unhandled CONNECT request without applying the unhandled frame strategy', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: new FetchRequest('http://localhost/127.0.0.1:443', {
      method: 'CONNECT',
    }),
  })
  const { events } = spyOnNetworkFrame(frame)
  const unhandledFrameCallback = vi.fn()

  const matches = await frame.resolve(
    [
      http.all('*', () => {
        return new Response(null, { status: 204 })
      }),
    ],
    unhandledFrameCallback,
    { quiet: true },
  )

  expect.soft(matches).toBe(null)
  expect.soft(frame.passthrough).toHaveBeenCalledOnce()
  expect.soft(frame.respondWith).not.toHaveBeenCalled()
  expect.soft(frame.errorWith).not.toHaveBeenCalled()
  expect.soft(unhandledFrameCallback).not.toHaveBeenCalled()
  expect.soft(events).toEqual([
    expect.objectContaining({
      type: 'request:start',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:end',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  ])
})

test('resolves a matched passthrough', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: new Request('http://localhost/api'),
  })
  const { events } = spyOnNetworkFrame(frame)
  const unhandledFrameCallback = vi.fn()

  const matches = await frame.resolve(
    [
      http.get('http://localhost/api', () => {
        // Intentionally do nothing to passthrough.
      }),
    ],
    unhandledFrameCallback,
    { quiet: true },
  )

  expect.soft(matches).toBeNull()
  expect.soft(frame.passthrough).toHaveBeenCalledOnce()
  expect.soft(frame.respondWith).not.toHaveBeenCalled()
  expect.soft(frame.errorWith).not.toHaveBeenCalled()
  expect.soft(unhandledFrameCallback).not.toHaveBeenCalled()
  expect.soft(events).toEqual([
    expect.objectContaining({
      type: 'request:start',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:match',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:end',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  ])
})

test('resolves a bypassed request', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: bypass(new Request('http://localhost/api')),
  })
  const { events } = spyOnNetworkFrame(frame)
  const unhandledFrameCallback = vi.fn()

  const matches = await frame.resolve(
    [
      http.get('http://localhost/api', () => {
        return new Response(null, { status: 204 })
      }),
    ],
    unhandledFrameCallback,
    { quiet: true },
  )

  expect.soft(matches).toBeNull()
  expect.soft(frame.passthrough).toHaveBeenCalledOnce()
  expect.soft(frame.respondWith).not.toHaveBeenCalled()
  expect.soft(frame.errorWith).not.toHaveBeenCalled()
  expect.soft(unhandledFrameCallback).not.toHaveBeenCalled()
  expect.soft(events).toEqual([
    expect.objectContaining({
      type: 'request:start',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'request:end',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  ])
})

test('errors the request on unhandled exception', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: new Request('http://localhost/api'),
  })
  const { events } = spyOnNetworkFrame(frame)
  const unhandledFrameCallback = vi.fn()

  const exception = new Error('Unhandled exception')
  const matches = await frame.resolve(
    [
      http.get('http://localhost/api', () => {
        throw exception
      }),
    ],
    unhandledFrameCallback,
    { quiet: true },
  )

  expect.soft(matches).toBeNull()
  expect.soft(frame.errorWith).toHaveBeenCalledExactlyOnceWith(exception)
  expect.soft(frame.passthrough).not.toHaveBeenCalled()
  expect.soft(frame.respondWith).not.toHaveBeenCalled()
  expect.soft(unhandledFrameCallback).not.toHaveBeenCalled()
  expect.soft(events).toEqual([
    expect.objectContaining({
      type: 'request:start',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'unhandledException',
      error: exception,
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  ])

  expect.soft(console.error).toHaveBeenCalledTimes(2)
  expect.soft(console.error).toHaveBeenNthCalledWith(1, exception)
  expect
    .soft(console.error)
    .toHaveBeenNthCalledWith(
      2,
      '[MSW] Encountered an unhandled exception during the handler lookup for "GET http://localhost/api". Please see the original error above.',
    )
})

test('does not print an unhandled exception if the "unhandledException" listener is present', async () => {
  class HttpFrame extends HttpNetworkFrame {
    respondWith = vi.fn()
    passthrough = vi.fn()
    errorWith = vi.fn()
  }

  const frame = new HttpFrame({
    request: new Request('http://localhost/api'),
  })
  const { events } = spyOnNetworkFrame(frame)
  const unhandledFrameCallback = vi.fn()

  const unhandledExceptionListener = vi.fn()
  frame.events.on('unhandledException', unhandledExceptionListener)

  const exception = new Error('Unhandled exception')
  const matches = await frame.resolve(
    [
      http.get('http://localhost/api', () => {
        throw exception
      }),
    ],
    unhandledFrameCallback,
    { quiet: true },
  )

  expect.soft(matches).toBeNull()
  expect.soft(frame.errorWith).toHaveBeenCalledExactlyOnceWith(exception)
  expect.soft(frame.passthrough).not.toHaveBeenCalled()
  expect.soft(frame.respondWith).not.toHaveBeenCalled()
  expect.soft(unhandledFrameCallback).not.toHaveBeenCalled()
  expect.soft(events).toEqual([
    expect.objectContaining({
      type: 'request:start',
      requestId: frame.data.id,
      request: frame.data.request,
    }),
    expect.objectContaining({
      type: 'unhandledException',
      error: exception,
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  ])

  expect.soft(console.error).not.toHaveBeenCalled()
  expect.soft(unhandledExceptionListener).toHaveBeenCalledExactlyOnceWith(
    expect.objectContaining({
      error: exception,
      requestId: frame.data.id,
      request: frame.data.request,
    }),
  )
})
