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

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(): z.ZodMiniOptional<z.ZodMiniArray<typeof Category>> {
      return z.optional(z.array(Category));
    },
  });
  Category.parse(data);

  type Category = z.infer<typeof Category>;
  interface _Category {
    name: string;
    subcategories?: _Category[] | undefined;
  }
  expectTypeOf<Category>().toEqualTypeOf<_Category>();
});

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(): z.ZodMiniNullable<typeof LL> {
      return z.nullable(LL);
    },
  });

  LL.parse(data);
  type LL = z.infer<typeof LL>;
  type _LL = {
    value: number;
    next: _LL | null;
  };
  expectTypeOf<LL>().toEqualTypeOf<_LL>();
});

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

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

  Alazy.parse(testData);
  Blazy.parse(testData.b);

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

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

  const PickedCategory = z.pick(Category, { name: true });
  const OmittedCategory = z.omit(Category, { subcategories: true });
  type PickedCategory = z.infer<typeof PickedCategory>;
  type OmittedCategory = z.infer<typeof OmittedCategory>;
  interface _PickedCategory {
    name: string;
  }
  interface _OmittedCategory {
    name: string;
  }
  expectTypeOf<PickedCategory>().toEqualTypeOf<_PickedCategory>();
  expectTypeOf<OmittedCategory>().toEqualTypeOf<_OmittedCategory>();

  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("deferred self-recursion", () => {
  const Feature = z.object({
    title: z.string(),
    get features(): z.ZodMiniOptional<z.ZodMiniArray<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(),
    features: z.array(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("recursion compatibility", () => {
  // array
  const A = z.object({
    get subcategories() {
      return z.array(A);
    },
  });
  // tuple
  const B = z.object({
    get subcategories() {
      return z.tuple([B, B]);
    },
  });
  // object
  const C = z.object({
    get subcategories() {
      return z.object({
        subcategories: C,
      });
    },
  });
  // union
  const D = z.object({
    get subcategories() {
      return z.union([D, z.string()]);
    },
  });
  // intersection
  const E = z.object({
    get subcategories() {
      return z.intersection(E, E);
    },
  });
  // record
  const F = z.object({
    get subcategories() {
      return z.record(z.string(), F);
    },
  });
  // map
  const G = z.object({
    get subcategories() {
      return z.map(z.string(), G);
    },
  });
  // set
  const H = z.object({
    get subcategories() {
      return z.set(H);
    },
  });
  // optional
  const I = z.object({
    get subcategories() {
      return z.optional(I);
    },
  });
  // nullable
  const J = z.object({
    get subcategories() {
      return z.nullable(J);
    },
  });
  // optional
  const L = z.object({
    get subcategories() {
      return z.optional(L);
    },
  });
  // nullable
  const M = z.object({
    get subcategories() {
      return z.nullable(M);
    },
  });
  // nonoptional
  const N = z.object({
    get subcategories() {
      return z.nonoptional(N);
    },
  });
});

test("shape stays writeable through object/strictObject/looseObject/extend with getters", () => {
  const Cat = z.object({
    name: z.string(),
    get sub(): z.ZodMiniArray<typeof Cat> {
      return z.array(Cat);
    },
  });
  type CatShape = (typeof Cat)["shape"];
  expectTypeOf<CatShape>().toEqualTypeOf<{
    name: z.ZodMiniString<string>;
    sub: z.ZodMiniArray<typeof Cat>;
  }>();

  const StrictCat = z.strictObject({
    name: z.string(),
    get sub(): z.ZodMiniArray<typeof StrictCat> {
      return z.array(StrictCat);
    },
  });
  type StrictShape = (typeof StrictCat)["shape"];
  expectTypeOf<StrictShape>().toEqualTypeOf<{
    name: z.ZodMiniString<string>;
    sub: z.ZodMiniArray<typeof StrictCat>;
  }>();

  const LooseCat = z.looseObject({
    name: z.string(),
    get sub(): z.ZodMiniArray<typeof LooseCat> {
      return z.array(LooseCat);
    },
  });
  type LooseShape = (typeof LooseCat)["shape"];
  expectTypeOf<LooseShape>().toEqualTypeOf<{
    name: z.ZodMiniString<string>;
    sub: z.ZodMiniArray<typeof LooseCat>;
  }>();

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