/*
 *  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.plugins.lock;

import java.io.File;
import java.text.ParseException;

import org.apache.ivy.core.cache.DefaultRepositoryCacheManager;
import org.apache.ivy.core.cache.RepositoryCacheManager;
import org.apache.ivy.core.event.EventManager;
import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.resolve.ResolveData;
import org.apache.ivy.core.resolve.ResolveEngine;
import org.apache.ivy.core.resolve.ResolveOptions;
import org.apache.ivy.core.resolve.ResolvedModuleRevision;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.core.sort.SortEngine;
import org.apache.ivy.plugins.repository.RepositoryCopyProgressListener;
import org.apache.ivy.plugins.repository.file.FileRepository;
import org.apache.ivy.plugins.resolver.FileSystemResolver;
import org.apache.ivy.util.CopyProgressEvent;
import org.apache.ivy.util.FileUtil;
import org.apache.ivy.util.Message;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class ArtifactLockStrategyTest {
    @Before
    public void setUp() {
        FileUtil.forceDelete(new File("build/test/cache"));
    }

    @After
    public void tearDown() {
        FileUtil.forceDelete(new File("build/test/cache"));
    }

    @Test
    public void testConcurrentResolve() throws Exception {
        // we use different settings because Ivy do not support multi thread resolve with the same
        // settings yet and this is not what this test is about: the focus of this test is running
        // concurrent resolves in separate vms but using the same cache. We don't span the test on
        // multiple vms, but using separate settings we should only run into shared cache related
        // issues, and not multi thread related issues.
        IvySettings settings1 = new IvySettings();
        IvySettings settings2 = new IvySettings();
        IvySettings settings3 = new IvySettings();

        // run 3 concurrent resolves, one taking 100ms to download files, one 20ms and one 5ms
        // the first one do 10 resolves, the second one 20 and the third 50
        // note that the download time is useful only at the very beginning, then the cached file is
        // used
        ResolveThread t1 = asyncResolve(settings1, createSlowResolver(settings1, 100),
            "org6#mod6.4;3", 10);
        ResolveThread t2 = asyncResolve(settings2, createSlowResolver(settings2, 20),
            "org6#mod6.4;3", 20);
        ResolveThread t3 = asyncResolve(settings3, createSlowResolver(settings3, 5),
            "org6#mod6.4;3", 50);
        t1.join(100000);
        t2.join(20000);
        t3.join(20000);
        assertEquals(10, t1.getCount());
        assertFound("org6#mod6.4;3", t1.getFinalResult());
        assertEquals(20, t2.getCount());
        assertFound("org6#mod6.4;3", t2.getFinalResult());
        assertEquals(50, t3.getCount());
        assertFound("org6#mod6.4;3", t3.getFinalResult());
    }

    private RepositoryCacheManager newCacheManager(IvySettings settings) {
        DefaultRepositoryCacheManager cacheManager = new DefaultRepositoryCacheManager("cache",
                settings, new File("build/test/cache"));
        cacheManager.setLockStrategy(new CreateFileLockStrategy(false));
        return cacheManager;
    }

    private FileSystemResolver createSlowResolver(IvySettings settings, final int sleep) {
        FileSystemResolver resolver = new FileSystemResolver();
        resolver.setRepositoryCacheManager(newCacheManager(settings));
        resolver.setRepository(new FileRepository() {
            private RepositoryCopyProgressListener progress = new RepositoryCopyProgressListener(
                    this) {
                public void progress(CopyProgressEvent evt) {
                    super.progress(evt);
                    sleepSilently(sleep); // makes the file copy longer to test concurrency issues
                }
            };

            protected RepositoryCopyProgressListener getProgressListener() {
                return progress;
            }
        });
        resolver.setName("test");
        resolver.setSettings(settings);
        resolver.addIvyPattern(settings.getBaseDir()
                + "/test/repositories/1/[organisation]/[module]/[type]s/[artifact]-[revision].[ext]");
        resolver.addArtifactPattern(settings.getBaseDir()
                + "/test/repositories/1/[organisation]/[module]/[type]s/[artifact]-[revision].[ext]");
        return resolver;
    }

    private ResolveThread asyncResolve(IvySettings settings, FileSystemResolver resolver,
            String module, int loop) {
        ResolveThread thread = new ResolveThread(settings, resolver, module, loop);
        thread.start();
        return thread;
    }

    private void assertFound(String module, ResolvedModuleRevision rmr) {
        assertNotNull(rmr);
        assertEquals(module, rmr.getId().toString());
    }

    private ResolvedModuleRevision resolveModule(IvySettings settings, FileSystemResolver resolver,
            String module) throws ParseException {
        return resolver.getDependency(
            new DefaultDependencyDescriptor(ModuleRevisionId.parse(module), false),
            new ResolveData(new ResolveEngine(settings, new EventManager(),
                    new SortEngine(settings)), new ResolveOptions()));
    }

    private void sleepSilently(int timeout) {
        try {
            Thread.sleep(timeout);
        } catch (InterruptedException e) {
        }
    }

    private class ResolveThread extends Thread {
        private IvySettings settings;

        private FileSystemResolver resolver;

        private String module;

        private final int loop;

        private ResolvedModuleRevision finalResult;

        private int count;

        public ResolveThread(IvySettings settings, FileSystemResolver resolver, String module,
                int loop) {
            this.settings = settings;
            this.resolver = resolver;
            this.module = module;
            this.loop = loop;
        }

        public synchronized ResolvedModuleRevision getFinalResult() {
            return finalResult;
        }

        public synchronized int getCount() {
            return count;
        }

        public void run() {
            ResolvedModuleRevision rmr = null;
            for (int i = 0; i < loop; i++) {
                try {
                    rmr = resolveModule(settings, resolver, module);
                    if (rmr == null) {
                        throw new RuntimeException("module not found: " + module);
                    }
                    synchronized (this) {
                        // Message.info(this.toString() + " count = " + count);
                        count++;
                    }
                } catch (ParseException e) {
                    Message.info("parse exception " + e);
                } catch (RuntimeException | Error e) {
                    Message.info("exception " + e);
                    e.printStackTrace();
                    throw e;
                }
            }
            synchronized (this) {
                finalResult = rmr;
            }
        }
    }

}
