import crypto from 'node:crypto'
import http from 'node:http'
import type { AddressInfo } from 'node:net'
import { createGitHubWebhookServer } from '../providers/githubWebhookReceiver'
import {
  dispatchGitHubWebhook,
  verifyGitHubWebhookSignature,
} from '../providers/githubWebhook'

describe('githubWebhook', () => {
  test('verifyGitHubWebhookSignature valida HMAC SHA-256', () => {
    const body = JSON.stringify({ action: 'opened' })
    const secret = 'webhook-secret'
    const signature = `sha256=${crypto
      .createHmac('sha256', secret)
      .update(body)
      .digest('hex')}`

    expect(verifyGitHubWebhookSignature(body, signature, secret)).toBe(true)
    expect(verifyGitHubWebhookSignature(body, 'sha256=bad', secret)).toBe(false)
  })

  test('dispatchGitHubWebhook mapeia eventos relevantes', () => {
    expect(dispatchGitHubWebhook('issues', {
      action: 'opened',
      issue: { number: 10, html_url: 'https://github.com/acme/api/issues/10' },
    })).toEqual({
      type: 'issue_opened',
      issueNumber: 10,
      url: 'https://github.com/acme/api/issues/10',
    })

    expect(dispatchGitHubWebhook('projects_v2_item', { action: 'edited' })).toEqual({
      type: 'projects_v2_event',
      action: 'edited',
    })
  })

  test('receiver HTTP valida assinatura e despacha raw body', async () => {
    const secret = 'webhook-secret'
    const actions: string[] = []
    const server = createGitHubWebhookServer({
      secret,
      path: '/hook',
      onAction: (action) => {
        actions.push(action.type)
      },
    })

    try {
      const port = await listen(server)
      const body = JSON.stringify({
        action: 'created',
        issue: { number: 42 },
        comment: { body: '/aprovado' },
      })
      const response = await postWebhook(port, '/hook', body, secret, 'issue_comment')

      expect(response.statusCode).toBe(202)
      expect(actions).toEqual(['issue_comment'])
      expect(JSON.parse(response.body)).toEqual({ ok: true, type: 'issue_comment' })
    } finally {
      await close(server)
    }
  })

  test('receiver HTTP rejeita assinatura inválida', async () => {
    const server = createGitHubWebhookServer({ secret: 'webhook-secret', path: '/hook' })

    try {
      const port = await listen(server)
      const response = await postWebhook(port, '/hook', '{"action":"opened"}', 'wrong-secret', 'issues')

      expect(response.statusCode).toBe(401)
      expect(JSON.parse(response.body)).toEqual({ ok: false, error: 'invalid_signature' })
    } finally {
      await close(server)
    }
  })

  test('receiver HTTP rejeita payload acima do limite sem erro interno', async () => {
    const server = createGitHubWebhookServer({
      secret: 'webhook-secret',
      path: '/hook',
      maxBodyBytes: 8,
    })

    try {
      const port = await listen(server)
      const response = await postWebhook(port, '/hook', '{"large":true}', 'webhook-secret', 'issues')

      expect(response.statusCode).toBe(413)
      expect(JSON.parse(response.body)).toEqual({ ok: false, error: 'payload_too_large' })
    } finally {
      await close(server)
    }
  })
})

function listen(server: http.Server): Promise<number> {
  return new Promise((resolve) => {
    server.listen(0, () => {
      resolve((server.address() as AddressInfo).port)
    })
  })
}

function close(server: http.Server): Promise<void> {
  return new Promise((resolve, reject) => {
    server.close((err) => {
      if (err) reject(err)
      else resolve()
    })
  })
}

function postWebhook(
  port: number,
  path: string,
  body: string,
  secret: string,
  event: string,
): Promise<{ statusCode: number; body: string }> {
  const signature = `sha256=${crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex')}`

  return new Promise((resolve, reject) => {
    const req = http.request({
      method: 'POST',
      port,
      path,
      headers: {
        'content-type': 'application/json',
        'content-length': Buffer.byteLength(body),
        'x-github-event': event,
        'x-hub-signature-256': signature,
      },
    }, (res) => {
      const chunks: Buffer[] = []
      res.on('data', (chunk: Buffer) => chunks.push(chunk))
      res.on('end', () => {
        resolve({
          statusCode: res.statusCode ?? 0,
          body: Buffer.concat(chunks).toString('utf8'),
        })
      })
    })
    req.on('error', reject)
    req.end(body)
  })
}
