/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.bootstrap;

import org.apache.lucene.util.Constants;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.Version;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.PluginTestUtil;
import org.elasticsearch.plugins.Platforms;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;

/**
 * Create a simple "daemon controller", put it in the right place and check that it runs.
 *
 * Extends LuceneTestCase rather than ESTestCase as ESTestCase installs a system call filter, and
 * that prevents the Spawner class from doing its job. Also needs to run in a separate JVM to other
 * tests that extend ESTestCase for the same reason.
 */
public class SpawnerNoBootstrapTests extends LuceneTestCase {

    private static final String CONTROLLER_SOURCE = "#!/bin/bash\n"
            + "\n"
            + "echo I am alive\n"
            + "\n"
            + "read SOMETHING\n";

    /**
     * Simplest case: a plugin with no controller daemon.
     */
    public void testNoControllerSpawn() throws IOException, InterruptedException {
        Path esHome = createTempDir().resolve("esHome");
        Settings.Builder settingsBuilder = Settings.builder();
        settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.toString());
        Settings settings = settingsBuilder.build();

        Environment environment = new Environment(settings);

        // This plugin will NOT have a controller daemon
        Path plugin = environment.pluginsFile().resolve("a_plugin");
        Files.createDirectories(plugin);
        PluginTestUtil.writeProperties(
                plugin,
                "description", "a_plugin",
                "version", Version.CURRENT.toString(),
                "elasticsearch.version", Version.CURRENT.toString(),
                "name", "a_plugin",
                "java.version", "1.8",
                "classname", "APlugin",
                "has.native.controller", "false");

        try (Spawner spawner = new Spawner()) {
            spawner.spawnNativePluginControllers(environment);
            assertThat(spawner.getProcesses(), hasSize(0));
        }
    }

    /**
     * Two plugins - one with a controller daemon and one without.
     */
    public void testControllerSpawn() throws IOException, InterruptedException {
        /*
         * On Windows you can not directly run a batch file - you have to run cmd.exe with the batch
         * file as an argument and that's out of the remit of the controller daemon process spawner.
         */
        assumeFalse("This test does not work on Windows", Constants.WINDOWS);

        Path esHome = createTempDir().resolve("esHome");
        Settings.Builder settingsBuilder = Settings.builder();
        settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.toString());
        Settings settings = settingsBuilder.build();

        Environment environment = new Environment(settings);

        // this plugin will have a controller daemon
        Path plugin = environment.pluginsFile().resolve("test_plugin");
        Files.createDirectories(plugin);
        PluginTestUtil.writeProperties(
                plugin,
                "description", "test_plugin",
                "version", Version.CURRENT.toString(),
                "elasticsearch.version", Version.CURRENT.toString(),
                "name", "test_plugin",
                "java.version", "1.8",
                "classname", "TestPlugin",
                "has.native.controller", "true");
        Path controllerProgram = Platforms.nativeControllerPath(plugin);
        createControllerProgram(controllerProgram);

        // this plugin will not have a controller daemon
        Path otherPlugin = environment.pluginsFile().resolve("other_plugin");
        Files.createDirectories(otherPlugin);
        PluginTestUtil.writeProperties(
                otherPlugin,
                "description", "other_plugin",
                "version", Version.CURRENT.toString(),
                "elasticsearch.version", Version.CURRENT.toString(),
                "name", "other_plugin",
                "java.version", "1.8",
                "classname", "OtherPlugin",
                "has.native.controller", "false");

        Spawner spawner = new Spawner();
        spawner.spawnNativePluginControllers(environment);

        List<Process> processes = spawner.getProcesses();
        /*
         * As there should only be a reference in the list for the plugin that had the controller
         * daemon, we expect one here.
         */
        assertThat(processes, hasSize(1));
        Process process = processes.get(0);
        final InputStreamReader in =
                new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8);
        try (BufferedReader stdoutReader = new BufferedReader(in)) {
            String line = stdoutReader.readLine();
            assertEquals("I am alive", line);
            spawner.close();
            /*
             * Fail if the process does not die within one second; usually it will be even quicker
             * but it depends on OS scheduling.
             */
            assertTrue(process.waitFor(1, TimeUnit.SECONDS));
        }
    }

    public void testControllerSpawnWithIncorrectDescriptor() throws IOException {
        // this plugin will have a controller daemon
        Path esHome = createTempDir().resolve("esHome");
        Settings.Builder settingsBuilder = Settings.builder();
        settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.toString());
        Settings settings = settingsBuilder.build();

        Environment environment = new Environment(settings);

        Path plugin = environment.pluginsFile().resolve("test_plugin");
        Files.createDirectories(plugin);
        PluginTestUtil.writeProperties(
                plugin,
                "description", "test_plugin",
                "version", Version.CURRENT.toString(),
                "elasticsearch.version", Version.CURRENT.toString(),
                "name", "test_plugin",
                "java.version", "1.8",
                "classname", "TestPlugin",
                "has.native.controller", "false");
        Path controllerProgram = Platforms.nativeControllerPath(plugin);
        createControllerProgram(controllerProgram);

        Spawner spawner = new Spawner();
        IllegalArgumentException e = expectThrows(
                IllegalArgumentException.class,
                () -> spawner.spawnNativePluginControllers(environment));
        assertThat(
                e.getMessage(),
                equalTo("plugin [test_plugin] does not have permission to fork native controller"));
    }

    private void createControllerProgram(final Path outputFile) throws IOException {
        final Path outputDir = outputFile.getParent();
        Files.createDirectories(outputDir);
        Files.write(outputFile, CONTROLLER_SOURCE.getBytes(StandardCharsets.UTF_8));
        final PosixFileAttributeView view =
                Files.getFileAttributeView(outputFile, PosixFileAttributeView.class);
        if (view != null) {
            final Set<PosixFilePermission> perms = new HashSet<>();
            perms.add(PosixFilePermission.OWNER_READ);
            perms.add(PosixFilePermission.OWNER_WRITE);
            perms.add(PosixFilePermission.OWNER_EXECUTE);
            perms.add(PosixFilePermission.GROUP_READ);
            perms.add(PosixFilePermission.GROUP_EXECUTE);
            perms.add(PosixFilePermission.OTHERS_READ);
            perms.add(PosixFilePermission.OTHERS_EXECUTE);
            Files.setPosixFilePermissions(outputFile, perms);
        }
    }

}
