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

const args1 = z.tuple([z.string()]);
const returns1 = z.number();
const func1 = z.function({
  input: args1,

  output: returns1,
});

test("function parsing", () => {
  const parsed = func1.implement((arg: any) => arg.length);
  const result = parsed("asdf");
  expect(result).toBe(4);
});

test("parsed function fail 1", () => {
  // @ts-expect-error
  const parsed = func1.implement((x: string) => x);
  expect(() => parsed("asdf")).toThrow();
});

test("parsed function fail 2", () => {
  // @ts-expect-error
  const parsed = func1.implement((x: string) => x);
  expect(() => parsed(13 as any)).toThrow();
});

test("function inference 1", () => {
  type func1 = (typeof func1)["_input"];
  expectTypeOf<func1>().toEqualTypeOf<(k: string) => number>();
});

test("method parsing", () => {
  const methodObject = z.object({
    property: z.number(),
    method: z
      .function()
      .input(z.tuple([z.string()]))
      .output(z.number()),
  });
  const methodInstance = {
    property: 3,
    method: function (s: string) {
      return s.length + this.property;
    },
  };
  const parsed = methodObject.parse(methodInstance);
  expect(parsed.method("length=8")).toBe(11); // 8 length + 3 property
});

test("async method parsing", async () => {
  const methodObject = z.object({
    property: z.number(),
    method: z.function().input([z.string()]).output(z.promise(z.number())),
  });
  const methodInstance = {
    property: 3,
    method: async function (s: string) {
      return s.length + this.property;
    },
  };
  const parsed = methodObject.parse(methodInstance);
  expect(await parsed.method("length=8")).toBe(11); // 8 length + 3 property
});

test("args method", () => {
  const t1 = z.function();
  type t1 = (typeof t1)["_input"];
  expectTypeOf<t1>().toEqualTypeOf<(...args_1: never[]) => unknown>();
  t1._input;

  const t2args = z.tuple([z.string()], z.unknown());

  const t2 = t1.input(t2args);
  type t2 = (typeof t2)["_input"];
  expectTypeOf<t2>().toEqualTypeOf<(arg: string, ...args_1: unknown[]) => unknown>();

  const t3 = t2.output(z.boolean());
  type t3 = (typeof t3)["_input"];
  expectTypeOf<t3>().toEqualTypeOf<(arg: string, ...args_1: unknown[]) => boolean>();
});

// test("custom args", () => {
//   const fn = z.function().implement((_a: string, _b: number) => {
//     return new Date();
//   });

//   expectTypeOf(fn).toEqualTypeOf<(a: string, b: number) => Date>();
// });

const args2 = z.tuple([
  z.object({
    f1: z.number(),
    f2: z.string().nullable(),
    f3: z.array(z.boolean().optional()).optional(),
  }),
]);
const returns2 = z.union([z.string(), z.number()]);

const func2 = z.function({
  input: args2,
  output: returns2,
});

test("function inference 2", () => {
  type func2 = (typeof func2)["_input"];

  expectTypeOf<func2>().toEqualTypeOf<
    (arg: {
      f3?: (boolean | undefined)[] | undefined;
      f1: number;
      f2: string | null;
    }) => string | number
  >();
});

test("valid function run", () => {
  const validFunc2Instance = func2.implement((_x) => {
    _x.f2;
    _x.f3![0];
    return "adf" as any;
  });

  validFunc2Instance({
    f1: 21,
    f2: "asdf",
    f3: [true, false],
  });
});

const args3 = [
  z.object({
    f1: z.number(),
    f2: z.string().nullable(),
    f3: z.array(z.boolean().optional()).optional(),
  }),
] as const;
const returns3 = z.union([z.string(), z.number()]);

const func3 = z.function({
  input: args3,
  output: returns3,
});

test("function inference 3", () => {
  type func3 = (typeof func3)["_input"];

  expectTypeOf<func3>().toEqualTypeOf<
    (arg: {
      f3?: (boolean | undefined)[] | undefined;
      f1: number;
      f2: string | null;
    }) => string | number
  >();
});

test("valid function run", () => {
  const validFunc3Instance = func3.implement((_x) => {
    _x.f2;
    _x.f3![0];
    return "adf" as any;
  });

  validFunc3Instance({
    f1: 21,
    f2: "asdf",
    f3: [true, false],
  });
});

test("input validation error", () => {
  const schema = z.function({
    input: z.tuple([z.string()]),
    output: z.void(),
  });
  const fn = schema.implement(() => 1234 as any);

  // @ts-expect-error
  const checker = () => fn();

  try {
    checker();
  } catch (e: any) {
    expect(e.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "too_small",
          "inclusive": true,
          "message": "Too small: expected array to have >=1 items",
          "minimum": 1,
          "origin": "array",
          "path": [],
        },
      ]
    `);
  }
});

test("array inputs", () => {
  const a = z.function({
    input: [
      z.object({
        name: z.string(),
        age: z.number().int(),
      }),
    ],
    output: z.string(),
  });

  a.implement((args) => {
    return `${args.age}`;
  });

  const b = z.function({
    input: [
      z.object({
        name: z.string(),
        age: z.number().int(),
      }),
    ],
  });
  b.implement((args) => {
    return `${args.age}`;
  });
});

test("output validation error", () => {
  const schema = z.function({
    input: z.tuple([]),
    output: z.string(),
  });
  const fn = schema.implement(() => 1234 as any);
  try {
    fn();
  } catch (e: any) {
    expect(e.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "invalid_type",
          "expected": "string",
          "message": "Invalid input: expected string, received number",
          "path": [],
        },
      ]
    `);
  }
});

test("function with async refinements", async () => {
  const schema = z
    .function()
    .input([z.string().refine(async (val) => val.length > 10)])
    .output(z.promise(z.number().refine(async (val) => val > 10)));

  const func = schema.implementAsync(async (val) => {
    return val.length;
  });
  const results = [];
  try {
    await func("asdfasdf");
    results.push("success");
  } catch (_) {
    results.push("fail");
  }
  try {
    await func("asdflkjasdflkjsf");
    results.push("success");
  } catch (_) {
    results.push("fail");
  }

  expect(results).toEqual(["fail", "success"]);
});

test("implement async with transforms", async () => {
  const typeGuard = (data: string): data is "1234" => data === "1234";
  const codeSchema = z.string().transform((data, ctx) => {
    if (typeGuard(data)) {
      return data;
    } else {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Invalid code",
      });
      return z.NEVER;
    }
  });
  const inputSchema = z.object({
    code: codeSchema,
  });
  const outputSchema = z.object({
    data: z.array(z.string()).default([]),
  });
  const fnImplementation = async (data: z.infer<typeof inputSchema>): Promise<z.infer<typeof outputSchema>> => {
    return {
      data: [data.code],
    };
  };
  const schema = z.function().input([inputSchema]).output(outputSchema);

  const func = schema.implementAsync(fnImplementation);
  type TheInterface = {
    myFunction: (data: z.infer<typeof inputSchema>) => Promise<z.infer<typeof outputSchema>>;
  };
  const theImplementation: TheInterface = {
    myFunction: func,
  };
  const results = [];
  try {
    await theImplementation.myFunction({
      code: "1234",
    });
    results.push("success");
  } catch (_) {
    results.push("fail");
  }
  try {
    await func({ data: "asdflkjasdflkjsf" } as any);
    results.push("success");
  } catch (_) {
    results.push("fail");
  }

  expect(results).toEqual(["success", "fail"]);
});

test("non async function with async refinements should fail", async () => {
  const func = z
    .function()
    .input([z.string().refine(async (val) => val.length > 10)])
    .output(z.number().refine(async (val) => val > 10))
    .implement((val) => {
      return val.length;
    });

  const results = [];
  try {
    await func("asdasdfasdffasdf");
    results.push("success");
  } catch (_) {
    results.push("fail");
  }

  expect(results).toEqual(["fail"]);
});

test("extra parameters with rest", () => {
  const maxLength5 = z
    .function()
    .input([z.string()], z.unknown())
    .output(z.boolean())
    .implement((str, _arg, _qewr) => {
      return str.length <= 5;
    });

  const filteredList = ["apple", "orange", "pear", "banana", "strawberry"].filter(maxLength5);
  expect(filteredList.length).toEqual(2);
});
