import { describe, expect, it } from "vitest";
import { TRPCError } from "@trpc/server";
import { t } from "@/trpc/init";
import { defineErrorMiddleware } from "@/trpc/define-error-middleware";

class FooNotFoundError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "FooNotFoundError";
  }
}

class FooBadRequestError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "FooBadRequestError";
  }
}

const errorRouter = t.router({
  notFound: t.procedure
    .use(
      defineErrorMiddleware([
        [FooNotFoundError, "NOT_FOUND"],
        [FooBadRequestError, "BAD_REQUEST"],
      ]),
    )
    .query(() => {
      throw new FooNotFoundError("nope");
    }),
  badRequest: t.procedure
    .use(
      defineErrorMiddleware([
        [FooNotFoundError, "NOT_FOUND"],
        [FooBadRequestError, "BAD_REQUEST"],
      ]),
    )
    .query(() => {
      throw new FooBadRequestError("oops");
    }),
  unmapped: t.procedure
    .use(defineErrorMiddleware([[FooNotFoundError, "NOT_FOUND"]]))
    .query(() => {
      throw new Error("plain");
    }),
});

describe("defineErrorMiddleware", () => {
  it("translates a mapped error to TRPCError with the configured code", async () => {
    const caller = errorRouter.createCaller({});
    await expect(caller.notFound()).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("translates a different mapped error to a different code", async () => {
    const caller = errorRouter.createCaller({});
    await expect(caller.badRequest()).rejects.toMatchObject({
      code: "BAD_REQUEST",
    });
  });

  it("does not translate unmapped errors but preserves the original via cause", async () => {
    const caller = errorRouter.createCaller({});
    try {
      await caller.unmapped();
      throw new Error("expected throw");
    } catch (e) {
      expect(e).toBeInstanceOf(TRPCError);
      const trpcErr = e as TRPCError;
      expect(trpcErr.code).toBe("INTERNAL_SERVER_ERROR");
      expect(trpcErr.cause).toBeInstanceOf(Error);
      expect(trpcErr.cause).not.toBeInstanceOf(TRPCError);
      expect((trpcErr.cause as Error).message).toBe("plain");
    }
  });

  it("preserves the original error as the cause", async () => {
    const caller = errorRouter.createCaller({});
    try {
      await caller.notFound();
      throw new Error("expected throw");
    } catch (e) {
      expect(e).toBeInstanceOf(TRPCError);
      const trpcErr = e as TRPCError;
      expect(trpcErr.cause).toBeInstanceOf(FooNotFoundError);
      expect((trpcErr.cause as Error).message).toBe("nope");
    }
  });
});
