import { expect, expectTypeOf, test } from "vitest";
import * as z from "zod/v4";
import * as core from "zod/v4/core";

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()]) })),
});

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

  expectTypeOf<z.TypeOf<typeof Test>>().toEqualTypeOf<TestType>();
});

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).toBeInstanceOf(z.ZodNumber);
  expect(f2Schema).toBeInstanceOf(z.ZodOptional);
  expect(f3Schema).toBeInstanceOf(z.ZodNullable);
  expect(f4Schema).toBeInstanceOf(z.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("nonstrict by default", () => {
  z.object({ points: z.number() }).parse({
    points: 2314,
    unknown: "asdf",
  });
});

test("parse optional keys ", () => {
  const schema = z.object({
    a: z.string().optional(),
  });
  expect(schema.parse({ a: "asdf" })).toEqual({ a: "asdf" });
});

test("empty object", () => {
  const schema = z.object({});
  expect(schema.parse({})).toEqual({});
  expect(schema.parse({ name: "asdf" })).toEqual({});
  expect(schema.safeParse(null).success).toEqual(false);
  expect(schema.safeParse("asdf").success).toEqual(false);
  expectTypeOf<z.output<typeof schema>>().toEqualTypeOf<Record<string, never>>();
});

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().passthrough().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 });
  // expectTypeOf<(typeof d1)["asdf"]>().toEqualTypeOf<number>();
  expectTypeOf<(typeof d1)["first"]>().toEqualTypeOf<string>();
});

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("optional keys are unset", () => {
  const SNamedEntity = z.object({
    id: z.string(),
    set: z.string().optional(),
    unset: z.string().optional(),
  });
  const result = SNamedEntity.parse({
    id: "asdf",
    set: undefined,
  });
  expect(Object.keys(result)).toEqual(["id", "set"]);
});

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("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>;

  expectTypeOf<asdf>().toEqualTypeOf<{ a: number }>();
});

test("inferred type with Record shape", () => {
  type A = z.ZodObject<Record<string, z.ZodType<string, number>>>;
  expectTypeOf<z.infer<A>>().toEqualTypeOf<Record<string, string>>();
  expectTypeOf<z.input<A>>().toEqualTypeOf<Record<string, number>>();

  type B = z.ZodObject;
  expectTypeOf<z.infer<B>>().toEqualTypeOf<Record<string, unknown>>();
  expectTypeOf<z.input<B>>().toEqualTypeOf<Record<string, unknown>>();
});

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>;
  expectTypeOf<Merged>().toEqualTypeOf<{ a?: string; b: string }>();
  expectTypeOf<Merged>().toEqualTypeOf<{ a?: string; b: string }>();
});

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>;
  expectTypeOf<Unioned>().toEqualTypeOf<{ a: string; b?: string } | { a?: string; b: string }>();
});

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

  expect(Enum.enum).toEqual({
    a: "a",
    b: "b",
  });

  expect(Enum._zod.def.entries).toEqual({
    a: "a",
    b: "b",
  });
  type Enum = z.infer<typeof Enum>;
  expectTypeOf<Enum>().toEqualTypeOf<"a" | "b">();
});

test("z.keyof returns enum", () => {
  const User = z.object({ name: z.string(), age: z.number() });
  const keysSchema = z.keyof(User);
  expect(keysSchema.enum).toEqual({
    name: "name",
    age: "age",
  });
  expect(keysSchema._zod.def.entries).toEqual({
    name: "name",
    age: "age",
  });
  type Keys = z.infer<typeof keysSchema>;
  expectTypeOf<Keys>().toEqualTypeOf<"name" | "age">();
});

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>;
  expectTypeOf<Partial>().toEqualTypeOf<{ a?: string; b?: string }>();
});

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>;
  expectTypeOf<Picked>().toEqualTypeOf<{ b?: string }>();
});

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>;
  expectTypeOf<myType>().toEqualTypeOf<{
    anyOptional?: any;
    anyRequired: any;
    unknownOptional?: unknown;
    unknownRequired: unknown;
  }>();
});

test("strictObject", 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(z.intersection(schema, 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(z.intersection(schema, 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 person = z
    .object({
      name: z.string(),
    })
    .strict();

  expect(() =>
    person.parse({
      name: "bob dylan",
      constructor: 61,
    })
  ).toThrow();
});

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

  type Example = z.infer<typeof Example>;
  expectTypeOf<keyof Example>().toEqualTypeOf<"prop" | "opt" | "arr">();
});

test("catchall", () => {
  const a = z.object({});
  expect(a._zod.def.catchall).toBeUndefined();

  const b = z.strictObject({});
  expect(b._zod.def.catchall).toBeInstanceOf(core.$ZodNever);

  const c = z.looseObject({});
  expect(c._zod.def.catchall).toBeInstanceOf(core.$ZodUnknown);

  const d = z.object({}).catchall(z.number());
  expect(d._zod.def.catchall).toBeInstanceOf(core.$ZodNumber);
});

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

  const b = z.strictObject({ b: z.string() });

  // incoming object overrides
  const c = a.merge(b);
  expect(c._zod.def.catchall).toBeInstanceOf(core.$ZodNever);
});

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);
  expectTypeOf<keyof PersonWithNickname>().toEqualTypeOf<"firstName" | "lastName" | "nickName">();
  expectTypeOf<PersonWithNickname>().toEqualTypeOf<{ firstName: string; lastName: string; nickName: string }>();
});

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);
  expectTypeOf<PersonWithNumberAsLastName>().toEqualTypeOf<{ firstName: string; lastName: number }>();
});

test("safeExtend() should have power to override existing key", () => {
  const PersonWithMinLastName = personToExtend.safeExtend({
    lastName: z.string().min(3),
  });
  type PersonWithMinLastName = z.infer<typeof PersonWithMinLastName>;

  const expected = { firstName: "f", lastName: "abc" };
  const actual = PersonWithMinLastName.parse(expected);

  expect(actual).toEqual(expected);
  expect(() => PersonWithMinLastName.parse({ firstName: "f", lastName: "ab" })).toThrow();
  expectTypeOf<PersonWithMinLastName>().toEqualTypeOf<{ firstName: string; lastName: string }>();
});

test("safeExtend() maintains refinements", () => {
  const schema = z.object({ name: z.string().min(1) });
  const extended = schema.safeExtend({ name: z.string().min(2) });
  expect(() => extended.parse({ name: "" })).toThrow();
  expect(extended.parse({ name: "ab" })).toEqual({ name: "ab" });
  type Extended = z.infer<typeof extended>;
  expectTypeOf<Extended>().toEqualTypeOf<{ name: string }>();
  // @ts-expect-error
  schema.safeExtend({ name: z.number() });
});

test("passthrough index signature", () => {
  const a = z.object({ a: z.string() });
  type a = z.infer<typeof a>;
  expectTypeOf<a>().toEqualTypeOf<{ a: string }>();
  const b = a.passthrough();
  type b = z.infer<typeof b>;
  expectTypeOf<b>().toEqualTypeOf<{ a: string; [k: string]: unknown }>();
});

// 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.object({
//     data: z.union([z.object({ name: z.string(), a: z.number() }), z.object({ name: z.string(), b: z.number() })]),
//   }) satisfies z.ZodType<Outer, any>;
// });

test("assignability", () => {
  z.object({ a: z.string() }) satisfies z.ZodObject<{ a: z.ZodString }>;
  z.object({ a: z.string() }).catchall(z.number()) satisfies z.ZodObject<{ a: z.ZodString }>;
  z.object({ a: z.string() }).strict() satisfies z.ZodObject;
  z.object({}) satisfies z.ZodObject;

  z.looseObject({ name: z.string() }) satisfies z.ZodObject<
    {
      name: z.ZodString;
    },
    z.core.$loose
  >;
  z.looseObject({ name: z.string() }) satisfies z.ZodObject<{
    name: z.ZodString;
  }>;
  z.strictObject({ name: z.string() }) satisfies z.ZodObject<
    {
      name: z.ZodString;
    },
    z.core.$loose
  >;
  z.strictObject({ name: z.string() }) satisfies z.ZodObject<
    {
      name: z.ZodString;
    },
    z.core.$strict
  >;
  z.object({ name: z.string() }) satisfies z.ZodObject<{
    name: z.ZodString;
  }>;
  z.object({
    a: z.string(),
    b: z.number(),
    c: z.boolean(),
  }) satisfies z.core.$ZodObject;
});

test("null prototype", () => {
  const schema = z.object({ a: z.string() });
  const obj = Object.create(null);
  obj.a = "foo";
  expect(schema.parse(obj)).toEqual({ a: "foo" });
});

test("empty objects", () => {
  const A = z.looseObject({});
  type Ain = z.input<typeof A>;
  expectTypeOf<Ain>().toEqualTypeOf<Record<string, unknown>>();
  type Aout = z.output<typeof A>;
  expectTypeOf<Aout>().toEqualTypeOf<Record<string, unknown>>();

  const B = z.object({});
  type Bout = z.output<typeof B>;
  expectTypeOf<Bout>().toEqualTypeOf<Record<string, never>>();
  type Bin = z.input<typeof B>;
  expectTypeOf<Bin>().toEqualTypeOf<Record<string, never>>();

  const C = z.strictObject({});
  type Cout = z.output<typeof C>;
  expectTypeOf<Cout>().toEqualTypeOf<Record<string, never>>();
  type Cin = z.input<typeof C>;
  expectTypeOf<Cin>().toEqualTypeOf<Record<string, never>>();
});

test("preserve key order", () => {
  const schema = z.object({
    a: z.string().optional(),
    b: z.string(),
  });
  const r1 = schema.safeParse({ a: "asdf", b: "qwer" });
  const r2 = schema.safeParse({ a: "asdf", b: "qwer" }, { jitless: true });

  expect(Object.keys(r1.data!)).toMatchInlineSnapshot(`
    [
      "a",
      "b",
    ]
  `);
  expect(Object.keys(r1.data!)).toEqual(Object.keys(r2.data!));
});

test("empty shape", () => {
  const a = z.object({});

  a.parse({});
  a.parse({}, { jitless: true });
  a.parse(Object.create(null));
  a.parse(Object.create(null), { jitless: true });

  expect(() => a.parse([])).toThrow();
  expect(() => a.parse([], { jitless: true })).toThrow();
});

test("zodtype assignability", () => {
  // Does not error
  z.object({ hello: z.string().optional() }) satisfies z.ZodType<{ hello?: string | undefined }>;
  z.object({ hello: z.string() }) satisfies z.ZodType<{ hello?: string | undefined }>;
  // @ts-expect-error
  z.object({}) satisfies z.ZodType<{ hello: string | undefined }>;
  // @ts-expect-error
  z.object({ hello: z.string().optional() }) satisfies z.ZodType<{ hello: string | undefined }>;
  // @ts-expect-error
  z.object({ hello: z.string().optional() }) satisfies z.ZodType<{ hello: string }>;
  // @ts-expect-error
  z.object({ hello: z.number() }) satisfies z.ZodType<{ hello?: string | undefined }>;
});

test("index signature in shape", () => {
  function makeZodObj<const T extends string>(key: T) {
    return z.looseObject({
      [key]: z.string(),
    });
  }

  const schema = makeZodObj("foo");
  type schema = z.infer<typeof schema>;

  expectTypeOf<schema>().toEqualTypeOf<Record<string, string>>();
});

test("extend() on object with refinements should throw when overwriting properties", () => {
  const schema = z
    .object({
      a: z.string(),
    })
    .refine(() => true);

  expect(() => schema.extend({ a: z.number() })).toThrow();
});

test("extend() on object with refinements should not throw when adding new properties", () => {
  const schema = z
    .object({
      a: z.string(),
    })
    .refine((data) => data.a.length > 0);

  // Should not throw since 'b' doesn't overlap with 'a'
  const extended = schema.extend({ b: z.number() });

  // Verify the extended schema works correctly
  expect(extended.parse({ a: "hello", b: 42 })).toEqual({ a: "hello", b: 42 });

  // Verify the original refinement still applies
  expect(() => extended.parse({ a: "", b: 42 })).toThrow();
});

test("safeExtend() on object with refinements should not throw", () => {
  const schema = z
    .object({
      a: z.string(),
    })
    .refine(() => true);

  expect(() => schema.safeExtend({ b: z.string() })).not.toThrow();
});
