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

z.config(en());

const isoDateCodec = z.codec(
  z.iso.datetime(), // Input: ISO string (validates to string)
  z.date(), // Output: Date object
  {
    decode: (isoString) => new Date(isoString), // Forward: ISO string → Date
    encode: (date) => date.toISOString(), // Backward: Date → ISO string
  }
);

test("instanceof", () => {
  expect(isoDateCodec instanceof z.ZodMiniCodec).toBe(true);
  expect(isoDateCodec instanceof z.ZodMiniPipe).toBe(true);
  expect(isoDateCodec instanceof z.ZodMiniType).toBe(true);
  expect(isoDateCodec instanceof z.core.$ZodCodec).toBe(true);
  expect(isoDateCodec instanceof z.core.$ZodPipe).toBe(true);
  expect(isoDateCodec instanceof z.core.$ZodType).toBe(true);

  expectTypeOf(isoDateCodec.def).toEqualTypeOf<z.core.$ZodCodecDef<z.ZodMiniISODateTime, z.ZodMiniDate<Date>>>();
});

test("codec basic functionality", () => {
  // ISO string -> Date codec using z.iso.datetime() for input validation

  const testIsoString = "2024-01-15T10:30:00.000Z";
  const testDate = new Date("2024-01-15T10:30:00.000Z");

  // Forward decoding (ISO string -> Date)
  const decodedResult = z.decode(isoDateCodec, testIsoString);
  expect(decodedResult).toBeInstanceOf(Date);
  expect(decodedResult.toISOString()).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);

  // Backward encoding (Date -> ISO string)
  const encodedResult = z.encode(isoDateCodec, testDate);
  expect(typeof encodedResult).toBe("string");
  expect(encodedResult).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);
});

test("codec round trip", () => {
  const isoDateCodec = z.codec(z.iso.datetime(), z.date(), {
    decode: (isoString) => new Date(isoString),
    encode: (date) => date.toISOString(),
  });

  const original = "2024-12-25T15:45:30.123Z";
  const toDate = z.decode(isoDateCodec, original);
  const backToString = z.encode(isoDateCodec, toDate);

  expect(backToString).toMatchInlineSnapshot(`"2024-12-25T15:45:30.123Z"`);
  expect(toDate).toBeInstanceOf(Date);
  expect(toDate.getTime()).toMatchInlineSnapshot(`1735141530123`);
});

test("codec with refinement", () => {
  const isoDateCodec = z
    .codec(z.iso.datetime(), z.date(), {
      decode: (isoString) => new Date(isoString),
      encode: (date) => date.toISOString(),
    })
    .check(z.refine((val) => val.getFullYear() === 2024, { error: "Year must be 2024" }));

  // Valid 2024 date
  const validDate = z.decode(isoDateCodec, "2024-01-15T10:30:00.000Z");
  expect(validDate.getFullYear()).toMatchInlineSnapshot(`2024`);
  expect(validDate.getTime()).toMatchInlineSnapshot(`1705314600000`);

  // Invalid year should fail safely
  const invalidYearResult = z.safeDecode(isoDateCodec, "2023-01-15T10:30:00.000Z");
  expect(invalidYearResult.success).toBe(false);
  if (!invalidYearResult.success) {
    expect(invalidYearResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "custom",
          "message": "Year must be 2024",
          "path": [],
        },
      ]
    `);
  }
});

test("safe codec operations", () => {
  const isoDateCodec = z.codec(z.iso.datetime(), z.date(), {
    decode: (isoString) => new Date(isoString),
    encode: (date) => date.toISOString(),
  });

  // Safe decode with invalid input
  const safeDecodeResult = z.safeDecode(isoDateCodec, "invalid-date");
  expect(safeDecodeResult.success).toBe(false);
  if (!safeDecodeResult.success) {
    expect(safeDecodeResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "invalid_format",
          "format": "datetime",
          "message": "Invalid ISO datetime",
          "origin": "string",
          "path": [],
          "pattern": "/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/",
        },
      ]
    `);
  }

  // Safe decode with valid input
  const safeDecodeValid = z.safeDecode(isoDateCodec, "2024-01-15T10:30:00.000Z");
  expect(safeDecodeValid.success).toBe(true);
  if (safeDecodeValid.success) {
    expect(safeDecodeValid.data).toBeInstanceOf(Date);
    expect(safeDecodeValid.data.getTime()).toMatchInlineSnapshot(`1705314600000`);
  }

  // Safe encode with valid input
  const safeEncodeResult = z.safeEncode(isoDateCodec, new Date("2024-01-01"));
  expect(safeEncodeResult.success).toBe(true);
  if (safeEncodeResult.success) {
    expect(safeEncodeResult.data).toMatchInlineSnapshot(`"2024-01-01T00:00:00.000Z"`);
  }
});

test("codec with different types", () => {
  // String -> Number codec
  const stringNumberCodec = z.codec(z.string(), z.number(), {
    decode: (str) => Number.parseFloat(str),
    encode: (num) => num.toString(),
  });

  const decodedNumber = z.decode(stringNumberCodec, "42.5");
  expect(decodedNumber).toMatchInlineSnapshot(`42.5`);
  expect(typeof decodedNumber).toBe("number");

  const encodedString = z.encode(stringNumberCodec, 42.5);
  expect(encodedString).toMatchInlineSnapshot(`"42.5"`);
  expect(typeof encodedString).toBe("string");
});

test("async codec operations", async () => {
  const isoDateCodec = z.codec(z.iso.datetime(), z.date(), {
    decode: (isoString) => new Date(isoString),
    encode: (date) => date.toISOString(),
  });

  // Async decode
  const decodedResult = await z.decodeAsync(isoDateCodec, "2024-01-15T10:30:00.000Z");
  expect(decodedResult).toBeInstanceOf(Date);
  expect(decodedResult.getTime()).toMatchInlineSnapshot(`1705314600000`);

  // Async encode
  const encodedResult = await z.encodeAsync(isoDateCodec, new Date("2024-01-15T10:30:00.000Z"));
  expect(typeof encodedResult).toBe("string");
  expect(encodedResult).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);

  // Safe async operations
  const safeDecodeResult = await z.safeDecodeAsync(isoDateCodec, "2024-01-15T10:30:00.000Z");
  expect(safeDecodeResult.success).toBe(true);
  if (safeDecodeResult.success) {
    expect(safeDecodeResult.data.getTime()).toMatchInlineSnapshot(`1705314600000`);
  }

  const safeEncodeResult = await z.safeEncodeAsync(isoDateCodec, new Date("2024-01-15T10:30:00.000Z"));
  expect(safeEncodeResult.success).toBe(true);
  if (safeEncodeResult.success) {
    expect(safeEncodeResult.data).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);
  }
});

test("codec type inference", () => {
  const codec = z.codec(z.string(), z.number(), {
    decode: (str) => Number.parseInt(str),
    encode: (num) => num.toString(),
  });

  // These should compile without type errors
  const decoded: number = z.decode(codec, "123");
  const encoded: string = z.encode(codec, 123);

  expect(decoded).toMatchInlineSnapshot(`123`);
  expect(encoded).toMatchInlineSnapshot(`"123"`);
});

test("nested codec with object containing codec property", () => {
  // Nested schema: object containing a codec as one of its properties, with refinements at all levels
  const waypointSchema = z
    .object({
      name: z.string().check(z.minLength(1, "Waypoint name required")),
      difficulty: z.enum(["easy", "medium", "hard"]),
      coordinate: z
        .codec(
          z
            .string()
            .check(z.regex(/^-?\d+,-?\d+$/, "Must be 'x,y' format")), // Input: coordinate string
          z
            .object({ x: z.number(), y: z.number() })
            .check(z.refine((coord) => coord.x >= 0 && coord.y >= 0, { error: "Coordinates must be non-negative" })), // Output: coordinate object
          {
            decode: (coordString: string) => {
              const [x, y] = coordString.split(",").map(Number);
              return { x, y };
            },
            encode: (coord: { x: number; y: number }) => `${coord.x},${coord.y}`,
          }
        )
        .check(z.refine((coord) => coord.x <= 1000 && coord.y <= 1000, { error: "Coordinates must be within bounds" })),
    })
    .check(
      z.refine((waypoint) => waypoint.difficulty !== "hard" || waypoint.coordinate.x >= 100, {
        error: "Hard waypoints must be at least 100 units from origin",
      })
    );

  // Test data
  const inputWaypoint = {
    name: "Summit Point",
    difficulty: "medium" as const,
    coordinate: "150,200",
  };

  // Forward decoding (object with string coordinate -> object with coordinate object)
  const decodedWaypoint = z.decode(waypointSchema, inputWaypoint);
  expect(decodedWaypoint).toMatchInlineSnapshot(`
    {
      "coordinate": {
        "x": 150,
        "y": 200,
      },
      "difficulty": "medium",
      "name": "Summit Point",
    }
  `);

  // Backward encoding (object with coordinate object -> object with string coordinate)
  const encodedWaypoint = z.encode(waypointSchema, decodedWaypoint);
  expect(encodedWaypoint).toMatchInlineSnapshot(`
    {
      "coordinate": "150,200",
      "difficulty": "medium",
      "name": "Summit Point",
    }
  `);

  // Test refinements at all levels
  // String validation (empty waypoint name)
  const emptyNameResult = z.safeDecode(waypointSchema, {
    name: "",
    difficulty: "easy",
    coordinate: "10,20",
  });
  expect(emptyNameResult.success).toBe(false);
  if (!emptyNameResult.success) {
    expect(emptyNameResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "too_small",
          "inclusive": true,
          "message": "Waypoint name required",
          "minimum": 1,
          "origin": "string",
          "path": [
            "name",
          ],
        },
      ]
    `);
  }

  // Enum validation (invalid difficulty)
  const invalidDifficultyResult = z.safeDecode(waypointSchema, {
    name: "Test Point",
    difficulty: "impossible" as any,
    coordinate: "10,20",
  });
  expect(invalidDifficultyResult.success).toBe(false);
  if (!invalidDifficultyResult.success) {
    expect(invalidDifficultyResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "invalid_value",
          "message": "Invalid option: expected one of "easy"|"medium"|"hard"",
          "path": [
            "difficulty",
          ],
          "values": [
            "easy",
            "medium",
            "hard",
          ],
        },
      ]
    `);
  }

  // Codec string format validation (invalid coordinate format)
  const invalidFormatResult = z.safeDecode(waypointSchema, {
    name: "Test Point",
    difficulty: "easy",
    coordinate: "invalid",
  });
  expect(invalidFormatResult.success).toBe(false);
  if (!invalidFormatResult.success) {
    expect(invalidFormatResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "invalid_format",
          "format": "regex",
          "message": "Must be 'x,y' format",
          "origin": "string",
          "path": [
            "coordinate",
          ],
          "pattern": "/^-?\\d+,-?\\d+$/",
        },
      ]
    `);
  }

  // Codec object refinement (negative coordinates)
  const negativeCoordResult = z.safeDecode(waypointSchema, {
    name: "Test Point",
    difficulty: "easy",
    coordinate: "-5,10",
  });
  expect(negativeCoordResult.success).toBe(false);
  if (!negativeCoordResult.success) {
    expect(negativeCoordResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "custom",
          "message": "Coordinates must be non-negative",
          "path": [
            "coordinate",
          ],
        },
      ]
    `);
  }

  // Codec-level refinement (coordinates out of bounds)
  const outOfBoundsResult = z.safeDecode(waypointSchema, {
    name: "Test Point",
    difficulty: "easy",
    coordinate: "1500,2000",
  });
  expect(outOfBoundsResult.success).toBe(false);
  if (!outOfBoundsResult.success) {
    expect(outOfBoundsResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "custom",
          "message": "Coordinates must be within bounds",
          "path": [
            "coordinate",
          ],
        },
      ]
    `);
  }

  // Object-level refinement (hard waypoint too close to origin)
  const hardWaypointResult = z.safeDecode(waypointSchema, {
    name: "Expert Point",
    difficulty: "hard",
    coordinate: "50,60", // x < 100, but hard waypoints need x >= 100
  });
  expect(hardWaypointResult.success).toBe(false);
  if (!hardWaypointResult.success) {
    expect(hardWaypointResult.error.issues).toMatchInlineSnapshot(`
      [
        {
          "code": "custom",
          "message": "Hard waypoints must be at least 100 units from origin",
          "path": [],
        },
      ]
    `);
  }

  // Round trip test
  const roundTripResult = z.encode(waypointSchema, z.decode(waypointSchema, inputWaypoint));
  expect(roundTripResult).toMatchInlineSnapshot(`
    {
      "coordinate": "150,200",
      "difficulty": "medium",
      "name": "Summit Point",
    }
  `);
});

test("mutating refinements", () => {
  const A = z.codec(z.string(), z.string().check(z.trim()), {
    decode: (val) => val,
    encode: (val) => val,
  });

  expect(z.decode(A, " asdf ")).toMatchInlineSnapshot(`"asdf"`);
  expect(z.encode(A, " asdf ")).toMatchInlineSnapshot(`"asdf"`);
});

test("codec type enforcement - correct encode/decode signatures", () => {
  // Test that codec functions have correct type signatures
  const stringToNumberCodec = z.codec(z.string(), z.number(), {
    decode: (value: string) => Number(value), // core.output<A> -> core.input<B>
    encode: (value: number) => String(value), // core.input<B> -> core.output<A>
  });

  // These should compile without errors - correct types (async support)
  expectTypeOf<(value: string, payload: z.core.ParsePayload<string>) => z.core.util.MaybeAsync<number>>(
    stringToNumberCodec.def.transform
  ).toBeFunction();
  expectTypeOf<(value: number, payload: z.core.ParsePayload<number>) => z.core.util.MaybeAsync<string>>(
    stringToNumberCodec.def.reverseTransform
  ).toBeFunction();

  // Test that decode parameter type is core.output<A> (string)
  const validDecode = (value: string) => Number(value);
  expectTypeOf(validDecode).toMatchTypeOf<(value: string) => number>();

  // Test that encode parameter type is core.input<B> (number)
  const validEncode = (value: number) => String(value);
  expectTypeOf(validEncode).toMatchTypeOf<(value: number) => string>();

  z.codec(z.string(), z.number(), {
    // @ts-expect-error - decode should NOT accept core.input<A> as parameter
    decode: (value: never, _payload) => Number(value), // Wrong: should be string, not unknown
    encode: (value: number, _payload) => String(value),
  });

  z.codec(z.string(), z.number(), {
    decode: (value: string) => Number(value),
    // @ts-expect-error - encode should NOT accept core.output<B> as parameter
    encode: (value: never) => String(value), // Wrong: should be number, not unknown
  });

  z.codec(z.string(), z.number(), {
    // @ts-expect-error - decode return type should be core.input<B>
    decode: (value: string) => String(value), // Wrong: should return number, not string
    encode: (value: number) => String(value),
  });

  z.codec(z.string(), z.number(), {
    decode: (value: string) => Number(value),
    // @ts-expect-error - encode return type should be core.output<A>
    encode: (value: number) => Number(value), // Wrong: should return string, not number
  });
});

test("async codec functionality", async () => {
  // Test that async encode/decode functions work properly
  const asyncCodec = z.codec(z.string(), z.number(), {
    decode: async (str) => {
      await new Promise((resolve) => setTimeout(resolve, 1)); // Simulate async work
      return Number.parseFloat(str);
    },
    encode: async (num) => {
      await new Promise((resolve) => setTimeout(resolve, 1)); // Simulate async work
      return num.toString();
    },
  });

  // Test async decode/encode
  const decoded = await z.decodeAsync(asyncCodec, "42.5");
  expect(decoded).toBe(42.5);

  const encoded = await z.encodeAsync(asyncCodec, 42.5);
  expect(encoded).toBe("42.5");

  // Test that both sync and async work
  const mixedCodec = z.codec(z.string(), z.number(), {
    decode: async (str) => Number.parseFloat(str),
    encode: (num) => num.toString(), // sync encode
  });

  const mixedResult = await z.decodeAsync(mixedCodec, "123");
  expect(mixedResult).toBe(123);
});

test("codec type enforcement - complex types", () => {
  type User = { id: number; name: string };
  type UserInput = { id: string; name: string };

  const userCodec = z.codec(
    z.object({ id: z.string(), name: z.string() }),
    z.object({ id: z.number(), name: z.string() }),
    {
      decode: (input: UserInput) => ({ id: Number(input.id), name: input.name }),
      encode: (user: User) => ({ id: String(user.id), name: user.name }),
    }
  );

  // Verify correct types are inferred (async support)
  expectTypeOf<(input: UserInput, payload: z.core.ParsePayload<UserInput>) => z.core.util.MaybeAsync<User>>(
    userCodec.def.transform
  ).toBeFunction();
  expectTypeOf<(user: User, payload: z.core.ParsePayload<User>) => z.core.util.MaybeAsync<UserInput>>(
    userCodec.def.reverseTransform
  ).toBeFunction();

  z.codec(
    z.object({
      id: z.string(),
      name: z.string(),
    }),
    z.object({ id: z.number(), name: z.string() }),
    {
      // @ts-expect-error - decode parameter should be UserInput, not User
      decode: (input: User) => ({ id: Number(input.id), name: input.name }), // Wrong type
      encode: (user: User) => ({ id: String(user.id), name: user.name }),
    }
  );

  z.codec(
    z.object({
      id: z.string(),
      name: z.string(),
    }),
    z.object({ id: z.number(), name: z.string() }),
    {
      decode: (input: UserInput) => ({ id: Number(input.id), name: input.name }),
      // @ts-expect-error - encode parameter should be User, not UserInput
      encode: (user: UserInput) => ({ id: String(user.id), name: user.name }), // Wrong type
    }
  );
});

test("invertCodec", () => {
  const inverted = z.invertCodec(isoDateCodec);

  type InvIn = z.input<typeof inverted>;
  type InvOut = z.output<typeof inverted>;
  expectTypeOf<InvIn>().toEqualTypeOf<Date>();
  expectTypeOf<InvOut>().toEqualTypeOf<string>();

  const testDate = new Date("2024-01-15T10:30:00.000Z");
  expect(z.decode(inverted, testDate)).toBe("2024-01-15T10:30:00.000Z");

  const encoded = z.encode(inverted, "2024-01-15T10:30:00.000Z");
  expect(encoded).toBeInstanceOf(Date);
  expect(encoded.toISOString()).toBe("2024-01-15T10:30:00.000Z");

  const doubleInverted = z.invertCodec(z.invertCodec(isoDateCodec));
  expect(z.decode(doubleInverted, "2024-01-15T10:30:00.000Z")).toBeInstanceOf(Date);
});
