/*
 * The MIT License
 *
 * Copyright (c) 2016, CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.jenkinsci.plugins.pipeline.maven.eventspy;

import java.io.File;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.maven.eventspy.EventSpy;
import org.apache.maven.execution.DefaultMavenExecutionRequest;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.hamcrest.CoreMatchers;
import org.jenkinsci.plugins.pipeline.maven.eventspy.reporter.FileMavenEventReporter;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class JenkinsMavenEventSpyMTTest {

    MavenProject project;

    @Before
    public void before() throws Exception {
        System.setProperty("org.jenkinsci.plugins.pipeline.maven.reportsFolder", "target");
    }

    private JenkinsMavenEventSpy createSpy() throws Exception {
        FileMavenEventReporter reporter = new FileMavenEventReporter();

        JenkinsMavenEventSpy spy = new JenkinsMavenEventSpy(reporter) {
            @Override
            protected boolean isEventSpyDisabled() {
                return false;
            }
        };

        spy.init(new EventSpy.Context() {
            @Override
            public Map<String, Object> getData() {
                return new HashMap();
            }
        });

        MavenXpp3Reader mavenXpp3Reader = new MavenXpp3Reader();
        InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("org/jenkinsci/plugins/pipeline/maven/eventspy/pom.xml");

        Assert.assertThat(in, CoreMatchers.notNullValue());
        Model model = mavenXpp3Reader.read(in);
        project = new MavenProject(model);
        project.setGroupId(model.getGroupId());
        project.setArtifactId(model.getArtifactId());
        project.setVersion(model.getVersion());
        project.setName(model.getName());
        return spy;
    }

    @Test //Issue JENKINS-46579
    public void testMavenExecutionMTSpyReporters() throws Exception {
        int numThreads = 100;
        final CyclicBarrier barrier = new CyclicBarrier(numThreads + 1); //we need to also stop the test thread (current)
        final AtomicInteger counter = new AtomicInteger(0);
        final ExceptionHolder exceptionHolder = new ExceptionHolder();

        final Vector<JenkinsMavenEventSpy> spyList = new Vector<JenkinsMavenEventSpy>(numThreads);

        //Test some concurrency around persisted state. Launch 100 threads from which 1/3 will try to change the
        //persisted state, the rest will read it a couple of times.
        for (int i = 0; i < numThreads; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                        //Thread.sleep(RandomUtils.nextInt(0, 500));
                        JenkinsMavenEventSpy spy = createSpy();
                        spyList.add(spy);
                        DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest();
                        request.setPom(new File("path/to/pom.xml"));
                        request.setGoals(Arrays.asList("clean", "source:jar", "deploy"));

                        for (int i = 0; i < 100; i++) {
                            spy.onEvent(request);
                        }

                    } catch (Exception e) {
                        exceptionHolder.e = e;
                    }
                    counter.incrementAndGet();
                }
            }).start();
        }
        barrier.await();

        long start = System.currentTimeMillis();

        while (true) {
            int c = counter.get();
            long finish = System.currentTimeMillis();
            if ((finish - start) > 2000L) { // 2 seconds is the limit
                //ThreadDumps.threadDumpModern(System.out); //FIXME
                Assert.fail("Threads taking too long to finish " + (finish - start) + "ms");
            }

            if (c >= numThreads) {
                System.out.println("==== All threads finished");
                System.out.println("==== Time: " + (finish - start) + " ms.");
                break;
            }
            System.out.println("==== Waiting for threads to finish. Counter " + c + ". Waiting 200 ms.");
            Thread.sleep(200);
        }

        if (exceptionHolder.e != null) {
            // fail the test is there was some exception on threads
            throw exceptionHolder.e;
        }

        for (JenkinsMavenEventSpy spy : spyList) {
            spy.close();
            File outFile = ((FileMavenEventReporter) spy.getReporter()).getFinalFile();
            System.out.println("Generated file: " + outFile);
            String actual = FileUtils.fileRead(outFile);
            Assert.assertThat(actual, CoreMatchers.containsString("MavenExecutionRequest"));
            validateXMLDocument(outFile);
        }
    }

    @Test //Issue JENKINS-46579
    public void testMavenExecutionMTRequestsSingleSpyReporter() throws Exception {
        int numThreads = 100;
        final CyclicBarrier barrier = new CyclicBarrier(numThreads + 1); //we need to also stop the test thread (current)
        final AtomicInteger counter = new AtomicInteger(0);
        final ExceptionHolder exceptionHolder = new ExceptionHolder();

        final JenkinsMavenEventSpy spy = createSpy();

        //Test some concurrency around persisted state. Launch 100 threads from which 1/3 will try to change the
        //persisted state, the rest will read it a couple of times.
        for (int i = 0; i < numThreads; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                        //Thread.sleep(RandomUtils.nextInt(0, 500));

                        DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest();
                        request.setPom(new File("path/to/pom.xml"));
                        request.setGoals(Arrays.asList("clean", "source:jar", "deploy"));
                        for (int i = 0; i < 100; i++) {
                            spy.onEvent(request);
                        }

                    } catch (Exception e) {
                        exceptionHolder.e = e;
                    }
                    counter.incrementAndGet();
                }
            }).start();
        }
        barrier.await();

        long start = System.currentTimeMillis();

        while (true) {
            int c = counter.get();
            long finish = System.currentTimeMillis();
            if ((finish - start) > 2000L) { // 2 seconds is the limit
                //ThreadDumps.threadDumpModern(System.out); //FIXME
                Assert.fail("Threads taking too long to finish " + (finish - start) + "ms");
            }

            if (c >= numThreads) {
                System.out.println("==== All threads finished");
                System.out.println("==== Time: " + (finish - start) + " ms.");
                break;
            }
            System.out.println("==== Waiting for threads to finish. Counter " + c + ". Waiting 200 ms.");
            Thread.sleep(200);
        }

        if (exceptionHolder.e != null) {
            // fail the test is there was some exception on threads
            throw exceptionHolder.e;
        }

        spy.close();
        File outFile = ((FileMavenEventReporter) spy.getReporter()).getFinalFile();
        System.out.println("Generated file: " + outFile);
        String actual = FileUtils.fileRead(outFile);
        Assert.assertThat(actual, CoreMatchers.containsString("MavenExecutionRequest"));
        validateXMLDocument(outFile);
    }

    DocumentBuilder documentBuilder;

    public void validateXMLDocument(File document) {
        if (documentBuilder == null) {
            try {
                documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            } catch (ParserConfigurationException e) {
                throw new IllegalStateException("Failure to create a DocumentBuilder", e);
            }
        }

        try {
            documentBuilder.parse(document);
        } catch (Exception e) {
            e.printStackTrace();
            Assert.fail("Failed to parse spylog: " + document + " error:" + e);
        }

    }

    public static class ExceptionHolder {
        public Exception e;
    }
}