/**
 * Copyright 2020 Inrupt Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
 * Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import { jest, describe, it, expect } from "@jest/globals";
import { addMockAcrTo, mockAcrFor } from "../acp/mock";
import { mockSolidDatasetFrom } from "../resource/mock";
import {
  Access,
  getAgentAccess,
  getAgentAccessAll,
  getGroupAccess,
  getGroupAccessAll,
  getPublicAccess,
  setAgentAccess,
  setGroupAccess,
  setPublicAccess,
  getAccessFor as reexport_getAccessFor,
  getAccessForAll as reexport_getAccessForAll,
  setAccessFor as reexport_setAccessFor,
} from "./universal";
import * as acpLowLevel from "../acp/acp";
import * as acpModule from "./acp";
import * as wacModule from "./wac";
import { addMockResourceAclTo } from "../acl/mock";

describe("getAgentAccess", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_getAgentAccess");

    await getAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me"
    );

    expect(acpModule.internal_getAgentAccess).toHaveBeenCalledTimes(1);
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetAgentAccess = jest.spyOn(wacModule, "getAgentAccess");
    wacGetAgentAccess.mockResolvedValue(null);

    await getAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me"
    );

    expect(wacGetAgentAccess).toHaveBeenCalledTimes(1);
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    getAgentAccess(
      "https://some.pod/resource",
      "https://arbitrary.pod/profile#me",
      options
    );

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetAgentAccess = jest.spyOn(wacModule, "getAgentAccess");
    wacGetAgentAccess.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await getAgentAccess(
      "https://arbitrary.pod/resource",
      "https://some.pod/profile#me",
      options
    );

    expect(wacGetAgentAccess).toHaveBeenCalledWith(
      expect.anything(),
      "https://some.pod/profile#me",
      options
    );
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await getAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me"
    );

    expect(access).toBeNull();
  });
});

describe("setAgentAccess", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_setAgentAccess");

    await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true }
    );

    expect(acpModule.internal_setAgentAccess).toHaveBeenCalledTimes(1);
  });

  it("returns the new effective access for ACP-controlled Resources", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest
      .spyOn(acpModule, "internal_setAgentAccess")
      .mockReturnValueOnce(mockedResourceWithAcr);
    jest
      .spyOn(acpLowLevel, "saveAcrFor")
      .mockResolvedValueOnce(undefined as any);
    jest.spyOn(acpModule, "internal_getAgentAccess").mockReturnValueOnce({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });

    const returnedAccess = await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true }
    );

    expect(returnedAccess).toStrictEqual({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetAgentAccess = jest.spyOn(wacModule, "setAgentResourceAccess");
    wacSetAgentAccess.mockResolvedValue(null);

    await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true }
    );

    expect(wacSetAgentAccess).toHaveBeenCalledTimes(1);
  });

  it("returns the new effective access for WAC-controlled Resources", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetAgentAccess = jest.spyOn(wacModule, "setAgentResourceAccess");
    wacSetAgentAccess.mockResolvedValue(null);
    jest.spyOn(wacModule, "getAgentAccess").mockResolvedValueOnce({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });

    const returnedAccess = await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true }
    );

    expect(returnedAccess).toStrictEqual({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });
  });

  it("throws an error when setting differing controlRead and controlWrite for a WAC-controlled Resource", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom("https://some.pod/resource");
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetAgentAccess = jest.spyOn(wacModule, "setAgentResourceAccess");
    wacSetAgentAccess.mockResolvedValue(null);

    await expect(
      setAgentAccess(
        "https://some.pod/resource",
        "https://arbitrary.pod/profile#me",
        { controlRead: true, controlWrite: undefined }
      )
    ).rejects.toThrow(
      "When setting access for a Resource in a Pod implementing Web Access Control (i.e. [https://some.pod/resource]), `controlRead` and `controlWrite` should have the same value."
    );
  });

  it("returns null when the ACP module could not update the ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_setAgentAccess").mockReturnValueOnce(null);

    const returnValue = await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true }
    );

    expect(returnValue).toBeNull();
  });

  it("returns null when the WAC module could not update the ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetAgentAccess = jest.spyOn(wacModule, "setAgentResourceAccess");
    wacSetAgentAccess.mockResolvedValue(null);

    const returnedAccess = await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true }
    );

    expect(returnedAccess).toBeNull();
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    setAgentAccess(
      "https://some.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true },
      options
    );

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetAgentAccess = jest.spyOn(wacModule, "setAgentResourceAccess");
    wacSetAgentAccess.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://some.pod/profile#me",
      { read: true },
      options
    );

    expect(wacSetAgentAccess).toHaveBeenCalledWith(
      expect.anything(),
      "https://some.pod/profile#me",
      { read: true },
      options
    );
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await setAgentAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/profile#me",
      { read: true }
    );

    expect(access).toBeNull();
  });
});

describe("getGroupAccess", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_getGroupAccess");

    await getGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group"
    );

    expect(acpModule.internal_getGroupAccess).toHaveBeenCalledTimes(1);
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetGroupAccess = jest.spyOn(wacModule, "getGroupAccess");
    wacGetGroupAccess.mockResolvedValue(null);

    await getGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group"
    );

    expect(wacGetGroupAccess).toHaveBeenCalledTimes(1);
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    getGroupAccess(
      "https://some.pod/resource",
      "https://arbitrary.pod/groups#group",
      options
    );

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetGroupAccess = jest.spyOn(wacModule, "getGroupAccess");
    wacGetGroupAccess.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await getGroupAccess(
      "https://arbitrary.pod/resource",
      "https://some.pod/groups#group",
      options
    );

    expect(wacGetGroupAccess).toHaveBeenCalledWith(
      expect.anything(),
      "https://some.pod/groups#group",
      options
    );
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await getGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group"
    );

    expect(access).toBeNull();
  });
});

describe("setGroupAccess", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_setGroupAccess");

    await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true }
    );

    expect(acpModule.internal_setGroupAccess).toHaveBeenCalledTimes(1);
  });

  it("returns the new effective access for ACP-controlled Resources", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest
      .spyOn(acpModule, "internal_setGroupAccess")
      .mockReturnValueOnce(mockedResourceWithAcr);
    jest
      .spyOn(acpLowLevel, "saveAcrFor")
      .mockResolvedValueOnce(undefined as any);
    jest.spyOn(acpModule, "internal_getGroupAccess").mockReturnValueOnce({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });

    const returnedAccess = await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true }
    );

    expect(returnedAccess).toStrictEqual({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetGroupAccess = jest.spyOn(wacModule, "setGroupResourceAccess");
    wacSetGroupAccess.mockResolvedValue(null);

    await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true }
    );

    expect(wacSetGroupAccess).toHaveBeenCalledTimes(1);
  });

  it("returns the new effective access for WAC-controlled Resources", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetGroupAccess = jest.spyOn(wacModule, "setGroupResourceAccess");
    wacSetGroupAccess.mockResolvedValue(null);
    jest.spyOn(wacModule, "getGroupAccess").mockResolvedValueOnce({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });

    const returnedAccess = await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true }
    );

    expect(returnedAccess).toStrictEqual({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });
  });
  it("throws an error when setting differing controlRead and controlWrite for a WAC-controlled Resource", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom("https://some.pod/resource");
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetGroupAccess = jest.spyOn(wacModule, "setGroupResourceAccess");
    wacSetGroupAccess.mockResolvedValue(null);

    await expect(
      setGroupAccess(
        "https://some.pod/resource",
        "https://arbitrary.pod/groups#group",
        { controlRead: true, controlWrite: undefined }
      )
    ).rejects.toThrow(
      "When setting access for a Resource in a Pod implementing Web Access Control (i.e. [https://some.pod/resource]), `controlRead` and `controlWrite` should have the same value."
    );
  });

  it("returns null when the ACP module could not update the ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_setGroupAccess").mockReturnValueOnce(null);

    const returnValue = await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true }
    );

    expect(returnValue).toBeNull();
  });

  it("returns null when the WAC module could not update the ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetGroupAccess = jest.spyOn(wacModule, "setGroupResourceAccess");
    wacSetGroupAccess.mockResolvedValue(null);

    const returnedAccess = await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true }
    );

    expect(returnedAccess).toBeNull();
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    setGroupAccess(
      "https://some.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true },
      options
    );

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetGroupAccess = jest.spyOn(wacModule, "setGroupResourceAccess");
    wacSetGroupAccess.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://some.pod/groups#group",
      { read: true },
      options
    );

    expect(wacSetGroupAccess).toHaveBeenCalledWith(
      expect.anything(),
      "https://some.pod/groups#group",
      { read: true },
      options
    );
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await setGroupAccess(
      "https://arbitrary.pod/resource",
      "https://arbitrary.pod/groups#group",
      { read: true }
    );

    expect(access).toBeNull();
  });
});

describe("getPublicAccess", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_getPublicAccess");

    await getPublicAccess("https://arbitrary.pod/resource");

    expect(acpModule.internal_getPublicAccess).toHaveBeenCalledTimes(1);
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetPublicAccess = jest.spyOn(wacModule, "getPublicAccess");
    wacGetPublicAccess.mockResolvedValue(null);

    await getPublicAccess("https://arbitrary.pod/resource");

    expect(wacGetPublicAccess).toHaveBeenCalledTimes(1);
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    getPublicAccess("https://some.pod/resource", options);

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetPublicAccess = jest.spyOn(wacModule, "getPublicAccess");
    wacGetPublicAccess.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await getPublicAccess("https://arbitrary.pod/resource", options);

    expect(wacGetPublicAccess).toHaveBeenCalledWith(expect.anything(), options);
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await getPublicAccess("https://arbitrary.pod/resource");

    expect(access).toBeNull();
  });
});

describe("setPublicAccess", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_setPublicAccess");

    await setPublicAccess("https://arbitrary.pod/resource", { read: true });

    expect(acpModule.internal_setPublicAccess).toHaveBeenCalledTimes(1);
  });

  it("returns the new effective access for ACP-controlled Resources", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest
      .spyOn(acpModule, "internal_setPublicAccess")
      .mockReturnValueOnce(mockedResourceWithAcr);
    jest
      .spyOn(acpLowLevel, "saveAcrFor")
      .mockResolvedValueOnce(undefined as any);
    jest.spyOn(acpModule, "internal_getPublicAccess").mockReturnValueOnce({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });

    const returnedAccess = await setPublicAccess(
      "https://arbitrary.pod/resource",
      { read: true }
    );

    expect(returnedAccess).toStrictEqual({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetPublicAccess = jest.spyOn(wacModule, "setPublicResourceAccess");
    wacSetPublicAccess.mockResolvedValue(null);

    await setPublicAccess("https://arbitrary.pod/resource", { read: true });

    expect(wacSetPublicAccess).toHaveBeenCalledTimes(1);
  });

  it("returns the new effective access for WAC-controlled Resources", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetPublicAccess = jest.spyOn(wacModule, "setPublicResourceAccess");
    wacSetPublicAccess.mockResolvedValue(null);
    jest.spyOn(wacModule, "getPublicAccess").mockResolvedValueOnce({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });

    const returnedAccess = await setPublicAccess(
      "https://arbitrary.pod/resource",
      { read: true }
    );

    expect(returnedAccess).toStrictEqual({
      read: false,
      append: true,
      write: false,
      controlRead: false,
      controlWrite: false,
    });
  });
  it("throws an error when setting differing controlRead and controlWrite for a WAC-controlled Resource", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom("https://some.pod/resource");
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetPublicAccess = jest.spyOn(wacModule, "setPublicResourceAccess");
    wacSetPublicAccess.mockResolvedValue(null);

    await expect(
      setPublicAccess("https://some.pod/resource", {
        controlRead: true,
        controlWrite: undefined,
      })
    ).rejects.toThrow(
      "When setting access for a Resource in a Pod implementing Web Access Control (i.e. [https://some.pod/resource]), `controlRead` and `controlWrite` should have the same value."
    );
  });

  it("returns null when the ACP module could not update the ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_setPublicAccess").mockReturnValueOnce(null);

    const returnValue = await setPublicAccess(
      "https://arbitrary.pod/resource",
      { read: true }
    );

    expect(returnValue).toBeNull();
  });

  it("returns null when the WAC module could not update the ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetPublicAccess = jest.spyOn(wacModule, "setPublicResourceAccess");
    wacSetPublicAccess.mockResolvedValue(null);

    const returnedAccess = await setPublicAccess(
      "https://arbitrary.pod/resource",
      { read: true }
    );

    expect(returnedAccess).toBeNull();
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    setPublicAccess("https://some.pod/resource", { read: true }, options);

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacSetPublicAccess = jest.spyOn(wacModule, "setPublicResourceAccess");
    wacSetPublicAccess.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await setPublicAccess(
      "https://arbitrary.pod/resource",
      { read: true },
      options
    );

    expect(wacSetPublicAccess).toHaveBeenCalledWith(
      expect.anything(),
      { read: true },
      options
    );
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await setPublicAccess("https://arbitrary.pod/resource", {
      read: true,
    });

    expect(access).toBeNull();
  });
});

describe("getAgentAccessAll", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_getAgentAccessAll");

    await getAgentAccessAll("https://arbitrary.pod/resource");

    expect(acpModule.internal_getAgentAccessAll).toHaveBeenCalledTimes(1);
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetAgentAccessAll = jest.spyOn(wacModule, "getAgentAccessAll");
    wacGetAgentAccessAll.mockResolvedValue(null);

    await getAgentAccessAll("https://arbitrary.pod/resource");

    expect(wacGetAgentAccessAll).toHaveBeenCalledTimes(1);
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    getAgentAccessAll("https://some.pod/resource", options);

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetAgentAccessAll = jest.spyOn(wacModule, "getAgentAccessAll");
    wacGetAgentAccessAll.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await getAgentAccessAll("https://arbitrary.pod/resource", options);

    expect(wacGetAgentAccessAll).toHaveBeenCalledWith(
      expect.anything(),
      options
    );
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await getAgentAccessAll("https://arbitrary.pod/resource");

    expect(access).toBeNull();
  });
});

describe("getGroupAccessAll", () => {
  it("calls out to the well-tested ACP API for Resources with an ACR", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedAcr = mockAcrFor("https://arbitrary.pod/resource");
    const mockedResourceWithAcr = addMockAcrTo(mockedResource, mockedAcr);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcr);
    jest.spyOn(acpModule, "internal_getGroupAccessAll");

    await getGroupAccessAll("https://arbitrary.pod/resource");

    expect(acpModule.internal_getGroupAccessAll).toHaveBeenCalledTimes(1);
  });

  it("calls out to the well-tested WAC API for Resources with an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetGroupAccessAll = jest.spyOn(wacModule, "getGroupAccessAll");
    wacGetGroupAccessAll.mockResolvedValue(null);

    await getGroupAccessAll("https://arbitrary.pod/resource");

    expect(wacGetGroupAccessAll).toHaveBeenCalledTimes(1);
  });

  it("uses the provided fetch function", () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const options = { fetch: () => new Promise(jest.fn()) as any };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    getGroupAccessAll("https://some.pod/resource", options);

    expect(getResourceInfoWithAcr).toHaveBeenCalledWith(
      "https://some.pod/resource",
      options
    );
  });

  it("passes the provided fetch function to the WAC module", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    const mockedResourceWithAcl = addMockResourceAclTo(mockedResource);
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResourceWithAcl as any);
    const wacGetGroupAccessAll = jest.spyOn(wacModule, "getGroupAccessAll");
    wacGetGroupAccessAll.mockResolvedValue(null);
    const options = { fetch: () => new Promise(jest.fn()) as any };

    await getGroupAccessAll("https://arbitrary.pod/resource", options);

    expect(wacGetGroupAccessAll).toHaveBeenCalledWith(
      expect.anything(),
      options
    );
  });

  it("returns null if the Resource has neither an ACR nor an ACL", async () => {
    const getResourceInfoWithAcr = jest.spyOn(
      acpLowLevel,
      "getResourceInfoWithAcr"
    );
    const mockedResource = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    getResourceInfoWithAcr.mockResolvedValueOnce(mockedResource as any);

    const access = await getGroupAccessAll("https://arbitrary.pod/resource");

    expect(access).toBeNull();
  });
});

describe("access/for reexports", () => {
  it("re-exports getAccessFor", () => {
    expect(reexport_getAccessFor).toBeDefined();
  });

  it("re-exports getAccessForAll", () => {
    expect(reexport_getAccessForAll).toBeDefined();
  });

  it("re-exports setAccessFor", () => {
    expect(reexport_setAccessFor).toBeDefined();
  });
});
