var assert = require("assert");
var path = require("path");
var promisify = require("promisify-node");
var fse = promisify(require("fs-extra"));

var garbageCollect = require("../utils/garbage_collect.js");
var leakTest = require("../utils/leak_test");

var local = path.join.bind(path, __dirname);

var exec = require("../../utils/execPromise");

describe("Commit", function() {
  var NodeGit = require("../../");
  var Repository = NodeGit.Repository;
  var Diff = NodeGit.Diff;

  var reposPath = local("../repos/workdir");
  var oid = "fce88902e66c72b5b93e75bdb5ae717038b221f6";

  function reinitialize(test) {
    return Repository.open(reposPath)
      .then(function(repository) {
        test.repository = repository;

        return repository.getCommit(oid);
      })
      .then(function(commit) {
        test.commit = commit;
      });
  }

  function commitFile(repo, fileName, fileContent, commitMessage) {
    var index;
    var treeOid;
    var parent;

    return fse.writeFile(path.join(repo.workdir(), fileName), fileContent)
    .then(function() {
      return repo.refreshIndex();
    })
    .then(function(indexResult) {
      index = indexResult;
    })
    .then(function() {
      return index.addByPath(fileName);
    })
    .then(function() {
      return index.write();
    })
    .then(function() {
      return index.writeTree();
    })
    .then(function(oidResult) {
      treeOid = oidResult;
      return NodeGit.Reference.nameToId(repo, "HEAD");
    })
    .then(function(head) {
      return repo.getCommit(head);
    })
    .then(function(parentResult) {
      parent = parentResult;
      return Promise.all([
        NodeGit.Signature.create("Foo Bar", "foo@bar.com", 123456789, 60),
        NodeGit.Signature.create("Foo A Bar", "foo@bar.com", 987654321, 90)
      ]);
    })
    .then(function(signatures) {
      var author = signatures[0];
      var committer = signatures[1];

      return repo.createCommit(
        "HEAD",
        author,
        committer,
        "message",
        treeOid,
        [parent]);
    });
  }

  function undoCommit() {
    return exec("git reset --hard HEAD~1", {cwd: reposPath});
  }

  beforeEach(function() {
    return reinitialize(this);
  });

  it("will fail with an invalid sha", function() {
    return this.repository.getCommit("invalid").then(null, function(err) {
      assert.ok(err instanceof Error);
    });
  });

  it("has a message", function() {
    assert.equal(this.commit.message(), "Update README.md");
  });

  it("has a raw message", function() {
    assert.equal(this.commit.messageRaw(), "Update README.md");
  });

  it("has a message encoding", function() {
    var encoding = this.commit.messageEncoding();
    assert.ok(encoding === "UTF-8" || encoding === undefined);
  });

  it("has a summary", function() {
    assert.equal(this.commit.summary(), "Update README.md");
  });

  it("has a sha", function() {
    assert.equal(this.commit.sha(), oid);
  });

  it("has a time", function() {
    assert.equal(this.commit.timeMs(), 1362012884000);
  });

  it("has a date", function() {
    assert.equal(this.commit.date().getTime(), 1362012884000);
  });

  it("has a time offset", function() {
    assert.equal(this.commit.timeOffset(), 780);
  });

  it("can create a commit", function() {
    var test = this;
    var expectedCommitId = "315e77328ef596f3bc065d8ac6dd2c72c09de8a5";
    var fileName = "newfile.txt";
    var fileContent = "hello world";

    var repo;
    var index;
    var treeOid;
    var parent;

    return NodeGit.Repository.open(reposPath)
    .then(function(repoResult) {
      repo = repoResult;
      return fse.writeFile(path.join(repo.workdir(), fileName), fileContent);
    })
    .then(function() {
      return repo.refreshIndex();
    })
    .then(function(indexResult) {
      index = indexResult;
    })
    .then(function() {
      return index.addByPath(fileName);
    })
    .then(function() {
      return index.write();
    })
    .then(function() {
      return index.writeTree();
    })
    .then(function(oidResult) {
      treeOid = oidResult;
      return NodeGit.Reference.nameToId(repo, "HEAD");
    })
    .then(function(head) {
      return repo.getCommit(head);
    })
    .then(function(parentResult) {
      parent = parentResult;
      return Promise.all([
        NodeGit.Signature.create("Foo Bar", "foo@bar.com", 123456789, 60),
        NodeGit.Signature.create("Foo A Bar", "foo@bar.com", 987654321, 90)
      ]);
    })
    .then(function(signatures) {
      var author = signatures[0];
      var committer = signatures[1];

      return repo.createCommit(
        "HEAD",
        author,
        committer,
        "message",
        treeOid,
        [parent]);
    })
    .then(function(commitId) {
      assert.equal(expectedCommitId, commitId);
      return undoCommit()
      .then(function(){
        return reinitialize(test);
      });
    }, function(reason) {
      return reinitialize(test)
        .then(function() {
          return Promise.reject(reason);
        });
    });
  });


  it("can amend commit", function(){
    var commitToAmendId = "315e77328ef596f3bc065d8ac6dd2c72c09de8a5";
    var expectedAmendedCommitId = "57836e96555243666ea74ea888310cc7c41d4613";
    var fileName = "newfile.txt";
    var fileContent = "hello world";
    var newFileName = "newerfile.txt";
    var newFileContent = "goodbye world";
    var messageEncoding = "US-ASCII";
    var message = "First commit";

    var repo;
    var index;
    var treeOid;
    var parent;
    var author;
    var committer;
    var amendedCommitId;

    return NodeGit.Repository.open(reposPath)
    .then(function(repoResult) {
      repo = repoResult;
      return fse.writeFile(path.join(repo.workdir(), fileName), fileContent);
    })
    .then(function() {
      return repo.refreshIndex();
    })
    .then(function(indexResult) {
      index = indexResult;
    })
    .then(function() {
      return index.addByPath(fileName);
    })
    .then(function() {
      return index.write();
    })
    .then(function() {
      return index.writeTree();
    })
    .then(function(oidResult) {
      treeOid = oidResult;
      return NodeGit.Reference.nameToId(repo, "HEAD");
    })
    .then(function(head) {
      return repo.getCommit(head);
    })
    .then(function(parentResult) {
      parent = parentResult;
      return Promise.all([
        NodeGit.Signature.create("Foo Bar", "foo@bar.com", 123456789, 60),
        NodeGit.Signature.create("Foo A Bar", "foo@bar.com", 987654321, 90)
      ]);
    })
    .then(function(signatures) {
      var author = signatures[0];
      var committer = signatures[1];

      return repo.createCommit(
        "HEAD",
        author,
        committer,
        "message",
        treeOid,
        [parent]);
    })
    .then(function() {
      return fse.writeFile(
        path.join(repo.workdir(), newFileName),
        newFileContent
      );
    })
    .then(function() {
      return repo.refreshIndex();
    })
    .then(function(indexResult) {
      index = indexResult;
    })
    .then(function() {
      return index.addByPath(newFileName);
    })
    .then(function() {
      return index.write();
    })
    .then(function() {
      return index.writeTree();
    })
    .then(function(resultOid){
      treeOid = resultOid;
       return Promise.all([
         repo.getCommit(commitToAmendId),
         NodeGit.Signature.create(
           "New Foo Bar",
           "newfoo@bar.com",
           246802468,
           12
         ),
         NodeGit.Signature.create(
           "New Foo A Bar",
           "newfoo@bar.com",
           4807891730,
           32
         )
       ]);

    })
    .then(function(amendInfo){
      var commit = amendInfo[0];
      author = amendInfo[1];
      committer = amendInfo[2];
      return commit.amend(
        "HEAD",
        author,
        committer,
        messageEncoding,
        message,
        treeOid
      );
    })
    .then(function(commitId){
      amendedCommitId = commitId;
      return undoCommit();
    })
    .then(function(){
      assert.equal(amendedCommitId, expectedAmendedCommitId);
    });
  });

  it("can amend commit and update reference separately", function() {
    var customReflogMessage = "updating reference manually";

    var head, repo, oid, originalReflogCount;

    return NodeGit.Repository.open(reposPath)
    .then(function(repoResult) {
      repo = repoResult;
      // grab the original reflog entry count (to make sure .amend
      // doesn't add a reflog entry when not given a reference)
      return NodeGit.Reflog.read(repo, "HEAD");
    })
    .then(function(reflog) {
      originalReflogCount = reflog.entrycount();
      // get the head reference and commit
      return repo.head();
    })
    .then(function(headResult) {
      head = headResult;
      return repo.getHeadCommit();
    })
    .then(function(headCommit) {
      // amend the commit but don't update any reference
      // (passing null as update_ref)
      return headCommit.amend(
        null,
        null,
        null,
        "message",
        null,
        null);
    }).then(function(oidResult) {
      oid = oidResult;
      // update the reference manually
      return head.setTarget(oid, customReflogMessage);
    }).then(function() {
      // load reflog and make sure the last message is what we expected
      return NodeGit.Reflog.read(repo, "HEAD");
    }).then(function(reflog) {
      var reflogEntry = reflog.entryByIndex(0);
      assert.equal(
        reflogEntry.message(),
        customReflogMessage
      );
      assert.equal(
        reflogEntry.idNew().toString(),
        oid
      );
      // only setTarget should have added to the entrycount
      assert.equal(reflog.entrycount(), originalReflogCount + 1);
    });
  });

  it("has an owner", function() {
    var owner = this.commit.owner();
    assert.ok(owner instanceof Repository);
  });

  it("can walk its repository's history", function(done) {
    var historyCount = 0;
    var expectedHistoryCount = 364;

    var history = this.commit.history();

    history.on("commit", function(commit) {
      historyCount++;
    });

    history.on("end", function(commits) {
      assert.equal(historyCount, expectedHistoryCount);
      assert.equal(commits.length, expectedHistoryCount);

      done();
    });

    history.on("error", function(err) {
      assert.ok(false);
    });

    history.start();
  });

  it("can fetch the master branch HEAD", function() {
    var repository = this.repository;

    return repository.getBranchCommit("master").then(function(commit) {
      return repository.getCommit(commit.sha());
    });
  });

  it("can fetch all of its parents", function() {
    return this.commit.getParents().then(function(parents) {
      assert.equal(parents.length, 1);

      var sha = parents[0].sha();
      assert.equal(sha, "ecfd36c80a3e9081f200dfda2391acadb56dac27");
    });
  });

  it("can specify a parents limit", function() {
    return this.commit.getParents(0).then(function(parents) {
      assert.equal(parents.length, 0);
    });
  });

  it("can specify limit higher than actual parents", function() {
    return this.commit.getParents(2).then(function(parents) {
      assert.equal(parents.length, 1);
    });
  });

  it("can fetch parents of a merge commit", function () {
    return NodeGit.Repository.open(reposPath)
    .then(function (repo) {
      return repo.getCommit("bf1da765e357a9b936d6d511f2c7b78e0de53632");
    })
    .then(function (commit) {
      return commit.getParents();
    })
    .then(function (parents) {
      assert.equal(parents.length, 2);
    });
  });

  it("has a parent count", function() {
    assert.equal(1, this.commit.parentcount());
  });

  it("can retrieve and walk a commit tree", function() {
    var commitTreeEntryCount = 0;
    var expectedCommitTreeEntryCount = 198;

    return this.commit.getTree().then(function(tree) {
      return new Promise(function(resolve, fail) {

        var treeWalker = tree.walk();

        treeWalker.on("entry", function(entry) {
          commitTreeEntryCount++;
        });

        treeWalker.on("error", function(error) {
          fail(error);
        });

        treeWalker.on("end", function(entries) {
          try {
            assert.equal(commitTreeEntryCount, expectedCommitTreeEntryCount);
            resolve();
          }
          catch (e) {
            fail(e);
          }
        });

        treeWalker.start();
      });
    });
  });

  it("can get the commit diff", function() {
    return this.commit.getDiff().then(function(diff) {
      assert.equal(diff.length, 1);
    });
  });

  // it("can get the commit diff in large context", function() {
    // For displaying the full file we can set context_lines of options.
    // Eventually this should work, but right now there is a
    // comment in diff.c in libgit2 of "/* TODO: parse thresholds */"
    // It will add the "--unified" but not with the "=x" part.
    // options.context_lines = 20000;
  // });

  it("can get the commit diff without whitespace", function() {
    var repo;
    var options = {};
    var GIT_DIFF_IGNORE_WHITESPACE = (1 << 22);
    options.flags = GIT_DIFF_IGNORE_WHITESPACE;

    var fileName = "whitespacetest.txt";
    var fileContent = "line a\nline b\nline c\nline d\n	line e\nline f\n" +
                  "line g\nline h\nline i\n		line j\nline k\nline l\n" +
                  "line m\nline n\n			line o\nline p\nline q\n" +
                  "line r\nline s\nline t\nline u\nline v\nline w\n" +
                  "line x\nline y\nline z\n";
    var changedFileContent = "line a\nline b\n        line c\nline d\n" +
                  "line e\nline f\nline g\n  line h\nline i\nline j\n" +
                  "line k\nline l\nline m\nline n\nline o\nlinep\n" +
                  " line q\nline r\nline   s\nline t\n\nline u\n" +
                  "line v1\nline w\nline x\n			\nline y\nline z\n";

    return NodeGit.Repository.open(reposPath)
    .then(function(repoResult) {
      repo = repoResult;
      return commitFile(repo, fileName, fileContent, "commit this");
    })
    .then(function(){
      return commitFile(repo, fileName, changedFileContent, "commit that");
    })
    .then (function() {
      return repo.getHeadCommit();
    })
    .then (function(wsCommit) {
      return wsCommit.getDiffWithOptions(options);
    })
    .then(function(diff) {
      assert.equal(diff.length, 1);
      return diff[0].patches();
    })
    .then(function(patches) {
      assert.equal(patches.length, 1);
      var patch = patches[0];

      assert.equal(patch.oldFile().path(), fileName);
      assert.equal(patch.newFile().path(), fileName);
      assert.ok(patch.isModified());

      return patch.hunks();
    })
    .then(function(hunks) {
      return hunks[0].lines();
    })
    .then(function(lines) {
      //check all hunk lines
      assert.equal(lines.length, 12);
      assert.equal(lines[0].origin(), Diff.LINE.CONTEXT);

      assert.equal(lines[1].content().length, 9);
      assert.equal(lines[1].content(), "line   s\n");
      assert.equal(lines[1].origin(), Diff.LINE.CONTEXT);

      assert.equal(lines[2].origin(), Diff.LINE.CONTEXT);

      assert.equal(lines[3].content().length, 1);
      assert.equal(lines[3].content(), "\n");
      assert.equal(lines[3].origin(), Diff.LINE.ADDITION);

      assert.equal(lines[4].origin(), Diff.LINE.CONTEXT);

      assert.equal(lines[5].content().length, 7);
      assert.equal(lines[5].content(), "line v\n");
      assert.equal(lines[5].origin(), Diff.LINE.DELETION);

      assert.equal(lines[6].content().length, 8);
      assert.equal(lines[6].content(), "line v1\n");
      assert.equal(lines[6].origin(), Diff.LINE.ADDITION);

      assert.equal(lines[7].origin(), Diff.LINE.CONTEXT);

      assert.equal(lines[8].origin(), Diff.LINE.CONTEXT);

      assert.equal(lines[9].content().length, 4);
      assert.equal(lines[9].content(), "\t\t\t\n");
      assert.equal(lines[9].origin(), Diff.LINE.ADDITION);

      assert.equal(lines[10].origin(), Diff.LINE.CONTEXT);

      assert.equal(lines[11].origin(), Diff.LINE.CONTEXT);
    });
  });

  describe("Commit's Author", function() {
    before(function() {
      this.author = this.commit.author();
    });

    it("is available", function() {
      assert.ok(this.author instanceof NodeGit.Signature);
    });

    it("has a name", function() {
      assert.equal(this.author.name(), "Michael Robinson");
    });

    it("has an email", function() {
      assert.equal(this.author.email(), "mike@panmedia.co.nz");
    });
  });

  describe("Commit's Committer", function() {
    before(function() {
      this.committer = this.commit.committer();
    });

    it("is available", function() {
      assert.ok(this.committer instanceof NodeGit.Signature);
    });

    it("has a name", function() {
      assert.equal(this.committer.name(), "Michael Robinson");
    });

    it("has an email", function() {
      assert.equal(this.committer.email(), "mike@panmedia.co.nz");
    });
  });

  it("does not leak", function() {
    var test = this;

    return leakTest(NodeGit.Commit, function() {
        return NodeGit.Commit.lookup(test.repository, oid);
    });
  });

  it("duplicates signature", function() {
    garbageCollect();
    var Signature = NodeGit.Signature;
    var startSelfFreeingCount = Signature.getSelfFreeingInstanceCount();
    var startNonSelfFreeingCount =
      Signature.getNonSelfFreeingConstructedCount();
    var signature = this.commit.author();

    garbageCollect();
    var endSelfFreeingCount = Signature.getSelfFreeingInstanceCount();
    var endNonSelfFreeingCount = Signature.getNonSelfFreeingConstructedCount();
    // we should get one duplicated, self-freeing signature
    assert.equal(startSelfFreeingCount + 1, endSelfFreeingCount);
    assert.equal(startNonSelfFreeingCount, endNonSelfFreeingCount);

    signature = null;
    garbageCollect();
    endSelfFreeingCount = Signature.getSelfFreeingInstanceCount();
    // the self-freeing signature should get freed
    assert.equal(startSelfFreeingCount, endSelfFreeingCount);
  });
});
