/*
 * Copyright 2023 Thoughtworks, Inc.
 *
 * 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 com.thoughtworks.go.domain;

import com.thoughtworks.go.config.CaseInsensitiveString;
import com.thoughtworks.go.config.materials.Filter;
import com.thoughtworks.go.config.materials.IgnoredFiles;
import com.thoughtworks.go.config.materials.ScmMaterialConfig;
import com.thoughtworks.go.config.materials.SubprocessExecutionContext;
import com.thoughtworks.go.config.materials.dependency.DependencyMaterial;
import com.thoughtworks.go.config.materials.mercurial.HgMaterial;
import com.thoughtworks.go.config.materials.svn.SvnMaterial;
import com.thoughtworks.go.domain.materials.*;
import com.thoughtworks.go.domain.materials.dependency.DependencyMaterialRevision;
import com.thoughtworks.go.domain.materials.mercurial.StringRevision;
import com.thoughtworks.go.helper.GoConfigMother;
import com.thoughtworks.go.helper.HgTestRepo;
import com.thoughtworks.go.helper.MaterialsMother;
import com.thoughtworks.go.helper.ModificationsMother;
import com.thoughtworks.go.util.TempDirUtils;
import com.thoughtworks.go.util.command.InMemoryStreamConsumer;
import org.apache.commons.io.FileUtils;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.nio.file.Path;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import static com.thoughtworks.go.helper.ModificationsMother.*;
import static com.thoughtworks.go.util.command.ProcessOutputStreamConsumer.inMemoryConsumer;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class MaterialRevisionTest {
    private static final StringRevision REVISION_0 = new StringRevision("b61d12de515d82d3a377ae3aae6e8abe516a2651");
    private static final StringRevision REVISION_2 = new StringRevision("ca3ebb67f527c0ad7ed26b789056823d8b9af23f");
    @TempDir
    Path tempDir;

    private HgMaterial hgMaterial;
    private File workingFolder;

    @BeforeEach
    public void setUp() throws Exception {
        HgTestRepo hgTestRepo = new HgTestRepo("hgTestRepo1", tempDir);
        hgMaterial = MaterialsMother.hgMaterial(hgTestRepo.projectRepositoryUrl());
        workingFolder = TempDirUtils.createRandomDirectoryIn(tempDir).toFile();
    }

    @Test
    public void shouldGetModifiedTimeFromTheLatestModification() {
        final MaterialRevision materialRevision = new MaterialRevision(MaterialsMother.hgMaterial(), multipleModificationsInHg());
        assertThat(materialRevision.getDateOfLatestModification(), is(ModificationsMother.TODAY_CHECKIN));
    }

    @Test
    public void shouldDetectChangesAfterACheckin() throws Exception {
        MaterialRevision original = new MaterialRevision(hgMaterial,
                hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        checkInOneFile(hgMaterial);
        checkInOneFile(hgMaterial);
        checkInOneFile(hgMaterial);

        final MaterialRevision after = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());

        assertThat(after, not(original));
        assertThat(after.numberOfModifications(), is(3));
        assertThat(after.getRevision(), is(not(original.getRevision())));
        assertThat(after.hasChangedSince(original), is(true));

    }

    @Test
    public void shouldMarkRevisionAsChanged() throws Exception {
        MaterialRevision original = new MaterialRevision(hgMaterial, hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        checkInFiles(hgMaterial, "user.doc");

        MaterialRevision newRev = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRev.isChanged(), is(true));
    }

    @Test
    public void shouldMarkRevisionAsNotChanged() throws Exception {
        List<Modification> modifications = hgMaterial.latestModification(workingFolder, new TestSubprocessExecutionContext());
        MaterialRevision original = new MaterialRevision(hgMaterial, modifications);
        checkInFiles(hgMaterial, "user.doc");
        original = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());

        MaterialRevision newRev = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRev.isChanged(), is(false));
    }

    @Test
    public void shouldIgnoreDocumentCheckin() throws Exception {
        MaterialRevision previousRevision = new MaterialRevision(hgMaterial, hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        Filter filter = new Filter(new IgnoredFiles("**/*.doc"));
        hgMaterial.setFilter(filter);

        checkInFiles(hgMaterial, "user.doc");

        MaterialRevision newRevision = findNewRevision(previousRevision, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRevision.filter(previousRevision), is(previousRevision));
    }

    @Test
    public void shouldIgnoreDocumentWhenCheckin() throws Exception {
        MaterialRevision original = new MaterialRevision(hgMaterial, hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        Filter filter = new Filter(new IgnoredFiles("helper/**/*.*"));
        hgMaterial.setFilter(filter);

        checkInFiles(hgMaterial,
                "helper/topics/installing_go_agent.xml",
                "helper/topics/installing_go_server.xml");

        MaterialRevision newRev = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRev.filter(original), is(original));
    }

    @Test
    public void shouldIgnoreDocumentsWithSemanticallyEqualsIgnoreFilter() throws Exception {
        MaterialRevision original = new MaterialRevision(hgMaterial, hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        Filter filter = new Filter(new IgnoredFiles("**/*.doc"), new IgnoredFiles("*.doc"));
        hgMaterial.setFilter(filter);

        checkInFiles(hgMaterial, "user.doc");

        MaterialRevision newRev = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRev.filter(original), is(original));
    }

    @Test
    public void shouldIncludeJavaFileWithSemanticallyEqualsIgnoreFilter() throws Exception {
        MaterialRevision original = new MaterialRevision(hgMaterial,
                hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        Filter filter = new Filter(new IgnoredFiles("**/*.doc"), new IgnoredFiles("*.doc"));
        GoConfigMother.createPipelineConfig(filter, (ScmMaterialConfig) hgMaterial.config());
        checkInFiles(hgMaterial, "A.java");
        checkInFiles(hgMaterial, "B.doc");
        checkInFiles(hgMaterial, "C.pdf");

        MaterialRevision newRev = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRev.filter(original), is(newRev));
    }

    @Test
    public void shouldNotIgnoreJavaFile() throws Exception {
        MaterialRevision original = new MaterialRevision(hgMaterial,
                hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        Filter filter = new Filter(new IgnoredFiles("**/*.doc"));
        GoConfigMother.createPipelineConfig(filter, (ScmMaterialConfig) hgMaterial.config());
        checkInFiles(hgMaterial, "A.java");

        MaterialRevision newRev = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRev.filter(original), is(newRev));
    }

    @Test
    public void shouldNotIgnoreAnyFileIfFilterIsNotDefinedForTheGivenMaterial() throws Exception {
        MaterialRevision original = new MaterialRevision(hgMaterial,
                hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        Filter filter = new Filter();
        GoConfigMother.createPipelineConfig(filter, (ScmMaterialConfig) hgMaterial.config());
        checkInFiles(hgMaterial, "A.java");

        MaterialRevision newRev = findNewRevision(original, hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        assertThat(newRev.filter(original), is(newRev));
    }

    @Test
    public void shouldMarkRevisionChangeFalseIfNoNewChangesAvailable() {
        Modification modificationForRevisionTip = new Modification(new Date(), REVISION_2.getRevision(), "MOCK_LABEL-12", null);
        MaterialRevision revision = new MaterialRevision(hgMaterial, modificationForRevisionTip);
        MaterialRevision unchangedRevision = findNewRevision(revision, hgMaterial, workingFolder, new TestSubprocessExecutionContext());

        assertThat(unchangedRevision.isChanged(), is(false));
    }

    @Test
    public void shouldReturnOnlyLatestModificationIfNoNewChangesAvailable() {
        Modification modificationForRevisionTip = new Modification("Unknown", "Unknown", null, new Date(), REVISION_2.getRevision());
        Modification olderModification = new Modification("Unknown", "Unknown", null, new Date(), REVISION_0.getRevision());
        MaterialRevision revision = new MaterialRevision(hgMaterial, modificationForRevisionTip, olderModification);
        MaterialRevision unchangedRevision = findNewRevision(revision, hgMaterial, workingFolder, new TestSubprocessExecutionContext());

        assertThat(unchangedRevision.getModifications().size(), is(1));
        assertThat(unchangedRevision.getModifications().get(0), is(modificationForRevisionTip));
    }

    @Test
    public void shouldNotConsiderChangedFlagAsPartOfEqualityAndHashCodeCheck() {
        Modification modification = oneModifiedFile("revision1");
        SvnMaterial material = MaterialsMother.svnMaterial();

        MaterialRevision notChanged = new MaterialRevision(material, false, modification);
        MaterialRevision changed = new MaterialRevision(material, true, modification);
        changed.markAsChanged();

        assertThat(changed, is(notChanged));
        assertThat(changed.hashCode(), is(notChanged.hashCode()));
    }

    @Test
    public void shouldDetectChangedRevision() {
        Modification modification1 = oneModifiedFile("revision1");
        Modification modification2 = oneModifiedFile("revision2");
        SvnMaterial material = MaterialsMother.svnMaterial();
        MaterialRevision materialRevision1 = new MaterialRevision(material, modification1);
        MaterialRevision materialRevision2 = new MaterialRevision(material, modification2);
        assertThat(materialRevision1.hasChangedSince(materialRevision2), is(true));
    }

    @Test
    public void shouldDisplayRevisionAsBuildCausedByForDependencyMaterial() {
        DependencyMaterial dependencyMaterial = new DependencyMaterial(new CaseInsensitiveString("upstream"), new CaseInsensitiveString("stage"));
        MaterialRevision materialRevision = new MaterialRevision(dependencyMaterial, new Modification(new Date(), "upstream/2/stage/1", "1.3-2", null));
        assertThat(materialRevision.buildCausedBy(), is("upstream/2/stage/1"));
    }

    @Test
    public void shouldUseLatestMaterial() {
        MaterialRevision original = new MaterialRevision(hgMaterial,
                hgMaterial.modificationsSince(workingFolder, REVISION_0, new TestSubprocessExecutionContext()));

        HgMaterial newMaterial = MaterialsMother.hgMaterial(hgMaterial.getUrl());
        newMaterial.setFilter(new Filter(new IgnoredFiles("**/*.txt")));
        final MaterialRevision after = findNewRevision(original, newMaterial, workingFolder, new TestSubprocessExecutionContext());

        assertThat(after.getMaterial(), is(newMaterial));
    }

    @Test
    public void shouldDetectLatestAndOldestModification() {
        MaterialRevision materialRevision = new MaterialRevision(hgMaterial, modification("3"), modification("2"), modification("1"));

        assertThat(materialRevision.getLatestModification(), is(modification("3")));
        assertThat(materialRevision.getOldestModification(), is(modification("1")));
    }

    @Test
    public void shouldDetectLatestRevision() {
        MaterialRevision materialRevision = new MaterialRevision(hgMaterial, modification("3"), modification("2"), modification("1"));
        assertThat(materialRevision.getRevision(), is(new StringRevision("3")));
    }

    @Test
    public void shouldDetectOldestScmRevision() {
        MaterialRevision materialRevision = new MaterialRevision(hgMaterial, modification("3"), modification("2"), modification("1"));
        assertThat(materialRevision.getOldestRevision(), is(new StringRevision("1")));
    }

    @Test
    public void shouldDetectOldestAndLatestDependencyRevision() {
        DependencyMaterial dependencyMaterial = new DependencyMaterial(new CaseInsensitiveString("upstream"), new CaseInsensitiveString("stage"));
        MaterialRevision materialRevision = new MaterialRevision(dependencyMaterial, new Modification(new Date(), "upstream/3/stage/1", "1.3-3", null),
                new Modification(new Date(), "upstream/2/stage/1", "1.3-2", null));
        assertThat(materialRevision.getOldestRevision(), is(DependencyMaterialRevision.create("upstream/2/stage/1", "1.3-2")));
        assertThat(materialRevision.getRevision(), is(DependencyMaterialRevision.create("upstream/3/stage/1", "1.3-3")));
    }

    @Test
    public void shouldReturnNullRevisionWhenThereIsNoMaterial() {
        Revision revision = new MaterialRevision(null).getRevision();
        assertThat(revision, is(not(nullValue())));
        assertThat(revision.getRevision(), is(""));
    }

    @Test
    public void shouldReturnFullRevisionForTheLatestModification() {
        assertThat(hgRevision().getLatestRevisionString(), is("012345678901234567890123456789"));
    }

    private MaterialRevision hgRevision() {
        return new MaterialRevision(hgMaterial, modification("012345678901234567890123456789"), modification("2"), modification("1"));
    }

    @Test
    public void shouldReturnShortRevisionForTheLatestModification() {
        assertThat(hgRevision().getLatestShortRevision(), is("012345678901"));
    }

    @Test
    public void shouldReturnMaterialName() {
        assertThat(hgRevision().getMaterialName(), Matchers.is(hgMaterial.getDisplayName()));
    }

    @Test
    public void shouldReturnTruncatedMaterialName() {
        assertThat(hgRevision().getTruncatedMaterialName(), Matchers.is(hgMaterial.getTruncatedDisplayName()));
    }

    @Test
    public void shouldReturnMaterialType() {
        assertThat(hgRevision().getMaterialType(), is("Mercurial"));
    }

    @Test
    public void shouldReturnLatestComments() {
        assertThat(hgRevision().getLatestComment(), is("Checkin 012345678901234567890123456789"));
    }

    @Test
    public void shouldReturnLatestUser() {
        assertThat(hgRevision().getLatestUser(), is("user"));
    }

    @Test
    public void shouldRecomputeIsChanged() {
        MaterialRevision original = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev2"), oneModifiedFile("rev1")).getMaterialRevision(0);
        MaterialRevision recomputed = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev1")).getMaterialRevision(0);
        MaterialRevision recomputedAnother = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev0")).getMaterialRevision(0);

        recomputed.updateRevisionChangedStatus(original);
        recomputedAnother.updateRevisionChangedStatus(original);
        assertThat(recomputed.isChanged(), is(false));
        assertThat(recomputedAnother.isChanged(), is(false));

        original.markAsChanged();
        recomputed.updateRevisionChangedStatus(original);
        recomputedAnother.updateRevisionChangedStatus(original);
        assertThat(recomputed.isChanged(), is(true));
        assertThat(recomputedAnother.isChanged(), is(false));
    }

    @Test
    public void shouldRemoveFromThisWhateverModificationIsPresentInThePassedInRevision() {
        MaterialRevision revision = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev2"), oneModifiedFile("rev1")).getMaterialRevision(0);
        MaterialRevision passedIn = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev1")).getMaterialRevision(0);

        MaterialRevision expected = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev2")).getMaterialRevision(0);

        assertThat(revision.subtract(passedIn), is(expected));
    }

    @Test
    public void shouldReturnCurrentIfThePassedInDoesNotHaveAnythingThatCurrentHas() {
        MaterialRevision revision = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev2")).getMaterialRevision(0);
        MaterialRevision passedIn = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev1")).getMaterialRevision(0);

        MaterialRevision expected = createHgMaterialWithMultipleRevisions(1, oneModifiedFile("rev2")).getMaterialRevision(0);
        assertThat(revision.subtract(passedIn), is(expected));
    }

    private Modification modification(String revision) {
        return new Modification("user", "Checkin "
                + revision, null, null, revision);
    }

    private void checkInOneFile(HgMaterial hgMaterial) throws Exception {
        checkInFiles(hgMaterial, UUID.randomUUID().toString());
    }

    private void checkInFiles(HgMaterial hgMaterial, String... fileNames) throws Exception {
        final File localDir = TempDirUtils.createRandomDirectoryIn(tempDir).toFile();
        InMemoryStreamConsumer consumer = inMemoryConsumer();
        Revision revision = latestRevision(hgMaterial, workingFolder, new TestSubprocessExecutionContext());
        hgMaterial.updateTo(consumer, localDir, new RevisionContext(revision), new TestSubprocessExecutionContext());
        for (String fileName : fileNames) {
            File file = new File(localDir, fileName);
            FileUtils.writeStringToFile(file, "", UTF_8);
            hgMaterial.add(localDir, consumer, file);
        }
        hgMaterial.commit(localDir, consumer, "Adding a new file.", "TEST");
        hgMaterial.push(localDir, consumer);
    }

    private Revision latestRevision(HgMaterial material, File workingDir, TestSubprocessExecutionContext execCtx) {
        List<Modification> modifications = material.latestModification(workingDir, execCtx);
        return new Modifications(modifications).latestRevision(material);
    }

    public MaterialRevision findNewRevision(MaterialRevision materialRevision, HgMaterial material, File workingFolder, final SubprocessExecutionContext execCtx) {
        List<Modification> newModifications = material.modificationsSince(workingFolder, materialRevision.getRevision(), execCtx);
        return materialRevision.latestChanges(material, materialRevision.getModifications(), newModifications);
    }
}
