const stream = require("stream");
const walkDir = require("klaw");
const fse = require("fs-extra");
const path = require("path");
const s3Upload = require("../upload");
const getFactory = require("../get");
const logger = require("../../logger");

jest.mock("fs-extra");
jest.mock("klaw");
jest.mock("../get");
jest.mock("../../logger");
const mockBuildId = "1hCeVQzuD6WJQAxuV3hwc";

describe("s3Upload", () => {
  let upload;
  let walkStream;
  let awsProvider;
  let get;

  beforeEach(() => {
    get = jest.fn();
    awsProvider = jest.fn();
    getFactory.mockReturnValue(get);
    walkStream = new stream.Readable();
    walkStream._read = () => {};
    walkDir.mockReturnValueOnce(walkStream);
    fse.lstat.mockResolvedValue({ isDirectory: () => false });
    fse.createReadStream.mockReturnValue("readStream");

    upload = s3Upload(awsProvider, mockBuildId);
  });

  it("should read from the directory given", () => {
    expect.assertions(1);

    const dir = "/path/to/dir";

    const r = upload(dir, { bucket: "my-bucket" }).then(() => {
      expect(walkDir).toBeCalledWith(dir);
    });

    walkStream.emit("end");

    return r;
  });

  it("should upload build assets files to S3 with correct parameters and resolve with file count", () => {
    expect.assertions(5);

    const bucket = "my-bucket";

    const r = upload("/path/to/dir", {
      bucket,
      rootPrefix: "_next"
    }).then((result) => {
      expect(logger.log).toBeCalledWith(
        `Uploading /path/to/dir to ${bucket} ...`
      );

      expect(awsProvider).toBeCalledWith("S3", "upload", {
        ContentType: "application/javascript",
        ACL: "public-read",
        Bucket: bucket,
        CacheControl: "public, max-age=31536000, immutable",
        Key: "_next/runtime/foo.js",
        Body: "readStream"
      });

      expect(awsProvider).toBeCalledWith("S3", "upload", {
        ContentType: "application/javascript",
        ACL: "public-read",
        Bucket: bucket,
        CacheControl: "public, max-age=31536000, immutable",
        Key: "_next/chunk/foo.js",
        Body: "readStream"
      });

      expect(awsProvider).toBeCalledWith("S3", "upload", {
        ContentType: "application/javascript",
        ACL: "public-read",
        Bucket: bucket,
        CacheControl: "public, max-age=31536000, immutable",
        Key: `_next/${mockBuildId}/path/foo.js`,
        Body: "readStream"
      });

      expect(result.count).toEqual(3);
    });

    walkStream.emit("data", {
      path: "/chunk/foo.js"
    });

    walkStream.emit("data", {
      path: "/runtime/foo.js"
    });

    walkStream.emit("data", {
      path: `/${mockBuildId}/path/foo.js`
    });

    walkStream.emit("end");

    return r;
  });

  it("should upload files to S3 with correct parameters and resolve with file count", () => {
    expect.assertions(5);

    const bucket = "my-bucket";

    const r = upload("/path/to/dir", {
      bucket
    }).then((result) => {
      expect(logger.log).toBeCalledWith(
        `Uploading /path/to/dir to ${bucket} ...`
      );

      expect(awsProvider).toBeCalledWith("S3", "upload", {
        ContentType: "application/javascript",
        ACL: "public-read",
        Bucket: bucket,
        CacheControl: undefined,
        Key: "/path/to/foo.js",
        Body: "readStream"
      });

      expect(awsProvider).toBeCalledWith("S3", "upload", {
        ContentType: "text/css",
        ACL: "public-read",
        Bucket: bucket,
        CacheControl: undefined,
        Key: "/path/to/bar.css",
        Body: "readStream"
      });

      expect(awsProvider).toBeCalledWith("S3", "upload", {
        ContentType: "text/plain",
        ACL: "public-read",
        Bucket: bucket,
        CacheControl: undefined,
        Key: "/path/to/readme.txt",
        Body: "readStream"
      });

      expect(result.count).toEqual(3);
    });

    walkStream.emit("data", {
      path: "/path/to/foo.js"
    });

    walkStream.emit("data", {
      path: "/path/to/bar.css"
    });

    walkStream.emit("data", {
      path: "/path/to/readme.txt"
    });

    walkStream.emit("end");

    return r;
  });

  it("should not try uploading directories", () => {
    expect.assertions(1);

    fse.lstat.mockResolvedValue({ isDirectory: () => true });

    const r = upload("/path/to/dir", {
      bucket: "my-bucket"
    }).then(() => {
      expect(awsProvider).not.toBeCalledWith(
        "S3",
        "upload",
        expect.objectContaining({
          Key: "/path/to/dir/subdir"
        })
      );
    });

    walkStream.emit("data", {
      path: "/path/to/dir/subdir"
    });

    walkStream.emit("end");

    return r;
  });

  it("should handle windows paths", () => {
    expect.assertions(1);

    const r = upload("/path/to/dir", {
      bucket: "my-bucket"
    }).then(() => {
      expect(awsProvider).toBeCalledWith(
        "S3",
        "upload",
        expect.objectContaining({
          Key: "/path/to/foo.js"
        })
      );
    });

    walkStream.emit("data", {
      path: path.win32.normalize("/path/to/foo.js")
    });

    walkStream.emit("end");

    return r;
  });

  it("should reject when a file upload fails", () => {
    expect.assertions(1);

    awsProvider.mockRejectedValueOnce(new Error("Boom!"));

    const promise = upload("/path/to/dir", {
      bucket: "my-bucket"
    }).catch((err) => {
      expect(err.message).toContain("Boom");
    });

    walkStream.emit("data", {
      path: "/path/to/foo.js"
    });
    walkStream.emit("end");

    return promise;
  });

  it.each`
    filePath                           | truncate    | expectedKey
    ${"/some/path/to/foo.js"}          | ${"to"}     | ${"to/foo.js"}
    ${"/static-app/static/foo/bar.js"} | ${"static"} | ${"static/foo/bar.js"}
  `(
    "When file path is $filePath S3 Key should be $expectedKey",
    ({ filePath, truncate, expectedKey }) => {
      expect.assertions(2);

      const promise = upload("/path/to/dir", {
        bucket: "my-bucket",
        truncate: truncate
      }).then(() => {
        [1, 2].forEach((i) => {
          expect(awsProvider).toHaveBeenNthCalledWith(
            i,
            "S3",
            "upload",
            expect.objectContaining({
              Key: expectedKey
            })
          );
        });
      });

      walkStream.emit("data", {
        path: filePath
      });

      walkStream.emit("data", {
        path: path.win32.normalize(filePath)
      });

      walkStream.emit("end");

      return promise;
    }
  );

  it("S3 Key should use rootPrefix", () => {
    expect.assertions(2);

    const promise = upload("/path/to/dir", {
      bucket: "my-bucket",
      truncate: "to",
      rootPrefix: "blah"
    }).then(() => {
      expect(awsProvider).toBeCalledWith(
        "S3",
        "upload",
        expect.objectContaining({
          Key: "blah/to/foo.js"
        })
      );
      expect(awsProvider).toBeCalledWith(
        "S3",
        "upload",
        expect.objectContaining({
          Key: "blah/to/bar.js"
        })
      );
    });

    walkStream.emit("data", {
      path: "/some/path/to/foo.js"
    });

    walkStream.emit("data", {
      path: path.win32.normalize("/some/path/to/bar.js")
    });

    walkStream.emit("end");

    return promise;
  });

  it("should not try to upload file that is already uploaded with same file size", () => {
    expect.assertions(2);

    const size = 100;
    const key = "/path/to/happyface.jpg";

    fse.lstat.mockResolvedValueOnce({
      isDirectory: () => false,
      size
    });

    get.mockResolvedValue({
      ETag: '"70ee1738b6b21e2c8a43f3a5ab0eee71"',
      Key: key,
      LastModified: "<Date Representation>",
      Size: size,
      StorageClass: "STANDARD"
    });

    const bucket = "my-bucket";

    const promise = upload("/path/to/dir", {
      bucket
    }).then(() => {
      expect(get).toBeCalledWith(key, bucket);
      expect(awsProvider).not.toBeCalledWith("S3", "upload", expect.anything());
    });

    walkStream.emit("data", {
      path: "/path/to/happyface.jpg"
    });

    walkStream.emit("end");

    return promise;
  });

  it("should upload file that is already uploaded with with different file size", () => {
    expect.assertions(2);

    const size = 100;
    const key = "/path/to/happyface.jpg";

    fse.lstat.mockResolvedValueOnce({
      isDirectory: () => false,
      size
    });

    get.mockResolvedValue({
      ETag: '"70ee1738b6b21e2c8a43f3a5ab0eee71"',
      Key: key,
      LastModified: "<Date Representation>",
      Size: size + 1,
      StorageClass: "STANDARD"
    });

    const bucket = "my-bucket";

    const promise = upload("/path/to/dir", {
      bucket
    }).then(() => {
      expect(get).toBeCalledWith(key, bucket);
      expect(awsProvider).toBeCalledWith("S3", "upload", expect.anything());
    });

    walkStream.emit("data", {
      path: "/path/to/happyface.jpg"
    });

    walkStream.emit("end");

    return promise;
  });
});
