import { handler } from "../../src/default-handler";
import { createCloudFrontEvent } from "../test-utils";
import {
  CloudFrontRequest,
  CloudFrontResultResponse,
  CloudFrontOrigin
} from "aws-lambda";
import { isBlacklistedHeader } from "../../src/headers/removeBlacklistedHeaders";

jest.mock("node-fetch", () => require("fetch-mock-jest").sandbox());

jest.mock(
  "../../src/manifest.json",
  () => require("./default-build-manifest.json"),
  {
    virtual: true
  }
);

jest.mock(
  "../../src/api-manifest.json",
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  () => require("./api-build-manifest.json"),
  {
    virtual: true
  }
);

jest.mock(
  "../../src/routes-manifest.json",
  () => require("./default-routes-manifest.json"),
  {
    virtual: true
  }
);

jest.mock(
  "../../src/prerender-manifest.json",
  () => require("./prerender-manifest.json"),
  {
    virtual: true
  }
);

jest.mock(
  "../../src/images-manifest.json",
  () => require("./images-manifest.json"),
  {
    virtual: true
  }
);

jest.mock("../../src/lib/triggerStaticRegeneration", () => ({
  __esModule: true,
  triggerStaticRegeneration: jest.fn()
}));
const mockTriggerStaticRegeneration =
  require("../../src/lib/triggerStaticRegeneration").triggerStaticRegeneration;

jest.mock("../../src/s3/s3StorePage", () => ({
  __esModule: true,
  s3StorePage: jest.fn()
}));

const mockPageRequire = (mockPagePath: string): void => {
  jest.mock(
    `../../src/${mockPagePath}`,
    () => require(`../shared-fixtures/built-artifact/${mockPagePath}`),
    {
      virtual: true
    }
  );
};

describe("Lambda@Edge", () => {
  let consoleWarnSpy: jest.SpyInstance;

  beforeEach(() => {
    consoleWarnSpy = jest.spyOn(console, "error").mockReturnValue();
  });

  afterEach(() => {
    consoleWarnSpy.mockRestore();
  });

  describe("HTML pages routing", () => {
    it.each`
      path               | expectedPage
      ${"/"}             | ${"/index.html"}
      ${"/terms"}        | ${"/terms.html"}
      ${"/users/batman"} | ${"/users/[...user].html"}
    `(
      "serves page $expectedPage from S3 for path $path",
      async ({ path, expectedPage }) => {
        const event = createCloudFrontEvent({
          uri: path,
          host: "mydistribution.cloudfront.net"
        });

        const result = await handler(event);

        const request = result as CloudFrontRequest;

        expect(request.origin).toEqual({
          s3: {
            authMethod: "origin-access-identity",
            domainName: "my-bucket.s3.amazonaws.com",
            path: "/static-pages/build-id",
            region: "us-east-1"
          }
        });
        expect(request.uri).toEqual(expectedPage);
        expect(request.headers.host[0].key).toEqual("host");
        expect(request.headers.host[0].value).toEqual(
          "my-bucket.s3.amazonaws.com"
        );
      }
    );

    it("terms.html should return 200 status after successful S3 Origin response", async () => {
      const event = createCloudFrontEvent({
        uri: "/terms.html",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200"
        } as any
      });

      const response = (await handler(event)) as CloudFrontResultResponse;

      expect(response.status).toEqual("200");
    });
  });

  describe("Public files routing", () => {
    it.each`
      path
      ${"/manifest.json"}
      ${"/file%20with%20spaces.json"}
    `(
      "serves public file at path $path from S3 /public folder",
      async ({ path }) => {
        const event = createCloudFrontEvent({
          uri: path,
          host: "mydistribution.cloudfront.net"
        });

        const result = await handler(event);

        const request = result as CloudFrontRequest;

        expect(request.origin).toEqual({
          s3: {
            authMethod: "origin-access-identity",
            domainName: "my-bucket.s3.amazonaws.com",
            path: "/public",
            region: "us-east-1"
          }
        });
        expect(request.uri).toEqual(path);
      }
    );

    it("public file should return 200 status after successful S3 Origin response", async () => {
      const event = createCloudFrontEvent({
        uri: "/manifest.json",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200"
        } as any
      });

      const response = (await handler(event)) as CloudFrontResultResponse;

      expect(response.status).toEqual("200");
    });
  });

  describe("SSR pages routing", () => {
    it.each`
      path                              | expectedPage
      ${"/abc"}                         | ${"pages/[root].js"}
      ${"/blog/foo"}                    | ${"pages/blog/[id].js"}
      ${"/customers"}                   | ${"pages/customers.js"}
      ${"/customers/superman"}          | ${"pages/customers/[customer].js"}
      ${"/customers/superman/howtofly"} | ${"pages/customers/[customer]/[post].js"}
      ${"/customers/superman/profile"}  | ${"pages/customers/[customer]/profile.js"}
      ${"/customers/test/catch/all"}    | ${"pages/customers/[...catchAll].js"}
    `(
      "renders page $expectedPage for path $path",
      async ({ path, expectedPage }) => {
        const event = createCloudFrontEvent({
          uri: path,
          host: "mydistribution.cloudfront.net"
        });

        mockPageRequire(expectedPage);

        const response = await handler(event);

        const cfResponse = response as CloudFrontResultResponse;
        const decodedBody = Buffer.from(
          cfResponse.body as string,
          "base64"
        ).toString("utf8");

        expect(decodedBody).toEqual(expectedPage);
        expect(cfResponse.status).toEqual("200");

        // Verify no blacklisted headers are present
        for (const header in response.headers) {
          expect(isBlacklistedHeader(header)).toBe(false);
        }
      }
    );
  });

  describe("Data Requests", () => {
    it.each`
      path                                                      | expectedPage
      ${"/_next/data/build-id/customers.json"}                  | ${"pages/customers.js"}
      ${"/_next/data/build-id/customers/superman.json"}         | ${"pages/customers/[customer].js"}
      ${"/_next/data/build-id/customers/superman/profile.json"} | ${"pages/customers/[customer]/profile.js"}
      ${"/_next/data/build-id/customers/test/catch/all.json"}   | ${"pages/customers/[...catchAll].js"}
    `(
      "serves json data via SSR for SSR path $path",
      async ({ path, expectedPage }) => {
        const event = createCloudFrontEvent({
          uri: path,
          host: "mydistribution.cloudfront.net",
          config: { eventType: "origin-request" } as any
        });

        mockPageRequire(expectedPage);

        const result = await handler(event);

        const cfResponse = result as CloudFrontResultResponse;
        const decodedBody = Buffer.from(
          cfResponse.body as string,
          "base64"
        ).toString("utf8");

        expect(decodedBody).toEqual(JSON.stringify({ page: expectedPage }));
        expect(cfResponse.status).toEqual("200");
      }
    );

    it.each`
      path                                                  | expectedUri
      ${"/_next/data/build-id"}                             | ${"/_next/data/build-id/index.json"}
      ${"/_next/data/build-id/index.json"}                  | ${"/_next/data/build-id/index.json"}
      ${"/_next/data/build-id/fallback/not-yet-built.json"} | ${"/_next/data/build-id/fallback/not-yet-built.json"}
    `(
      "serves json data via S3 for SSG path $path",
      async ({ path, expectedUri }) => {
        const event = createCloudFrontEvent({
          uri: path,
          host: "mydistribution.cloudfront.net",
          config: { eventType: "origin-request" } as any
        });

        const result = await handler(event);

        const request = result as CloudFrontRequest;

        expect(request.origin).toEqual({
          s3: {
            authMethod: "origin-access-identity",
            domainName: "my-bucket.s3.amazonaws.com",
            path: "",
            region: "us-east-1"
          }
        });
        expect(request.uri).toEqual(expectedUri);
      }
    );

    it("correctly removes the expires header if set in the response for an ssg page", async () => {
      mockTriggerStaticRegeneration.mockReturnValueOnce(
        Promise.resolve({ throttle: false })
      );

      const event = createCloudFrontEvent({
        uri: "/preview",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200",
          statusDescription: "ok",
          headers: {
            expires: [
              {
                value: new Date().toJSON(),
                key: "Expires"
              }
            ]
          }
        }
      });

      const response = await handler(event);
      expect(mockTriggerStaticRegeneration).toBeCalledTimes(1);
      expect(mockTriggerStaticRegeneration.mock.calls[0][0]).toEqual(
        expect.objectContaining({
          basePath: "",
          pagePath: "pages/preview.js",
          request: expect.objectContaining({
            uri: `/preview`
          })
        })
      );

      expect(response.headers).not.toHaveProperty("expires");
      expect(response.headers).not.toHaveProperty("Expires");
    });

    it("returns a correct cache control header when an expiry header in the future is sent", async () => {
      const event = createCloudFrontEvent({
        uri: "/customers",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200",
          statusDescription: "ok",
          headers: {
            expires: [
              {
                value: new Date(new Date().getTime() + 3000).toJSON(),
                key: "Expires"
              }
            ]
          }
        }
      });

      const response = await handler(event);
      expect(response.headers).toHaveProperty("cache-control");
      expect(response.headers["cache-control"][0].value).toBe(
        "public, max-age=0, s-maxage=3, must-revalidate"
      );
    });

    it("returns a correct cache control header when an expiry header in the past is sent", async () => {
      const event = createCloudFrontEvent({
        uri: "/customers",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200",
          statusDescription: "ok",
          headers: {
            expires: [
              {
                value: new Date(new Date().getTime() - 3000).toJSON(),
                key: "Expires"
              }
            ]
          }
        }
      });

      const response = await handler(event);
      expect(response.headers).toHaveProperty("cache-control");
      expect(response.headers["cache-control"][0].value).toBe(
        "public, max-age=0, s-maxage=0, must-revalidate"
      );
    });

    it("returns a correct cache control header when a last-modified header is sent", async () => {
      const event = createCloudFrontEvent({
        uri: "/preview",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200",
          statusDescription: "ok",
          headers: {
            ["last-modified"]: [
              {
                value: new Date(new Date().getTime() - 3000).toJSON(),
                key: "Last-Modified"
              }
            ]
          }
        }
      });

      const response = await handler(event);
      expect(response.headers).toHaveProperty("cache-control");
      expect(response.headers["cache-control"][0].value).toBe(
        "public, max-age=0, s-maxage=2, must-revalidate"
      );
    });

    it("returns a correct throttled cache header when 'throttle' value is returned true", async () => {
      mockTriggerStaticRegeneration.mockReturnValueOnce(
        Promise.resolve({ throttle: true })
      );
      const event = createCloudFrontEvent({
        uri: "/preview",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200",
          statusDescription: "ok",
          headers: {
            expires: [
              {
                key: "Expires",
                value: new Date().toJSON()
              }
            ]
          }
        }
      });

      const response = await handler(event);
      expect(response.headers).toHaveProperty("cache-control");
      expect(response.headers["cache-control"][0].value).toBe(
        "public, max-age=0, s-maxage=1, must-revalidate"
      );
      expect(mockTriggerStaticRegeneration).toHaveBeenCalled();
    });
  });

  it("uses default s3 endpoint when bucket region is us-east-1", async () => {
    const event = createCloudFrontEvent({
      uri: "/terms",
      host: "mydistribution.cloudfront.net",
      s3Region: "us-east-1"
    });

    const result = await handler(event);

    const request = result as CloudFrontRequest;
    const origin = request.origin as CloudFrontOrigin;

    expect(origin.s3).toEqual(
      expect.objectContaining({
        domainName: "my-bucket.s3.amazonaws.com"
      })
    );
    expect(request.headers.host[0].key).toEqual("host");
    expect(request.headers.host[0].value).toEqual("my-bucket.s3.amazonaws.com");
  });

  it("uses regional endpoint for static page when bucket region is not us-east-1", async () => {
    const event = createCloudFrontEvent({
      uri: "/terms",
      host: "mydistribution.cloudfront.net",
      s3DomainName: "my-bucket.s3.amazonaws.com",
      s3Region: "eu-west-1"
    });

    const result = await handler(event);

    const request = result as CloudFrontRequest;
    const origin = request.origin as CloudFrontOrigin;

    expect(origin).toEqual({
      s3: {
        authMethod: "origin-access-identity",
        domainName: "my-bucket.s3.eu-west-1.amazonaws.com",
        path: "/static-pages/build-id",
        region: "eu-west-1"
      }
    });
    expect(request.uri).toEqual("/terms.html");
    expect(request.headers.host[0].key).toEqual("host");
    expect(request.headers.host[0].value).toEqual(
      "my-bucket.s3.eu-west-1.amazonaws.com"
    );
  });

  it("uses regional endpoint for public asset when bucket region is not us-east-1", async () => {
    const event = createCloudFrontEvent({
      uri: "/favicon.ico",
      host: "mydistribution.cloudfront.net",
      s3DomainName: "my-bucket.s3.amazonaws.com",
      s3Region: "eu-west-1"
    });

    const result = await handler(event);

    const request = result as CloudFrontRequest;
    const origin = request.origin as CloudFrontOrigin;

    expect(origin).toEqual({
      s3: {
        authMethod: "origin-access-identity",
        domainName: "my-bucket.s3.eu-west-1.amazonaws.com",
        path: "/public",
        region: "eu-west-1"
      }
    });
    expect(request.uri).toEqual("/favicon.ico");
    expect(request.headers.host[0].key).toEqual("host");
    expect(request.headers.host[0].value).toEqual(
      "my-bucket.s3.eu-west-1.amazonaws.com"
    );
  });

  describe("404 page", () => {
    it("returns 404 page if request path can't be matched to any page / api routes", async () => {
      const event = createCloudFrontEvent({
        uri: "/page/does/not/exist",
        host: "mydistribution.cloudfront.net"
      });

      const request = (await handler(event)) as CloudFrontRequest;
      expect(request.uri).toEqual("/404.html");
    });

    it.each`
      path
      ${"/_next/data/unmatched"}
    `(
      "returns 404 page if data request can't be matched for path: $path",
      async ({ path }) => {
        const event = createCloudFrontEvent({
          uri: path,
          origin: {
            s3: {
              domainName: "my-bucket.s3.amazonaws.com"
            }
          },
          config: { eventType: "origin-request" } as any
        });

        const request = (await handler(event)) as CloudFrontRequest;
        expect(request.uri).toEqual("/404.html");
      }
    );

    it("404.html should return 404 status after successful S3 Origin response", async () => {
      const event = createCloudFrontEvent({
        uri: "/404.html",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200"
        } as any
      });

      const response = (await handler(event)) as CloudFrontResultResponse;

      expect(response.status).toEqual("404");
    });

    it("path ending 404 should return 200 status after successful S3 Origin response", async () => {
      const event = createCloudFrontEvent({
        uri: "/fallback/404.html",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200"
        } as any
      });

      const response = (await handler(event)) as CloudFrontResultResponse;

      expect(response.status).toEqual("200");
    });
  });

  describe("500 page", () => {
    it("renders 500 page if page render has an error", async () => {
      const event = createCloudFrontEvent({
        uri: "/erroredPage",
        host: "mydistribution.cloudfront.net"
      });

      mockPageRequire("pages/erroredPage.js");
      mockPageRequire("pages/_error.js");

      const response = (await handler(event)) as CloudFrontResultResponse;
      const body = response.body as string;
      const decodedBody = Buffer.from(body, "base64").toString("utf8");

      expect(decodedBody).toEqual("pages/_error.js - 500");
      expect(response.status).toEqual("500");
    });

    it("path ending 500 should return 200 status after successful S3 Origin response", async () => {
      const event = createCloudFrontEvent({
        uri: "/fallback/500.html",
        host: "mydistribution.cloudfront.net",
        config: { eventType: "origin-response" } as any,
        response: {
          status: "200"
        } as any
      });

      const response = (await handler(event)) as CloudFrontResultResponse;

      expect(response.status).toEqual("200");
    });
  });

  describe("Custom Rewrites", () => {
    it.each`
      uri                    | rewriteUri                | method
      ${"/external-rewrite"} | ${"https://external.com"} | ${"GET"}
      ${"/external-rewrite"} | ${"https://external.com"} | ${"POST"}
    `(
      "serves external rewrite $rewriteUri for rewritten path $uri and method $method",
      async ({ uri, rewriteUri, method }) => {
        const { default: fetchMock } = await import("node-fetch");

        const mockFetchResponse = {
          body: "external",
          headers: {
            "Content-Type": "text/plain",
            Host: "external.com",
            "x-amz-cf-pop": "SEA19-C1"
          }, // host and x-amz-cf-pop header are blacklisted and won't be added
          status: 200
        };

        fetchMock.mock(rewriteUri, mockFetchResponse);

        let [path, querystring] = uri.split("?");

        const event = createCloudFrontEvent({
          uri: path,
          querystring: querystring,
          host: "mydistribution.cloudfront.net",
          method: method,
          body:
            method === "POST"
              ? {
                  action: "read-only",
                  data: "eyJhIjoiYiJ9",
                  encoding: "base64",
                  inputTruncated: false
                }
              : undefined
        });

        const response: CloudFrontResultResponse = await handler(event);

        expect(response).toEqual({
          body: "ZXh0ZXJuYWw=",
          bodyEncoding: "base64",
          headers: {
            "content-type": [
              {
                key: "content-type",
                value: "text/plain"
              }
            ]
          },
          status: "200",
          statusDescription: "OK"
        });

        expect(fetchMock).toHaveLastFetched("https://external.com", {
          method: method
        });

        fetchMock.reset();
      }
    );
  });
});
