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

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

  describe("creates directory if it doesn't exist", () => {
    const expectations = () => {
      path("x").shouldBeDirectory();
    };

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

    it("async", done => {
      jetpack.dirAsync("x").then(() => {
        expectations();
        done();
      });
    });
  });

  describe("does nothing if directory already exists", () => {
    const preparations = () => {
      fse.mkdirsSync("x");
    };

    const expectations = () => {
      path("x").shouldBeDirectory();
    };

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

    it("async", done => {
      preparations();
      jetpack.dirAsync("x").then(() => {
        expectations();
        done();
      });
    });
  });

  describe("creates nested directories if necessary", () => {
    const expectations = () => {
      path("a/b/c").shouldBeDirectory();
    };

    it("sync", () => {
      jetpack.dir("a/b/c");
      expectations();
    });

    it("async", done => {
      jetpack.dirAsync("a/b/c").then(() => {
        expectations();
        done();
      });
    });
  });

  describe("handles well two calls racing to create the same directory", () => {
    const expectations = () => {
      path("a/b/c").shouldBeDirectory();
    };

    it("async", done => {
      let doneCount = 0;
      const check = () => {
        doneCount += 1;
        if (doneCount === 2) {
          expectations();
          done();
        }
      };
      jetpack.dirAsync("a/b/c").then(check);
      jetpack.dirAsync("a/b/c").then(check);
    });
  });

  describe("doesn't touch directory content by default", () => {
    const preparations = () => {
      fse.mkdirsSync("a/b");
      fse.outputFileSync("a/c.txt", "abc");
    };

    const expectations = () => {
      path("a/b").shouldBeDirectory();
      path("a/c.txt").shouldBeFileWithContent("abc");
    };

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

    it("async", done => {
      preparations();
      jetpack.dirAsync("a").then(() => {
        expectations();
        done();
      });
    });
  });

  describe("makes directory empty if that option specified", () => {
    const preparations = () => {
      fse.outputFileSync("a/b/file.txt", "abc");
    };

    const expectations = () => {
      path("a/b/file.txt").shouldNotExist();
      path("a").shouldBeDirectory();
    };

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

    it("async", done => {
      preparations();
      jetpack.dirAsync("a", { empty: true }).then(() => {
        expectations();
        done();
      });
    });
  });

  describe("throws if given path is something other than directory", () => {
    const preparations = () => {
      fse.outputFileSync("a", "abc");
    };

    const expectations = (err: any) => {
      expect(err.message).to.have.string("exists but is not a directory");
    };

    it("sync", () => {
      preparations();
      try {
        jetpack.dir("a");
        throw new Error("Expected error to be thrown");
      } catch (err) {
        expectations(err);
      }
    });

    it("async", done => {
      preparations();
      jetpack.dirAsync("a").catch(err => {
        expectations(err);
        done();
      });
    });
  });

  describe("respects internal CWD of jetpack instance", () => {
    const expectations = () => {
      path("a/b").shouldBeDirectory();
    };

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

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

  describe("returns jetack instance pointing on this directory", () => {
    const expectations = (jetpackContext: FSJetpack) => {
      expect(jetpackContext.cwd()).to.equal(pathUtil.resolve("a"));
    };

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

    it("async", done => {
      jetpack.dirAsync("a").then(jetpackContext => {
        expectations(jetpackContext);
        done();
      });
    });
  });

  if (process.platform !== "win32") {
    describe("sets mode to newly created directory (unix only)", () => {
      const expectations = () => {
        path("a").shouldHaveMode("511");
      };

      it("sync, mode passed as string", () => {
        jetpack.dir("a", { mode: "511" });
        expectations();
      });

      it("sync, mode passed as number", () => {
        jetpack.dir("a", { mode: 0o511 });
        expectations();
      });

      it("async, mode passed as string", done => {
        jetpack.dirAsync("a", { mode: "511" }).then(() => {
          expectations();
          done();
        });
      });

      it("async, mode passed as number", done => {
        jetpack.dirAsync("a", { mode: 0o511 }).then(() => {
          expectations();
          done();
        });
      });
    });

    describe("sets desired mode to every created directory (unix only)", () => {
      const expectations = () => {
        path("a").shouldHaveMode("711");
        path("a/b").shouldHaveMode("711");
      };

      it("sync", () => {
        jetpack.dir("a/b", { mode: "711" });
        expectations();
      });

      it("async", done => {
        jetpack.dirAsync("a/b", { mode: "711" }).then(() => {
          expectations();
          done();
        });
      });
    });

    describe("changes mode of existing directory to desired (unix only)", () => {
      const preparations = () => {
        fse.mkdirSync("a", "777");
      };
      const expectations = () => {
        path("a").shouldHaveMode("511");
      };

      it("sync", () => {
        preparations();
        jetpack.dir("a", { mode: "511" });
        expectations();
      });

      it("async", done => {
        preparations();
        jetpack.dirAsync("a", { mode: "511" }).then(() => {
          expectations();
          done();
        });
      });
    });

    describe("leaves mode of directory intact by default (unix only)", () => {
      const preparations = () => {
        fse.mkdirSync("a", "700");
      };

      const expectations = () => {
        path("a").shouldHaveMode("700");
      };

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

      it("async", done => {
        preparations();
        jetpack.dirAsync("a").then(() => {
          expectations();
          done();
        });
      });
    });
  } else {
    describe("specyfying mode have no effect and throws no error (windows only)", () => {
      const expectations = () => {
        path("x").shouldBeDirectory();
      };

      it("sync", () => {
        jetpack.dir("x", { mode: "511" });
        expectations();
      });

      it("async", done => {
        jetpack.dirAsync("x", { mode: "511" }).then(() => {
          expectations();
          done();
        });
      });
    });
  }

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

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

    describe('"criteria" object', () => {
      describe('"empty" argument', () => {
        tests.forEach(test => {
          it(test.type, () => {
            expect(() => {
              test.method("abc", { empty: 1 });
            }).to.throw(
              `Argument "criteria.empty" passed to ${
                test.methodName
              }(path, [criteria]) must be a boolean. Received number`
            );
          });
        });
      });
      describe('"mode" argument', () => {
        tests.forEach(test => {
          it(test.type, () => {
            expect(() => {
              test.method("abc", { mode: true });
            }).to.throw(
              `Argument "criteria.mode" passed to ${
                test.methodName
              }(path, [criteria]) must be a string or a number. Received boolean`
            );
          });
        });
      });
    });
  });
});
