/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gradle.api.internal.changedetection;

import org.gradle.api.DefaultTask;
import org.gradle.api.internal.TaskInternal;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.invocation.Gradle;
import org.gradle.cache.CacheBuilder;
import org.gradle.cache.CacheRepository;
import org.gradle.cache.PersistentCache;
import org.gradle.cache.PersistentIndexedCache;
import org.gradle.util.*;
import org.hamcrest.Matcher;
import org.jmock.Expectations;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.util.*;

import static org.gradle.util.Matchers.*;
import static org.gradle.util.WrapUtil.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

@RunWith(JMock.class)
public class DefaultTaskArtifactStateRepositoryTest {
    @Rule
    public TemporaryFolder tmpDir = new TemporaryFolder();
    private final JUnit4Mockery context = new JUnit4GroovyMockery();
    private final CacheRepository cacheRepository = context.mock(CacheRepository.class);
    private final ProjectInternal project = HelperUtil.createRootProject();
    private final Gradle gradle = project.getGradle();
    private final TestFile outputFile = tmpDir.file("output-file");
    private final TestFile outputDir = tmpDir.file("output-dir");
    private final TestFile outputDirFile = outputDir.file("some-file");
    private final TestFile outputDirFile2 = outputDir.file("some-file-2");
    private final TestFile emptyOutputDir = tmpDir.file("empty-output-dir");
    private final TestFile missingOutputFile = tmpDir.file("missing-output-file");
    private final TestFile inputFile = tmpDir.createFile("input-file");
    private final TestFile inputDir = tmpDir.createDir("input-dir");
    private final TestFile inputDirFile = inputDir.file("input-file2").createFile();
    private final TestFile missingInputFile = tmpDir.file("missing-input-file");
    private final Set<TestFile> inputFiles = toSet(inputFile, inputDir, missingInputFile);
    private final Set<TestFile> outputFiles = toSet(outputFile, outputDir, emptyOutputDir, missingOutputFile);
    private final Set<TestFile> createFiles = toSet(outputFile, outputDirFile, outputDirFile2);
    private final PersistentCache persistentCache = context.mock(PersistentCache.class);
    private DefaultTaskArtifactStateRepository repository;

    @Before
    public void setup() {
        context.checking(new Expectations(){{
            CacheBuilder builder = context.mock(CacheBuilder.class);

            one(cacheRepository).cache("outputFileStates");
            will(returnValue(builder));

            one(builder).open();
            will(returnValue(persistentCache));

            one(persistentCache).openIndexedCache();
            will(returnValue(new TestIndexedCache()));
        }});

        FileSnapshotter inputFilesSnapshotter = new DefaultFileSnapshotter(new DefaultHasher());
        FileSnapshotter outputFilesSnapshotter = new OutputFilesSnapshotter(inputFilesSnapshotter, new RandomLongIdGenerator(), cacheRepository);
        repository = new DefaultTaskArtifactStateRepository(cacheRepository, inputFilesSnapshotter, outputFilesSnapshotter);
    }

    @Test
    public void artifactsAreNotUpToDateWhenCacheIsEmpty() {
        expectEmptyCacheLocated();

        TaskArtifactState state = repository.getStateFor(task());
        assertNotNull(state);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyOutputFileNoLongerExists() {
        execute();

        outputFile.delete();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyFileInOutputDirNoLongerExists() {
        execute();

        outputDirFile.delete();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyOutputFileHasChangedType() {
        execute();

        outputFile.delete();
        outputFile.createDir();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyFileInOutputDirHasChangedType() {
        execute();

        outputDirFile.delete();
        outputDirFile.createDir();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyOutputFileHasChangedHash() {
        execute();

        outputFile.write("new content");

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyFileInOutputDirHasChangedHash() {
        execute();

        outputDirFile.write("new content");

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyOutputFilesAddedToSet() {
        execute();

        TaskInternal task = builder().withOutputFiles(outputFile, outputDir, tmpDir.createFile("output-file-2"), emptyOutputDir, missingOutputFile).task();

        TaskArtifactState state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyOutputFilesRemovedFromSet() {
        execute();

        TaskInternal task = builder().withOutputFiles(outputFile, emptyOutputDir, missingOutputFile).task();

        TaskArtifactState state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenTaskWithDifferentTypeGeneratedAnyOutputFiles() {
        TaskInternal task1 = builder().withOutputFiles(outputFile).task();
        TaskInternal task2 = builder().withType(TaskSubType.class).withOutputFiles(outputFile).task();

        execute(task1, task2);

        TaskArtifactState state = repository.getStateFor(task1);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputFilesAddedToSet() {
        execute();

        TaskInternal task = builder().withInputFiles(inputFile, inputDir, tmpDir.createFile("other-input"), missingInputFile).task();
        TaskArtifactState state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputFilesRemovedFromSet() {
        execute();

        TaskInternal task = builder().withInputFiles(inputFile).task();
        TaskArtifactState state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputFileHasChangedHash() {
        execute();

        inputFile.write("some new content");

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputFileHasChangedType() {
        execute();

        inputFile.delete();
        inputFile.createDir();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputFileNoLongerExists() {
        execute();

        inputFile.delete();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyFileCreatedInInputDir() {
        execute();

        inputDir.file("other-file").createFile();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyFileDeletedFromInputDir() {
        execute();

        inputDirFile.delete();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyFileInInputDirChangesHash() {
        execute();

        inputDirFile.writelns("new content");

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }
    
    @Test
    public void artifactsAreNotUpToDateWhenAnyFileInInputDirChangesType() {
        execute();

        inputDirFile.delete();
        inputDirFile.mkdir();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputPropertyValueChanged() {
        execute();

        TaskArtifactState state = repository.getStateFor(builder().withProperty("prop", "new value").task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void inputPropertyValueCanBeNull() {
        TaskInternal task = builder().withProperty("prop", null).task();
        execute(task);

        TaskArtifactState state = repository.getStateFor(task);
        assertTrue(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputPropertyAdded() {
        execute();

        TaskArtifactState state = repository.getStateFor(builder().withProperty("prop2", "value").task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenAnyInputPropertyRemoved() {
        execute(builder().withProperty("prop2", "value").task());

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenStateHasNotBeenUpdated() {
        expectEmptyCacheLocated();
        repository.getStateFor(task());

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenOutputDirWhichUsedToExistHasBeenDeleted() {
        // Output dir already exists before first execution of task
        outputDirFile.createFile();
        expectEmptyCacheLocated();

        TaskInternal task1 = builder().withOutputFiles(outputDir).createsFiles(outputDirFile).task();
        TaskInternal task2 = builder().withPath("other").withOutputFiles(outputDir).createsFiles(outputDirFile2).task();

        TaskArtifactState state = repository.getStateFor(task1);
        assertFalse(state.isUpToDate());
        state.update();

        outputDir.deleteDir();

        // Another task creates dir
        state = repository.getStateFor(task2);
        assertFalse(state.isUpToDate());
        task2.execute();
        state.update();

        // Task should be out-of-date
        state = repository.getStateFor(task1);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreUpToDateWhenNothingHasChangedSinceOutputFilesWereGenerated() {
        execute();

        TaskArtifactState state = repository.getStateFor(task());
        assertTrue(state.isUpToDate());

        state = repository.getStateFor(task());
        assertTrue(state.isUpToDate());
    }

    @Test
    public void artifactsAreUpToDateWhenOutputFileWhichDidNotExistNowExists() {
        execute();

        missingOutputFile.touch();

        TaskArtifactState state = repository.getStateFor(task());
        assertTrue(state.isUpToDate());
    }

    @Test
    public void artifactsAreUpToDateWhenOutputDirWhichWasEmptyIsNoLongerEmpty() {
        execute();

        emptyOutputDir.file("some-file").touch();

        TaskArtifactState state = repository.getStateFor(task());
        assertTrue(state.isUpToDate());
    }

    @Test
    public void hasEmptyTaskHistoryWhenTaskHasNeverBeenExecuted() {
        expectEmptyCacheLocated();

        TaskArtifactState state = repository.getStateFor(task());
        assertThat(state.getOutputFiles().getFiles(), isEmpty());
    }
    
    @Test
    public void hasTaskHistoryFromPreviousExecution() {
        execute();

        TaskArtifactState state = repository.getStateFor(task());
        assertThat(state.getOutputFiles().getFiles(), equalTo(toLinkedSet((File) outputFile, outputDirFile, outputDirFile2)));
    }

    @Test
    public void multipleTasksCanProduceFilesIntoTheSameOutputDirectory() {
        TaskInternal task1 = task();
        TaskInternal task2 = builder().withPath("other").withOutputFiles(outputDir).createsFiles(outputDir.file("output2")).task();
        execute(task1, task2);

        TaskArtifactState state = repository.getStateFor(task1);
        assertTrue(state.isUpToDate());

        state = repository.getStateFor(task2);
        assertTrue(state.isUpToDate());
    }

    @Test
    public void multipleTasksCanProduceTheSameFileWithTheSameContents() {
        TaskInternal task1 = builder().withOutputFiles(outputFile).task();
        TaskInternal task2 = builder().withPath("other").withOutputFiles(outputFile).task();
        execute(task1, task2);

        TaskArtifactState state = repository.getStateFor(task1);
        assertTrue(state.isUpToDate());

        state = repository.getStateFor(task2);
        assertTrue(state.isUpToDate());
    }

    @Test
    public void multipleTasksCanProduceTheSameEmptyDir() {
        TaskInternal task1 = task();
        TaskInternal task2 = builder().withPath("other").withOutputFiles(outputDir).task();
        execute(task1, task2);

        TaskArtifactState state = repository.getStateFor(task1);
        assertTrue(state.isUpToDate());

        state = repository.getStateFor(task2);
        assertTrue(state.isUpToDate());
    }

    @Test
    public void doesNotConsiderExistingFilesInOutputDirectoryAsProducedByTask() {
        TestFile otherFile = outputDir.file("other").createFile();

        execute();

        otherFile.delete();

        TaskArtifactState state = repository.getStateFor(task());
        assertTrue(state.isUpToDate());
        assertThat(state.getOutputFiles().getFiles(), (Matcher) not(hasItem(otherFile)));
    }

    @Test
    public void considersExistingFileInOutputDirectoryWhichIsUpdatedByTheTaskAsProducedByTask() {
        expectEmptyCacheLocated();
        
        TestFile otherFile = outputDir.file("other").createFile();

        TaskInternal task = task();
        TaskArtifactState state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());

        task.execute();
        otherFile.write("new content");

        state.update();

        otherFile.delete();

        state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
        assertThat(state.getOutputFiles().getFiles(), (Matcher) hasItem(otherFile));
    }

    @Test
    public void fileIsNoLongerConsideredProducedByTaskOnceItIsDeleted() {
        execute();

        outputDirFile.delete();

        TaskArtifactState state = repository.getStateFor(task());
        assertFalse(state.isUpToDate());
        state.update();

        outputDirFile.write("ignore me");

        state = repository.getStateFor(task());
        assertTrue(state.isUpToDate());
        assertThat(state.getOutputFiles().getFiles(), (Matcher) not(hasItem(outputDirFile)));
        state.update();
    }

    @Test
    public void artifactsAreUpToDateWhenTaskDoesNotAcceptAnyInputs() {
        TaskInternal task = builder().doesNotAcceptInput().task();
        execute(task);

        TaskArtifactState state = repository.getStateFor(task);
        assertTrue(state.isUpToDate());

        outputDirFile.delete();

        state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void artifactsAreNotUpToDateWhenTaskDoesNotProduceAnyOutputs() {
        TaskInternal task = builder().doesNotProduceOutput().task();
        execute(task);

        TaskArtifactState state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());
    }

    @Test
    public void taskHistoryIsEmptyWhenTaskDoesNotProduceAnyOutout() {
        TaskInternal task = builder().doesNotProduceOutput().task();
        execute(task);

        TaskArtifactState state = repository.getStateFor(task);
        assertFalse(state.isUpToDate());
        assertThat(state.getOutputFiles(), isEmpty());
    }

    @Test
    public void artifactsAreUpToDateWhenTaskHasNoInputFiles() {
        TaskInternal task = builder().withInputFiles().task();
        execute(task);

        TaskArtifactState state = repository.getStateFor(task);
        assertTrue(state.isUpToDate());
    }

    @Test
    public void artifactsAreUpToDateWhenTaskHasNoOutputs() {
        TaskInternal task = builder().withOutputFiles().task();
        execute(task);

        TaskArtifactState state = repository.getStateFor(task);
        assertTrue(state.isUpToDate());
    }

    @Test
    public void taskCanProduceIntoDifferentSetsOfOutputFiles() {
        TestFile outputDir2 = tmpDir.createDir("output-dir-2");
        TestFile outputDirFile2 = outputDir2.file("output-file-2");
        TaskInternal instance1 = builder().withOutputFiles(outputDir).createsFiles(outputDirFile).task();
        TaskInternal instance2 = builder().withOutputFiles(outputDir2).createsFiles(outputDirFile2).task();

        execute(instance1, instance2);

        TaskArtifactState state = repository.getStateFor(instance1);
        assertTrue(state.isUpToDate());
        assertThat(state.getOutputFiles().getFiles(), equalTo(toLinkedSet((File) outputDirFile)));

        state = repository.getStateFor(instance2);
        assertTrue(state.isUpToDate());
        assertThat(state.getOutputFiles().getFiles(), equalTo(toLinkedSet((File) outputDirFile2)));
    }

    private void execute() {
        execute(task());
    }

    private void execute(TaskInternal... tasks) {
        expectEmptyCacheLocated();
        for (TaskInternal task : tasks) {
            TaskArtifactState state = repository.getStateFor(task);
            state.isUpToDate();
            task.execute();
            state.update();
        }
    }
    
    private void expectEmptyCacheLocated() {
        context.checking(new Expectations(){{
            CacheBuilder builder = context.mock(CacheBuilder.class);

            one(cacheRepository).cache("taskArtifacts");
            will(returnValue(builder));

            one(builder).forObject(gradle);
            will(returnValue(builder));

            one(builder).open();
            will(returnValue(persistentCache));
            
            one(persistentCache).openIndexedCache();
            will(returnValue(new TestIndexedCache()));
        }});
    }

    private TaskInternal task() {
        return builder().task();
    }

    private TaskBuilder builder() {
        return new TaskBuilder();
    }

    private class TaskBuilder {
        private String path = "task";
        private Collection<? extends File> inputs = inputFiles;
        private Collection<? extends File> outputs = outputFiles;
        private Collection<? extends TestFile> create = createFiles;
        private Class<? extends TaskInternal> type = TaskInternal.class;
        private Map<String, Object> inputProperties = new HashMap<String, Object>(toMap("prop", "value"));

        TaskBuilder withInputFiles(File... inputFiles) {
            inputs = Arrays.asList(inputFiles);
            return this;
        }

        TaskBuilder withOutputFiles(File... outputFiles) {
            outputs = Arrays.asList(outputFiles);
            return this;
        }

        TaskBuilder createsFiles(TestFile... outputFiles) {
            create = Arrays.asList(outputFiles);
            return this;
        }

        TaskBuilder withPath(String path) {
            this.path = path;
            return this;
        }

        TaskBuilder withType(Class<? extends TaskInternal> type) {
            this.type = type;
            return this;
        }

        TaskBuilder doesNotAcceptInput() {
            inputs = null;
            inputProperties = null;
            return this;
        }

        public TaskBuilder doesNotProduceOutput() {
            outputs = null;
            return this;
        }

        public TaskBuilder withProperty(String name, Object value) {
            inputProperties.put(name, value);
            return this;
        }

        TaskInternal task() {
            final TaskInternal task = HelperUtil.createTask(type, project, path);
            if (inputs != null) {
                task.getInputs().files(inputs);
            }
            if (inputProperties != null) {
                task.getInputs().properties(inputProperties);
            }
            if (outputs != null) {
                task.getOutputs().files(outputs);
            }
            task.doLast(new org.gradle.api.Action<Object>() {
                public void execute(Object o) {
                    for (TestFile file : create) {
                        file.createFile();
                    }
                }
            });

            return task;
        }
    }

    public static class TaskSubType extends DefaultTask {
    }

    public static class TestIndexedCache implements PersistentIndexedCache<Object, Object> {
        Map<Object, Object> entries = new HashMap<Object, Object>();

        public Object get(Object key) {
            return entries.get(key);
        }

        public void put(Object key, Object value) {
            entries.put(key, value);
        }

        public void remove(Object key) {
            entries.remove(key);
        }
    }
}
