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

test("instanceof", async () => {
  class Test {}
  class Subtest extends Test {}
  abstract class AbstractBar {
    constructor(public val: string) {}
  }
  class Bar extends AbstractBar {}

  const TestSchema = z.instanceof(Test);
  const SubtestSchema = z.instanceof(Subtest);
  const AbstractSchema = z.instanceof(AbstractBar);
  const BarSchema = z.instanceof(Bar);

  TestSchema.parse(new Test());
  TestSchema.parse(new Subtest());
  SubtestSchema.parse(new Subtest());
  AbstractSchema.parse(new Bar("asdf"));
  const bar = BarSchema.parse(new Bar("asdf"));
  expect(bar.val).toEqual("asdf");

  await expect(() => SubtestSchema.parse(new Test())).toThrow();
  await expect(() => TestSchema.parse(12)).toThrow();

  expectTypeOf<Test>().toEqualTypeOf<z.infer<typeof TestSchema>>();
});

test("instanceof fatal", () => {
  const schema = z.instanceof(Date).refine((d) => d.toString());
  const res = schema.safeParse(null);
  expect(res.success).toBe(false);
});

test("instanceof respects customError", () => {
  class Test {
    name!: string;
  }

  z.config({
    customError: () => {
      return "This is invalid!";
    },
  });

  const TestSchema = z.instanceof(Test);
  const result = TestSchema.safeParse("whatever");
  expect(result.success).toBe(false);
  if (!result.success) {
    const issue = result.error.issues[0];
    expect(issue.code).toBe("invalid_type");
    if (issue.code === "invalid_type") {
      expect(issue.expected).toBe("Test");
    }
    expect(issue.message).toBe("This is invalid!");
  }

  z.config({ customError: undefined });
});

test("z.properties", () => {
  const httpsUrl = z.instanceof(URL).check(
    ...z.properties({
      protocol: z.literal("https:"),
      hostname: z.string().regex(z.regexes.domain),
    })
  );

  expectTypeOf<URL>().toEqualTypeOf<z.infer<typeof httpsUrl>>();
  expect(httpsUrl.safeParse(new URL("https://example.com")).success).toBe(true);

  // One check iterates the whole shape, so every failing property reports — unlike the equivalent chain of z.property() calls, which aborts on the first.
  const both = httpsUrl.safeParse(new URL("http://localhost"));
  expect(both.error!.issues.map((i) => i.path)).toEqual([["protocol"], ["hostname"]]);

  // A failing base schema yields its own issue and no property issues.
  for (const input of ["not a url", null]) {
    const issues = httpsUrl.safeParse(input).error!.issues;
    expect(issues.map((i) => [i.code, i.path])).toEqual([["custom", []]]);
  }

  // Not specific to z.instanceof(). A base whose type mismatch aborts implicitly rather than explicitly must still not run the property checks against the rejected value.
  const obj = z
    .object({ a: z.string(), b: z.string() })
    .check(...z.properties({ a: z.literal("x"), b: z.literal("y") }));
  expect(obj.safeParse({ a: "x", b: "y" }).success).toBe(true);
  expect(obj.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
  for (const input of [null, undefined, 5]) {
    expect(obj.safeParse(input).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
  }

  // Known looseness versus the longhand, pinned so it stays deliberate: the check is typed over the whole shape, and `$ZodCheckInternals.check()` is a method, so TypeScript compares it bivariantly and accepts a check type that is a subtype of the target. Naming a key the target lacks therefore compiles here and fails at parse time, where the equivalent chain of `z.property()` calls rejects it outright.
  z.object({ a: z.string() }).check(...z.properties({ a: z.literal("x"), b: z.literal("y") }));
  expect(
    z
      .object({ a: z.string() })
      .check(...z.properties({ a: z.string(), b: z.literal("y") }))
      .safeParse({ a: "ok" })
      .error!.issues.map((i) => [i.code, i.path])
  ).toEqual([["invalid_value", ["b"]]]);

  // literal and enum inputs widen to their primitive, so a `string`/`boolean` property accepts them without a cast, while a genuine type mismatch or an unknown key still fails to compile
  z.instanceof(URL).check(z.property("protocol", z.literal("https:")));
  z.instanceof(Request).check(...z.properties({ method: z.enum(["POST", "PUT"]), bodyUsed: z.literal(false) }));
  // @ts-expect-error length is a number
  z.string().check(z.property("length", z.string()));
  // @ts-expect-error no such key
  z.instanceof(URL).check(z.property("nope", z.string()));

  // the check feeds the property value into its schema, so it is typed over the input side rather than the output
  const stringToLength = z.property(
    "a",
    z.string().transform((s) => s.length)
  );
  z.object({ a: z.string() }).check(stringToLength);
  // @ts-expect-error a is a number, the schema takes a string
  z.object({ a: z.number() }).check(stringToLength);

  // The properties check carries no `when`, so the schema stays on the compiled fast path.
  const compiled = z.compile(httpsUrl);
  expect(compiled.safeParse(new URL("https://example.com")).success).toBe(true);
  expect(compiled.safeParse(new URL("http://localhost")).error!.issues.map((i) => i.path)).toEqual([
    ["protocol"],
    ["hostname"],
  ]);
});
