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

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 java.util.Iterator;

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.plugins.parser.xml.XmlModuleDescriptorParser;
import org.apache.ivy.plugins.resolver.MockResolver;
import org.apache.ivy.plugins.trigger.AbstractTrigger;

import junit.framework.TestCase;

public class PublishEventsTest extends TestCase {

    //maps ArtifactRevisionId to PublishTestCase instance.
    private HashMap 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 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;

    protected void setUp() throws Exception {
        super.setUp();
        
        //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.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);
    }
    
    protected void tearDown() throws Exception {
        super.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;
    }

    protected 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.
     */
    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 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.
     */
    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 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.
     */
    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 = (PublishTestCase)expectedPublications.get(dataArtifact.getId());
        dataPublish.expectedSuccess = false;
        Collection missing = publishEngine.publish(publishModule.getModuleRevisionId(), publishSources, "default", publishOptions);
        assertEquals("one missing artifact", 1, missing.size());
        assertSameArtifact("missing artifact was returned", dataArtifact, (Artifact)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.
     */
    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 (Iterator it = expectedPublications.values().iterator(); it.hasNext(); )
            ((PublishTestCase)it.next()).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.
     */
    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 guaranteean 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").toString();
                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 = (PublishTestCase)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;
        }
    }
    
}