/* Copyright (c) 2012-2014 Boundless and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/org/documents/edl-v10.html
 *
 * Contributors:
 * Gabriel Roldan (Boundless) - initial implementation
 */
package org.locationtech.geogig.test.integration;

import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.junit.Test;
import org.locationtech.geogig.api.Node;
import org.locationtech.geogig.api.NodeRef;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RevCommit;
import org.locationtech.geogig.api.RevObject.TYPE;
import org.locationtech.geogig.api.plumbing.DiffIndex;
import org.locationtech.geogig.api.plumbing.DiffTree;
import org.locationtech.geogig.api.plumbing.DiffWorkTree;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry.ChangeType;
import org.locationtech.geogig.api.porcelain.AddOp;
import org.locationtech.geogig.api.porcelain.CommitOp;
import org.locationtech.geogig.api.porcelain.DiffOp;
import org.locationtech.geogig.repository.WorkingTree;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.Name;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

/**
 * Unit test suite for {@link DiffOp}, must cover {@link DiffIndex}, {@link DiffWorkTree}, and
 * {@link DiffTree}
 * 
 */
public class DiffOpTest extends RepositoryTestCase {

    private DiffOp diffOp;

    @Override
    protected void setUpInternal() throws Exception {
        this.diffOp = geogig.command(DiffOp.class);
    }

    @Test
    public void testDiffPreconditions() throws Exception {
        Iterator<DiffEntry> difflist = geogig.command(DiffOp.class).call();
        assertNotNull(difflist);
        assertFalse(difflist.hasNext());

        final ObjectId oid1 = insertAndAdd(points1);
        final RevCommit commit1_1 = geogig.command(CommitOp.class).call();
        try {
            diffOp.setOldVersion(oid1.toString()).setNewVersion(Ref.HEAD).call();
            fail("Expected IAE as oldVersion is not a commit");
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage(), e.getMessage().contains(oid1.toString()));
            assertTrue(e.getMessage(),
                    e.getMessage().contains("doesn't resolve to a tree-ish object"));
        }
        try {
            diffOp.setOldVersion(commit1_1.getId().toString()).setNewVersion(oid1.toString())
                    .call();
            fail("Expected IAE as newVersion is not a commit");
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage(), e.getMessage().contains(oid1.toString()));
            assertTrue(e.getMessage(),
                    e.getMessage().contains("doesn't resolve to a tree-ish object"));
        }
    }

    @Test
    public void testEmptyRepo() throws Exception {
        Iterator<DiffEntry> difflist = diffOp.setOldVersion(ObjectId.NULL.toString()).call();
        assertNotNull(difflist);
        assertFalse(difflist.hasNext());
    }

    @Test
    public void testNoChangeSameCommit() throws Exception {

        insertAndAdd(points1);
        final RevCommit commit = geogig.command(CommitOp.class).setAll(true).call();

        assertFalse(diffOp.setOldVersion(commit.getId().toString())
                .setNewVersion(commit.getId().toString()).call().hasNext());
    }

    @Test
    public void testSingleAddition() throws Exception {

        final ObjectId newOid = insertAndAdd(points1);
        geogig.command(CommitOp.class).setAll(true).call();

        List<DiffEntry> difflist = toList(diffOp.setOldVersion(ObjectId.NULL)
                .setNewVersion(Ref.HEAD).call());

        assertNotNull(difflist);
        assertEquals(1, difflist.size());
        DiffEntry de = difflist.get(0);

        assertNull(de.getOldObject());
        assertNotNull(de.getNewObject());

        String expectedPath = NodeRef.appendChild(pointsName, points1.getIdentifier().getID());
        assertEquals(expectedPath, de.newPath());

        assertEquals(DiffEntry.ChangeType.ADDED, de.changeType());
        assertEquals(ObjectId.NULL, de.oldObjectId());

        assertEquals(newOid, de.newObjectId());
        assertFalse(de.getNewObject().getMetadataId().isNull());
    }

    @Test
    public void testSingleAdditionReverseOrder() throws Exception {

        final ObjectId newOid = insertAndAdd(points1);
        final RevCommit commit = geogig.command(CommitOp.class).setAll(true).call();

        List<DiffEntry> difflist = toList(diffOp.setOldVersion(commit.getId())
                .setNewVersion(ObjectId.NULL).call());

        assertNotNull(difflist);
        assertEquals(1, difflist.size());
        DiffEntry de = difflist.get(0);

        assertNull(de.getNewObject());
        assertNotNull(de.getOldObject());

        assertEquals(DiffEntry.ChangeType.REMOVED, de.changeType());
        assertEquals(ObjectId.NULL, de.newObjectId());

        assertEquals(newOid, de.oldObjectId());
        assertFalse(de.getOldObject().getMetadataId().isNull());
    }

    @Test
    public void testSingleDeletion() throws Exception {
        final ObjectId featureContentId = insertAndAdd(points1);
        final RevCommit addCommit = geogig.command(CommitOp.class).setAll(true).call();

        assertTrue(deleteAndAdd(points1));
        final RevCommit deleteCommit = geogig.command(CommitOp.class).setAll(true).call();

        List<DiffEntry> difflist = toList(diffOp.setOldVersion(addCommit.getId())
                .setNewVersion(deleteCommit.getId()).call());

        final String path = NodeRef.appendChild(pointsName, points1.getIdentifier().getID());

        assertNotNull(difflist);
        assertEquals(1, difflist.size());
        DiffEntry de = difflist.get(0);
        assertEquals(path, de.oldPath());

        assertEquals(DiffEntry.ChangeType.REMOVED, de.changeType());

        assertEquals(featureContentId, de.oldObjectId());

        assertEquals(ObjectId.NULL, de.newObjectId());
    }

    @Test
    public void testSingleDeletionReverseOrder() throws Exception {

        final ObjectId featureContentId = insertAndAdd(points1);
        final RevCommit addCommit = geogig.command(CommitOp.class).setAll(true).call();

        assertTrue(deleteAndAdd(points1));
        final RevCommit deleteCommit = geogig.command(CommitOp.class).setAll(true).call();

        // set old/new version in reverse order
        List<DiffEntry> difflist = toList(diffOp.setOldVersion(deleteCommit.getId())
                .setNewVersion(addCommit.getId()).call());

        final String path = NodeRef.appendChild(pointsName, points1.getIdentifier().getID());

        // then the diff should report an ADD instead of a DELETE
        assertNotNull(difflist);
        assertEquals(1, difflist.size());
        DiffEntry de = difflist.get(0);
        assertNull(de.oldPath());
        assertEquals(path, de.newPath());

        assertEquals(DiffEntry.ChangeType.ADDED, de.changeType());

        assertEquals(ObjectId.NULL, de.oldObjectId());

        assertEquals(featureContentId, de.newObjectId());
    }

    @Test
    public void testSingleModification() throws Exception {

        final ObjectId oldOid = insertAndAdd(points1);
        final RevCommit insertCommit = geogig.command(CommitOp.class).setAll(true).call();

        final String featureId = points1.getIdentifier().getID();
        final Feature modifiedFeature = feature((SimpleFeatureType) points1.getType(), featureId,
                "changedProp", new Integer(1500), null);

        final ObjectId newOid = insertAndAdd(modifiedFeature);

        final RevCommit changeCommit = geogig.command(CommitOp.class).setAll(true).call();

        List<DiffEntry> difflist = toList(diffOp.setOldVersion(insertCommit.getId())
                .setNewVersion(changeCommit.getId()).call());

        assertNotNull(difflist);
        assertEquals(1, difflist.size());
        DiffEntry de = difflist.get(0);
        String expectedPath = NodeRef.appendChild(pointsName, featureId);
        assertEquals(expectedPath, de.newPath());

        assertEquals(DiffEntry.ChangeType.MODIFIED, de.changeType());
        assertEquals(oldOid, de.oldObjectId());

        assertEquals(newOid, de.newObjectId());
    }

    @Test
    public void testFilterNamespaceNoChanges() throws Exception {

        // two commits on different trees
        insertAndAdd(points1);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        insertAndAdd(lines1);
        final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        diffOp.setOldVersion(commit1.getId()).setNewVersion(commit2.getId());
        diffOp.setFilter(pointsName);

        Iterator<DiffEntry> diffs = diffOp.call();
        assertFalse(diffs.hasNext());
    }

    @Test
    public void testFilterTypeNameNoChanges() throws Exception {

        // two commits on different trees
        insertAndAdd(points1);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        insertAndAdd(lines1);
        final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        diffOp.setOldVersion(commit1.getId()).setNewVersion(commit2.getId());
        diffOp.setFilter(pointsName);

        Iterator<DiffEntry> diffs = diffOp.call();
        assertFalse(diffs.hasNext());
    }

    @Test
    public void testFilterDidntMatchAnything() throws Exception {

        // two commits on different trees
        insertAndAdd(points1);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        insertAndAdd(lines1);
        final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        // set a filter that doesn't produce any match

        diffOp.setOldVersion(commit1.getId()).setNewVersion(commit2.getId());
        diffOp.setFilter(NodeRef.appendChild(pointsName, "nonExistentId"));

        Iterator<DiffEntry> diffs = diffOp.call();
        assertNotNull(diffs);
        assertFalse(diffs.hasNext());
    }

    @Test
    public void testFilterFeatureIdNoChanges() throws Exception {

        // two commits on different trees
        insertAndAdd(points1);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        insertAndAdd(lines1);
        final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        // filter on feature1_1, it didn't change between commit2 and commit1

        diffOp.setOldVersion(commit1.getId()).setNewVersion(commit2.getId());
        diffOp.setFilter(NodeRef.appendChild(pointsName, points1.getIdentifier().getID()));

        Iterator<DiffEntry> diffs = diffOp.call();
        assertFalse(diffs.hasNext());
    }

    @Test
    public void testFilterMatchesSingleBlobChange() throws Exception {
        final ObjectId initialOid = insertAndAdd(points1);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        insertAndAdd(lines1);
        final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        ((SimpleFeature) points1).setAttribute("sp", "modified");
        final ObjectId modifiedOid = insertAndAdd(points1);
        final RevCommit commit3 = geogig.command(CommitOp.class).setAll(true).call();

        diffOp.setOldVersion(commit1.getId()).setNewVersion(commit3.getId());
        diffOp.setFilter(NodeRef.appendChild(pointsName, points1.getIdentifier().getID()));

        List<DiffEntry> diffs;
        DiffEntry diff;

        diffs = toList(diffOp.call());
        assertEquals(1, diffs.size());
        diff = diffs.get(0);
        assertEquals(ChangeType.MODIFIED, diff.changeType());
        assertEquals(initialOid, diff.oldObjectId());
        assertEquals(modifiedOid, diff.newObjectId());

        assertTrue(deleteAndAdd(points1));
        final RevCommit commit4 = geogig.command(CommitOp.class).setAll(true).call();
        diffOp.setOldVersion(commit2.getId()).setNewVersion(commit4.getId());
        diffOp.setFilter(NodeRef.appendChild(pointsName, points1.getIdentifier().getID()));
        diffs = toList(diffOp.call());
        assertEquals(1, diffs.size());
        diff = diffs.get(0);
        assertEquals(ChangeType.REMOVED, diff.changeType());
        assertEquals(initialOid, diff.oldObjectId());
        assertEquals(ObjectId.NULL, diff.newObjectId());

        // invert the order of old and new commit
        diffOp.setOldVersion(commit4.getId()).setNewVersion(commit1.getId());
        diffOp.setFilter(NodeRef.appendChild(pointsName, points1.getIdentifier().getID()));
        diffs = toList(diffOp.call());
        assertEquals(1, diffs.size());
        diff = diffs.get(0);
        assertEquals(ChangeType.ADDED, diff.changeType());
        assertEquals(ObjectId.NULL, diff.oldObjectId());
        assertEquals(initialOid, diff.newObjectId());

        // different commit range
        diffOp.setOldVersion(commit4.getId()).setNewVersion(commit3.getId());
        diffOp.setFilter(NodeRef.appendChild(pointsName, points1.getIdentifier().getID()));
        diffs = toList(diffOp.call());
        assertEquals(1, diffs.size());
        diff = diffs.get(0);
        assertEquals(ChangeType.ADDED, diff.changeType());
        assertEquals(ObjectId.NULL, diff.oldObjectId());
        assertEquals(modifiedOid, diff.newObjectId());
    }

    // @Test
    // public void testFilterAddressesNamespaceTree() throws Exception {
    //
    // // two commits on different trees
    // final ObjectId oid11 = insertAndAdd(points1);
    // final ObjectId oid12 = insertAndAdd(points2);
    // final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();
    //
    // final ObjectId oid21 = insertAndAdd(lines1);
    // final ObjectId oid22 = insertAndAdd(lines2);
    // final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();
    //
    // List<DiffEntry> diffs;
    //
    // // filter on namespace1, no changes between commit1 and commit2
    // diffOp.setOldVersion(commit1.getId());
    // diffOp.setFilter(pointsNs);
    //
    // diffs = toList(diffOp.call());
    // assertEquals(0, diffs.size());
    //
    // // filter on namespace2, all additions between commit1 and commit2
    // diffOp.setOldVersion(commit1.getId());
    // diffOp.setFilter(linesNs);
    //
    // diffs = toList(diffOp.call());
    // assertEquals(2, diffs.size());
    // assertEquals(ChangeType.ADD, diffs.get(0).getType());
    // assertEquals(ChangeType.ADD, diffs.get(1).getType());
    //
    // assertEquals(ObjectId.NULL, diffs.get(0).getOldObjectId());
    // assertEquals(ObjectId.NULL, diffs.get(1).getOldObjectId());
    //
    // // don't care about order
    // Set<ObjectId> expected = new HashSet<ObjectId>();
    // expected.add(oid21);
    // expected.add(oid22);
    // Set<ObjectId> actual = new HashSet<ObjectId>();
    // actual.add(diffs.get(0).getNewObjectId());
    // actual.add(diffs.get(1).getNewObjectId());
    // assertEquals(expected, actual);
    // }

    @SuppressWarnings("unused")
    @Test
    public void testMultipleDeletes() throws Exception {

        // two commits on different trees
        final ObjectId oid11 = insertAndAdd(points1);
        final ObjectId oid12 = insertAndAdd(points2);
        final ObjectId oid13 = insertAndAdd(points3);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        final ObjectId oid21 = insertAndAdd(lines1);
        final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        deleteAndAdd(points1);
        deleteAndAdd(points3);
        final RevCommit commit3 = geogig.command(CommitOp.class).setAll(true).call();

        List<DiffEntry> diffs;

        // filter on namespace1, no changes between commit1 and commit2
        diffOp.setOldVersion(commit1.getId()).setNewVersion(commit3.getId());
        diffOp.setFilter(pointsName);

        diffs = toList(diffOp.call());

        assertEquals(2, diffs.size());
        assertEquals(ChangeType.REMOVED, diffs.get(0).changeType());
        assertEquals(ChangeType.REMOVED, diffs.get(1).changeType());

        Set<ObjectId> ids = Sets.newHashSet(diffs.get(0).oldObjectId(), diffs.get(1).oldObjectId());

        assertEquals(Sets.newHashSet(oid11, oid13), ids);
    }

    @SuppressWarnings("unused")
    @Test
    public void testTreeDeletes() throws Exception {

        // two commits on different trees
        final ObjectId oid11 = insertAndAdd(points1);
        final ObjectId oid12 = insertAndAdd(points2);
        final ObjectId oid13 = insertAndAdd(points3);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        final ObjectId oid21 = insertAndAdd(lines1);
        final ObjectId oid22 = insertAndAdd(lines2);
        final RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        deleteAndAdd(points1);
        deleteAndAdd(points2);
        deleteAndAdd(points3);
        final RevCommit commit3 = geogig.command(CommitOp.class).setAll(true).call();

        List<DiffEntry> diffs;

        // filter on namespace1, no changes between commit1 and commit2
        diffOp.setOldVersion(commit1.getId());
        diffOp.setNewVersion(Ref.HEAD);
        diffOp.setFilter(pointsName);

        diffs = toList(diffOp.call());
        assertEquals(3, diffs.size());
    }

    @Test
    public void testReportTreesEmptyTree() throws Exception {

        WorkingTree workingTree = geogig.getRepository().workingTree();
        workingTree.createTypeTree(linesName, linesType);

        List<DiffEntry> difflist = toList(diffOp.setReportTrees(true).setOldVersion(ObjectId.NULL)
                .setNewVersion(Ref.WORK_HEAD).call());

        assertNotNull(difflist);
        assertEquals(1, difflist.size());
        DiffEntry de = difflist.get(0);

        assertNull(de.getOldObject());
        assertNotNull(de.getNewObject());

        assertEquals(linesName, de.newPath());

        assertEquals(DiffEntry.ChangeType.ADDED, de.changeType());
        assertEquals(ObjectId.NULL, de.oldObjectId());
        assertFalse(de.getNewObject().getMetadataId().isNull());
    }

    @Test
    public void testReportRename() throws Exception {

        insertAndAdd(lines1);
        final RevCommit commit1 = geogig.command(CommitOp.class).setAll(true).call();

        Feature lines1B = feature(linesType, idL2, "StringProp2_1", new Integer(1000),
                "LINESTRING (1 1, 2 2)");
        delete(lines1);
        // insert(lines2);
        WorkingTree workTree = repo.workingTree();
        Name name = lines1.getType().getName();
        String parentPath = name.getLocalPart();
        @SuppressWarnings("unused")
        Node ref = workTree.insert(parentPath, lines1B);
        geogig.command(AddOp.class).call();
        RevCommit commit2 = geogig.command(CommitOp.class).setAll(true).call();

        List<DiffEntry> diffs;
        diffOp.setOldVersion(commit1.getId());
        diffOp.setNewVersion(commit2.getId());
        diffs = toList(diffOp.call());
        assertEquals(2, diffs.size()); // this is reported as an addition and a removal, with both
                                       // nodes pointing to same ObjectId
        assertEquals(diffs.get(0).newObjectId(), diffs.get(1).oldObjectId());
        assertEquals(diffs.get(1).newObjectId(), diffs.get(0).oldObjectId());

    }

    @Test
    public void testReportTreesEmptyTreeFromFeatureDeletion() throws Exception {
        insert(lines1);
        delete(lines1);

        List<DiffEntry> difflist = toList(diffOp.setReportTrees(true).setOldVersion(ObjectId.NULL)
                .setNewVersion(Ref.WORK_HEAD).call());

        assertNotNull(difflist);
        assertEquals(1, difflist.size());
        assertEquals(linesName, difflist.get(0).newName());

        DiffEntry de = difflist.get(0);

        assertNull(de.getOldObject());
        assertNotNull(de.getNewObject());

        assertEquals(linesName, de.newPath());

        assertEquals(DiffEntry.ChangeType.ADDED, de.changeType());
        assertEquals(ObjectId.NULL, de.oldObjectId());
        assertFalse(de.getNewObject().getMetadataId().isNull());
    }

    @Test
    public void testReportTrees() throws Exception {

        insert(points1);
        insert(lines1);

        List<DiffEntry> difflist = toList(diffOp.setReportTrees(true).setOldVersion(ObjectId.NULL)
                .setNewVersion(Ref.WORK_HEAD).call());

        assertNotNull(difflist);
        assertEquals(4, difflist.size());
        Set<String> expected = ImmutableSet.of(linesName, pointsName,
                NodeRef.appendChild(linesName, idL1), NodeRef.appendChild(pointsName, idP1));
        Set<String> actual = Sets.newHashSet(Collections2.transform(difflist,
                new Function<DiffEntry, String>() {

                    @Override
                    public String apply(DiffEntry input) {
                        return input.newPath();
                    }
                }));
        assertEquals(expected, actual);
    }

    @Test
    public void testChangedFeatureType() throws Exception {

        insertAndAdd(points1, points2);
        geogig.getRepository().workingTree().updateTypeTree(pointsName, modifiedPointsType);
        List<DiffEntry> difflist = toList(diffOp.setReportTrees(true).call());

        assertNotNull(difflist);
        assertEquals(1, difflist.size());

    }

    @Test
    public void testTreeModifiedByAddingExtraFeature() throws Exception {

        insertAndAdd(points1, points2);
        insert(points3);
        List<DiffEntry> difflist = toList(diffOp.setReportTrees(true).call());
        assertNotNull(difflist);
        assertEquals(2, difflist.size());
        assertEquals(ChangeType.MODIFIED, difflist.get(0).changeType());
        assertEquals(TYPE.TREE, difflist.get(0).getOldObject().getType());
        assertEquals(ChangeType.ADDED, difflist.get(1).changeType());
        assertEquals(TYPE.FEATURE, difflist.get(1).getNewObject().getType());
    }

}
