import { test, expect } from 'vitest'
import * as z from '../index'
import { defaultErrorMap, setErrorMap, ZodError } from './index'
import { ZodErrorMap } from '../typings'

test('error creation', () => {
  const err1 = ZodError.create([])
  err1.addIssue({
    code: 'invalid_type',
    expected: 'object',
    received: 'string',
    path: [],
    message: '',
    fatal: true,
  })
  err1.isEmpty

  const err2 = ZodError.create(err1.issues)
  const err3 = new ZodError([])
  err3.addIssues(err1.issues)
  err3.addIssue(err1.issues[0]!)
  err1.message
  err2.message
  err3.message
})

const errorMap: ZodErrorMap = (error, ctx) => {
  if (error.code === 'invalid_type') {
    if (error.expected === 'string') {
      return { message: 'bad type!' }
    }
  }
  if (error.code === 'custom') {
    return { message: `less-than-${(error.params || {}).minimum}` }
  }
  return { message: ctx.defaultError }
}

test('type error with custom error map', () => {
  try {
    z.string().parse(234, { errorMap })
  } catch (err) {
    const zerr = err as z.ZodError

    expect(zerr.issues[0]?.code).toEqual('invalid_type')
    expect(zerr.issues[0]?.message).toEqual(`bad type!`)
  }
})

test('refinement fail with params', () => {
  try {
    z.number()
      .refine((val) => val >= 3, {
        params: { minimum: 3 },
      })
      .parse(2, { errorMap })
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues[0]?.code).toEqual('custom')
    expect(zerr.issues[0]?.message).toEqual(`less-than-3`)
  }
})

test('custom error with custom errormap', () => {
  try {
    z.string()
      .refine((val) => val.length > 12, {
        params: { minimum: 13 },
        message: 'override',
      })
      .parse('asdf', { errorMap })
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues[0]?.message).toEqual('override')
  }
})

test('default error message', () => {
  try {
    z.number()
      .refine((x) => x > 3)
      .parse(2)
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual('Invalid input')
  }
})

test('override error in refine', () => {
  try {
    z.number()
      .refine((x) => x > 3, 'override')
      .parse(2)
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual('override')
  }
})

test('override error in refinement', () => {
  try {
    z.number()
      .refine((x) => x > 3, {
        message: 'override',
      })
      .parse(2)
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual('override')
  }
})

test('array minimum', () => {
  try {
    z.array(z.string()).min(3, 'tooshort').parse(['asdf', 'qwer'])
  } catch (err) {
    const zerr = err as ZodError
    expect(zerr.issues[0]?.code).toEqual('too_small')
    expect(zerr.issues[0]?.message).toEqual('tooshort')
  }
  try {
    z.array(z.string()).min(3).parse(['asdf', 'qwer'])
  } catch (err) {
    const zerr = err as ZodError
    expect(zerr.issues[0]?.code).toEqual('too_small')
    expect(zerr.issues[0]?.message).toEqual(`Array must contain at least 3 element(s)`)
  }
})

test('custom path in custom error map', () => {
  const schema = z.object({
    items: z.array(z.string()).refine((data) => data.length > 3, {
      path: ['items-too-few'],
    }),
  })

  const errorMap: ZodErrorMap = (error) => {
    expect(error.path.length).toBe(2)
    return { message: 'doesnt matter' }
  }
  const result = schema.safeParse({ items: ['first'] }, { errorMap })
  expect(result.success).toEqual(false)
  if (!result.success) {
    expect(result.error.issues[0]?.path).toEqual(['items', 'items-too-few'])
  }
})

test('error metadata from value', () => {
  const dynamicRefine = z.string().refine(
    (val) => val === val.toUpperCase(),
    (val) => ({ params: { val } })
  )

  const result = dynamicRefine.safeParse('asdf')
  expect(result.success).toEqual(false)
  if (!result.success) {
    const sub = result.error.issues[0]
    expect(result.error.issues[0]?.code).toEqual('custom')
    if (sub?.code === 'custom') {
      expect(sub!.params!.val).toEqual('asdf')
    }
  }
})

// test("don't call refine after validation failed", () => {
//   const asdf = z
//     .union([
//       z.number(),
//       z.string().transform(z.number(), (val) => {
//         return parseFloat(val);
//       }),
//     ])
//     .refine((v) => v >= 1);

//   expect(() => asdf.safeParse("foo")).not.toThrow();
// });

test('root level formatting', () => {
  const schema = z.string().email()
  const result = schema.safeParse('asdfsdf')
  expect(result.success).toEqual(false)
  if (!result.success) {
    expect(result.error.format()._errors).toEqual(['Invalid email'])
  }
})

test('custom path', () => {
  const schema = z
    .object({
      password: z.string(),
      confirm: z.string(),
    })
    .refine((val) => val.confirm === val.password, { path: ['confirm'] })

  const result = schema.safeParse({
    password: 'peanuts',
    confirm: 'qeanuts',
  })

  expect(result.success).toEqual(false)
  if (!result.success) {
    // nested errors
    const error = result.error.format()
    expect(error._errors).toEqual([])
    expect(error.password?._errors).toEqual(undefined)
    expect(error.confirm?._errors).toEqual(['Invalid input'])
  }
})

test('custom path', () => {
  const schema = z
    .object({
      password: z.string().min(6),
      confirm: z.string().min(6),
    })
    .refine((val) => val.confirm === val.password)

  const result = schema.safeParse({
    password: 'qwer',
    confirm: 'asdf',
  })

  expect(result.success).toEqual(false)
  if (!result.success) {
    expect(result.error.issues.length).toEqual(3)
  }
})

const schema = z.object({
  inner: z.object({
    name: z
      .string()
      .refine((val) => val.length > 5)
      .array()
      .refine((val) => val.length <= 1),
  }),
})

test('no abort early on refinements', () => {
  const invalidItem = {
    inner: { name: ['aasd', 'asdfasdfasfd'] },
  }

  const result1 = schema.safeParse(invalidItem)
  expect(result1.success).toEqual(false)
  if (!result1.success) {
    expect(result1.error.issues.length).toEqual(2)
  }
})
test('formatting', () => {
  const invalidItem = {
    inner: { name: ['aasd', 'asdfasdfasfd'] },
  }
  const invalidArray = {
    inner: { name: ['asdfasdf', 'asdfasdfasfd'] },
  }
  const result1 = schema.safeParse(invalidItem)
  const result2 = schema.safeParse(invalidArray)

  expect(result1.success).toEqual(false)
  expect(result2.success).toEqual(false)
  if (!result1.success) {
    const error = result1.error.format()

    expect(error._errors).toEqual([])
    expect(error.inner?._errors).toEqual([])
    // expect(error.inner?.name?._errors).toEqual(["Invalid input"]);
    // expect(error.inner?.name?.[0]._errors).toEqual(["Invalid input"]);
    expect(error.inner?.name?.[1]).toEqual(undefined)
  }
  if (!result2.success) {
    const error = result2.error.format()
    expect(error._errors).toEqual([])
    expect(error.inner?._errors).toEqual([])
    expect(error.inner?.name?._errors).toEqual(['Invalid input'])
    expect(error.inner?.name?.[0]).toEqual(undefined)
    expect(error.inner?.name?.[1]).toEqual(undefined)
    expect(error.inner?.name?.[2]).toEqual(undefined)
  }

  // test custom mapper
  if (!result2.success) {
    const error = result2.error.format(() => 5)
    expect(error._errors).toEqual([])
    expect(error.inner?._errors).toEqual([])
    expect(error.inner?.name?._errors).toEqual([5])
  }
})

test('formatting with nullable and optional fields', () => {
  const nameSchema = z.string().refine((val) => val.length > 5)
  const schema = z.object({
    nullableObject: z.object({ name: nameSchema }).nullable(),
    nullableArray: z.array(nameSchema).nullable(),
    nullableTuple: z.tuple([nameSchema, nameSchema, z.number()]).nullable(),
    optionalObject: z.object({ name: nameSchema }).optional(),
    optionalArray: z.array(nameSchema).optional(),
    optionalTuple: z.tuple([nameSchema, nameSchema, z.number()]).optional(),
  })
  const invalidItem = {
    nullableObject: { name: 'abcd' },
    nullableArray: ['abcd'],
    nullableTuple: ['abcd', 'abcd', 1],
    optionalObject: { name: 'abcd' },
    optionalArray: ['abcd'],
    optionalTuple: ['abcd', 'abcd', 1],
  }
  const result = schema.safeParse(invalidItem)
  expect(result.success).toEqual(false)
  if (!result.success) {
    const error = result.error.format()
    expect(error._errors).toEqual([])
    expect(error.nullableObject?._errors).toEqual([])
    expect(error.nullableObject?.name?._errors).toEqual(['Invalid input'])
    expect(error.nullableArray?._errors).toEqual([])
    expect(error.nullableArray?.[0]?._errors).toEqual(['Invalid input'])
    expect(error.nullableTuple?._errors).toEqual([])
    expect(error.nullableTuple?.[0]?._errors).toEqual(['Invalid input'])
    expect(error.nullableTuple?.[1]?._errors).toEqual(['Invalid input'])
    expect(error.optionalObject?._errors).toEqual([])
    expect(error.optionalObject?.name?._errors).toEqual(['Invalid input'])
    expect(error.optionalArray?._errors).toEqual([])
    expect(error.optionalArray?.[0]?._errors).toEqual(['Invalid input'])
    expect(error.optionalTuple?._errors).toEqual([])
    expect(error.optionalTuple?.[0]?._errors).toEqual(['Invalid input'])
    expect(error.optionalTuple?.[1]?._errors).toEqual(['Invalid input'])
  }
})

const stringWithCustomError = z.string({
  errorMap: (issue, ctx) => ({
    message: issue.code === 'invalid_type' ? (ctx.data ? 'Invalid name' : 'Name is required') : ctx.defaultError,
  }),
})

test('schema-bound error map', () => {
  const result = stringWithCustomError.safeParse(1234)
  expect(result.success).toEqual(false)
  if (!result.success) {
    expect(result.error.issues[0]?.message).toEqual('Invalid name')
  }

  const result2 = stringWithCustomError.safeParse(undefined)
  expect(result2.success).toEqual(false)
  if (!result2.success) {
    expect(result2.error.issues[0]?.message).toEqual('Name is required')
  }

  // support contextual override
  const result3 = stringWithCustomError.safeParse(undefined, {
    errorMap: () => ({ message: 'OVERRIDE' }),
  })
  expect(result3.success).toEqual(false)
  if (!result3.success) {
    expect(result3.error.issues[0]?.message).toEqual('OVERRIDE')
  }
})

test('overrideErrorMap', () => {
  // support overrideErrorMap
  setErrorMap(() => ({ message: 'OVERRIDE' }))
  const result4 = stringWithCustomError.min(10).safeParse('tooshort')
  expect(result4.success).toEqual(false)
  if (!result4.success) {
    expect(result4.error.issues[0]?.message).toEqual('OVERRIDE')
  }
  setErrorMap(defaultErrorMap)
})

test('invalid and required', () => {
  const str = z.string({
    invalid_type_error: 'Invalid name',
    required_error: 'Name is required',
  })
  const result1 = str.safeParse(1234)
  expect(result1.success).toEqual(false)
  if (!result1.success) {
    expect(result1.error.issues[0]?.message).toEqual('Invalid name')
  }
  const result2 = str.safeParse(undefined)
  expect(result2.success).toEqual(false)
  if (!result2.success) {
    expect(result2.error.issues[0]?.message).toEqual('Name is required')
  }
})

test('Fallback to default required error', () => {
  const str = z.string({
    invalid_type_error: 'Invalid name',
    // required_error: "Name is required",
  })

  const result2 = str.safeParse(undefined)
  expect(result2.success).toEqual(false)
  if (!result2.success) {
    expect(result2.error.issues[0]?.message).toEqual('Required')
  }
})

test('invalid and required and errorMap', () => {
  expect(() => {
    return z.string({
      invalid_type_error: 'Invalid name',
      required_error: 'Name is required',
      errorMap: () => ({ message: 'OVERRIDE' }),
    })
  }).toThrow()
})

test('strict error message', () => {
  const errorMsg = 'Invalid object'
  const obj = z.object({ x: z.string() }).strict(errorMsg)
  const result = obj.safeParse({ x: 'a', y: 'b' })
  expect(result.success).toEqual(false)
  if (!result.success) {
    expect(result.error.issues[0]?.message).toEqual(errorMsg)
  }
})

test('enum error message, invalid enum elementstring', () => {
  try {
    z.enum(['Tuna', 'Trout']).parse('Salmon')
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual("Invalid enum value. Expected 'Tuna' | 'Trout', received 'Salmon'")
  }
})

test('enum error message, invalid type', () => {
  try {
    z.enum(['Tuna', 'Trout']).parse(12)
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual("Expected 'Tuna' | 'Trout', received number")
  }
})

test('nativeEnum default error message', () => {
  enum Fish {
    Tuna = 'Tuna',
    Trout = 'Trout',
  }
  try {
    z.nativeEnum(Fish).parse('Salmon')
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual("Invalid enum value. Expected 'Tuna' | 'Trout', received 'Salmon'")
  }
})

test('literal default error message', () => {
  try {
    z.literal('Tuna').parse('Trout')
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual(`Invalid literal value, expected "Tuna"`)
  }
})

test('literal bigint default error message', () => {
  try {
    z.literal(BigInt(12)).parse(BigInt(13))
  } catch (err) {
    const zerr = err as z.ZodError
    expect(zerr.issues.length).toEqual(1)
    expect(zerr.issues[0]?.message).toEqual(`Invalid literal value, expected "12"`)
  }
})

test('when the message is falsy, it is used as is provided', () => {
  const schema = z.string().max(1, { message: '' })
  const result = schema.safeParse('asdf')
  expect(result.success).toEqual(false)
  if (!result.success) {
    expect(result.error.issues[0]?.message).toEqual('')
  }
})

// test("dont short circuit on continuable errors", () => {
//   const user = z
//     .object({
//       password: z.string().min(6),
//       confirm: z.string(),
//     })
//     .refine((data) => data.password === data.confirm, {
//       message: "Passwords don't match",
//       path: ["confirm"],
//     });
//   const result = user.safeParse({ password: "asdf", confirm: "qwer" });
//   if (!result.success) {
//     expect(result.error.issues.length).toEqual(2);
//   }
// });
