/*
 *  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
 *
 *      https://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.ivy.core.publish;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;

import org.apache.ivy.Ivy;
import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.event.IvyEvent;
import org.apache.ivy.core.event.publish.EndArtifactPublishEvent;
import org.apache.ivy.core.event.publish.PublishEvent;
import org.apache.ivy.core.event.publish.StartArtifactPublishEvent;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.MDArtifact;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ArtifactRevisionId;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorParser;
import org.apache.ivy.plugins.resolver.MockResolver;
import org.apache.ivy.plugins.trigger.AbstractTrigger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class PublishEventsTest {

    // maps ArtifactRevisionId to PublishTestCase instance.
    private HashMap<ArtifactRevisionId, PublishTestCase> expectedPublications;

    // expected values for the current artifact being published.
    private PublishTestCase currentTestCase;

    private boolean expectedOverwrite;

    // number of times PrePublishTrigger has been invoked successfully
    private int preTriggers;

    // number of times PostPublishTrigger has been invoked successfully
    private int postTriggers;

    // number of times an artifact has been successfully published by the resolver
    private int publications;

    // dummy test data that is reused by all cases.
    private File ivyFile;

    private Artifact ivyArtifact;

    private File dataFile;

    private Artifact dataArtifact;

    private ModuleDescriptor publishModule;

    private Collection<String> publishSources;

    private PublishOptions publishOptions;

    // if non-null, InstrumentedResolver will throw this exception during publish
    private IOException publishError;

    // the ivy instance under test
    private Ivy ivy;

    private PublishEngine publishEngine;

    @Before
    public void setUp() throws Exception {
        // reset test case state.
        resetCounters();

        // this ivy settings should configure an InstrumentedResolver, PrePublishTrigger, and
        // PostPublishTrigger
        // (see inner classes below).
        ivy = Ivy.newInstance();
        ivy.configure(PublishEventsTest.class.getResource("ivysettings-publisheventstest.xml"));
        ivy.pushContext();
        publishEngine = ivy.getPublishEngine();

        // setup dummy ivy and data files to test publishing. since we're testing the engine and not
        // the resolver,
        // we don't really care whether the file actually gets published. we just want to make sure
        // that the engine calls the correct methods in the correct order, and fires required
        // events.
        ivyFile = new File("test/java/org/apache/ivy/core/publish/ivy-1.0-dev.xml");
        assertTrue("path to ivy file not found in test environment", ivyFile.exists());
        // the contents of the data file don't matter.
        dataFile = File.createTempFile("ivydata", ".jar");
        dataFile.deleteOnExit();

        publishModule = XmlModuleDescriptorParser.getInstance().parseDescriptor(ivy.getSettings(),
            ivyFile.toURI().toURL(), false);
        // always use the same source data file, no pattern substitution is required.
        publishSources = Collections.singleton(dataFile.getAbsolutePath());
        // always use the same ivy file, no pattern substitution is required.
        publishOptions = new PublishOptions();
        publishOptions.setSrcIvyPattern(ivyFile.getAbsolutePath());

        // set up our expectations for the test. these variables will
        // be checked by the resolver and triggers during publication.
        dataArtifact = publishModule.getAllArtifacts()[0];
        assertEquals("sanity check", "foo", dataArtifact.getName());
        ivyArtifact = MDArtifact.newIvyArtifact(publishModule);

        expectedPublications = new HashMap<>();
        expectedPublications.put(dataArtifact.getId(), new PublishTestCase(dataArtifact, dataFile,
                true));
        expectedPublications.put(ivyArtifact.getId(), new PublishTestCase(ivyArtifact, ivyFile,
                true));
        assertEquals("hashCode sanity check:  two artifacts expected during publish", 2,
            expectedPublications.size());

        // push the TestCase instance onto the context stack, so that our
        // triggers and resolver instances can interact with it it.
        IvyContext.getContext().push(PublishEventsTest.class.getName(), this);
    }

    @After
    public void tearDown() {
        // reset test state.
        resetCounters();

        // test case is finished, pop the test context off the stack.
        IvyContext.getContext().pop(PublishEventsTest.class.getName());

        // cleanup ivy resources
        if (ivy != null) {
            ivy.popContext();
            ivy = null;
        }
        publishEngine = null;
        if (dataFile != null) {
            dataFile.delete();
        }
        dataFile = null;
        ivyFile = null;
    }

    private void resetCounters() {
        preTriggers = 0;
        postTriggers = 0;
        publications = 0;

        expectedPublications = null;
        expectedOverwrite = false;
        publishError = null;
        currentTestCase = null;

        ivyArtifact = null;
        dataArtifact = null;
    }

    /**
     * Test a simple artifact publish, without errors or overwrite settings.
     *
     * @throws IOException if something goes wrong
     */
    @Test
    public void testPublishNoOverwrite() throws IOException {
        // no modifications to input required for this case -- call out to the resolver, and verify
        // that
        // all of our test counters have been incremented.
        Collection<Artifact> missing = publishEngine.publish(publishModule.getModuleRevisionId(),
            publishSources, "default", publishOptions);
        assertEquals("no missing artifacts", 0, missing.size());

        // if all tests passed, all of our counter variables should have been updated.
        assertEquals("pre-publish trigger fired and passed all tests", 2, preTriggers);
        assertEquals("post-publish trigger fired and passed all tests", 2, postTriggers);
        assertEquals("resolver received a publish() call, and passed all tests", 2, publications);
        assertEquals("all expected artifacts have been published", 0, expectedPublications.size());
    }

    /**
     * Test a simple artifact publish, with overwrite set to true.
     *
     * @throws IOException if something goes wrong
     */
    @Test
    public void testPublishWithOverwrite() throws IOException {
        // we expect the overwrite settings to be passed through the event listeners and into the
        // publisher.
        this.expectedOverwrite = true;

        // set overwrite to true. InstrumentedResolver will verify that the correct argument value
        // was provided.
        publishOptions.setOverwrite(true);
        Collection<Artifact> missing = publishEngine.publish(publishModule.getModuleRevisionId(),
            publishSources, "default", publishOptions);
        assertEquals("no missing artifacts", 0, missing.size());

        // if all tests passed, all of our counter variables should have been updated.
        assertEquals("pre-publish trigger fired and passed all tests", 2, preTriggers);
        assertEquals("post-publish trigger fired and passed all tests", 2, postTriggers);
        assertEquals("resolver received a publish() call, and passed all tests", 2, publications);
        assertEquals("all expected artifacts have been published", 0, expectedPublications.size());
    }

    /**
     * Test an attempted publish with an invalid data file path.
     *
     * @throws IOException if something goes wrong
     */
    @Test
    public void testPublishMissingFile() throws IOException {
        // delete the datafile. the publish should fail
        // and the ivy artifact should still publish successfully.
        assertTrue("datafile has been destroyed", dataFile.delete());
        PublishTestCase dataPublish = expectedPublications.get(dataArtifact.getId());
        dataPublish.expectedSuccess = false;
        Collection<Artifact> missing = publishEngine.publish(publishModule.getModuleRevisionId(),
            publishSources, "default", publishOptions);
        assertEquals("one missing artifact", 1, missing.size());
        assertSameArtifact("missing artifact was returned", dataArtifact, missing.iterator().next());

        // if all tests passed, all of our counter variables should have been updated.
        assertEquals("pre-publish trigger fired and passed all tests", 1, preTriggers);
        assertEquals("post-publish trigger fired and passed all tests", 1, postTriggers);
        assertEquals("only the ivy file published successfully", 1, publications);
        assertEquals("publish of all expected artifacts has been attempted", 1,
            expectedPublications.size());
    }

    /**
     * Test an attempted publish in which the target resolver throws an IOException.
     */
    @Test
    public void testPublishWithException() {
        // set an error to be thrown during publication of the data file.
        this.publishError = new IOException("boom!");
        // we don't care which artifact is attempted; either will fail with an IOException.
        for (PublishTestCase publishTestCase : expectedPublications.values()) {
            publishTestCase.expectedSuccess = false;
        }

        try {
            publishEngine.publish(publishModule.getModuleRevisionId(), publishSources, "default",
                publishOptions);
            fail("if the resolver throws an exception, the engine should too");
        } catch (IOException expected) {
            assertSame("exception thrown by the resolver should be propagated by the engine",
                this.publishError, expected);
        }

        // the publish engine gives up after the resolver throws an exception on the first artifact,
        // so only one set of events should have been fired.
        // note that the initial publish error shouldn't prevent the post-publish trigger from
        // firing.
        assertEquals("pre-publish trigger fired and passed all tests", 1, preTriggers);
        assertEquals("post-publish trigger fired and passed all tests", 1, postTriggers);
        assertEquals("resolver never published successfully", 0, publications);
        assertEquals("publication aborted after first failure", 1, expectedPublications.size());
    }

    /**
     * Assert that two Artifact instances refer to the same artifact and contain the same metadata.
     *
     * @param message
     *            String
     * @param expected
     *            Artifact
     * @param actual
     *            Artifact
     */
    public static void assertSameArtifact(String message, Artifact expected, Artifact actual) {
        assertEquals(message + ": name", expected.getName(), actual.getName());
        assertEquals(message + ": id", expected.getId(), actual.getId());
        assertEquals(message + ": moduleRevisionId", expected.getModuleRevisionId(),
            actual.getModuleRevisionId());
        assertTrue(message + ": configurations",
            Arrays.equals(expected.getConfigurations(), actual.getConfigurations()));
        assertEquals(message + ": type", expected.getType(), actual.getType());
        assertEquals(message + ": ext", expected.getExt(), actual.getExt());
        assertEquals(message + ": publicationDate", expected.getPublicationDate(),
            actual.getPublicationDate());
        assertEquals(message + ": attributes", expected.getAttributes(), actual.getAttributes());
        assertEquals(message + ": url", expected.getUrl(), actual.getUrl());
    }

    public static class PublishTestCase {
        public Artifact expectedArtifact;

        public File expectedData;

        public boolean expectedSuccess;

        public boolean preTriggerFired;

        public boolean published;

        public boolean postTriggerFired;

        public PublishTestCase(Artifact artifact, File data, boolean success) {
            this.expectedArtifact = artifact;
            this.expectedData = data;
            this.expectedSuccess = success;
        }
    }

    /**
     * Base class for pre- and post-publish-artifact triggers. When the trigger receives an event,
     * the contents of the publish event are examined to make sure they match the variable settings
     * on the calling {@link PublishEventsTest#currentTestCase} instance.
     */
    public static class TestPublishTrigger extends AbstractTrigger {

        public void progress(IvyEvent event) {
            PublishEventsTest test = (PublishEventsTest) IvyContext.getContext().peek(
                PublishEventsTest.class.getName());
            InstrumentedResolver resolver = (InstrumentedResolver) test.ivy.getSettings()
                    .getResolver("default");

            assertNotNull("instrumented resolver configured", resolver);
            assertNotNull("got a reference to the current unit test case", test);

            // test the proper sequence of events by comparing the number of pre-events,
            // post-events, and actual publications.
            assertTrue("event is of correct base type", event instanceof PublishEvent);

            PublishEvent pubEvent = (PublishEvent) event;
            Artifact expectedArtifact = test.currentTestCase.expectedArtifact;
            File expectedData = test.currentTestCase.expectedData;

            assertSameArtifact("event records correct artifact", expectedArtifact,
                pubEvent.getArtifact());
            try {
                assertEquals("event records correct file", expectedData.getCanonicalPath(),
                    pubEvent.getData().getCanonicalPath());

                assertEquals("event records correct overwrite setting", test.expectedOverwrite,
                    pubEvent.isOverwrite());
                assertSame("event presents correct resolver", resolver, pubEvent.getResolver());

                String[] attributes = {"organisation", "module", "revision", "artifact", "type",
                        "ext", "resolver", "overwrite"};
                String[] values = {"apache", "PublishEventsTest", "1.0-dev",
                        expectedArtifact.getName(), expectedArtifact.getType(),
                        expectedArtifact.getExt(), "default",
                        String.valueOf(test.expectedOverwrite)};

                for (int i = 0; i < attributes.length; ++i) {
                    assertEquals("event declares correct value for " + attributes[i], values[i],
                        event.getAttributes().get(attributes[i]));
                }
                // we test file separately, since it is hard to guarantee an exact path match, but
                // we want to make sure that both paths point to the same canonical location on the
                // filesystem
                String filePath = event.getAttributes().get("file");
                assertEquals("event declares correct value for file",
                    expectedData.getCanonicalPath(), new File(filePath).getCanonicalPath());
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }
        }

    }

    /**
     * Extends the tests done by {@link TestPublishTrigger} to check that pre-publish events are
     * fired before DependencyResolver.publish() is called, and before post-publish events are
     * fired.
     */
    public static class PrePublishTrigger extends TestPublishTrigger {

        public void progress(IvyEvent event) {

            PublishEventsTest test = (PublishEventsTest) IvyContext.getContext().peek(
                PublishEventsTest.class.getName());
            assertTrue("event is of correct concrete type",
                event instanceof StartArtifactPublishEvent);
            StartArtifactPublishEvent startEvent = (StartArtifactPublishEvent) event;

            // verify that the artifact being publish was in the expected set. set the
            // 'currentTestCase'
            // pointer so that the resolver and post-publish trigger can check against it.
            Artifact artifact = startEvent.getArtifact();
            assertNotNull("event defines artifact", artifact);

            PublishTestCase currentTestCase = test.expectedPublications.remove(artifact.getId());
            assertNotNull("artifact " + artifact.getId() + " was expected for publication",
                currentTestCase);
            assertFalse("current publication has not been visited yet",
                currentTestCase.preTriggerFired);
            assertFalse("current publication has not been visited yet", currentTestCase.published);
            assertFalse("current publication has not been visited yet",
                currentTestCase.postTriggerFired);
            test.currentTestCase = currentTestCase;

            // superclass tests common attributes of publish events
            super.progress(event);

            // increment the call counter in the test
            currentTestCase.preTriggerFired = true;
            ++test.preTriggers;
        }

    }

    /**
     * Extends the tests done by {@link TestPublishTrigger} to check that post-publish events are
     * fired after DependencyResolver.publish() is called, and that the "status" attribute is set to
     * the correct value.
     */
    public static class PostPublishTrigger extends TestPublishTrigger {

        public void progress(IvyEvent event) {
            // superclass tests common attributes of publish events
            super.progress(event);

            PublishEventsTest test = (PublishEventsTest) IvyContext.getContext().peek(
                PublishEventsTest.class.getName());

            // test the proper sequence of events by comparing the current count of pre-events,
            // post-events, and actual publications.
            assertTrue("event is of correct concrete type",
                event instanceof EndArtifactPublishEvent);
            assertTrue("pre-publish event has been triggered", test.preTriggers > 0);

            // test sequence of events
            assertTrue("pre-trigger event has already been fired for this artifact",
                test.currentTestCase.preTriggerFired);
            assertEquals("publication has been done if possible",
                test.currentTestCase.expectedSuccess, test.currentTestCase.published);
            assertFalse("post-publish event has not yet been fired for this artifact",
                test.currentTestCase.postTriggerFired);

            // test the "status" attribute of the post- event.
            EndArtifactPublishEvent endEvent = (EndArtifactPublishEvent) event;
            assertEquals("status bit is set correctly", test.currentTestCase.expectedSuccess,
                endEvent.isSuccessful());

            String expectedStatus = test.currentTestCase.expectedSuccess ? "successful" : "failed";
            assertEquals("status attribute is set to correct value", expectedStatus, endEvent
                    .getAttributes().get("status"));

            // increment the call counter in the wrapper test
            test.currentTestCase.postTriggerFired = true;
            ++test.postTriggers;
        }

    }

    /**
     * When publish() is called, verifies that a pre-publish event has been fired, and also verifies
     * that the method arguments have the correct value. Also simulates an IOException if the
     * current test case demands it.
     */
    public static class InstrumentedResolver extends MockResolver {

        public void publish(Artifact artifact, File src, boolean overwrite) throws IOException {

            // verify that the data from the current test case has been handed down to us
            PublishEventsTest test = (PublishEventsTest) IvyContext.getContext().peek(
                PublishEventsTest.class.getName());

            // test sequence of events.
            assertNotNull(test.currentTestCase);
            assertTrue("preTrigger has already fired", test.currentTestCase.preTriggerFired);
            assertFalse("postTrigger has not yet fired", test.currentTestCase.postTriggerFired);
            assertFalse("publish has not been called", test.currentTestCase.published);

            // test event data
            assertSameArtifact("publisher has received correct artifact",
                test.currentTestCase.expectedArtifact, artifact);
            assertEquals("publisher has received correct datafile",
                test.currentTestCase.expectedData.getCanonicalPath(), src.getCanonicalPath());
            assertEquals("publisher has received correct overwrite setting",
                test.expectedOverwrite, overwrite);
            assertTrue("publisher only invoked when source file exists",
                test.currentTestCase.expectedData.exists());

            // simulate a publisher error if the current test case demands it.
            if (test.publishError != null) {
                throw test.publishError;
            }

            // all assertions pass. increment the publication count
            test.currentTestCase.published = true;
            ++test.publications;
        }
    }

}
