import * as fse from "fs-extra";
import { expect } from "chai";
import path from "./assert_path";
import helper from "./helper";
import * as jetpack from "..";
import { InspectTreeResult } from "../types";

describe("inspectTree", () => {
  beforeEach(helper.setCleanTestCwd);
  afterEach(helper.switchBackToCorrectCwd);

  describe("inspects whole tree of files", () => {
    const preparations = () => {
      fse.outputFileSync("dir/file.txt", "abc");
      fse.outputFileSync("dir/subdir/file.txt", "defg");
    };

    const expectations = (data: InspectTreeResult) => {
      expect(data).to.eql({
        name: "dir",
        type: "dir",
        size: 7,
        children: [
          {
            name: "file.txt",
            type: "file",
            size: 3
          },
          {
            name: "subdir",
            type: "dir",
            size: 4,
            children: [
              {
                name: "file.txt",
                type: "file",
                size: 4
              }
            ]
          }
        ]
      });
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("dir"));
    });

    it("async", done => {
      preparations();
      jetpack.inspectTreeAsync("dir").then(tree => {
        expectations(tree);
        done();
      });
    });
  });

  describe("can calculate size of a whole tree", () => {
    const preparations = () => {
      fse.mkdirsSync("dir/empty");
      fse.outputFileSync("dir/empty.txt", "");
      fse.outputFileSync("dir/file.txt", "abc");
      fse.outputFileSync("dir/subdir/file.txt", "defg");
    };

    const expectations = (data: InspectTreeResult) => {
      // dir
      expect(data.size).to.equal(7);
      // dir/empty
      expect(data.children[0].size).to.equal(0);
      // dir/empty.txt
      expect(data.children[1].size).to.equal(0);
      // dir/file.txt
      expect(data.children[2].size).to.equal(3);
      // dir/subdir
      expect(data.children[3].size).to.equal(4);
      // dir/subdir/file.txt
      expect(data.children[3].children[0].size).to.equal(4);
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("dir"));
    });

    it("async", done => {
      preparations();
      jetpack.inspectTreeAsync("dir").then(tree => {
        expectations(tree);
        done();
      });
    });
  });

  describe("can output relative path for every tree node", () => {
    const preparations = () => {
      fse.outputFileSync("dir/subdir/file.txt", "defg");
    };

    const expectations = (data: InspectTreeResult) => {
      // data will look like...
      // {
      //   name: 'dir',
      //   relativePath: '.',
      //   children: [
      //     {
      //       name: 'subdir',
      //       relativePath: './subdir',
      //       children: [
      //         {
      //           name: 'file.txt',
      //           relativePath: './subdir/file.txt'
      //         }
      //       ]
      //     }
      //   ]
      // }
      expect(data.relativePath).to.equal(".");
      expect(data.children[0].relativePath).to.equal("./subdir");
      expect(data.children[0].children[0].relativePath).to.equal(
        "./subdir/file.txt"
      );
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("dir", { relativePath: true }));
    });

    it("async", done => {
      preparations();
      jetpack.inspectTreeAsync("dir", { relativePath: true }).then(tree => {
        expectations(tree);
        done();
      });
    });
  });

  describe("if given path is a file just inspects that file", () => {
    const preparations = () => {
      fse.outputFileSync("dir/file.txt", "abc");
    };

    const expectations = (data: InspectTreeResult) => {
      expect(data).to.eql({
        name: "file.txt",
        type: "file",
        size: 3
      });
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("dir/file.txt"));
    });

    it("async", done => {
      preparations();
      jetpack.inspectTreeAsync("dir/file.txt").then(tree => {
        expectations(tree);
        done();
      });
    });
  });

  describe("behaves ok with empty directory", () => {
    const preparations = () => {
      fse.mkdirsSync("empty");
    };

    const expectations = (data: InspectTreeResult) => {
      expect(data).to.eql({
        name: "empty",
        type: "dir",
        size: 0,
        children: []
      });
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("empty"));
    });

    it("async", done => {
      preparations();
      jetpack.inspectTreeAsync("empty").then(tree => {
        expectations(tree);
        done();
      });
    });
  });

  describe("returns undefined if path doesn't exist", () => {
    const expectations = (data: InspectTreeResult) => {
      expect(data).to.equal(undefined);
    };

    it("sync", () => {
      expectations(jetpack.inspectTree("nonexistent"));
    });

    it("async", done => {
      jetpack.inspectTreeAsync("nonexistent").then(dataAsync => {
        expectations(dataAsync);
        done();
      });
    });
  });

  describe("respects internal CWD of jetpack instance", () => {
    const preparations = () => {
      fse.outputFileSync("a/b.txt", "abc");
    };

    const expectations = (data: InspectTreeResult) => {
      expect(data.name).to.equal("b.txt");
    };

    it("sync", () => {
      const jetContext = jetpack.cwd("a");
      preparations();
      expectations(jetContext.inspectTree("b.txt"));
    });

    it("async", done => {
      const jetContext = jetpack.cwd("a");
      preparations();
      jetContext.inspectTreeAsync("b.txt").then(data => {
        expectations(data);
        done();
      });
    });
  });

  describe("reports symlinks by default", () => {
    const preparations = () => {
      fse.outputFileSync("dir/file.txt", "abc");
      fse.symlinkSync("file.txt", "dir/symlinked_file.txt");
    };

    const expectations = (data: InspectTreeResult) => {
      expect(data).to.eql({
        name: "dir",
        type: "dir",
        size: 3,
        children: [
          {
            name: "file.txt",
            type: "file",
            size: 3
          },
          {
            name: "symlinked_file.txt",
            type: "symlink",
            pointsAt: "file.txt"
          }
        ]
      });
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("dir")); // implicit
      expectations(jetpack.inspectTree("dir", { symlinks: "report" })); // explicit
    });

    it("async", done => {
      preparations();
      jetpack
        .inspectTreeAsync("dir") // implicit
        .then(tree => {
          expectations(tree);
          return jetpack.inspectTreeAsync("dir", { symlinks: "report" }); // explicit
        })
        .then(tree => {
          expectations(tree);
          done();
        })
        .catch(done);
    });
  });

  describe("follows symlinks when option specified", () => {
    const preparations = () => {
      fse.outputFileSync("dir/file.txt", "abc");
      fse.symlinkSync("file.txt", "dir/symlinked_file.txt");
    };

    const expectations = (data: InspectTreeResult) => {
      expect(data).to.eql({
        name: "dir",
        type: "dir",
        size: 6,
        children: [
          {
            name: "file.txt",
            type: "file",
            size: 3
          },
          {
            name: "symlinked_file.txt",
            type: "file",
            size: 3
          }
        ]
      });
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("dir", { symlinks: "follow" }));
    });

    it("async", done => {
      preparations();
      jetpack
        .inspectTreeAsync("dir", { symlinks: "follow" })
        .then(tree => {
          expectations(tree);
          done();
        })
        .catch(done);
    });
  });

  describe("can compute checksum of a whole tree", () => {
    const preparations = () => {
      fse.outputFileSync("dir/a.txt", "abc");
      fse.outputFileSync("dir/b.txt", "defg");
    };

    const expectations = (data: InspectTreeResult) => {
      // md5 of
      // 'a.txt' + '900150983cd24fb0d6963f7d28e17f72' +
      // 'b.txt' + '025e4da7edac35ede583f5e8d51aa7ec'
      expect(data.md5).to.equal("b0ff9df854172efe752cb36b96c8bccd");
      // md5 of 'abc'
      expect(data.children[0].md5).to.equal("900150983cd24fb0d6963f7d28e17f72");
      // md5 of 'defg'
      expect(data.children[1].md5).to.equal("025e4da7edac35ede583f5e8d51aa7ec");
    };

    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("dir", { checksum: "md5" }));
    });

    it("async", done => {
      preparations();
      jetpack.inspectTreeAsync("dir", { checksum: "md5" }).then(tree => {
        expectations(tree);
        done();
      });
    });
  });

  describe("can count checksum of empty directory", () => {
    const preparations = () => {
      fse.mkdirsSync("empty_dir");
    };

    const expectations = (data: InspectTreeResult) => {
      // md5 of empty string
      expect(data.md5).to.equal("d41d8cd98f00b204e9800998ecf8427e");
    };

    // SYNC
    it("sync", () => {
      preparations();
      expectations(jetpack.inspectTree("empty_dir", { checksum: "md5" }));
    });

    it("async", done => {
      preparations();
      jetpack.inspectTreeAsync("empty_dir", { checksum: "md5" }).then(tree => {
        expectations(tree);
        done();
      });
    });
  });

  describe("input validation", () => {
    const tests = [
      {
        type: "sync",
        method: jetpack.inspectTree as any,
        methodName: "inspectTree"
      },
      {
        type: "async",
        method: jetpack.inspectTreeAsync as any,
        methodName: "inspectTreeAsync"
      }
    ];

    describe('"path" argument', () => {
      tests.forEach(test => {
        it(test.type, () => {
          expect(() => {
            test.method(undefined);
          }).to.throw(
            `Argument "path" passed to ${
              test.methodName
            }(path, [options]) must be a string. Received undefined`
          );
        });
      });
    });

    describe('"options" object', () => {
      describe('"checksum" argument', () => {
        tests.forEach(test => {
          it(test.type, () => {
            expect(() => {
              test.method("abc", { checksum: 1 });
            }).to.throw(
              `Argument "options.checksum" passed to ${
                test.methodName
              }(path, [options]) must be a string. Received number`
            );
          });
          it(test.type, () => {
            expect(() => {
              test.method("abc", { checksum: "foo" });
            }).to.throw(
              `Argument "options.checksum" passed to ${
                test.methodName
              }(path, [options]) must have one of values: md5, sha1, sha256`
            );
          });
        });
      });
      describe('"relativePath" argument', () => {
        tests.forEach(test => {
          it(test.type, () => {
            expect(() => {
              test.method("abc", { relativePath: 1 });
            }).to.throw(
              `Argument "options.relativePath" passed to ${
                test.methodName
              }(path, [options]) must be a boolean. Received number`
            );
          });
        });
      });
      describe('"symlinks" argument', () => {
        tests.forEach(test => {
          it(test.type, () => {
            expect(() => {
              test.method("abc", { symlinks: 1 });
            }).to.throw(
              `Argument "options.symlinks" passed to ${
                test.methodName
              }(path, [options]) must be a string. Received number`
            );
          });
          it(test.type, () => {
            expect(() => {
              test.method("abc", { symlinks: "foo" });
            }).to.throw(
              `Argument "options.symlinks" passed to ${
                test.methodName
              }(path, [options]) must have one of values: report, follow`
            );
          });
        });
      });
    });
  });
});
