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

test("basic defaults", () => {
  expect(z.string().default("default").parse(undefined)).toBe("default");
});

test("default with optional", () => {
  const schema = z.string().optional().default("default");
  expect(schema.parse(undefined)).toBe("default");
  expect(schema.unwrap().parse(undefined)).toBe(undefined);
});

test("default with transform", () => {
  const stringWithDefault = z
    .string()
    .transform((val) => val.toUpperCase())
    .default("default");
  expect(stringWithDefault.parse(undefined)).toBe("default");
  expect(stringWithDefault).toBeInstanceOf(z.ZodDefault);
  expect(stringWithDefault.unwrap()).toBeInstanceOf(z.ZodPipe);
  expect(stringWithDefault.unwrap().in).toBeInstanceOf(z.ZodString);
  expect(stringWithDefault.unwrap().out).toBeInstanceOf(z.ZodTransform);

  type inp = z.input<typeof stringWithDefault>;
  expectTypeOf<inp>().toEqualTypeOf<string | undefined>();
  type out = z.output<typeof stringWithDefault>;
  expectTypeOf<out>().toEqualTypeOf<string>();
});

test("default on existing optional", () => {
  const stringWithDefault = z.string().optional().default("asdf");
  expect(stringWithDefault.parse(undefined)).toBe("asdf");
  expect(stringWithDefault).toBeInstanceOf(z.ZodDefault);
  expect(stringWithDefault.unwrap()).toBeInstanceOf(z.ZodOptional);
  expect(stringWithDefault.unwrap().unwrap()).toBeInstanceOf(z.ZodString);

  type inp = z.input<typeof stringWithDefault>;
  expectTypeOf<inp>().toEqualTypeOf<string | undefined>();
  type out = z.output<typeof stringWithDefault>;
  expectTypeOf<out>().toEqualTypeOf<string>();
});

test("optional on default", () => {
  const stringWithDefault = z.string().default("asdf").optional();

  type inp = z.input<typeof stringWithDefault>;
  expectTypeOf<inp>().toEqualTypeOf<string | undefined>();
  type out = z.output<typeof stringWithDefault>;
  expectTypeOf<out>().toEqualTypeOf<string | undefined>();

  expect(stringWithDefault.parse(undefined)).toBe("asdf");
});

// test("complex chain example", () => {
//   const complex = z
//     .string()
//     .default("asdf")
//     .transform((val) => val.toUpperCase())
//     .default("qwer")
//     .unwrap()
//     .optional()
//     .default("asdfasdf");

//   expect(complex.parse(undefined)).toBe("asdfasdf");
// });

test("removeDefault", () => {
  const stringWithRemovedDefault = z.string().default("asdf").removeDefault();

  type out = z.output<typeof stringWithRemovedDefault>;
  expectTypeOf<out>().toEqualTypeOf<string>();
});

test("apply default at output", () => {
  const schema = z
    .string()
    .transform((_) => (Math.random() > 0 ? undefined : _))
    .default("asdf");
  expect(schema.parse("")).toEqual("asdf");
});

test("nested", () => {
  const inner = z.string().default("asdf");
  const outer = z.object({ inner }).default({
    inner: "qwer",
  });
  type input = z.input<typeof outer>;
  expectTypeOf<input>().toEqualTypeOf<{ inner?: string | undefined } | undefined>();
  type out = z.output<typeof outer>;
  expectTypeOf<out>().toEqualTypeOf<{ inner: string }>();
  expect(outer.parse(undefined)).toEqual({ inner: "qwer" });
  expect(outer.parse({})).toEqual({ inner: "asdf" });
  expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" });
});

test("chained defaults", () => {
  const stringWithDefault = z.string().default("inner").default("outer");
  const result = stringWithDefault.parse(undefined);
  expect(result).toEqual("outer");
});

test("object optionality", () => {
  const schema = z.object({
    hi: z.string().default("hi"),
  });
  type schemaInput = z.input<typeof schema>;
  type schemaOutput = z.output<typeof schema>;
  expectTypeOf<schemaInput>().toEqualTypeOf<{ hi?: string | undefined }>();
  expectTypeOf<schemaOutput>().toEqualTypeOf<{ hi: string }>();
  expect(schema.parse({})).toEqual({
    hi: "hi",
  });
});

test("nested prefault/default", () => {
  const a = z
    .string()
    .default("a")
    .refine((val) => val.startsWith("a"));
  const b = z
    .string()
    .refine((val) => val.startsWith("b"))
    .default("b");
  const c = z
    .string()
    .prefault("c")
    .refine((val) => val.startsWith("c"));
  const d = z
    .string()
    .refine((val) => val.startsWith("d"))
    .prefault("d");

  const obj = z.object({
    a,
    b,
    c,
    d,
  });

  expect(obj.safeParse({ a: "a1", b: "b1", c: "c1", d: "d1" })).toMatchInlineSnapshot(`
    {
      "data": {
        "a": "a1",
        "b": "b1",
        "c": "c1",
        "d": "d1",
      },
      "success": true,
    }
  `);

  expect(obj.safeParse({ a: "f", b: "f", c: "f", d: "f" })).toMatchInlineSnapshot(`
    {
      "error": [ZodError: [
      {
        "code": "custom",
        "path": [
          "a"
        ],
        "message": "Invalid input"
      },
      {
        "code": "custom",
        "path": [
          "b"
        ],
        "message": "Invalid input"
      },
      {
        "code": "custom",
        "path": [
          "c"
        ],
        "message": "Invalid input"
      },
      {
        "code": "custom",
        "path": [
          "d"
        ],
        "message": "Invalid input"
      }
    ]],
      "success": false,
    }
  `);

  expect(obj.safeParse({})).toMatchInlineSnapshot(`
    {
      "data": {
        "a": "a",
        "b": "b",
        "c": "c",
        "d": "d",
      },
      "success": true,
    }
  `);

  expect(obj.safeParse({ a: undefined, b: undefined, c: undefined, d: undefined })).toMatchInlineSnapshot(`
    {
      "data": {
        "a": "a",
        "b": "b",
        "c": "c",
        "d": "d",
      },
      "success": true,
    }
  `);

  const obj2 = z.object({
    a: a.optional(),
    b: b.optional(),
    c: c.optional(),
    d: d.optional(),
  });
  expect(obj2.safeParse({ a: undefined, b: undefined, c: undefined, d: undefined })).toMatchInlineSnapshot(`
    {
      "data": {
        "a": "a",
        "b": "b",
        "c": "c",
        "d": "d",
      },
      "success": true,
    }
  `);

  expect(a.parse(undefined)).toBe("a");
  expect(b.parse(undefined)).toBe("b");
  expect(c.parse(undefined)).toBe("c");
  expect(d.parse(undefined)).toBe("d");
});

test("failing default", () => {
  const a = z
    .string()
    .default("z")
    .refine((val) => val.startsWith("a"));
  const b = z
    .string()
    .refine((val) => val.startsWith("b"))
    .default("z");
  const c = z
    .string()
    .prefault("z")
    .refine((val) => val.startsWith("c"));
  const d = z
    .string()
    .refine((val) => val.startsWith("d"))
    .prefault("z");

  const obj = z.object({
    a,
    b,
    c,
    d,
  });

  expect(
    obj.safeParse({
      a: undefined,
      b: undefined,
      c: undefined,
      d: undefined,
    }).error!.issues
  ).toMatchInlineSnapshot(`
    [
      {
        "code": "custom",
        "message": "Invalid input",
        "path": [
          "a",
        ],
      },
      {
        "code": "custom",
        "message": "Invalid input",
        "path": [
          "c",
        ],
      },
      {
        "code": "custom",
        "message": "Invalid input",
        "path": [
          "d",
        ],
      },
    ]
  `);
});

test("partial should not clobber defaults", () => {
  const objWithDefaults = z.object({
    a: z.string().default("defaultA"),
    b: z.string().default("defaultB"),
    c: z.string().default("defaultC"),
  });

  const objPartialWithOneRequired = objWithDefaults.partial(); //.required({ a: true });

  const test = objPartialWithOneRequired.parse({});
  expect(test).toMatchInlineSnapshot(`
    {
      "a": "defaultA",
      "b": "defaultB",
      "c": "defaultC",
    }
  `);
});

test("defaulted object schema returns shallow clone", () => {
  const schema = z
    .object({
      a: z.string(),
    })
    .default({ a: "x" });
  const result1 = schema.parse(undefined);
  const result2 = schema.parse(undefined);
  expect(result1).not.toBe(result2);
  expect(result1).toEqual(result2);
});

test("defaulted array schema returns shallow clone", () => {
  const schema = z.array(z.string()).default(["x"]);
  const result1 = schema.parse(undefined);
  const result2 = schema.parse(undefined);
  expect(result1).not.toBe(result2);
  expect(result1).toEqual(result2);
});

test("defaulted Map schema returns shallow clone", () => {
  const schema = z.map(z.string(), z.number()).default(new Map([["a", 1]]));
  const result1 = schema.parse(undefined);
  const result2 = schema.parse(undefined);
  expect(result1).not.toBe(result2);
  expect(result1).toEqual(result2);
});

test("defaulted Set schema returns shallow clone", () => {
  const schema = z.set(z.string()).default(new Set(["x"]));
  const result1 = schema.parse(undefined);
  const result2 = schema.parse(undefined);
  expect(result1).not.toBe(result2);
  expect(result1).toEqual(result2);
});

test("mutations on defaulted Map do not affect subsequent parses", () => {
  const schema = z.map(z.string(), z.number()).default(new Map());
  const result1 = schema.parse(undefined);
  const result2 = schema.parse(undefined);
  result1.set("key1", 1);
  result2.set("key2", 2);
  expect(result1.size).toBe(1);
  expect(result1.get("key1")).toBe(1);
  expect(result1.has("key2")).toBe(false);
  expect(result2.size).toBe(1);
  expect(result2.get("key2")).toBe(2);
  expect(result2.has("key1")).toBe(false);
});

test("mutations on defaulted Set do not affect subsequent parses", () => {
  const schema = z.set(z.string()).default(new Set());
  const result1 = schema.parse(undefined);
  const result2 = schema.parse(undefined);
  result1.add("item1");
  result2.add("item2");
  expect(result1.size).toBe(1);
  expect(result1.has("item1")).toBe(true);
  expect(result1.has("item2")).toBe(false);
  expect(result2.size).toBe(1);
  expect(result2.has("item2")).toBe(true);
  expect(result2.has("item1")).toBe(false);
});

test("direction-aware defaults", () => {
  const schema = z.string().default("hello");

  // Forward direction (regular parse): defaults should be applied
  expect(schema.parse(undefined)).toBe("hello");
  expect(schema.parse("hello")).toBe("hello");

  // Reverse direction (encode): defaults should NOT be applied, undefined should fail validation
  expect(() => z.encode(schema, undefined as any)).toThrow();

  // But valid values should still work in reverse
  expect(z.safeEncode(schema, "world")).toMatchInlineSnapshot(`
    {
      "data": "world",
      "success": true,
    }
  `);
  expect(z.safeEncode(schema, undefined as any)).toMatchInlineSnapshot(`
    {
      "error": [ZodError: [
      {
        "expected": "string",
        "code": "invalid_type",
        "path": [],
        "message": "Invalid input: expected string, received undefined"
      }
    ]],
      "success": false,
    }
  `);
});
