import { test, expect } from 'vitest'
import * as assert from '../../../assertions.utils.test'
import * as z from '../../index'

const Test = z.object({
  f1: z.number(),
  f2: z.string().optional(),
  f3: z.string().nullable(),
  f4: z.array(z.object({ t: z.union([z.string(), z.boolean()]) })),
})
type Test = z.infer<typeof Test>

test('object type inference', () => {
  type TestType = {
    f1: number
    f2?: string | undefined
    f3: string | null
    f4: { t: string | boolean }[]
  }

  assert.assertEqual<z.TypeOf<typeof Test>, TestType>(true)
})

test('unknown throw', () => {
  const asdf: unknown = 35
  expect(() => Test.parse(asdf)).toThrow()
})

test('shape() should return schema of particular key', () => {
  const f1Schema = Test.shape.f1
  const f2Schema = Test.shape.f2
  const f3Schema = Test.shape.f3
  const f4Schema = Test.shape.f4

  expect(f1Schema.typeName).toBe('ZodNumber')
  expect(f2Schema.typeName).toBe('ZodOptional')
  expect(f3Schema.typeName).toBe('ZodNullable')
  expect(f4Schema.typeName).toBe('ZodArray')
})

test('correct parsing', () => {
  Test.parse({
    f1: 12,
    f2: 'string',
    f3: 'string',
    f4: [
      {
        t: 'string',
      },
    ],
  })

  Test.parse({
    f1: 12,
    f3: null,
    f4: [
      {
        t: false,
      },
    ],
  })
})

test('incorrect #1', () => {
  expect(() => Test.parse({} as any)).toThrow()
})

test('nonstrict by default', () => {
  z.object({ points: z.number() }).parse({
    points: 2314,
    unknown: 'asdf',
  })
})

const data = {
  points: 2314,
  unknown: 'asdf',
}

test('strip by default', () => {
  const val = z.object({ points: z.number() }).parse(data)
  expect(val).toEqual({ points: 2314 })
})

test('unknownkeys override', () => {
  const val = z.object({ points: z.number() }).strict().passthrough().strip().nonstrict().parse(data)

  expect(val).toEqual(data)
})

test('passthrough unknown', () => {
  const val = z.object({ points: z.number() }).passthrough().parse(data)

  expect(val).toEqual(data)
})

test('strip unknown', () => {
  const val = z.object({ points: z.number() }).strip().parse(data)

  expect(val).toEqual({ points: 2314 })
})

test('strict', () => {
  const val = z.object({ points: z.number() }).strict().safeParse(data)

  expect(val.success).toEqual(false)
})

test('catchall inference', () => {
  const o1 = z
    .object({
      first: z.string(),
    })
    .catchall(z.number())

  const d1 = o1.parse({ first: 'asdf', num: 1243 })

  const fnInput = (_input: z.input<typeof o1>) => {}
  const fnOutput = (_input: z.output<typeof o1>) => {}

  fnInput({ first: 'asdf', num: 1243 })
  fnOutput({ first: 'asdf', num: 1243 })

  const prop1 = d1.first
  const prop2 = d1.num

  assert.assertEqual<string, typeof prop1>(true)
  assert.assertEqual<unknown, typeof prop2>(true)
})

test('catchall overrides strict', () => {
  const o1 = z.object({ first: z.string().optional() }).strict().catchall(z.number())

  // should run fine
  // setting a catchall overrides the unknownKeys behavior
  o1.parse({
    asdf: 1234,
  })

  // should only run catchall validation
  // against unknown keys
  o1.parse({
    first: 'asdf',
    asdf: 1234,
  })
})

test('catchall overrides strict', () => {
  const o1 = z
    .object({
      first: z.string(),
    })
    .strict()
    .catchall(z.number())

  // should run fine
  // setting a catchall overrides the unknownKeys behavior
  o1.parse({
    first: 'asdf',
    asdf: 1234,
  })
})

test('test that optional keys are unset', async () => {
  const SNamedEntity = z.object({
    id: z.string(),
    set: z.string().optional(),
    unset: z.string().optional(),
  })
  const result = SNamedEntity.parse({
    id: 'asdf',
    set: undefined,
  })
  // eslint-disable-next-line ban/ban
  expect(Object.keys(result)).toEqual(['id', 'set'])
})

test('test catchall parsing', async () => {
  const result = z.object({ name: z.string() }).catchall(z.number()).parse({ name: 'Foo', validExtraKey: 61 })

  expect(result).toEqual({ name: 'Foo', validExtraKey: 61 })

  const result2 = z
    .object({ name: z.string() })
    .catchall(z.number())
    .safeParse({ name: 'Foo', validExtraKey: 61, invalid: 'asdf' })

  expect(result2.success).toEqual(false)
})

test('test nonexistent keys', async () => {
  const Schema = z.union([z.object({ a: z.string() }), z.object({ b: z.number() })])
  const obj = { a: 'A' }
  const result = await Schema.spa(obj) // Works with 1.11.10, breaks with 2.0.0-beta.21
  expect(result.success).toBe(true)
})

test('test async union', async () => {
  const Schema2 = z.union([
    z.object({
      ty: z.string(),
    }),
    z.object({
      ty: z.number(),
    }),
  ])

  const obj = { ty: 'A' }
  const result = await Schema2.spa(obj) // Works with 1.11.10, breaks with 2.0.0-beta.21
  expect(result.success).toEqual(true)
})

test('test inferred merged type', async () => {
  const asdf = z.object({ a: z.string() }).merge(z.object({ a: z.number() }))
  type asdf = z.infer<typeof asdf>
  assert.assertEqual<asdf, { a: number }>(true)
})

test('inferred merged object type with optional properties', async () => {
  const Merged = z
    .object({ a: z.string(), b: z.string().optional() })
    .merge(z.object({ a: z.string().optional(), b: z.string() }))
  type Merged = z.infer<typeof Merged>
  assert.assertEqual<Merged, { a?: string; b: string }>(true)
  // todo
  // assert.assertEqual<Merged, { a?: string; b: string }>(true);
})

test('inferred unioned object type with optional properties', async () => {
  const Unioned = z.union([
    z.object({ a: z.string(), b: z.string().optional() }),
    z.object({ a: z.string().optional(), b: z.string() }),
  ])
  type Unioned = z.infer<typeof Unioned>
  assert.assertEqual<Unioned, { a: string; b?: string } | { a?: string; b: string }>(true)
})

test('inferred enum type', async () => {
  const Enum = z.object({ a: z.string(), b: z.string().optional() }).keyof()

  expect(Enum.Values).toEqual({
    a: 'a',
    b: 'b',
  })
  expect(Enum.enum).toEqual({
    a: 'a',
    b: 'b',
  })
  expect(Enum._def.values).toEqual(['a', 'b'])
  type Enum = z.infer<typeof Enum>
  assert.assertEqual<Enum, 'a' | 'b'>(true)
})

test('inferred partial object type with optional properties', async () => {
  const Partial = z.object({ a: z.string(), b: z.string().optional() }).partial()
  type Partial = z.infer<typeof Partial>
  assert.assertEqual<Partial, { a?: string; b?: string }>(true)
})

test('inferred picked object type with optional properties', async () => {
  const Picked = z.object({ a: z.string(), b: z.string().optional() }).pick({ b: true })
  type Picked = z.infer<typeof Picked>
  assert.assertEqual<Picked, { b?: string }>(true)
})

test('inferred type for unknown/any keys', () => {
  const myType = z.object({
    anyOptional: z.any().optional(),
    anyRequired: z.any(),
    unknownOptional: z.unknown().optional(),
    unknownRequired: z.unknown(),
  })
  type myType = z.infer<typeof myType>
  assert.assertEqual<
    myType,
    {
      anyOptional?: any
      anyRequired?: any
      unknownOptional?: unknown
      unknownRequired?: unknown
    }
  >(true)
})

test('setKey', () => {
  const base = z.object({ name: z.string() })
  const withNewKey = base.setKey('age', z.number())

  type withNewKey = z.infer<typeof withNewKey>
  assert.assertEqual<withNewKey, { name: string; age: number }>(true)
  withNewKey.parse({ name: 'asdf', age: 1234 })
})

test('strictcreate', async () => {
  const strictObj = z.strictObject({
    name: z.string(),
  })

  const syncResult = strictObj.safeParse({ name: 'asdf', unexpected: 13 })
  expect(syncResult.success).toEqual(false)

  const asyncResult = await strictObj.spa({ name: 'asdf', unexpected: 13 })
  expect(asyncResult.success).toEqual(false)
})

test('object with refine', async () => {
  const schema = z
    .object({
      a: z.string().default('foo'),
      b: z.number(),
    })
    .refine(() => true)
  expect(schema.parse({ b: 5 })).toEqual({ b: 5, a: 'foo' })
  const result = await schema.parseAsync({ b: 5 })
  expect(result).toEqual({ b: 5, a: 'foo' })
})

test('intersection of object with date', async () => {
  const schema = z.object({
    a: z.date(),
  })
  expect(schema.and(schema).parse({ a: new Date(1637353595983) })).toEqual({
    a: new Date(1637353595983),
  })
  const result = await schema.parseAsync({ a: new Date(1637353595983) })
  expect(result).toEqual({ a: new Date(1637353595983) })
})

test('intersection of object with refine with date', async () => {
  const schema = z
    .object({
      a: z.date(),
    })
    .refine(() => true)
  expect(schema.and(schema).parse({ a: new Date(1637353595983) })).toEqual({
    a: new Date(1637353595983),
  })
  const result = await schema.parseAsync({ a: new Date(1637353595983) })
  expect(result).toEqual({ a: new Date(1637353595983) })
})

test('constructor key', () => {
  const Example = z.object({
    prop: z.string(),
    opt: z.number().optional(),
    arr: z.string().array(),
  })

  type Example = z.infer<typeof Example>
  assert.assertEqual<keyof Example, 'prop' | 'opt' | 'arr'>(true)
})

test('unknownkeys merging', () => {
  // This one is "strict"
  const schemaA = z
    .object({
      a: z.string(),
    })
    .strict()

  // This one is "strip"
  const schemaB = z
    .object({
      b: z.string(),
    })
    .catchall(z.string())

  const mergedSchema = schemaA.merge(schemaB)
  type mergedSchema = typeof mergedSchema

  assert.assertEqual<mergedSchema['_def']['unknownKeys'], z.ZodString>(true)
  expect(mergedSchema._def.unknownKeys.typeName).toBe('ZodString')
})

const personToExtend = z.object({
  firstName: z.string(),
  lastName: z.string(),
})

test('extend() should return schema with new key', () => {
  const PersonWithNickname = personToExtend.extend({ nickName: z.string() })
  type PersonWithNickname = z.infer<typeof PersonWithNickname>

  const expected = { firstName: 'f', nickName: 'n', lastName: 'l' }
  const actual = PersonWithNickname.parse(expected)

  expect(actual).toEqual(expected)
  assert.assertEqual<keyof PersonWithNickname, 'firstName' | 'lastName' | 'nickName'>(true)
  assert.assertEqual<PersonWithNickname, { firstName: string; lastName: string; nickName: string }>(true)
})

test('extend() should have power to override existing key', () => {
  const PersonWithNumberAsLastName = personToExtend.extend({
    lastName: z.number(),
  })
  type PersonWithNumberAsLastName = z.infer<typeof PersonWithNumberAsLastName>

  const expected = { firstName: 'f', lastName: 42 }
  const actual = PersonWithNumberAsLastName.parse(expected)

  expect(actual).toEqual(expected)
  assert.assertEqual<PersonWithNumberAsLastName, { firstName: string; lastName: number }>(true)
})

test('passthrough index signature', () => {
  const a = z.object({ a: z.string() })
  type a = z.infer<typeof a>
  assert.assertEqual<{ a: string }, a>(true)
  const b = a.passthrough()
  type b = z.infer<typeof b>
  assert.assertEqual<{ a: string } & { [k: string]: unknown }, b>(true)
})

test('xor', () => {
  type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
  type XOR<T, U> = T extends object ? (U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : U) : T

  type A = { name: string; a: number }
  type B = { name: string; b: number }
  type C = XOR<A, B>
  type Outer = { data: C }
  const Outer: z.ZodType<Outer> = z.object({
    data: z.union([z.object({ name: z.string(), a: z.number() }), z.object({ name: z.string(), b: z.number() })]),
  })
})

test('object with strip should strip keys', () => {
  const schema = z.object({ foo: z.string() }).strip()
  const result = schema.safeParse({ foo: '123', bar: 123 })
  expect(result).toEqual({
    success: true,
    data: { foo: '123' },
  })
})

test('object with passthrough should allow all extra keys', () => {
  const schema = z.object({ foo: z.string() }).passthrough()
  const result = schema.safeParse({ foo: '123', bar: 123 })
  expect(result).toEqual({
    success: true,
    data: { foo: '123', bar: 123 },
  })
})

test('object with strict should fail if extra keys', () => {
  const schema = z.object({ foo: z.string() }).strict()
  const result = schema.safeParse({ foo: '123', bar: 123 })
  expect(result).toMatchObject({
    success: false,
  })
})

test('object with catchall any should allow all keys', () => {
  const schema = z.object({ foo: z.string() }).catchall(z.any())
  const result = schema.safeParse({ foo: '123', bar: 123 })
  expect(result).toEqual({
    success: true,
    data: { foo: '123', bar: 123 },
  })
})

test('object with catchall never should fail if extra keys', () => {
  const schema = z.object({ foo: z.string() }).catchall(z.never())
  const result = schema.safeParse({ foo: '123', bar: 123 })
  expect(result).toMatchObject({
    success: false,
  })
})

test('object with catchall number should succeed for extra number keys but fail otherwise', () => {
  const schema = z.object({ foo: z.string() }).catchall(z.number())
  const result1 = schema.safeParse({ foo: '123', bar: 123 })
  expect(result1).toEqual({
    success: true,
    data: { foo: '123', bar: 123 },
  })

  const result2 = schema.safeParse({ foo: '123', bar: '456' })
  expect(result2).toMatchObject({
    success: false,
  })
})

test('object with conflicting passthrough and catchall never should behave as the last modifier', () => {
  const schema = z.object({ foo: z.string() }).passthrough().catchall(z.never())
  const result = schema.safeParse({ foo: '123', bar: '456' })
  expect(result).toMatchObject({
    success: false,
  })
})

test('object with conflicting passthrough and catchall number should behave as the last modifier', () => {
  const schema = z.object({ foo: z.string() }).passthrough().catchall(z.number())
  const result = schema.safeParse({ foo: '123', bar: '456' })
  expect(result).toMatchObject({
    success: false,
  })
})

test('object with conflicting strict and catchall any should behave as the last modifier', () => {
  const schema = z.object({ foo: z.string() }).strict().catchall(z.any())
  const result = schema.safeParse({ foo: '123', bar: '456' })
  expect(result).toEqual({
    success: true,
    data: { foo: '123', bar: '456' },
  })
})

test('object with conflicting strict and catchall number should behave as the last modifier', () => {
  const schema = z.object({ foo: z.string() }).strict().catchall(z.number())
  const result = schema.safeParse({ foo: '123', bar: 456 })
  expect(result).toEqual({
    success: true,
    data: { foo: '123', bar: 456 },
  })
})
