import { IncomingMessage, ServerResponse } from 'http'
import { mock } from 'node:test'
import { Socket } from 'net'
import { expectError, expectType } from 'tsd'
import pino, { LogFn, LoggerOptions } from '../../'
import Logger = pino.Logger

const log = pino()
const info = log.info
const error = log.error

info('hello world')
error('this is at error level')

// primitive types
info('simple string')
info(true)
info(42)
info(3.14)
info(null)
info(undefined)

// object types
info({ a: 1, b: '2' })
info(new Error())
info(new Date())
info([])
info(new Map())
info(new Set())

// placeholder messages
info('Hello %s', 'world')
info('The answer is %d', 42)
info('The object is %o', { a: 1, b: '2' })
info('The json is %j', { a: 1, b: '2' })
info('The object is %O', { a: 1, b: '2' })
info('The answer is %d and the question is %s with %o', 42, 'unknown', {
  correct: 'order',
})
info('Missing placeholder is fine %s')

// %s placeholder supports all primitive types
info('Boolean %s', true)
info('Boolean %s', false)
info('Number %s', 123)
info('Number %s', 3.14)
info('BigInt %s', BigInt(123))
info('Null %s', null)
info('Undefined %s', undefined)
info('Symbol %s', Symbol('test'))
info('String %s', 'hello')

// %s placeholder with multiple primitives
info('Multiple primitives %s %s %s', true, 42, 'world')
info(
  'All primitive types %s %s %s %s %s %s %s',
  'string',
  123,
  true,
  BigInt(123),
  null,
  undefined,
  Symbol('test')
)
declare const errorOrString: string | Error
info(errorOrString)

// %o placeholder supports primitives too (except undefined)
info('Boolean %o', true)
info('Boolean %o', false)
info('Number %o', 123)
info('Number %o', 3.14)
info('BigInt %o', BigInt(123))
info('Null %o', null)
info('Symbol %o', Symbol('test'))
info('String %o', 'hello')

// placeholder messages type errors
expectError(info('The answer is %d', 'not a number'))
expectError(
  info(
    'The answer is %d and the question is %s with %o',
    'unknown',
    { incorrect: 'order' },
    42
  )
)
expectError(info('Extra message %s', 'after placeholder', 'not allowed'))

// object types with messages
info({ obj: 42 }, 'hello world')
info({ obj: 42, b: 2 }, 'hello world')
info({ obj: { aa: 'bbb' } }, 'another')
info({ a: 1, b: '2' }, 'hello world with %s', 'extra data')

// Extra message after placeholder
expectError(info({ a: 1, b: '2' }, 'hello world with %d', 2, 'extra'))

// metadata with messages type passes, because of custom toString method
// We can't detect if the object has a custom toString method that returns a string
info({ a: 1, b: '2' }, 'hello world with %s', {})

// metadata after message
expectError(info('message', { a: 1, b: '2' }))

// multiple strings without placeholder
expectError(info('string1', 'string2'))
expectError(info('string1', 'string2', 'string3'))

setImmediate(info, 'after setImmediate')
error(new Error('an error'))

const writeSym = pino.symbols.writeSym

const testUniqSymbol = {
  [pino.symbols.needsMetadataGsym]: true,
}[pino.symbols.needsMetadataGsym]

const log2: pino.Logger = pino({
  name: 'myapp',
  safe: true,
  serializers: {
    req: pino.stdSerializers.req,
    res: pino.stdSerializers.res,
    err: pino.stdSerializers.err,
  },
})

pino({
  write (o) {},
})

pino({
  mixin () {
    return { customName: 'unknown', customId: 111 }
  },
})

pino({
  mixin: () => ({ customName: 'unknown', customId: 111 }),
})

pino({
  mixin: (context: object) => ({ customName: 'unknown', customId: 111 }),
})

pino({
  mixin: (context: object, level: number) => ({
    customName: 'unknown',
    customId: 111,
  }),
})

pino({
  redact: { paths: [], censor: 'SECRET' },
})

pino({
  redact: { paths: [], censor: () => 'SECRET' },
})

pino({
  redact: { paths: [], censor: (value) => value },
})

pino({
  redact: { paths: [], censor: (value, path) => path.join() },
})

pino({
  redact: {
    paths: [],
    censor: (value): string => 'SECRET',
  },
})

expectError(
  pino({
    redact: { paths: [], censor: (value: string) => value },
  })
)

pino({
  depthLimit: 1,
})

pino({
  edgeLimit: 1,
})

pino({
  browser: {
    write (o) {},
  },
})

pino({
  browser: {
    write: {
      info (o) {},
      error (o) {},
    },
    serialize: true,
    asObject: true,
    transmit: {
      level: 'fatal',
      send: (level, logEvent) => {
        level
        logEvent.bindings
        logEvent.level
        logEvent.ts
        logEvent.messages
      },
    },
    disabled: false,
  },
})

pino({
  browser: {
    asObjectBindingsOnly: true,
  },
})

pino({}, undefined)

pino({ base: null })
if ('pino' in log) console.log(`pino version: ${log.pino}`)

expectType<void>(log.flush())
log.flush((err?: Error) => undefined)
log.child({ a: 'property' }).info('hello child!')
log.level = 'error'
log.info('nope')
const child = log.child({ foo: 'bar' })
child.info('nope again')
child.level = 'info'
child.info('hooray')
log.info('nope nope nope')
log.child({ foo: 'bar' }, { level: 'debug' }).debug('debug!')
child.bindings()
const customSerializers = {
  test () {
    return 'this is my serializer'
  },
}
pino()
  .child({}, { serializers: customSerializers })
  .info({ test: 'should not show up' })
const child2 = log.child({ father: true })
const childChild = child2.child({ baby: true })
const childRedacted = pino().child({}, { redact: ['path'] })
childRedacted.info({
  msg: 'logged with redacted properties',
  path: 'Not shown',
})
const childAnotherRedacted = pino().child(
  {},
  {
    redact: {
      paths: ['anotherPath'],
      censor: 'Not the log you\re looking for',
    },
  }
)
childAnotherRedacted.info({
  msg: 'another logged with redacted properties',
  anotherPath: 'Not shown',
})

log.level = 'info'
if (log.levelVal === 30) {
  console.log('logger level is `info`')
}

const listener = (lvl: any, val: any, prevLvl: any, prevVal: any) => {
  console.log(lvl, val, prevLvl, prevVal)
}
log.on('level-change', (lvl, val, prevLvl, prevVal, logger) => {
  console.log(lvl, val, prevLvl, prevVal)
})
log.level = 'trace'
log.removeListener('level-change', listener)
log.level = 'info'

pino.levels.values.error === 50
pino.levels.labels[50] === 'error'

const logstderr: pino.Logger = pino(process.stderr)
logstderr.error('on stderr instead of stdout')

log.useLevelLabels = true
log.info('lol')
log.level === 'info'
const isEnabled: boolean = log.isLevelEnabled('info')

const redacted = pino({
  redact: ['path'],
})

redacted.info({
  msg: 'logged with redacted properties',
  path: 'Not shown',
})

const anotherRedacted = pino({
  redact: {
    paths: ['anotherPath'],
    censor: 'Not the log you\re looking for',
  },
})

anotherRedacted.info({
  msg: 'another logged with redacted properties',
  anotherPath: 'Not shown',
})

const withTimeFn = pino({
  timestamp: pino.stdTimeFunctions.isoTime,
})

const withRFC3339TimeFn = pino({
  timestamp: pino.stdTimeFunctions.isoTimeNano,
})

const withNestedKey = pino({
  nestedKey: 'payload',
})

const withHooks = pino({
  hooks: {
    logMethod (args, method, level) {
      expectType<pino.Logger>(this)
      return method.apply(this, args)
    },
    streamWrite (s) {
      expectType<string>(s)
      return s.replaceAll('secret-key', 'xxx')
    },
  },
})

// Properties/types imported from pino-std-serializers
const wrappedErrSerializer = pino.stdSerializers.wrapErrorSerializer(
  (err: pino.SerializedError) => {
    return { ...err, newProp: 'foo' }
  }
)
const wrappedReqSerializer = pino.stdSerializers.wrapRequestSerializer(
  (req: pino.SerializedRequest) => {
    return { ...req, newProp: 'foo' }
  }
)
const wrappedResSerializer = pino.stdSerializers.wrapResponseSerializer(
  (res: pino.SerializedResponse) => {
    return { ...res, newProp: 'foo' }
  }
)

const socket = new Socket()
const incomingMessage = new IncomingMessage(socket)
const serverResponse = new ServerResponse(incomingMessage)

const mappedHttpRequest: { req: pino.SerializedRequest } =
  pino.stdSerializers.mapHttpRequest(incomingMessage)
const mappedHttpResponse: { res: pino.SerializedResponse } =
  pino.stdSerializers.mapHttpResponse(serverResponse)

const serializedErr: pino.SerializedError = pino.stdSerializers.err(
  new Error()
)
const serializedReq: pino.SerializedRequest =
  pino.stdSerializers.req(incomingMessage)
const serializedRes: pino.SerializedResponse =
  pino.stdSerializers.res(serverResponse)

/**
 * Destination static method
 */
const destinationViaDefaultArgs = pino.destination()
const destinationViaStrFileDescriptor = pino.destination('/log/path')
const destinationViaNumFileDescriptor = pino.destination(2)
const destinationViaStream = pino.destination(process.stdout)
const destinationViaOptionsObject = pino.destination({
  dest: '/log/path',
  sync: false,
})

pino(destinationViaDefaultArgs)
pino({ name: 'my-logger' }, destinationViaDefaultArgs)
pino(destinationViaStrFileDescriptor)
pino({ name: 'my-logger' }, destinationViaStrFileDescriptor)
pino(destinationViaNumFileDescriptor)
pino({ name: 'my-logger' }, destinationViaNumFileDescriptor)
pino(destinationViaStream)
pino({ name: 'my-logger' }, destinationViaStream)
pino(destinationViaOptionsObject)
pino({ name: 'my-logger' }, destinationViaOptionsObject)

try {
  throw new Error('Some error')
} catch (err) {
  log.error(err)
}

interface StrictShape {
  activity: string;
  err?: unknown;
}

info<StrictShape>({
  activity: 'Required property',
})

const logLine: pino.LogDescriptor = {
  level: 20,
  msg: 'A log message',
  time: new Date().getTime(),
  aCustomProperty: true,
}

interface CustomLogger extends pino.Logger {
  customMethod(msg: string, ...args: unknown[]): void;
}

const serializerFunc: pino.SerializerFn = () => {}
const writeFunc: pino.WriteFn = () => {}

interface CustomBaseLogger extends pino.BaseLogger {
  child(): CustomBaseLogger;
}

const customBaseLogger: CustomBaseLogger = {
  level: 'info',
  fatal () {},
  error () {},
  warn () {},
  info () {},
  debug () {},
  trace () {},
  silent () {},
  child () {
    return this
  },
  msgPrefix: 'prefix',
}

// custom levels
const log3 = pino({ customLevels: { myLevel: 100 } })
expectError(log3.log())
log3.level = 'myLevel'
log3.myLevel('')
log3.child({}).myLevel('')

log3.on('level-change', (lvl, val, prevLvl, prevVal, instance) => {
  instance.myLevel('foo')
})

const clog3 = log3.child({}, { customLevels: { childLevel: 120 } })
// child inherit parent
clog3.myLevel('')
// child itself
clog3.childLevel('')
const cclog3 = clog3.child({}, { customLevels: { childLevel2: 130 } })
// child inherit root
cclog3.myLevel('')
// child inherit parent
cclog3.childLevel('')
// child itself
cclog3.childLevel2('')

const ccclog3 = clog3.child({})
expectError(ccclog3.nonLevel(''))

const withChildCallback = pino({
  onChild: (child: Logger) => {},
})
withChildCallback.onChild = (child: Logger) => {}

pino({
  crlf: true,
})

const customLevels = { foo: 99, bar: 42 }

const customLevelLogger = pino({ customLevels })

type CustomLevelLogger = typeof customLevelLogger
type CustomLevelLoggerLevels = pino.Level | keyof typeof customLevels

const fn = (logger: Pick<CustomLevelLogger, CustomLevelLoggerLevels>) => {}

const customLevelChildLogger = customLevelLogger.child({ name: 'child' })

fn(customLevelChildLogger) // missing foo typing

// unknown option
expectError(
  pino({
    hello: 'world',
  })
)

// unknown option
expectError(
  pino({
    hello: 'world',
    customLevels: {
      log: 30,
    },
  })
)

function dangerous () {
  throw Error('foo')
}

try {
  dangerous()
} catch (err) {
  log.error(err)
}

try {
  dangerous()
} catch (err) {
  log.error({ err })
}

const bLogger = pino({
  customLevels: {
    log: 5,
  },
  level: 'log',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
    },
  },
})

// Test that we can properly extract parameters from the log fn type
type LogParam = Parameters<LogFn>
const [param1, param2, param3, param4]: LogParam = [
  { multiple: 'params' },
  'should',
  'be',
  'accepted',
]

expectType<unknown>(param1)
expectType<string>(param2)
expectType<unknown>(param3)
expectType<unknown>(param4)

const logger = mock.fn<LogFn>()
logger.mock.calls[0].arguments[1]?.includes('I should be able to get params')

const hooks: LoggerOptions['hooks'] = {
  logMethod (this, parameters, method) {
    if (parameters.length >= 2) {
      const [parameter1, parameter2, ...remainingParameters] = parameters
      if (typeof parameter1 === 'string') {
        return method.apply(this, [
          parameter2,
          parameter1,
          ...remainingParameters,
        ])
      }
      return method.apply(this, [parameter2])
    }

    return method.apply(this, parameters)
  },
}

expectType<Logger<'log'>>(
  pino({
    customLevels: {
      log: 5,
    },
    level: 'log',
    transport: {
      target: 'pino-pretty',
      options: {
        colorize: true,
      },
    },
  })
)

const parentLogger1 = pino(
  {
    customLevels: { myLevel: 90 },
    onChild: (child) => {
      const a = child.myLevel
    },
  },
  process.stdout
)
parentLogger1.onChild = (child) => {
  child.myLevel('')
}

const childLogger1 = parentLogger1.child({})
childLogger1.myLevel('')
expectError(childLogger1.doesntExist(''))

const parentLogger2 = pino({}, process.stdin)
expectError(
  (parentLogger2.onChild = (child) => {
    const b = child.doesntExist
  })
)

const childLogger2 = parentLogger2.child({})
expectError(childLogger2.doesntExist)

expectError(
  pino(
    {
      onChild: (child) => {
        const a = child.doesntExist
      },
    },
    process.stdout
  )
)

const pinoWithoutLevelsSorting = pino({})
const pinoWithDescSortingLevels = pino({ levelComparison: 'DESC' })
const pinoWithAscSortingLevels = pino({ levelComparison: 'ASC' })
const pinoWithCustomSortingLevels = pino({ levelComparison: () => false })
// with wrong level comparison direction
expectError(pino({ levelComparison: 'SOME' }), process.stdout)
// with wrong level comparison type
expectError(pino({ levelComparison: 123 }), process.stdout)
// with wrong custom level comparison return type
expectError(pino({ levelComparison: () => null }), process.stdout)
expectError(pino({ levelComparison: () => 1 }), process.stdout)
expectError(pino({ levelComparison: () => 'string' }), process.stdout)

const customLevelsOnlyOpts = {
  useOnlyCustomLevels: true,
  customLevels: {
    customDebug: 10,
    info: 20, // to make sure the default names are also available for override
    customNetwork: 30,
    customError: 40,
  },
  level: 'customDebug',
} satisfies LoggerOptions

const loggerWithCustomLevelOnly = pino(customLevelsOnlyOpts)
loggerWithCustomLevelOnly.customDebug('test3')
loggerWithCustomLevelOnly.info('test4')
loggerWithCustomLevelOnly.customError('test5')
loggerWithCustomLevelOnly.customNetwork('test6')

expectError(loggerWithCustomLevelOnly.fatal('test'))
expectError(loggerWithCustomLevelOnly.error('test'))
expectError(loggerWithCustomLevelOnly.warn('test'))
expectError(loggerWithCustomLevelOnly.debug('test'))
expectError(loggerWithCustomLevelOnly.trace('test'))

// Module extension
declare module '../../' {
  interface LogFnFields {
    bannedField?: never;
    typeCheckedField?: string;
  }
}

info({ typeCheckedField: 'bar' })
expectError(info({ bannedField: 'bar' }))
expectError(info({ typeCheckedField: 123 }))

const someGenericFunction = <T extends string | number | symbol = never>(
  arg: Record<T, unknown>
) => {
  info(arg)
}
