import * as Hapi from "@hapi/hapi";
import HapiBasic from "@hapi/basic";
import NextAuthPlugin from ".";

describe("plugin option validation", () => {
  let server;
  beforeEach(async () => {
    server = new Hapi.Server();
  });

  it("should throw when options contain no next auth adapter", async () => {
    expect(server.register(NextAuthPlugin)).rejects.toThrow();
  });
});

describe("plugin runtime", () => {
  let server;
  const user = { id: "abc", email: "abc@abc.abc" };
  const session = {
    id: "zyx",
    userId: "abc",
    expires: Date.now(),
    sessionToken: "foo",
    accessToken: "bar",
  };

  const start = async (mock) => {
    await server.register(HapiBasic);
    await server.register({
      plugin: NextAuthPlugin,
      options: {
        nextAuthAdapterFactory: () => mock,
      },
    });

    await server.start();
    return server;
  };

  beforeEach(async () => {
    server = new Hapi.Server({ port: 0 });
  });

  afterEach(async () => {
    await server.stop();
  });

  it("createUser", async () => {
    const createUser = jest.fn(() => user);
    const profile = { email: "abc@abc.abc" };

    await start({ createUser });

    const { statusCode, result } = await server.inject({
      method: "post",
      url: "/api/nextauth/createUser",
      payload: profile,
    });
    expect(statusCode).toBe(200);
    expect(createUser).toHaveBeenCalledWith(profile);
    expect(result).toStrictEqual(user);
  });

  it("createUser fails with invalid payload", async () => {
    const createUser = jest.fn(() => user);
    const profile = { name: "name" };

    await start({ createUser });

    const { statusCode } = await server.inject({
      method: "post",
      url: "/api/nextauth/createUser",
      payload: profile,
    });
    expect(statusCode).toBe(400);
  });

  it("getUser", async () => {
    const getUser = jest.fn(() => user);

    await start({ getUser });

    const { statusCode, result } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUser/abc",
    });
    expect(statusCode).toBe(200);
    expect(getUser).toHaveBeenCalledWith("abc");
    expect(result).toBe(user);
  });

  it("getUserByEmail", async () => {
    const getUserByEmail = jest.fn(() => user);

    await start({ getUserByEmail });

    const { statusCode, result } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUserByEmail/abc@abc.abc",
    });
    expect(statusCode).toBe(200);
    expect(getUserByEmail).toHaveBeenCalledWith("abc@abc.abc");
    expect(result).toBe(user);
  });

  it("getUserByEmail fails with invalid email", async () => {
    const getUserByEmail = jest.fn(() => user);

    await start({ getUserByEmail });

    const { statusCode } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUserByEmail/notanemail@foo",
    });
    expect(statusCode).toBe(400);
  });

  it("getUserByProviderAccountId", async () => {
    const getUserByProviderAccountId = jest.fn(() => user);

    await start({ getUserByProviderAccountId });

    const { statusCode, result } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUserByProviderAccountId/foo/bar",
    });
    expect(statusCode).toBe(200);
    expect(getUserByProviderAccountId).toHaveBeenCalledWith("foo", "bar");
    expect(result).toBe(user);
  });

  it("updateUser", async () => {
    const updateUser = jest.fn(() => user);

    await start({ updateUser });

    const { statusCode, result } = await server.inject({
      method: "put",
      url: "/api/nextauth/updateUser",
      payload: user,
    });
    expect(statusCode).toBe(200);
    expect(updateUser).toHaveBeenCalledWith(user);
    expect(result).toStrictEqual(user);
  });

  it("updateUser fails with invalid payload", async () => {
    const updateUser = jest.fn(() => user);

    await start({ updateUser });

    const { statusCode } = await server.inject({
      method: "put",
      url: "/api/nextauth/updateUser",
      payload: {
        // id not specified
        email: "abc@abc.abc",
      },
    });
    expect(statusCode).toBe(400);
  });

  it("linkUser", async () => {
    const linkAccount = jest.fn(() => undefined);
    const args = {
      userId: "abc",
      providerId: "foo",
      providerType: "something",
      providerAccountId: "bar",
      refreshToken: "refreshToken",
      accessToken: "accessToken",
      accessTokenExpires: 10,
    };

    await start({ linkAccount });

    const { statusCode } = await server.inject({
      method: "put",
      url: "/api/nextauth/linkAccount",
      payload: args,
    });
    expect(statusCode).toBe(204);
    expect(linkAccount.mock.calls.length).toBe(1);
  });

  it("createSession", async () => {
    const createSession = jest.fn(() => session);

    await start({ createSession });

    const { statusCode, result } = await server.inject({
      method: "post",
      url: "/api/nextauth/createSession",
      payload: user,
    });
    expect(statusCode).toBe(200);
    expect(createSession).toHaveBeenCalledWith(user);
    expect(result).toStrictEqual(session);
  });

  it("getSession", async () => {
    const getSession = jest.fn(() => session);

    await start({ getSession });

    const { statusCode, result } = await server.inject({
      method: "get",
      url: "/api/nextauth/getSession/xyz",
    });
    expect(statusCode).toBe(200);
    expect(getSession).toHaveBeenCalledWith("xyz");
    expect(result).toBe(session);
  });

  it("updateSession", async () => {
    const updateSession = jest.fn(() => session);

    await start({ updateSession });

    const { statusCode, result } = await server.inject({
      method: "put",
      url: "/api/nextauth/updateSession",
      payload: session,
    });
    expect(statusCode).toBe(200);
    expect(updateSession).toHaveBeenCalledWith(
      {
        ...session,
        expires: new Date(session.expires),
      },
      false
    );
    expect(result).toStrictEqual(session);
  });

  it("updateSession - force", async () => {
    const updateSession = jest.fn(() => session);

    await start({ updateSession });

    const { statusCode, result } = await server.inject({
      method: "put",
      url: "/api/nextauth/updateSession?force=true",
      payload: session,
    });
    expect(statusCode).toBe(200);
    expect(updateSession).toHaveBeenCalledWith(
      {
        ...session,
        expires: new Date(session.expires),
      },
      true
    );
    expect(result).toStrictEqual(session);
  });

  it("deleteSession", async () => {
    const deleteSession = jest.fn(() => undefined);

    await start({ deleteSession });

    const { statusCode } = await server.inject({
      method: "delete",
      url: "/api/nextauth/deleteSession/xyz",
    });
    expect(statusCode).toBe(204);
    expect(deleteSession).toHaveBeenCalledWith("xyz");
  });
});

describe("plugin authentication", () => {
  const user = { id: "abc", email: "abc@abc.abc" };
  const sharedSecret = "secret";
  let server;

  const start = async (mock) => {
    await server.register(HapiBasic);
    await server.register({
      plugin: NextAuthPlugin,
      options: {
        nextAuthAdapterFactory: () => mock,
        sharedSecret,
      },
    });

    await server.start();
    return server;
  };

  const basicHeader = (username, password) =>
    "Basic " +
    Buffer.from(username + ":" + password, "utf8").toString("base64");

  beforeEach(async () => {
    server = new Hapi.Server({ port: 0 });
  });

  afterEach(async () => {
    await server.stop();
  });

  it("getUser - no auth header fails", async () => {
    const getUser = jest.fn(() => user);

    await start({ getUser });

    const { statusCode } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUser/abc",
    });
    expect(statusCode).toBe(401);
    expect(getUser).toHaveBeenCalledTimes(0);
  });

  it("getUser - with auth header suceeds", async () => {
    const getUser = jest.fn(() => user);

    await start({ getUser });

    const { statusCode, result } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUser/abc",
      headers: {
        authorization: basicHeader(sharedSecret, ""),
      },
    });
    expect(statusCode).toBe(200);
    expect(getUser).toHaveBeenCalledWith("abc");
    expect(result).toBe(user);
  });

  it("getUser - with invalid credentials fails", async () => {
    const getUser = jest.fn(() => user);

    await start({ getUser });

    const { statusCode } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUser/abc",
      headers: {
        authorization: basicHeader("wrong secret", ""),
      },
    });
    expect(statusCode).toBe(401);
    expect(getUser).toHaveBeenCalledTimes(0);
  });

  it("getUser - with secret in password field fails", async () => {
    const getUser = jest.fn(() => user);

    await start({ getUser });

    const { statusCode } = await server.inject({
      method: "get",
      url: "/api/nextauth/getUser/abc",
      headers: {
        authorization: basicHeader("", "sharedSecret"),
      },
    });
    expect(statusCode).toBe(401);
    expect(getUser).toHaveBeenCalledTimes(0);
  });
});
