var assert = require("assert");
var path = require("path");
var promisify = require("promisify-node");
var _ = require("lodash");
var fse = promisify(require("fs-extra"));
var local = path.join.bind(path, __dirname);

function getLinesFromDiff(diff) {
  return diff.patches()
    .then(function(patches) {
      return Promise.all(_.map(patches, function(patch) {
        return patch.hunks();
      }));
    })
    .then(function(listsOfHunks) {
      var hunks = _.flatten(listsOfHunks);
      return Promise.all(_.map(hunks, function(hunk) {
        return hunk.lines();
      }));
    })
    .then(function(listsOfLines) {
      var lines = _.flatten(listsOfLines);
      return _.map(lines, function(line) {
        return line.content();
      });
    });
}

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

  var reposPath = local("../repos/workdir");
  var oid = "fce88902e66c72b5b93e75bdb5ae717038b221f6";
  var diffFilename = "wddiff.txt";
  var diffFilepath = local("../repos/workdir", diffFilename);

  var moveFromFile = "README.md";
  var moveToFile = "MOVED_README.md";

  var moveFromPath = local("../repos/workdir", moveFromFile);
  var moveToPath = local("../repos/workdir", moveToFile);

  beforeEach(function() {
    var test = this;

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

      return repository.refreshIndex();
    })
    .then(function(index) {
      test.index = index;

      return test.repository.getBranchCommit("master");
    })
    .then(function(masterCommit) {
      return masterCommit.getTree();
    })
    .then(function(tree) {
      test.masterCommitTree = tree;

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

      return commit.getDiff();
    })
    .then(function(diff) {
      test.diff = diff;

      return fse.writeFile(diffFilepath, "1 line\n2 line\n3 line\n\n4");
    })
    .then(function() {
      return fse.move(moveFromPath, moveToPath);
    })
    .then(function() {
      return Diff.treeToWorkdirWithIndex(
        test.repository,
        test.masterCommitTree,
        { flags: Diff.OPTION.INCLUDE_UNTRACKED }
      );
    })
    .then(function(workdirDiff) {
      test.workdirDiff = workdirDiff;
    })
    .then(function() {
      var opts = {
        flags: Diff.OPTION.INCLUDE_UNTRACKED |
               Diff.OPTION.RECURSE_UNTRACKED_DIRS
      };

      return Diff.indexToWorkdir(test.repository, test.index, opts);
    })
    .then(function(diff) {
      test.indexToWorkdirDiff = diff;
    })
    .then(function() {
      return fse.remove(diffFilepath);
    })
    .then(function() {
      return fse.move(moveToPath, moveFromPath);
    })
    .catch(function(e) {
      return fse.remove(diffFilepath)
        .then(function() {
          return Promise.reject(e);
        });
    });
  });

  it("can walk a DiffList", function() {
    return this.diff[0].patches()
      .then(function(patches) {
        var patch = patches[0];

        assert.equal(patch.oldFile().path(), "README.md");
        assert.equal(patch.newFile().path(), "README.md");
        assert.equal(patch.size(), 1);
        assert.ok(patch.isModified());

        return patch.hunks();
      })
      .then(function(hunks) {
        var hunk = hunks[0];
        assert.equal(hunk.size(), 5);

        return hunk.lines();
      })
      .then(function(lines) {
        assert.equal(lines[0].origin(), Diff.LINE.CONTEXT);
        assert.equal(lines[1].origin(), Diff.LINE.CONTEXT);
        assert.equal(lines[2].origin(), Diff.LINE.CONTEXT);

        var oldContent = "__Before submitting a pull request, please ensure " +
          "both unit tests and lint checks pass.__\n";
        assert.equal(lines[3].content(), oldContent);
        assert.equal(lines[3].origin(), Diff.LINE.DELETION);
        assert.equal(lines[3].content().length, oldContent.length);

        var newContent = "__Before submitting a pull request, please ensure " +
          "both that you've added unit tests to cover your shiny new code, " +
          "and that all unit tests and lint checks pass.__\n";
        assert.equal(lines[4].content(), newContent);
        assert.equal(lines[4].origin(), Diff.LINE.ADDITION);
        assert.equal(lines[4].content().length, newContent.length);
      });
  });

  it("can diff the workdir with index", function() {
    return this.workdirDiff.patches()
      .then(function(patches) {
        assert.equal(patches.length, 3);
        assert(patches[2].isUntracked());

        var oldFile = patches[2].oldFile();
        assert.equal(oldFile.path(), "wddiff.txt");
        assert.equal(oldFile.size(), 0);

        var newFile = patches[2].newFile();
        assert.equal(newFile.path(), "wddiff.txt");
        assert.equal(newFile.size(), 23);
      });
  });

  it("can resolve individual line changes from the patch hunks", function() {
    return this.workdirDiff.patches()
      .then(function(patches) {
        var result = [];
        var hunkPromises = [];

        patches.forEach(function(patch) {
          hunkPromises.push(patch.hunks()
            .then(function(hunks) {
              result = result.concat(hunks);
            })
          );
        });

        return Promise.all(hunkPromises)
          .then(function() {
            return result;
          });
      })
      .then(function(hunks) {
        var result = [];
        var linePromises = [];

        hunks.forEach(function(hunk) {
          linePromises.push(hunk.lines()
            .then(function(lines) {
              result = result.concat(lines);
            })
          );
        });

        return Promise.all(linePromises)
          .then(function() {
            return result;
          });
      })
      .then(function(lines) {
        lines.forEach(function(line) {
          assert(/\n/.exec(line.content()));
          assert(/\n/.exec(line.rawContent()));
        });
      });
  });

  it("can diff the contents of a file to a string", function(done) {
    this.repository.getBranchCommit("master")
      .then(function(commit) {
        return commit.getEntry("LICENSE");
      })
      .then(function(entry) {
        var _entry = entry;
        return _entry.getBlob();
      })
      .then(function(blob) {
        var buffer = "New Text";
        return Diff.blobToBuffer(
          blob,
          null,
          buffer,
          null,
          null,
          null,
          null,
          function(delta, hunk, payload) {
            assert.equal(hunk.oldStart(), 1);
            assert.equal(hunk.oldLines(), 19);
            assert.equal(hunk.newStart(), 1);
            assert.equal(hunk.newLines(), 1);
            assert.equal(
              hunk.header().substring(0, hunk.headerLen() - 1),
              "@@ -1,19 +1 @@"
            );
            done();
          });
      });
  });

  it("can diff the contents of a file to a string with unicode characters",
    function(done) {
    var evilString = "Unicode’s fun!\nAnd it’s good for you!\n";
    var buffer = new Buffer(evilString);
    var test = this;
    Blob.createFromBuffer(test.repository, buffer, buffer.length)
      .then(function(oid) {
        return Blob.lookup(test.repository, oid);
      })
      .then(function(blob) {
        blob.repo = test.repository;
        return Diff.blobToBuffer(
          blob,
          null,
          evilString,
          null,
          null,
          null,
          null,
          function(delta, hunk, payload) {
            assert.fail(
              "There aren't any changes so this shouldn't be called.");
            done();
          });
      })
      .then(function() {
        done();
      });
  });

  it("can diff with a null tree", function() {
    var repo = this.repository;
    var tree = this.masterCommitTree;
    return Diff.treeToTree(repo, null, tree, null)
      .then(function(diff) {
        return diff.patches();
      })
      .then(function(patches) {
        // Number of patches returned is 84 or 85 depending
        // on something unknown at this time. Hopefully we can
        // eventually resolve the root cause of the difference.
        // https://github.com/nodegit/nodegit/issues/746
        assert.ok(patches.length === 84 || patches.length === 85);
      });
  });

  it("can diff the initial commit of a repository", function() {
    var repo = this.repository;
    var oid = "99c88fd2ac9c5e385bd1fe119d89c83dce326219"; // First commit
    return repo.getCommit(oid)
      .then(function(commit) {
        return commit.getDiff();
      })
      .then(function(diffs) {
        return diffs[0].patches();
      })
      .then(function(patches) {
        assert.equal(patches.length, 8);
      });
  });

  it("can diff tree to index", function() {
    var repo = this.repository;
    var tree = this.masterCommitTree;
    var index = this.index;
    var opts = { flags: Diff.OPTION.INCLUDE_UNTRACKED };

    return Diff.treeToIndex(repo, tree, index, opts)
      .then(function(diff) {
        return diff.patches();
      })
      .then(function(patches) {
        assert.equal(patches.length, 0);
      });
  });

  it("can diff index to workdir", function() {
    return this.indexToWorkdirDiff.patches()
      .then(function(patches) {
        assert.equal(patches.length, 3);
      });
  });

  it("can pass undefined pathspec as option to indexToWorkdir", function() {
    var test = this;

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

      return repository.refreshIndex();
    })
    .then(function(index) {
      test.index = index;

      return test.repository.getBranchCommit("master");
    })
    .then(function() {
      var opts = {
        flags: Diff.OPTION.INCLUDE_UNTRACKED |
               Diff.OPTION.RECURSE_UNTRACKED_DIRS,
        pathspec: undefined
      };

      // should not segfault
      return Diff.indexToWorkdir(test.repository, test.index, opts);
    });
  });


  it("can merge two commit diffs", function() {
    var linesOfFirstDiff;
    var linesOfSecondDiff;
    var firstDiff = this.diff[0];
    var secondDiff;
    var oid = "c88d39e70585199425b111c6a2c7fa7b4bc617ad";
    return this.repository.getCommit(oid)
      .then(function(testCommit) {
        return testCommit.getDiff();
      })
      .then(function(_secondDiff) {
        secondDiff = _secondDiff[0];
        return Promise.all([
          getLinesFromDiff(firstDiff),
          getLinesFromDiff(secondDiff)
        ]);
      })
      .then(function(listOfLines) {
        linesOfFirstDiff = listOfLines[0];
        linesOfSecondDiff = listOfLines[1];
        return firstDiff.merge(secondDiff);
      })
      .then(function() {
        return getLinesFromDiff(firstDiff);
      })
      .then(function(linesOfMergedDiff) {
        var allDiffLines = _.flatten([
          linesOfFirstDiff,
          linesOfSecondDiff
        ]);
        _.forEach(allDiffLines, function(diffLine) {
          assert.ok(_.includes(linesOfMergedDiff, diffLine));
        });
      });
  });

  describe(
    "merge between commit diff and workdir and index diff", function() {
    beforeEach(function() {
      var test = this;
      var diffOptions = new NodeGit.DiffOptions();
      var IGNORE_CASE_FLAG = 1 << 10;
      diffOptions.flags = diffOptions.flags |= IGNORE_CASE_FLAG;
      return fse.writeFile(
        path.join(test.repository.workdir(), "newFile.txt"), "some line\n"
      )
        .then(function() {
          return test.index.addAll(undefined, undefined, function() {
            // ensure that there is no deadlock if we call
            // a sync libgit2 function from the callback
            test.repository.path();

            return 0; // confirm add
          });
        })
        .then(function() {
          return test.repository.getHeadCommit();
        })
        .then(function(headCommit) {
          return headCommit.getTree();
        })
        .then(function(headTree) {
          return Promise.all([
            Diff.treeToWorkdirWithIndex(test.repository, headTree, diffOptions),
            test.commit.getDiffWithOptions(diffOptions)
          ]);
        })
        .then(function(diffs) {
          test.workDirWithIndexDiff = diffs[0];
          // The second item in `diffs` is the commit diff which contains and
          // array of diffs, one for each parent
          test.commitDiff = diffs[1][0];
        });
    });

    it("can merge a diff from a commit into a diff from a work dir and index",
      function() {
        var test = this;
        var linesOfWorkDirWithIndexDiff;
        var linesOfCommitDiff;
        return Promise.all([
          getLinesFromDiff(test.workDirWithIndexDiff),
          getLinesFromDiff(test.commitDiff)
        ])
          .then(function(linesOfDiffs) {
            linesOfWorkDirWithIndexDiff = linesOfDiffs[0];
            linesOfCommitDiff = linesOfDiffs[1];
            return test.workDirWithIndexDiff.merge(test.commitDiff);
          })
          .then(function() {
            return getLinesFromDiff(test.workDirWithIndexDiff);
          })
          .then(function(linesOfMergedDiff) {
            var allDiffLines = _.flatten([
              linesOfWorkDirWithIndexDiff,
              linesOfCommitDiff
            ]);
            _.forEach(allDiffLines, function(diffLine) {
              assert.ok(_.includes(linesOfMergedDiff, diffLine));
            });
          });
      });

      it("can merge a diff from a workdir and index into a diff from a commit",
        function() {
          var test = this;
          var linesOfWorkDirWithIndexDiff;
          var linesOfCommitDiff;
          return Promise.all([
            getLinesFromDiff(test.workDirWithIndexDiff),
            getLinesFromDiff(test.commitDiff)
          ])
            .then(function(linesOfDiffs) {
              linesOfWorkDirWithIndexDiff = linesOfDiffs[0];
              linesOfCommitDiff = linesOfDiffs[1];
              return test.commitDiff.merge(test.workDirWithIndexDiff);
            })
            .then(function() {
              return getLinesFromDiff(test.commitDiff);
            })
            .then(function(linesOfMergedDiff) {
              var allDiffLines = _.flatten([
                linesOfWorkDirWithIndexDiff,
                linesOfCommitDiff
              ]);
              _.forEach(allDiffLines, function(diffLine) {
                assert.ok(_.includes(linesOfMergedDiff, diffLine));
              });
            });
        });
  });

  // This wasn't working before. It was only passing because the promise chain
  // was broken
  it.skip("can find similar files in a diff", function() {
    var diff = this.indexToWorkdirDiff;
    var opts = {
      flags: Diff.FIND.RENAMES |
             Diff.FIND.RENAMES_FROM_REWRITES |
             Diff.FIND.FOR_UNTRACKED
    };

    return diff.patches()
      .then(function(patches) {
        assert.equal(patches.length, 3);

        return diff.findSimilar(opts);
      })
      .then(function() {
        return diff.patches();
      })
      .then(function(patches) {
        // Renamed file now treated as one diff, so 3 patches -> 2
        assert.equal(patches.length, 2);
      });
  });
});
