import { describe, expect, it, vi } from 'vitest'
import { createErrorReporter } from './error-reporter.js'

describe('createErrorReporter', () => {
  it('normalizes Error values into a stable event', async () => {
    const send = vi.fn()
    const reporter = createErrorReporter({
      context: { service: 'awp', region: 'base' },
      transport: { send },
      now: () => new Date('2026-08-19T04:00:00.000Z'),
    })

    const result = await reporter.report(
      new Error('boom', { cause: new Error('root') }),
      { region: 'call', requestId: 'r1' },
    )

    expect(result).toEqual({ ok: true })
    expect(send).toHaveBeenCalledWith(expect.objectContaining({
      timestamp: '2026-08-19T04:00:00.000Z',
      context: { service: 'awp', region: 'call', requestId: 'r1' },
      error: expect.objectContaining({
        name: 'Error',
        message: 'boom',
        cause: expect.objectContaining({ name: 'Error', message: 'root' }),
      }),
    }))
  })

  it('normalizes non-Error thrown values and circular context', async () => {
    const send = vi.fn()
    const circular: Record<string, unknown> = {}
    circular.self = circular
    const reporter = createErrorReporter({ transport: { send } })

    await reporter.report({ code: 12n }, circular)

    const event = send.mock.calls[0]![0]
    expect(event.error).toEqual({
      name: 'NonErrorThrown',
      message: 'Non-Error value thrown',
      value: { code: '12' },
    })
    expect(event.context).toEqual({ self: '[Circular]' })
    expect(() => JSON.stringify(event)).not.toThrow()
  })

  it('does not require arbitrary object coercion to create a report', async () => {
    const send = vi.fn()
    const nonCoercible = { code: 7, toString: 42, valueOf: 42 }
    const reporter = createErrorReporter({ transport: { send } })

    await expect(reporter.report(nonCoercible)).resolves.toEqual({ ok: true })
    expect(send.mock.calls[0]![0].error).toMatchObject({
      name: 'NonErrorThrown',
      message: 'Non-Error value thrown',
      value: { code: 7, toString: 42, valueOf: 42 },
    })
  })

  it('does not invoke arbitrary getters while normalizing a thrown value', async () => {
    const send = vi.fn()
    const getter = vi.fn(() => {
      throw new Error('getter must not run')
    })
    const hostile: Record<string, unknown> = { code: 7 }
    Object.defineProperty(hostile, 'secret', {
      enumerable: true,
      get: getter,
    })
    const reporter = createErrorReporter({ transport: { send } })

    await expect(reporter.report(hostile)).resolves.toEqual({ ok: true })
    expect(getter).not.toHaveBeenCalled()
    expect(send.mock.calls[0]![0].error).toMatchObject({
      name: 'NonErrorThrown',
      message: 'Non-Error value thrown',
      value: { code: 7, secret: '[Accessor]' },
    })
  })

  it('makes a circular Error cause JSON-safe', async () => {
    const send = vi.fn()
    const error = new Error('loop')
    error.cause = error
    const reporter = createErrorReporter({ transport: { send } })

    await reporter.report(error)

    expect(send.mock.calls[0]![0].error).toMatchObject({
      name: 'Error',
      message: 'loop',
      cause: '[Circular]',
    })
  })

  it('isolates transport failure from the application error', async () => {
    const transportError = new Error('reporter unavailable')
    const reporter = createErrorReporter({
      transport: {
        send() {
          throw transportError
        },
      },
    })

    await expect(reporter.report(new Error('application failure'))).resolves.toEqual({
      ok: false,
      transportError,
    })
  })
})
