/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.maven.artifact.repository.metadata;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import com.ctc.wstx.stax.WstxInputFactory;
import com.ctc.wstx.stax.WstxOutputFactory;
import org.apache.maven.artifact.repository.metadata.io.MetadataStaxReader;
import org.apache.maven.artifact.repository.metadata.io.MetadataStaxWriter;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MetadataTest {

    Artifact artifact;

    Metadata target;

    @BeforeEach
    void before() {
        artifact = new DefaultArtifact("myGroup:myArtifact:1.0-SNAPSHOT");
        target = createMetadataFromArtifact(artifact);
    }

    /*--- START test common metadata ---*/
    @Test
    void mergeEmptyMetadata() throws Exception {
        Metadata metadata = new Metadata();
        assertFalse(metadata.merge(new Metadata()));
    }

    @Test
    void mergeDifferentGAV() throws Exception {
        // merge implicitly assumes that merge is only called on the same GAV and does not perform any validation here!
        Metadata source = new Metadata();
        source.setArtifactId("source-artifact");
        source.setGroupId("source-group");
        source.setVersion("2.0");
        assertFalse(target.merge(source));
        assertEquals("myArtifact", target.getArtifactId());
        assertEquals("myGroup", target.getGroupId());
        assertEquals("1.0-SNAPSHOT", target.getVersion());
    }
    /*--- END test common metadata ---*/

    /*--- START test "groupId/artifactId/version" metadata ---*/
    @Test
    void mergeSnapshotWithEmptyList() throws Exception {
        Snapshot snapshot = new Snapshot();
        snapshot.setBuildNumber(3);
        snapshot.setTimestamp("20200710.072412");
        target.getVersioning().setSnapshot(snapshot);
        target.getVersioning().setLastUpdated("20200921071745");
        SnapshotVersion sv = new SnapshotVersion();
        sv.setClassifier("sources");
        sv.setExtension("jar");
        sv.setUpdated("20200710072412");
        target.getVersioning().addSnapshotVersion(sv);

        Metadata source = createMetadataFromArtifact(artifact);
        // nothing should be actually changed, but still merge returns true
        assertTrue(target.merge(source));

        // NOTE! Merge updates last updated to source
        assertEquals("20200921071745", source.getVersioning().getLastUpdated());

        assertEquals("myArtifact", target.getArtifactId());
        assertEquals("myGroup", target.getGroupId());

        assertEquals(3, target.getVersioning().getSnapshot().getBuildNumber());
        assertEquals("20200710.072412", target.getVersioning().getSnapshot().getTimestamp());

        assertEquals(1, target.getVersioning().getSnapshotVersions().size());
        assertEquals(
                "sources", target.getVersioning().getSnapshotVersions().get(0).getClassifier());
        assertEquals("jar", target.getVersioning().getSnapshotVersions().get(0).getExtension());
        assertEquals(
                "20200710072412",
                target.getVersioning().getSnapshotVersions().get(0).getUpdated());
    }

    @Test
    void mergeWithSameSnapshotWithDifferentVersionsAndNewerLastUpdated() {
        Metadata source = createMetadataFromArtifact(artifact);
        Date before = new Date(System.currentTimeMillis() - 5000);
        Date after = new Date(System.currentTimeMillis());
        addSnapshotVersion(target.getVersioning(), "jar", before, "1", 1);
        SnapshotVersion sv2 =
                addSnapshotVersion(source.getVersioning(), "jar", after, "1.0-" + formatDate(after, true) + "-2", 2);
        SnapshotVersion sv3 =
                addSnapshotVersion(source.getVersioning(), "pom", after, "1.0-" + formatDate(after, true) + "-2", 2);
        assertTrue(target.merge(source));
        Versioning actualVersioning = target.getVersioning();
        assertEquals(2, actualVersioning.getSnapshotVersions().size());
        assertEquals(sv2, actualVersioning.getSnapshotVersions().get(0));
        assertEquals(sv3, actualVersioning.getSnapshotVersions().get(1));
        assertEquals(formatDate(after, false), actualVersioning.getLastUpdated());
        assertEquals(formatDate(after, true), actualVersioning.getSnapshot().getTimestamp());
        assertEquals(2, actualVersioning.getSnapshot().getBuildNumber());
    }

    @Test
    void mergeWithSameSnapshotWithDifferentVersionsAndOlderLastUpdated() {
        Metadata source = createMetadataFromArtifact(artifact);
        Date before = new Date(System.currentTimeMillis() - 5000);
        Date after = new Date(System.currentTimeMillis());
        SnapshotVersion sv1 = addSnapshotVersion(target.getVersioning(), after, artifact);
        addSnapshotVersion(source.getVersioning(), before, artifact);
        // nothing should be updated, as the target was already updated at a later date than source
        assertFalse(target.merge(source));
        assertEquals(1, target.getVersioning().getSnapshotVersions().size());
        assertEquals(sv1, target.getVersioning().getSnapshotVersions().get(0));
        assertEquals(formatDate(after, false), target.getVersioning().getLastUpdated());
        assertEquals(
                formatDate(after, true), target.getVersioning().getSnapshot().getTimestamp());
    }

    @Test
    void mergeWithSameSnapshotWithSameVersionAndTimestamp() {
        Metadata source = createMetadataFromArtifact(artifact);
        Date date = new Date();
        addSnapshotVersion(target.getVersioning(), date, artifact);
        SnapshotVersion sv1 = addSnapshotVersion(source.getVersioning(), date, artifact);
        // although nothing has changed merge returns true, as the last modified date is equal
        // TODO: improve merge here?
        assertTrue(target.merge(source));
        assertEquals(1, target.getVersioning().getSnapshotVersions().size());
        assertEquals(sv1, target.getVersioning().getSnapshotVersions().get(0));
        assertEquals(formatDate(date, false), target.getVersioning().getLastUpdated());
        assertEquals(
                formatDate(date, true), target.getVersioning().getSnapshot().getTimestamp());
    }

    @Test
    void mergeLegacyWithSnapshotLegacy() {
        Metadata source = createMetadataFromArtifact(artifact);
        Date before = new Date(System.currentTimeMillis() - 5000);
        Date after = new Date(System.currentTimeMillis());
        // legacy metadata did not have "versioning.snapshotVersions"
        addSnapshotVersionLegacy(target.getVersioning(), before, 1);
        addSnapshotVersionLegacy(source.getVersioning(), after, 2);
        // although nothing has changed merge returns true, as the last modified date is equal
        // TODO: improve merge here?
        assertTrue(target.merge(source));
        assertEquals(0, target.getVersioning().getSnapshotVersions().size());
        assertEquals(formatDate(after, false), target.getVersioning().getLastUpdated());
        assertEquals(
                formatDate(after, true), target.getVersioning().getSnapshot().getTimestamp());
    }

    @Test
    void mergeLegacyWithSnapshot() {
        Metadata source = createMetadataFromArtifact(artifact);
        Date before = new Date(System.currentTimeMillis() - 5000);
        Date after = new Date(System.currentTimeMillis());
        // legacy metadata did not have "versioning.snapshotVersions"
        addSnapshotVersionLegacy(target.getVersioning(), before, 1);
        addSnapshotVersion(source.getVersioning(), after, artifact);
        // although nothing has changed merge returns true, as the last modified date is equal
        // TODO: improve merge here?
        assertTrue(target.merge(source));
        // never convert from legacy format to v1.1 format
        assertEquals(0, target.getVersioning().getSnapshotVersions().size());
        assertEquals(formatDate(after, false), target.getVersioning().getLastUpdated());
        assertEquals(
                formatDate(after, true), target.getVersioning().getSnapshot().getTimestamp());
    }

    @Test
    void mergeWithSnapshotLegacy() {
        Metadata source = createMetadataFromArtifact(artifact);
        Date before = new Date(System.currentTimeMillis() - 5000);
        Date after = new Date(System.currentTimeMillis());
        addSnapshotVersion(target.getVersioning(), before, artifact);
        // legacy metadata did not have "versioning.snapshotVersions"
        addSnapshotVersionLegacy(source.getVersioning(), after, 2);
        // although nothing has changed merge returns true, as the last modified date is equal
        // TODO: improve merge here?
        assertTrue(target.merge(source));
        // the result must be legacy format as well
        assertEquals(0, target.getVersioning().getSnapshotVersions().size());
        assertEquals(formatDate(after, false), target.getVersioning().getLastUpdated());
        assertEquals(
                formatDate(after, true), target.getVersioning().getSnapshot().getTimestamp());
        assertEquals(2, target.getVersioning().getSnapshot().getBuildNumber());
    }
    /*-- END test "groupId/artifactId/version" metadata ---*/

    @Test
    void testRoundtrip() throws Exception {
        System.setProperty(XMLInputFactory.class.getName(), WstxInputFactory.class.getName());
        System.setProperty(XMLOutputFactory.class.getName(), WstxOutputFactory.class.getName());

        Metadata source = new Metadata(org.apache.maven.artifact.repository.metadata.v4.Metadata.newBuilder(
                        createMetadataFromArtifact(artifact).getDelegate(), true)
                .modelEncoding("UTF-16")
                .build());
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new MetadataStaxWriter().write(baos, source.getDelegate());
        Metadata source2 =
                new Metadata(new MetadataStaxReader().read(new ByteArrayInputStream(baos.toByteArray()), true));
        assertNotNull(source2);
    }

    /*-- START helper methods to populate metadata objects ---*/
    private static final String SNAPSHOT = "SNAPSHOT";

    private static final String DEFAULT_SNAPSHOT_TIMESTAMP_FORMAT = "yyyyMMdd.HHmmss";

    private static final String DEFAULT_DATE_FORMAT = "yyyyMMddHHmmss";

    private static String formatDate(Date date, boolean forSnapshotTimestamp) {
        // logic from metadata.mdo, class "Versioning"
        TimeZone timezone = TimeZone.getTimeZone("UTC");
        DateFormat fmt =
                new SimpleDateFormat(forSnapshotTimestamp ? DEFAULT_SNAPSHOT_TIMESTAMP_FORMAT : DEFAULT_DATE_FORMAT);
        fmt.setCalendar(new GregorianCalendar());
        fmt.setTimeZone(timezone);
        return fmt.format(date);
    }

    private static Metadata createMetadataFromArtifact(Artifact artifact) {
        Metadata metadata = new Metadata();
        metadata.setArtifactId(artifact.getArtifactId());
        metadata.setGroupId(artifact.getGroupId());
        metadata.setVersion(artifact.getVersion());
        metadata.setVersioning(new Versioning());
        return metadata;
    }

    private static SnapshotVersion addSnapshotVersion(Versioning versioning, Date timestamp, Artifact artifact) {
        int buildNumber = 1;
        // this generates timestamped versions like maven-resolver-provider:
        // https://github.com/apache/maven/blob/03df5f7c639db744a3597c7175c92c8e2a27767b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/RemoteSnapshotMetadata.java#L79
        String version = artifact.getVersion();
        String qualifier = formatDate(timestamp, true) + '-' + buildNumber;
        version = version.substring(0, version.length() - SNAPSHOT.length()) + qualifier;
        return addSnapshotVersion(versioning, artifact.getExtension(), timestamp, version, buildNumber);
    }

    private static SnapshotVersion addSnapshotVersion(
            Versioning versioning, String extension, Date timestamp, String version, int buildNumber) {
        Snapshot snapshot = new Snapshot();
        snapshot.setBuildNumber(buildNumber);
        snapshot.setTimestamp(formatDate(timestamp, true));

        SnapshotVersion sv = new SnapshotVersion();
        sv.setExtension(extension);
        sv.setVersion(version);
        sv.setUpdated(formatDate(timestamp, false));
        versioning.addSnapshotVersion(sv);

        // make the new snapshot the current one
        versioning.setSnapshot(snapshot);
        versioning.setLastUpdatedTimestamp(timestamp);
        return sv;
    }

    // the format written by Maven 2
    // (https://maven.apache.org/ref/2.2.1/maven-repository-metadata/repository-metadata.html)
    private static void addSnapshotVersionLegacy(Versioning versioning, Date timestamp, int buildNumber) {
        Snapshot snapshot = new Snapshot();
        snapshot.setBuildNumber(buildNumber);
        snapshot.setTimestamp(formatDate(timestamp, true));

        versioning.setSnapshot(snapshot);
        versioning.setLastUpdatedTimestamp(timestamp);
    }
    /*-- END helper methods to populate metadata objects ---*/
}
