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

test("recursion with z.lazy", () => {
  const data = {
    name: "I",
    subcategories: [
      {
        name: "A",
        subcategories: [
          {
            name: "1",
            subcategories: [
              {
                name: "a",
                subcategories: [],
              },
            ],
          },
        ],
      },
    ],
  };

  const Category = z.object({
    name: z.string(),
    get subcategories() {
      return z.array(Category).optional().nullable();
    },
  });
  type Category = z.infer<typeof Category>;
  interface _Category {
    name: string;
    subcategories?: _Category[] | undefined | null;
  }
  expectTypeOf<Category>().toEqualTypeOf<_Category>();
  Category.parse(data);
});

test("recursion involving union type", () => {
  const data = {
    value: 1,
    next: {
      value: 2,
      next: {
        value: 3,
        next: {
          value: 4,
          next: null,
        },
      },
    },
  };

  const LL = z.object({
    value: z.number(),
    get next() {
      return LL.nullable();
    },
  });
  type LL = z.infer<typeof LL>;
  type _LL = {
    value: number;
    next: _LL | null;
  };
  expectTypeOf<LL>().toEqualTypeOf<_LL>();

  LL.parse(data);
});

test("mutual recursion - native", () => {
  const Alazy = z.object({
    val: z.number(),
    get b() {
      return Blazy;
    },
  });

  const Blazy = z.object({
    val: z.number(),
    get a() {
      return Alazy.optional();
    },
  });
  const testData = {
    val: 1,
    b: {
      val: 5,
      a: {
        val: 3,
        b: {
          val: 4,
          a: {
            val: 2,
            b: {
              val: 1,
            },
          },
        },
      },
    },
  };

  type Alazy = z.infer<typeof Alazy>;
  type Blazy = z.infer<typeof Blazy>;
  interface _Alazy {
    val: number;
    b: _Blazy;
  }
  interface _Blazy {
    val: number;
    a?: _Alazy | undefined;
  }
  expectTypeOf<Alazy>().toEqualTypeOf<_Alazy>();
  expectTypeOf<Blazy>().toEqualTypeOf<_Blazy>();
  Alazy.parse(testData);
  Blazy.parse(testData.b);

  expect(() => Alazy.parse({ val: "asdf" })).toThrow();
});

test("pick and omit with getter", () => {
  const Category = z.strictObject({
    name: z.string(),
    get subcategories() {
      return z.array(Category);
    },
  });

  type Category = z.infer<typeof Category>;

  interface _Category {
    name: string;
    subcategories: _Category[];
  }
  expectTypeOf<Category>().toEqualTypeOf<_Category>();

  // Shape should not surface `readonly` modifiers from getter-defined keys.
  // object/strictObject/looseObject all pass shape through util.Writeable<T>.
  type Shape = (typeof Category)["shape"];
  expectTypeOf<Shape>().toEqualTypeOf<{ name: z.ZodString; subcategories: z.ZodArray<typeof Category> }>();

  const PickedCategory = Category.pick({ name: true });
  const OmittedCategory = Category.omit({ subcategories: true });

  const picked = { name: "test" };
  const omitted = { name: "test" };

  PickedCategory.parse(picked);
  OmittedCategory.parse(omitted);

  expect(() => PickedCategory.parse({ name: "test", subcategories: [] })).toThrow();
  expect(() => OmittedCategory.parse({ name: "test", subcategories: [] })).toThrow();
});

test("shape stays writeable through extend/safeExtend/partial/required", () => {
  const Base = z.object({ name: z.string() });

  const Extended = Base.extend({
    get sub() {
      return z.array(Extended);
    },
  });
  type ExtendedShape = (typeof Extended)["shape"];
  expectTypeOf<ExtendedShape>().toEqualTypeOf<{
    name: z.ZodString;
    sub: z.ZodArray<typeof Extended>;
  }>();

  const SafeExt = Base.safeExtend({ extra: z.string() } as const);
  type SafeExtShape = (typeof SafeExt)["shape"];
  expectTypeOf<SafeExtShape>().toEqualTypeOf<{
    name: z.ZodString;
    extra: z.ZodString;
  }>();

  const FromConst = Base.extend({ a: z.string(), b: z.number() } as const);
  type FromConstShape = (typeof FromConst)["shape"];
  expectTypeOf<FromConstShape>().toEqualTypeOf<{
    name: z.ZodString;
    a: z.ZodString;
    b: z.ZodNumber;
  }>();

  const PartialExtended = Extended.partial();
  type PartialShape = (typeof PartialExtended)["shape"];
  expectTypeOf<PartialShape>().toEqualTypeOf<{
    name: z.ZodOptional<z.ZodString>;
    sub: z.ZodOptional<z.ZodArray<typeof Extended>>;
  }>();

  const RequiredExtended = Extended.required();
  type RequiredShape = (typeof RequiredExtended)["shape"];
  expectTypeOf<RequiredShape>().toEqualTypeOf<{
    name: z.ZodNonOptional<z.ZodString>;
    sub: z.ZodNonOptional<z.ZodArray<typeof Extended>>;
  }>();
});

test("deferred self-recursion", () => {
  const Feature = z.object({
    title: z.string(),
    get features(): z.ZodOptional<z.ZodArray<typeof Feature>> {
      return z.optional(z.array(Feature)); //.optional();
    },
  });
  // type Feature = z.infer<typeof Feature>;

  const Output = z.object({
    id: z.int(), //.nonnegative(),
    name: z.string(),
    get features(): z.ZodArray<typeof Feature> {
      return Feature.array();
    },
  });
  type Output = z.output<typeof Output>;

  type _Feature = {
    title: string;
    features?: _Feature[] | undefined;
  };

  type _Output = {
    id: number;
    name: string;
    features: _Feature[];
  };

  // expectTypeOf<Feature>().toEqualTypeOf<_Feature>();
  expectTypeOf<Output>().toEqualTypeOf<_Output>();
});

test("deferred mutual recursion", () => {
  const Slot = z.object({
    slotCode: z.string(),

    get blocks() {
      return z.array(Block);
    },
  });
  type Slot = z.infer<typeof Slot>;

  const Block = z.object({
    blockCode: z.string(),
    get slots() {
      return z.array(Slot).optional();
    },
  });
  type Block = z.infer<typeof Block>;

  const Page = z.object({
    slots: z.array(Slot),
  });
  type Page = z.infer<typeof Page>;

  type _Slot = {
    slotCode: string;
    blocks: _Block[];
  };
  type _Block = {
    blockCode: string;
    slots?: _Slot[] | undefined;
  };
  type _Page = {
    slots: _Slot[];
  };
  expectTypeOf<Slot>().toEqualTypeOf<_Slot>();
  expectTypeOf<Block>().toEqualTypeOf<_Block>();
  expectTypeOf<Page>().toEqualTypeOf<_Page>();
});

test("mutual recursion with meta", () => {
  const A = z
    .object({
      name: z.string(),
      get b() {
        return B;
      },
    })
    .readonly()
    .meta({ id: "A" })
    .optional();

  const B = z
    .object({
      name: z.string(),
      get a() {
        return A;
      },
    })
    .readonly()
    .meta({ id: "B" });

  type A = z.infer<typeof A>;
  type B = z.infer<typeof B>;

  type _A =
    | Readonly<{
        name: string;
        b: _B;
      }>
    | undefined;
  // | undefined;
  type _B = Readonly<{
    name: string;
    a?: _A;
  }>;
  expectTypeOf<A>().toEqualTypeOf<_A>();
  expectTypeOf<B>().toEqualTypeOf<_B>();
});

test("intersection with recursive types", () => {
  const A = z.discriminatedUnion("type", [
    z.object({
      type: z.literal("CONTAINER"),
    }),
    z.object({
      type: z.literal("SCREEN"),
      config: z.object({ x: z.number(), y: z.number() }),
    }),
  ]);
  // type A = z.infer<typeof A>;

  const B = z.object({
    get children() {
      return z.array(C).optional();
    },
  });
  // type B = z.infer<typeof B>;

  const C = z.intersection(A, B);
  type C = z.infer<typeof C>;

  type _C = (
    | {
        type: "CONTAINER";
      }
    | {
        type: "SCREEN";
        config: {
          x: number;
          y: number;
        };
      }
  ) & {
    children?: _C[] | undefined;
  };
  expectTypeOf<C>().toEqualTypeOf<_C>();
});

test("object utilities with recursive types", () => {
  const NodeBase = z.object({
    id: z.string(),
    name: z.string(),
    get children() {
      return z.array(Node).optional();
    },
  });

  // Test extend with new keys (extend throws when overwriting existing keys)
  const NodeOne = NodeBase.extend({
    name: z.literal("nodeOne"),
    get children() {
      return z.array(Node);
    },
  });

  const NodeTwo = NodeBase.extend({
    name: z.literal("nodeTwo"),
    get children() {
      return z.array(Node);
    },
  });

  // Test pick
  const PickedNode = NodeBase.pick({ id: true, name: true });

  // Test omit
  const OmittedNode = NodeBase.omit({ children: true });

  // Test merge
  const ExtraProps = {
    metadata: z.string(),
    get parent() {
      return Node.optional();
    },
  };
  const MergedNode = NodeBase.extend(ExtraProps);

  // Test partial
  const PartialNode = NodeBase.partial();
  const PartialMaskedNode = NodeBase.partial({ name: true });

  // Test required (assuming NodeBase has optional fields)
  const OptionalNodeBase = z.object({
    id: z.string().optional(),
    name: z.string().optional(),
    get children() {
      return z.array(Node).optional();
    },
  });
  const RequiredNode = OptionalNodeBase.required();
  const RequiredMaskedNode = OptionalNodeBase.required({ id: true });

  const Node = z.union([
    NodeOne,
    NodeTwo,
    PickedNode,
    OmittedNode,
    MergedNode,
    PartialNode,
    PartialMaskedNode,
    RequiredNode,
    RequiredMaskedNode,
  ]);
});

test("tuple with recursive types", () => {
  const TaskListNodeSchema = z.strictObject({
    type: z.literal("taskList"),
    get content() {
      return z.array(z.tuple([TaskListNodeSchema, z.union([TaskListNodeSchema])])).min(1);
    },
  });
  type TaskListNodeSchema = z.infer<typeof TaskListNodeSchema>;
  type _TaskListNodeSchema = {
    type: "taskList";
    content: [_TaskListNodeSchema, _TaskListNodeSchema][];
  };
  expectTypeOf<TaskListNodeSchema>().toEqualTypeOf<_TaskListNodeSchema>();
});

test("recursion compatibility", () => {
  // array
  const A = z.object({
    get array() {
      return A.array();
    },
    get optional() {
      return A.optional();
    },
    get nullable() {
      return A.nullable();
    },
    get nonoptional() {
      return A.nonoptional();
    },
    get readonly() {
      return A.readonly();
    },
    get describe() {
      return A.describe("A recursive type");
    },
    get meta() {
      return A.meta({ description: "A recursive type" });
    },
    get pipe() {
      return A.pipe(z.any());
    },
    get strict() {
      return A.strict();
    },
    get tuple() {
      return z.tuple([A, A]);
    },
    get object() {
      return z
        .object({
          subcategories: A,
        })
        .strict()
        .loose();
    },
    get union() {
      return z.union([A, A]);
    },
    get intersection() {
      return z.intersection(A, A);
    },
    get record() {
      return z.record(z.string(), A);
    },
    get map() {
      return z.map(z.string(), A);
    },
    get set() {
      return z.set(A);
    },
    get lazy() {
      return z.lazy(() => A);
    },
    get promise() {
      return z.promise(A);
    },
  });
});

test("recursive object with .check()", () => {
  const Category = z
    .object({
      id: z.string(),
      name: z.string(),
      get subcategories() {
        return z.array(Category).optional();
      },
    })
    .check((ctx) => {
      // Check for duplicate IDs among direct subcategories
      if (ctx.value.subcategories) {
        const siblingIds = new Set<string>();
        ctx.value.subcategories.forEach((sub, index) => {
          if (siblingIds.has(sub.id)) {
            ctx.issues.push({
              code: "custom",
              message: `Duplicate sibling ID found: ${sub.id}`,
              path: ["subcategories", index, "id"],
              input: ctx.value,
            });
          }
          siblingIds.add(sub.id);
        });
      }
    });

  // Valid - siblings have unique IDs
  const validData = {
    id: "electronics",
    name: "Electronics",
    subcategories: [
      {
        id: "computers",
        name: "Computers",
        subcategories: [
          { id: "laptops", name: "Laptops" },
          { id: "desktops", name: "Desktops" },
        ],
      },
      {
        id: "phones",
        name: "Phones",
      },
    ],
  };

  // Invalid - duplicate sibling IDs
  const invalidData = {
    id: "electronics",
    name: "Electronics",
    subcategories: [
      { id: "computers", name: "Computers" },
      { id: "phones", name: "Phones" },
      { id: "computers", name: "Computers Again" }, // Duplicate at index 2
    ],
  };

  expect(() => Category.parse(validData)).not.toThrow();
  expect(() => Category.parse(invalidData)).toThrow();
});

// biome-ignore lint: sadf
export type RecursiveA = z.ZodUnion<
  [
    z.ZodObject<{
      a: z.ZodDefault<RecursiveA>;
      b: z.ZodPrefault<RecursiveA>;
      c: z.ZodNonOptional<RecursiveA>;
      d: z.ZodOptional<RecursiveA>;
      e: z.ZodNullable<RecursiveA>;
      g: z.ZodReadonly<RecursiveA>;
      h: z.ZodPipe<RecursiveA, z.ZodString>;
      i: z.ZodArray<RecursiveA>;
      j: z.ZodSet<RecursiveA>;
      k: z.ZodMap<RecursiveA, RecursiveA>;
      l: z.ZodRecord<z.ZodString, RecursiveA>;
      m: z.ZodUnion<[RecursiveA, RecursiveA]>;
      n: z.ZodIntersection<RecursiveA, RecursiveA>;
      o: z.ZodLazy<RecursiveA>;
      p: z.ZodPromise<RecursiveA>;
      q: z.ZodCatch<RecursiveA>;
      r: z.ZodSuccess<RecursiveA>;
      s: z.ZodTransform<RecursiveA, string>;
      t: z.ZodTuple<[RecursiveA, RecursiveA]>;
      u: z.ZodObject<{
        a: RecursiveA;
      }>;
    }>,
  ]
>;

test("recursive type with `id` meta", () => {
  const AType = z.object({
    type: z.literal("a"),
    name: z.string(),
  });

  const BType = z.object({
    type: z.literal("b"),
    name: z.string(),
  });

  const CType = z.object({
    type: z.literal("c"),
    name: z.string(),
  });

  const Schema = z.object({
    type: z.literal("special").meta({ description: "Type" }),
    config: z.object({
      title: z.string().meta({ description: "Title" }),
      get elements() {
        return z.array(z.discriminatedUnion("type", [AType, BType, CType])).meta({
          id: "SpecialElements",
          title: "SpecialElements",
          description: "Array of elements",
        });
      },
    }),
  });

  Schema.parse({
    type: "special",
    config: {
      title: "Special",
      elements: [
        { type: "a", name: "John" },
        { type: "b", name: "Jane" },
        { type: "c", name: "Jim" },
      ],
    },
  });
});
