'use strict'

const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')

const pem = require('@metcoder95/https-pem')

const { Client } = require('..')

test('#2364 - Concurrent aborts', async t => {
  t = tspl(t, { plan: 10 })

  const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
  // Concurrent aborts can cause the client to RST_STREAM / close the session
  // while server-side timers are still pending; swallow the resulting
  // ECONNRESET that fires on the session, the socket, or the server itself
  // after the test body resolves (macOS/Windows surface this more often than
  // Linux).
  server.on('error', () => {})
  server.on('session', session => {
    session.on('error', () => {})
    session.socket?.on('error', () => {})
  })
  server.on('secureConnection', socket => socket.on('error', () => {}))

  server.on('stream', (stream, headers, _flags, rawHeaders) => {
    setTimeout(() => {
      // The peer may have reset the stream from an AbortSignal before we
      // respond; guard against ERR_HTTP2_INVALID_STREAM.
      if (stream.destroyed || stream.closed) return
      stream.respond({
        'content-type': 'text/plain; charset=utf-8',
        'x-custom-h2': 'hello',
        ':status': 200
      })
      stream.end('hello h2!')
    }, 100)
  })

  after(() => server.close())
  await once(server.listen(0), 'listening')

  const client = new Client(`https://localhost:${server.address().port}`, {
    connect: {
      rejectUnauthorized: false
    },
    allowH2: true
  })

  after(() => client.close().catch(() => {}))
  const signal = AbortSignal.timeout(100)

  client.request(
    {
      path: '/1',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      }
    },
    (err, response) => {
      t.ifError(err)
      t.strictEqual(
        response.headers['content-type'],
        'text/plain; charset=utf-8'
      )
      t.strictEqual(response.headers['x-custom-h2'], 'hello')
      t.strictEqual(response.statusCode, 200)
    }
  )

  client.request(
    {
      path: '/2',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      },
      signal
    },
    (err, response) => {
      t.strictEqual(err.name, 'TimeoutError')
    }
  )

  client.request(
    {
      path: '/3',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      }
    },
    (err, response) => {
      t.ifError(err)
      t.strictEqual(
        response.headers['content-type'],
        'text/plain; charset=utf-8'
      )
      t.strictEqual(response.headers['x-custom-h2'], 'hello')
      t.strictEqual(response.statusCode, 200)
    }
  )

  client.request(
    {
      path: '/4',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      },
      signal
    },
    (err, response) => {
      t.strictEqual(err.name, 'TimeoutError')
    }
  )

  await t.completed
  // Close the client inside the test body so the resulting session/socket
  // teardown (and any ECONNRESET it generates) lands while the test is still
  // running — otherwise node:test flags it as "async activity after the test
  // ended" on macOS/Windows.
  await client.close()
})

test('#2364 - Concurrent aborts (2nd variant)', async t => {
  t = tspl(t, { plan: 10 })

  const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
  // Concurrent aborts can cause the client to RST_STREAM / close the session
  // while server-side timers are still pending; swallow the resulting
  // ECONNRESET that fires on the session, the socket, or the server itself
  // after the test body resolves (macOS/Windows surface this more often than
  // Linux).
  server.on('error', () => {})
  server.on('session', session => {
    session.on('error', () => {})
    session.socket?.on('error', () => {})
  })
  server.on('secureConnection', socket => socket.on('error', () => {}))
  let counter = 0

  server.on('stream', (stream, headers, _flags, rawHeaders) => {
    counter++

    if (counter % 2 === 0) {
      setTimeout(() => {
        if (stream.destroyed) {
          return
        }

        stream.respond({
          'content-type': 'text/plain; charset=utf-8',
          'x-custom-h2': 'hello',
          ':status': 200
        })

        stream.end('hello h2!')
      }, 400)

      return
    }

    stream.respond({
      'content-type': 'text/plain; charset=utf-8',
      'x-custom-h2': 'hello',
      ':status': 200
    })

    stream.end('hello h2!')
  })

  after(() => server.close())
  await once(server.listen(0), 'listening')

  const client = new Client(`https://localhost:${server.address().port}`, {
    connect: {
      rejectUnauthorized: false
    },
    allowH2: true
  })
  after(() => client.close().catch(() => {}))

  const signal = AbortSignal.timeout(300)

  client.request(
    {
      path: '/1',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      }
    },
    (err, response) => {
      t.ifError(err)
      t.strictEqual(
        response.headers['content-type'],
        'text/plain; charset=utf-8'
      )
      t.strictEqual(response.headers['x-custom-h2'], 'hello')
      t.strictEqual(response.statusCode, 200)
    }
  )

  client.request(
    {
      path: '/2',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      },
      signal
    },
    (err, response) => {
      t.strictEqual(err.name, 'TimeoutError')
    }
  )

  client.request(
    {
      path: '/3',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      }
    },
    (err, response) => {
      t.ifError(err)
      t.strictEqual(
        response.headers['content-type'],
        'text/plain; charset=utf-8'
      )
      t.strictEqual(response.headers['x-custom-h2'], 'hello')
      t.strictEqual(response.statusCode, 200)
    }
  )

  client.request(
    {
      path: '/4',
      method: 'GET',
      headers: {
        'x-my-header': 'foo'
      },
      signal
    },
    (err, response) => {
      t.strictEqual(err.name, 'TimeoutError')
    }
  )

  await t.completed
  // See first variant — close inside the test body to keep teardown noise
  // inside the test lifetime. Also wait for the server's 400ms timer to
  // fire (it will no-op on a destroyed stream) before letting the test end.
  await client.close()
  await new Promise(resolve => setTimeout(resolve, 500))
})
