/**
 * 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";

jest.mock("../fetcher.ts", () => ({
  fetch: jest.fn(window.fetch).mockImplementation(() =>
    Promise.resolve(
      new Response(undefined, {
        headers: { Location: "https://arbitrary.pod/resource" },
      })
    )
  ),
}));

import { Response } from "cross-fetch";
import { acp, rdf } from "../constants";
import * as SolidDatasetModule from "../resource/solidDataset";
import * as FileModule from "../resource/file";
import * as ResourceModule from "../resource/resource";
import {
  getFileWithAccessDatasets,
  getFileWithAcr,
  getReferencedPolicyUrlAll,
  getResourceInfoWithAccessDatasets,
  getResourceInfoWithAcr,
  getSolidDatasetWithAccessDatasets,
  getSolidDatasetWithAcr,
  saveAcrFor,
} from "./acp";
import { UrlString, WithServerResourceInfo, File } from "../interfaces";
import { createThing, setThing } from "../thing/thing";
import { addIri } from "../thing/add";
import { AccessControlResource } from "./control";
import { mockSolidDatasetFrom } from "../resource/mock";
import { addMockAcrTo } from "./mock";

const defaultMockPolicies = {
  policies: ["https://some.pod/policies#policy"],
  memberPolicies: ["https://some.pod/policies#memberPolicy"],
  acrPolicies: [] as string[],
  memberAcrPolicies: [] as string[],
};
function mockAcr(accessTo: UrlString, policies = defaultMockPolicies) {
  let control = createThing({ name: "access-control" });
  control = addIri(control, rdf.type, acp.AccessControl);
  policies.policies.forEach((policyUrl) => {
    control = addIri(control, acp.apply, policyUrl);
  });
  policies.memberPolicies.forEach((policyUrl) => {
    control = addIri(control, acp.applyMembers, policyUrl);
  });

  const acrUrl = accessTo + "?ext=acr";
  let acrThing = createThing({ url: acrUrl });
  policies.acrPolicies.forEach((policyUrl) => {
    acrThing = addIri(acrThing, acp.access, policyUrl);
  });
  policies.memberAcrPolicies.forEach((policyUrl) => {
    acrThing = addIri(acrThing, acp.accessMembers, policyUrl);
  });

  let acr: AccessControlResource & WithServerResourceInfo = Object.assign(
    mockSolidDatasetFrom(acrUrl),
    {
      accessTo: accessTo,
    }
  );
  acr = setThing(acr, control);
  acr = setThing(acr, acrThing);

  return acr;
}

describe("getSolidDatasetWithAcr", () => {
  it("calls the included fetcher by default", async () => {
    const mockedFetcher = jest.requireMock("../fetcher.ts") as {
      fetch: jest.Mock<
        ReturnType<typeof window.fetch>,
        [RequestInfo, RequestInit?]
      >;
    };

    await getSolidDatasetWithAcr("https://some.pod/resource");

    expect(mockedFetcher.fetch.mock.calls[0][0]).toEqual(
      "https://some.pod/resource"
    );
  });

  it("uses the given fetcher if provided", async () => {
    const mockFetch = jest.fn(window.fetch).mockResolvedValue(new Response());

    await getSolidDatasetWithAcr("https://some.pod/resource", {
      fetch: mockFetch,
    });

    expect(mockFetch.mock.calls[0][0]).toEqual("https://some.pod/resource");
  });

  it("returns null for the ACR if it is not accessible to the current user", async () => {
    const mockFetch = jest
      .fn(window.fetch)
      .mockResolvedValueOnce(
        new Response(undefined, {
          headers: {
            Link: `<https://some.pod/acr.ttl>; rel="${acp.accessControl}"`,
          },
          url: "https://some.pod/resource",
        } as ResponseInit)
      )
      .mockResolvedValueOnce(new Response("Not allowed", { status: 401 }));

    const fetchedDataset = await getSolidDatasetWithAcr(
      "https://some.pod/resource",
      { fetch: mockFetch }
    );

    expect(mockFetch.mock.calls[0][0]).toEqual("https://some.pod/resource");
    expect(mockFetch.mock.calls[1][0]).toEqual("https://some.pod/acr.ttl");
    expect(fetchedDataset.internal_acp.acr).toBeNull();
  });

  it("attaches the fetched ACR to the returned SolidDataset", async () => {
    const mockedSolidDataset = mockSolidDatasetFrom(
      "https://arbitrary.pod/resource"
    );
    mockedSolidDataset.internal_resourceInfo.linkedResources = {
      [acp.accessControl]: ["https://arbitrary.pod/resource?ext=acr"],
    };
    const mockedAcr = mockAcr("https://arbitrary.pod/resource", {
      policies: [],
      memberPolicies: [],
      acrPolicies: [],
      memberAcrPolicies: [],
    });
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset
      .mockResolvedValueOnce(mockedSolidDataset)
      .mockResolvedValueOnce(mockedAcr);

    const fetchedDataset = await getSolidDatasetWithAcr(
      "https://some.pod/resource"
    );

    expect(fetchedDataset.internal_acp.acr).toBe(mockedAcr);
  });
});

describe("getFileWithAcr", () => {
  it("calls the included fetcher by default", async () => {
    const mockedFetcher = jest.requireMock("../fetcher.ts") as {
      fetch: jest.Mock<
        ReturnType<typeof window.fetch>,
        [RequestInfo, RequestInit?]
      >;
    };

    await getFileWithAcr("https://some.pod/resource");

    expect(mockedFetcher.fetch.mock.calls[0][0]).toEqual(
      "https://some.pod/resource"
    );
  });

  it("uses the given fetcher if provided", async () => {
    const mockFetch = jest.fn(window.fetch).mockResolvedValue(new Response());

    await getFileWithAcr("https://some.pod/resource", {
      fetch: mockFetch,
    });

    expect(mockFetch.mock.calls[0][0]).toEqual("https://some.pod/resource");
  });

  it("returns null for the ACR if it is not accessible to the current user", async () => {
    const mockFetch = jest
      .fn(window.fetch)
      .mockResolvedValueOnce(
        new Response(undefined, {
          headers: {
            Link: `<https://some.pod/acr.ttl>; rel="${acp.accessControl}"`,
          },
          url: "https://some.pod/resource",
        } as ResponseInit)
      )
      .mockResolvedValueOnce(new Response("Not allowed", { status: 401 }));

    const fetchedFile = await getFileWithAcr("https://some.pod/resource", {
      fetch: mockFetch,
    });

    expect(mockFetch.mock.calls[0][0]).toEqual("https://some.pod/resource");
    expect(mockFetch.mock.calls[1][0]).toEqual("https://some.pod/acr.ttl");
    expect(fetchedFile.internal_acp.acr).toBeNull();
  });

  it("attaches the fetched ACR to the returned File", async () => {
    const mockedFile: File & WithServerResourceInfo = Object.assign(
      new Blob(),
      {
        internal_resourceInfo: {
          sourceIri: "https://some.pod/resource",
          isRawData: true,
          linkedResources: {
            [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
          },
        },
      }
    );
    const mockedAcr = mockAcr("https://arbitrary.pod/resource", {
      policies: [],
      memberPolicies: [],
      acrPolicies: [],
      memberAcrPolicies: [],
    });
    const mockedGetFile = jest.spyOn(FileModule, "getFile");
    mockedGetFile.mockResolvedValueOnce(mockedFile);
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset.mockResolvedValueOnce(mockedAcr);

    const fetchedFile = await getFileWithAcr("https://some.pod/resource");

    expect(fetchedFile.internal_acp.acr).toBe(mockedAcr);
  });
});

describe("getResourceInfoWithAcr", () => {
  it("calls the included fetcher by default", async () => {
    const mockedFetcher = jest.requireMock("../fetcher.ts") as {
      fetch: jest.Mock<
        ReturnType<typeof window.fetch>,
        [RequestInfo, RequestInit?]
      >;
    };

    await getResourceInfoWithAcr("https://some.pod/resource");

    expect(mockedFetcher.fetch.mock.calls[0][0]).toEqual(
      "https://some.pod/resource"
    );
  });

  it("uses the given fetcher if provided", async () => {
    const mockFetch = jest.fn(window.fetch).mockResolvedValue(new Response());

    await getResourceInfoWithAcr("https://some.pod/resource", {
      fetch: mockFetch,
    });

    expect(mockFetch.mock.calls[0][0]).toEqual("https://some.pod/resource");
  });

  it("attaches the fetched ACR to the returned ResourceInfo", async () => {
    const mockedResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
        linkedResources: {
          [acp.accessControl]: ["https://arbitrary.pod/resource?ext=acr"],
        },
      },
    };
    const mockedAcr = mockAcr("https://arbitrary.pod/resource", {
      policies: [],
      memberPolicies: [],
      acrPolicies: [],
      memberAcrPolicies: [],
    });
    const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo");
    mockedGetResourceInfo.mockResolvedValueOnce(mockedResourceInfo);
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset.mockResolvedValueOnce(mockedAcr);

    const fetchedResourceInfo = await getResourceInfoWithAcr(
      "https://some.pod/resource"
    );

    expect(fetchedResourceInfo.internal_acp.acr).toBe(mockedAcr);
  });

  it("returns null for the ACR if it is not accessible to the current user", async () => {
    const mockFetch = jest
      .fn(window.fetch)
      .mockResolvedValueOnce(
        new Response(undefined, {
          headers: {
            Link: `<https://some.pod/acr.ttl>; rel="${acp.accessControl}"`,
          },
          url: "https://some.pod/resource",
        } as ResponseInit)
      )
      .mockResolvedValueOnce(new Response("Not allowed", { status: 401 }));

    const fetchedResourceInfo = await getResourceInfoWithAcr(
      "https://some.pod/resource",
      { fetch: mockFetch }
    );

    expect(mockFetch.mock.calls[0][0]).toEqual("https://some.pod/resource");
    expect(mockFetch.mock.calls[1][0]).toEqual("https://some.pod/acr.ttl");
    expect(fetchedResourceInfo.internal_acp.acr).toBeNull();
  });
});

describe("getReferencedPolicyUrlAll", () => {
  it("returns an empty Object if no APRs were referenced", async () => {
    const mockedResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
        linkedResources: {
          [acp.accessControl]: ["https://arbitrary.pod/resource?ext=acr"],
        },
      },
    };
    const mockedAcr = mockAcr("https://arbitrary.pod/resource", {
      policies: [],
      memberPolicies: [],
      acrPolicies: [],
      memberAcrPolicies: [],
    });
    const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr);

    const policyUrls = getReferencedPolicyUrlAll(withMockedAcr);

    expect(policyUrls).toHaveLength(0);
  });

  it("only includes one mention of a Resource that was referenced multiple times", async () => {
    const mockedResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        sourceIri: "https://some.pod/resource",
        isRawData: true,
        linkedResources: {
          [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
        },
      },
    };
    const mockedAcr = mockAcr("https://some.pod/resource", {
      policies: ["https://some.pod/policy-resource#a-policy"],
      memberPolicies: [
        "https://some.pod/policy-resource#a-member-policy",
        "https://some.pod/policy-resource#another-member-policy",
      ],
      acrPolicies: [],
      memberAcrPolicies: [],
    });
    const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr);

    const policyUrls = getReferencedPolicyUrlAll(withMockedAcr);

    expect(policyUrls).toEqual(["https://some.pod/policy-resource"]);
  });

  it("includes all referenced Policy Resources", async () => {
    const mockedResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        sourceIri: "https://some.pod/resource",
        isRawData: true,
        linkedResources: {
          [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
        },
      },
    };
    const mockedAcr = mockAcr("https://some.pod/resource", {
      policies: ["https://some.pod/policy-resource#a-policy"],
      memberPolicies: [
        "https://some.pod/policy-resource#a-member-policy",
        "https://some.pod/other-policy-resource#another-member-policy",
      ],
      acrPolicies: [],
      memberAcrPolicies: [],
    });
    const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr);

    const policyUrls = getReferencedPolicyUrlAll(withMockedAcr);

    expect(policyUrls).toEqual([
      "https://some.pod/policy-resource",
      "https://some.pod/other-policy-resource",
    ]);
  });

  it("includes referenced ACR Policy Resources", async () => {
    const mockedResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        sourceIri: "https://some.pod/resource",
        isRawData: true,
        linkedResources: {
          [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
        },
      },
    };
    const mockedAcr = mockAcr("https://some.pod/resource", {
      policies: [],
      memberPolicies: [],
      acrPolicies: ["https://some.pod/policy-resource#an-acr-policy"],
      memberAcrPolicies: [
        "https://some.pod/other-policy-resource#another-acr-policy",
      ],
    });
    const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr);

    const policyUrls = getReferencedPolicyUrlAll(withMockedAcr);

    expect(policyUrls).toEqual([
      "https://some.pod/policy-resource",
      "https://some.pod/other-policy-resource",
    ]);
  });
});

describe("getSolidDatasetWithAccessDatasets", () => {
  it("fetches the Resource at the given URL", async () => {
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );

    await getSolidDatasetWithAccessDatasets("https://some.pod/resource");

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource",
      expect.anything()
    );
  });

  it("fetches the ACL when the SolidDataset at the given URL exposes one", async () => {
    const mockDataset = mockSolidDatasetFrom("https://arbitrary.pod/resource");
    mockDataset.internal_resourceInfo.aclUrl = "https://some.pod/resource.acl";
    mockDataset.internal_resourceInfo.linkedResources = {
      acl: ["https://some.pod/resource.acl"],
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset.mockResolvedValueOnce(mockDataset);

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

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource.acl",
      expect.anything()
    );
  });

  it("fetches the ACR when the SolidDataset at the given URL exposes one", async () => {
    const mockDataset = mockSolidDatasetFrom("https://arbitrary.pod/resource");
    mockDataset.internal_resourceInfo.linkedResources = {
      [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset.mockResolvedValueOnce(mockDataset);

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

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource?ext=acr",
      expect.anything()
    );
  });

  it("does not fetch any Access Dataset if none is exposed", async () => {
    const mockDataset = mockSolidDatasetFrom("https://arbitrary.pod/resource");
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset.mockResolvedValueOnce(mockDataset);

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

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

  it("passes on the given fetcher to the Resource and ACL fetcher", async () => {
    const mockDataset = mockSolidDatasetFrom("https://some.pod/resource");
    mockDataset.internal_resourceInfo.aclUrl = "https://some.pod/resource.acl";
    mockDataset.internal_resourceInfo.linkedResources = {
      acl: ["https://some.pod/resource.acl"],
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset.mockResolvedValueOnce(mockDataset);
    const mockedFetcher = jest
      .fn(window.fetch)
      .mockResolvedValue(new Response());

    await getSolidDatasetWithAccessDatasets("https://some.pod/resource", {
      fetch: mockedFetcher,
    });

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2);
    expect(mockedGetSolidDataset).toHaveBeenNthCalledWith(
      1,
      "https://some.pod/resource",
      expect.objectContaining({ fetch: mockedFetcher })
    );
    expect(mockedGetSolidDataset).toHaveBeenNthCalledWith(
      2,
      "https://some.pod/resource.acl",
      expect.objectContaining({ fetch: mockedFetcher })
    );
  });

  it("passes on the given fetcher to the Resource and ACR fetcher", async () => {
    const mockDataset = mockSolidDatasetFrom("https://some.pod/resource");
    mockDataset.internal_resourceInfo.linkedResources = {
      [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    mockedGetSolidDataset.mockResolvedValueOnce(mockDataset);
    const mockedFetcher = jest
      .fn(window.fetch)
      .mockResolvedValue(new Response());

    await getSolidDatasetWithAccessDatasets("https://some.pod/resource", {
      fetch: mockedFetcher,
    });

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2);
    expect(mockedGetSolidDataset).toHaveBeenNthCalledWith(
      1,
      "https://some.pod/resource",
      expect.objectContaining({ fetch: mockedFetcher })
    );
    expect(mockedGetSolidDataset).toHaveBeenNthCalledWith(
      2,
      "https://some.pod/resource?ext=acr",
      expect.objectContaining({ fetch: mockedFetcher })
    );
  });
});

describe("getFileWithAccessDatasets", () => {
  it("fetches the Resource at the given URL", async () => {
    const mockedGetFile = jest.spyOn(FileModule, "getFile");

    await getFileWithAccessDatasets("https://some.pod/resource");

    expect(mockedGetFile).toHaveBeenCalledTimes(1);
    expect(mockedGetFile).toHaveBeenLastCalledWith(
      "https://some.pod/resource",
      expect.anything()
    );
  });

  it("fetches the ACL when the File at the given URL exposes one", async () => {
    const mockFile = Object.assign(new Blob(), {
      internal_resourceInfo: {
        aclUrl: "https://some.pod/resource.acl",
        linkedResources: { acl: ["https://some.pod/resource.acl"] },
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
        url: "https://arbitrary.pod/resource",
      },
    });
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetFile = jest.spyOn(FileModule, "getFile");
    mockedGetFile.mockResolvedValueOnce(mockFile);

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

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource.acl",
      expect.anything()
    );
  });

  it("fetches the ACR when the File at the given URL exposes one", async () => {
    const mockFile = Object.assign(new Blob(), {
      internal_resourceInfo: {
        linkedResources: {
          [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
        },
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
      },
    });
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetFile = jest.spyOn(FileModule, "getFile");
    mockedGetFile.mockResolvedValueOnce(mockFile);

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

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource?ext=acr",
      expect.anything()
    );
  });

  it("does not fetch any Access Dataset if none is exposed", async () => {
    const mockFile = Object.assign(new Blob(), {
      internal_resourceInfo: {
        linkedResources: {},
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
      },
    });
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetFile = jest.spyOn(FileModule, "getFile");
    mockedGetFile.mockResolvedValueOnce(mockFile);

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

    expect(mockedGetSolidDataset).not.toHaveBeenCalled();
  });

  it("passes on the given fetcher to the Resource and ACL fetcher", async () => {
    const mockFile = Object.assign(new Blob(), {
      internal_resourceInfo: {
        aclUrl: "https://some.pod/resource.acl",
        linkedResources: { acl: ["https://some.pod/resource.acl"] },
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
      },
    });
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetFile = jest.spyOn(FileModule, "getFile");
    mockedGetFile.mockResolvedValueOnce(mockFile);
    const mockedFetcher = jest
      .fn(window.fetch)
      .mockResolvedValue(new Response());

    await getFileWithAccessDatasets("https://some.pod/resource", {
      fetch: mockedFetcher,
    });

    expect(mockedGetFile).toHaveBeenCalledTimes(1);
    expect(mockedGetFile).toHaveBeenLastCalledWith(
      "https://some.pod/resource",
      expect.objectContaining({ fetch: mockedFetcher })
    );
    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource.acl",
      expect.objectContaining({ fetch: mockedFetcher })
    );
  });

  it("passes on the given fetcher to the Resource and ACR fetcher", async () => {
    const mockFile = Object.assign(new Blob(), {
      internal_resourceInfo: {
        linkedResources: {
          [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
        },
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
      },
    });
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetFile = jest.spyOn(FileModule, "getFile");
    mockedGetFile.mockResolvedValueOnce(mockFile);
    const mockedFetcher = jest
      .fn(window.fetch)
      .mockResolvedValue(new Response());

    await getFileWithAccessDatasets("https://some.pod/resource", {
      fetch: mockedFetcher,
    });

    expect(mockedGetFile).toHaveBeenCalledTimes(1);
    expect(mockedGetFile).toHaveBeenLastCalledWith(
      "https://some.pod/resource",
      expect.objectContaining({ fetch: mockedFetcher })
    );
    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource?ext=acr",
      expect.objectContaining({ fetch: mockedFetcher })
    );
  });
});

describe("getResourceInfoWithAccessDatasets", () => {
  it("fetches the ResourceInfo for the given URL", async () => {
    const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo");

    await getResourceInfoWithAccessDatasets("https://some.pod/resource");

    expect(mockedGetResourceInfo).toHaveBeenCalledTimes(1);
    expect(mockedGetResourceInfo).toHaveBeenLastCalledWith(
      "https://some.pod/resource",
      expect.anything()
    );
  });

  it("fetches the ACL when the Resource at the given URL exposes one", async () => {
    const mockResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        aclUrl: "https://some.pod/resource.acl",
        linkedResources: { acl: ["https://some.pod/resource.acl"] },
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
      },
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo");
    mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo);

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

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource.acl",
      expect.anything()
    );
  });

  it("fetches the ACR when the Resource at the given URL exposes one", async () => {
    const mockResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        linkedResources: {
          [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
        },
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
      },
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo");
    mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo);

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

    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource?ext=acr",
      expect.anything()
    );
  });

  it("does not fetch any Access Dataset if none is exposed", async () => {
    const mockResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        linkedResources: {},
        sourceIri: "https://arbitrary.pod/resource",
        isRawData: true,
      },
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo");
    mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo);

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

    expect(mockedGetSolidDataset).not.toHaveBeenCalled();
  });

  it("passes on the given fetcher to the ResourceInfo and ACL fetcher", async () => {
    const mockResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        aclUrl: "https://some.pod/.acl",
        linkedResources: { acl: ["https://some.pod/.acl"] },
        sourceIri: "https://some.pod/",
        isRawData: true,
      },
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo");
    mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo);
    const mockedFetcher = jest
      .fn(window.fetch)
      .mockResolvedValue(new Response());

    // We specifically use the root Resource here,
    // because otherwise `getResourceInfo` would be called on the parent Resource
    // to find a fallback ACL:
    await getResourceInfoWithAccessDatasets("https://some.pod/", {
      fetch: mockedFetcher,
    });

    expect(mockedGetResourceInfo).toHaveBeenCalledTimes(1);
    expect(mockedGetResourceInfo).toHaveBeenLastCalledWith(
      "https://some.pod/",
      expect.objectContaining({ fetch: mockedFetcher })
    );
    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/.acl",
      expect.objectContaining({ fetch: mockedFetcher })
    );
  });

  it("passes on the given fetcher to the ResourceInfo and ACR fetcher", async () => {
    const mockResourceInfo: WithServerResourceInfo = {
      internal_resourceInfo: {
        linkedResources: {
          [acp.accessControl]: ["https://some.pod/resource?ext=acr"],
        },
        sourceIri: "https://some.pod/resource",
        isRawData: true,
      },
    };
    const mockedGetSolidDataset = jest.spyOn(
      SolidDatasetModule,
      "getSolidDataset"
    );
    const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo");
    mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo);
    const mockedFetcher = jest
      .fn(window.fetch)
      .mockResolvedValue(new Response());

    await getResourceInfoWithAccessDatasets("https://some.pod/resource", {
      fetch: mockedFetcher,
    });

    expect(mockedGetResourceInfo).toHaveBeenCalledTimes(1);
    expect(mockedGetResourceInfo).toHaveBeenLastCalledWith(
      "https://some.pod/resource",
      expect.objectContaining({ fetch: mockedFetcher })
    );
    expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1);
    expect(mockedGetSolidDataset).toHaveBeenLastCalledWith(
      "https://some.pod/resource?ext=acr",
      expect.objectContaining({ fetch: mockedFetcher })
    );
  });
});

describe("saveAcrFor", () => {
  it("calls the included fetcher by default", async () => {
    const mockedResponse = new Response();
    jest
      .spyOn(mockedResponse, "url", "get")
      .mockReturnValue("https://arbitrary.pod/resource");

    const mockedFetcher = jest.requireMock("../fetcher.ts") as {
      fetch: jest.Mock<
        ReturnType<typeof window.fetch>,
        [RequestInfo, RequestInit?]
      >;
    };

    mockedFetcher.fetch.mockResolvedValue(mockedResponse);

    const mockedAcr = mockAcr("https://arbitrary.pod/resource");
    const mockedResource = addMockAcrTo(
      mockSolidDatasetFrom("https://arbitrary.pod/resource"),
      mockedAcr
    );

    await saveAcrFor(mockedResource);

    expect(mockedFetcher.fetch).toHaveBeenCalledTimes(1);
  });

  it("uses the given fetcher if provided", async () => {
    const mockedResponse = new Response();
    jest
      .spyOn(mockedResponse, "url", "get")
      .mockReturnValue("https://arbitrary.pod/resource");
    const mockFetch = jest.fn(window.fetch).mockResolvedValue(mockedResponse);
    const mockedAcr = mockAcr("https://arbitrary.pod/resource");
    const mockedResource = addMockAcrTo(
      mockSolidDatasetFrom("https://arbitrary.pod/resource"),
      mockedAcr
    );

    await saveAcrFor(mockedResource, {
      fetch: mockFetch,
    });

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

  it("sends the ACR to the Pod", async () => {
    const mockedResponse = new Response();
    jest
      .spyOn(mockedResponse, "url", "get")
      .mockReturnValue("https://arbitrary.pod/resource");
    const mockFetch = jest.fn(window.fetch).mockResolvedValue(mockedResponse);
    const mockedSaveSolidDatasetAt = jest.spyOn(
      SolidDatasetModule,
      "saveSolidDatasetAt"
    );
    const mockedAcr = mockAcr("https://some.pod/resource");
    const mockedResource = addMockAcrTo(
      mockSolidDatasetFrom("https://some.pod/resource"),
      mockedAcr
    );

    await saveAcrFor(mockedResource, {
      fetch: mockFetch,
    });

    expect(mockedSaveSolidDatasetAt).toHaveBeenCalledTimes(1);
    expect(mockedSaveSolidDatasetAt).toHaveBeenCalledWith(
      "https://some.pod/resource?ext=acr",
      mockedAcr,
      expect.objectContaining({ fetch: mockFetch })
    );
  });

  it("attaches the saved ACR to the returned Resource", async () => {
    const mockedSaveSolidDatasetAt = jest.spyOn(
      SolidDatasetModule,
      "saveSolidDatasetAt"
    );
    const mockedAcr = mockAcr("https://some.pod/resource");
    const mockedResource = addMockAcrTo(
      mockSolidDatasetFrom("https://some.pod/resource"),
      mockedAcr
    );
    const fakeReturnedAcr = { fake: "ACR" } as any;
    mockedSaveSolidDatasetAt.mockResolvedValueOnce(fakeReturnedAcr);

    const savedResource = await saveAcrFor(mockedResource);

    expect(savedResource.internal_acp.acr).toEqual(fakeReturnedAcr);
  });
});
