/*
 * 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.model.interpolation;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

import org.apache.maven.model.InputLocation;
import org.apache.maven.model.InputSource;
import org.apache.maven.model.Model;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.SimpleProblemCollector;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.powermock.reflect.Whitebox.getField;
import static org.powermock.reflect.Whitebox.getInternalState;

/**
 * @author jdcasey
 * @author Benjamin Bentmann
 */
public class StringSearchModelInterpolatorTest extends AbstractModelInterpolatorTest {

    protected ModelInterpolator interpolator;

    @BeforeEach
    protected void setUp() throws Exception {
        super.setUp();
        interpolator =
                new StringSearchModelInterpolator().setVersionPropertiesProcessor(new DefaultModelVersionProcessor());
    }

    protected ModelInterpolator createInterpolator(org.apache.maven.model.path.PathTranslator translator)
            throws Exception {
        return this.interpolator;
    }

    protected ModelInterpolator createInterpolator() throws Exception {
        return this.interpolator;
    }

    @Test
    public void testInterpolateStringArray() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        String[] values = {"${key}", "${key2}"};

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(values, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("value", values[0]);
        assertEquals("value2", values[1]);
    }

    private ModelBuildingRequest createModelBuildingRequest(Properties p) {
        ModelBuildingRequest config = new DefaultModelBuildingRequest();
        config.setSystemProperties(p);
        return config;
    }

    @Test
    public void testInterpolateObjectWithStringArrayField() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        String[] values = {"${key}", "${key2}"};

        ObjectWithStringArrayField obj = new ObjectWithStringArrayField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("value", obj.values[0]);
        assertEquals("value2", obj.values[1]);
    }

    @Test
    public void testInterpolateObjectWithStringListField() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        List<String> values = new ArrayList<>();
        values.add("${key}");
        values.add("${key2}");

        ObjectWithListField obj = new ObjectWithListField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("value", obj.values.get(0));
        assertEquals("value2", obj.values.get(1));
    }

    @Test
    public void testInterpolateObjectWithStringListFieldAndOneLiteralValue() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        List<String> values = new ArrayList<>();
        values.add("key");
        values.add("${key2}");

        ObjectWithListField obj = new ObjectWithListField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("key", obj.values.get(0));
        assertEquals("value2", obj.values.get(1));
    }

    @Test
    public void testInterpolateObjectWithUnmodifiableStringListField() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        List<String> values = Collections.unmodifiableList(Collections.singletonList("${key}"));

        ObjectWithListField obj = new ObjectWithListField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("${key}", obj.values.get(0));
    }

    @Test
    public void testInterpolateObjectWithStringArrayListField() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");
        p.setProperty("key3", "value3");
        p.setProperty("key4", "value4");

        List<String[]> values = new ArrayList<>();
        values.add(new String[] {"${key}", "${key2}"});
        values.add(new String[] {"${key3}", "${key4}"});

        ObjectWithListField obj = new ObjectWithListField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("value", ((String[]) obj.values.get(0))[0]);
        assertEquals("value2", ((String[]) obj.values.get(0))[1]);
        assertEquals("value3", ((String[]) obj.values.get(1))[0]);
        assertEquals("value4", ((String[]) obj.values.get(1))[1]);
    }

    @Test
    public void testInterpolateObjectWithStringToStringMapField() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        Map<String, String> values = new HashMap<>();
        values.put("key", "${key}");
        values.put("key2", "${key2}");

        ObjectWithMapField obj = new ObjectWithMapField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("value", obj.values.get("key"));
        assertEquals("value2", obj.values.get("key2"));
    }

    @Test
    public void testInterpolateObjectWithStringToStringMapFieldAndOneLiteralValue() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        Map<String, String> values = new HashMap<>();
        values.put("key", "val");
        values.put("key2", "${key2}");

        ObjectWithMapField obj = new ObjectWithMapField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("val", obj.values.get("key"));
        assertEquals("value2", obj.values.get("key2"));
    }

    @Test
    public void testInterpolateObjectWithUnmodifiableStringToStringMapField() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");

        Map<String, String> values = Collections.unmodifiableMap(Collections.singletonMap("key", "${key}"));

        ObjectWithMapField obj = new ObjectWithMapField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("${key}", obj.values.get("key"));
    }

    @Test
    public void testInterpolateObjectWithStringToStringArrayMapField() throws Exception {
        Model model = new Model();

        Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");
        p.setProperty("key3", "value3");
        p.setProperty("key4", "value4");

        Map<String, String[]> values = new HashMap<>();
        values.put("key", new String[] {"${key}", "${key2}"});
        values.put("key2", new String[] {"${key3}", "${key4}"});

        ObjectWithMapField obj = new ObjectWithMapField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        final SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals("value", ((String[]) obj.values.get("key"))[0]);
        assertEquals("value2", ((String[]) obj.values.get("key"))[1]);
        assertEquals("value3", ((String[]) obj.values.get("key2"))[0]);
        assertEquals("value4", ((String[]) obj.values.get("key2"))[1]);
    }

    @Test
    public void testInterpolateObjectWithPomFile() throws Exception {
        Model model = new Model();
        model.setPomFile(new File(System.getProperty("user.dir"), "pom.xml"));
        File baseDir = model.getProjectDirectory();

        Properties p = new Properties();

        Map<String, String> values = new HashMap<>();
        values.put("key", "${project.basedir}" + File.separator + "target");

        ObjectWithMapField obj = new ObjectWithMapField(values);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        assertEquals(System.getProperty("user.dir"), baseDir.getAbsolutePath());
        assertEquals(1, obj.values.size());
        assertThat(
                (String) obj.values.get("key"),
                is(anyOf(
                        is(System.getProperty("user.dir") + File.separator + "target"),
                        // TODO why MVN adds dot /./ in paths???
                        is(System.getProperty("user.dir") + File.separator + '.' + File.separator + "target"))));
    }

    @Test
    public void testNotInterpolateObjectWithFile() throws Exception {
        Model model = new Model();

        File baseDir = new File(System.getProperty("user.dir"));

        Properties p = new Properties();

        ObjectWithNotInterpolatedFile obj = new ObjectWithNotInterpolatedFile(baseDir);

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(obj, model, new File("."), config, collector);
        assertProblemFree(collector);

        //noinspection unchecked
        Map<Class<?>, ?> cache = (Map<Class<?>, ?>)
                getField(StringSearchModelInterpolator.class, "CACHED_ENTRIES").get(null);

        Object objCacheItem = cache.get(Object.class);
        Object fileCacheItem = cache.get(File.class);

        assertNotNull(objCacheItem);
        assertNotNull(fileCacheItem);

        assertEquals(0, ((Object[]) getInternalState(objCacheItem, "fields")).length);
        assertEquals(0, ((Object[]) getInternalState(fileCacheItem, "fields")).length);
    }

    @Test
    public void testNotInterpolateFile() throws Exception {
        Model model = new Model();

        File baseDir = new File(System.getProperty("user.dir"));

        Properties p = new Properties();

        StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        ModelBuildingRequest config = createModelBuildingRequest(p);

        SimpleProblemCollector collector = new SimpleProblemCollector();
        interpolator.interpolateObject(baseDir, model, new File("."), config, collector);
        assertProblemFree(collector);

        //noinspection unchecked
        Map<Class<?>, ?> cache = (Map<Class<?>, ?>)
                getField(StringSearchModelInterpolator.class, "CACHED_ENTRIES").get(null);

        Object fileCacheItem = cache.get(File.class);

        assertNotNull(fileCacheItem);

        assertEquals(0, ((Object[]) getInternalState(fileCacheItem, "fields")).length);
    }

    @Test
    public void testConcurrentInterpolation() throws Exception {
        final Model model = new Model();

        final Properties p = new Properties();
        p.setProperty("key", "value");
        p.setProperty("key2", "value2");
        p.setProperty("key3", "value3");
        p.setProperty("key4", "value4");
        p.setProperty("key5", "value5");

        final StringSearchModelInterpolator interpolator = (StringSearchModelInterpolator) createInterpolator();

        int numItems = 100;
        final CountDownLatch countDownLatch = new CountDownLatch(1);

        List<Future<SimpleProblemCollector>> futures = new ArrayList<>();
        for (int i = 0; i < numItems; i++) {
            Callable<SimpleProblemCollector> future = new Callable<SimpleProblemCollector>() {
                public SimpleProblemCollector call() throws Exception {
                    final ObjectWithMixedProtection obj = getValueList();
                    final ModelBuildingRequest config = createModelBuildingRequest(p);

                    countDownLatch.await();
                    final SimpleProblemCollector collector = new SimpleProblemCollector();
                    interpolator.interpolateObject(obj, model, new File("."), config, collector);
                    return collector;
                }
            };
            FutureTask<SimpleProblemCollector> task = new FutureTask<>(future);
            futures.add(task);
            new Thread(task).start();
        }
        countDownLatch.countDown(); // Start all the threads
        for (Future<SimpleProblemCollector> result : futures) {
            SimpleProblemCollector problemCollector =
                    result.get(); // ArrayIndexOutOfBoundsException are typical indication of threading issues
            assertProblemFree(problemCollector);
        }
    }

    private ObjectWithMixedProtection getValueList() {
        List<String[]> values = new ArrayList<>();

        values.add(new String[] {"${key}", "${key2}"});
        values.add(new String[] {"${key3}", "${key4}"});
        List<String> values2 = new ArrayList<>();
        values.add(new String[] {"${key}", "${key2}"});
        values.add(new String[] {"${key3}", "${key4}"});
        List<String> values3 = new ArrayList<>();
        values.add(new String[] {"${key}", "${key2}"});
        values.add(new String[] {"${key3}", "${key4}"});

        return new ObjectWithMixedProtection(values, values2, values3, "${key5}");
    }

    private static final class ObjectWithStringArrayField {
        private final String[] values;

        ObjectWithStringArrayField(String[] values) {
            this.values = values;
        }
    }

    private static final class ObjectWithListField {
        private final List<?> values;

        ObjectWithListField(List<?> values) {
            this.values = values;
        }
    }

    private static final class ObjectWithMapField {
        private final Map<?, ?> values;

        ObjectWithMapField(Map<?, ?> values) {
            this.values = values;
        }
    }

    private static final class ObjectWithNotInterpolatedFile {
        private final File f;

        ObjectWithNotInterpolatedFile(File f) {
            this.f = f;
        }
    }

    @SuppressWarnings("unused")
    private static final class ObjectWithMixedProtection {
        private List<?> values1;
        protected List<?> values2;
        List<?> values3;
        private String fooBar;

        private ObjectWithMixedProtection(List<?> values1, List<?> values2, List<?> values3) {
            this.values1 = values1;
            this.values2 = values2;
            this.values3 = values3;
        }

        private ObjectWithMixedProtection(List<?> values1, List<?> values2, List<?> values3, String fooBar) {
            this.values1 = values1;
            this.values2 = values2;
            this.values3 = values3;
            this.fooBar = fooBar;
        }

        public String getFooBar() {
            return fooBar;
        }
    }

    @Test
    public void testFinalFieldsExcludedFromInterpolation() {
        Properties props = new Properties();
        props.setProperty("expression", "value");
        DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
        request.setUserProperties(props);

        SimpleProblemCollector problems = new SimpleProblemCollector();
        StringSearchModelInterpolator interpolator = new StringSearchModelInterpolator();
        interpolator.setVersionPropertiesProcessor(new DefaultModelVersionProcessor());
        interpolator.interpolateObject(new ClassWithFinalField(), new Model(), null, request, problems);

        assertProblemFree(problems);
    }

    static class ClassWithFinalField {
        public static final String CONSTANT = "${expression}";
    }

    @Test
    public void testLocationTrackerShouldBeExcludedFromInterpolation() {
        Properties props = new Properties();
        props.setProperty("expression", "value");
        DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
        request.setUserProperties(props);

        InputSource source = new InputSource();
        source.setLocation("${expression}");
        source.setModelId("${expression}");
        Model model = new Model();
        model.setLocation("", new InputLocation(1, 1, source));

        SimpleProblemCollector problems = new SimpleProblemCollector();
        StringSearchModelInterpolator interpolator = new StringSearchModelInterpolator();
        interpolator.setVersionPropertiesProcessor(new DefaultModelVersionProcessor());
        interpolator.interpolateObject(model, model, null, request, problems);

        assertProblemFree(problems);
        assertEquals("${expression}", source.getLocation());
        assertEquals("${expression}", source.getModelId());
    }
}
