/*
 * 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.nifi.integration.processor;

import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.PropertyDescriptor.Builder;
import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.integration.DirectInjectionExtensionManager;
import org.apache.nifi.integration.FrameworkIntegrationTest;
import org.apache.nifi.parameter.Parameter;
import org.apache.nifi.parameter.ParameterContext;
import org.apache.nifi.parameter.ParameterDescriptor;
import org.apache.nifi.parameter.ParameterReferenceManager;
import org.apache.nifi.parameter.StandardParameterContext;
import org.apache.nifi.processor.AbstractProcessor;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.Processor;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;

import static org.apache.nifi.processor.util.StandardValidators.NON_EMPTY_VALIDATOR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ProcessorParameterTokenIT extends FrameworkIntegrationTest {

    @Override
    protected void injectExtensionTypes(final DirectInjectionExtensionManager extensionManager) {
        extensionManager.injectExtensionType(Processor.class, WriteText.class);
    }

    @Test
    public void testEscapedParameterReference() throws ExecutionException, InterruptedException {
        final ProcessorNode procNode = createProcessorNode(WriteText.class);
        procNode.setAutoTerminatedRelationships(Collections.singleton(REL_SUCCESS));

        verifyText(procNode, "hello", "hello");
        verifyText(procNode, "##{foo}", "#{foo}");
        verifyText(procNode, "####{foo}", "##{foo}");
        verifyText(procNode, "## hello ##{foo} ##{bar}", "## hello #{foo} #{bar}");
    }

    @Test
    public void testProperReferences() throws ExecutionException, InterruptedException {
        final ProcessorNode procNode = createProcessorNode(WriteText.class);
        procNode.setAutoTerminatedRelationships(Collections.singleton(REL_SUCCESS));

        final ParameterContext parameterContext = createParameterContext("testEscapedParameterReference");
        getRootGroup().setParameterContext(parameterContext);

        final Map<String, Parameter> parameters = new HashMap<>();
        parameters.put("foo", new Parameter(new ParameterDescriptor.Builder().name("foo").build(), "bar"));
        parameters.put("sensitive", new Parameter(new ParameterDescriptor.Builder().name("sensitive").sensitive(true).build(), "*password*"));
        parameterContext.setParameters(parameters);

        verifyText(procNode, "hello", "hello");
        verifyText(procNode, "##{foo}", "#{foo}");
        verifyText(procNode, "#{foo}", "bar");
        verifyText(procNode, "####{foo}", "##{foo}");
        verifyText(procNode, "#####{foo}", "##bar");
        verifyText(procNode, "## hello #{foo} ##{bar}", "## hello bar #{bar}");

        try {
            verifyText(procNode, "#{bar}", "ISE");
        } catch (final IllegalStateException expected) {
            // Expect IllegalStateException because processor is not valid because it references a non-existent parameter.
        }

        verifyText(procNode, "#{foo}", "password", "barpassword");
        verifyText(procNode, "#{foo}", "#{sensitive}", "bar*password*");
    }

    @Test
    public void testSensitiveParameters() throws ExecutionException, InterruptedException {
        final ProcessorNode procNode = createProcessorNode(WriteText.class);
        procNode.setAutoTerminatedRelationships(Collections.singleton(REL_SUCCESS));

        final ParameterContext parameterContext = createParameterContext("testEscapedParameterReference");
        getRootGroup().setParameterContext(parameterContext);

        final Map<String, Parameter> parameters = new HashMap<>();
        parameters.put("foo", new Parameter(new ParameterDescriptor.Builder().name("foo").build(), "bar"));
        parameters.put("sensitive", new Parameter(new ParameterDescriptor.Builder().name("sensitive").sensitive(true).build(), "*password*"));
        parameterContext.setParameters(parameters);

        verifyCannotSetParameter(procNode, "#{sensitive}", null);
        verifyCannotSetParameter(procNode, "abc#{sensitive}foo", null);
        verifyCannotSetParameter(procNode, "#{foo}", "#{foo}");
        verifyCannotSetParameter(procNode, null, "#{sensitive}#{sensitive}");
        verifyCannotSetParameter(procNode, null, "#{sensitive}123");
        verifyCannotSetParameter(procNode, null, "123#{sensitive}");
        verifyCannotSetParameter(procNode, null, "#{foo}");

        verifyText(procNode, "#{foo}", "#{sensitive}", "bar*password*");
        verifyText(procNode, "#{foo}", "##{sensitive}##{sensitive}##{sensitive}", "bar#{sensitive}#{sensitive}#{sensitive}");
    }

    private void verifyCannotSetParameter(final ProcessorNode procNode, final String text, final String password) {
        final Map<String, String> properties = new HashMap<>();
        properties.put(WriteText.TEXT.getName(), text);
        properties.put(WriteText.PASSWORD.getName(), password);

        assertThrows(IllegalArgumentException.class, () -> procNode.setProperties(properties),
                "Expected to fail when setting properties to " + properties);
    }

    private void verifyText(final ProcessorNode procNode, final String text, final String expectedOutput) throws ExecutionException, InterruptedException {
        verifyText(procNode, text, null, expectedOutput);
    }

    private void verifyText(final ProcessorNode procNode, final String text, final String password, final String expectedOutput) throws ExecutionException, InterruptedException {
        final Map<String, String> properties = new HashMap<>();
        properties.put(WriteText.TEXT.getName(), text);
        properties.put(WriteText.PASSWORD.getName(), password);

        procNode.setProperties(properties);
        triggerOnce(procNode);

        final WriteText writeText = (WriteText) procNode.getProcessor();
        final String textWritten = writeText.getTextLastWritten();

        assertEquals("For input text <" + text+ "> and password <" + password + ">, expected output was <" + expectedOutput + "> but got <" + textWritten + ">", expectedOutput, textWritten);
    }


    public static class WriteText extends AbstractProcessor {
        private volatile String textLastWritten = null;

        static final PropertyDescriptor TEXT = new Builder()
            .name("Text")
            .displayName("Text")
            .description("The text to write")
            .required(true)
            .addValidator(NON_EMPTY_VALIDATOR)
            .build();

        static final PropertyDescriptor PASSWORD = new Builder()
            .name("password")
            .displayName("password")
            .description("Password")
            .required(false)
            .addValidator(NON_EMPTY_VALIDATOR)
            .sensitive(true)
            .build();

        @Override
        protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
            return Arrays.asList(TEXT, PASSWORD);
        }

        @Override
        public Set<Relationship> getRelationships() {
            return Collections.singleton(REL_SUCCESS);
        }

        @Override
        public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
            FlowFile flowFile = session.create();

            final String text = context.getProperty(TEXT).getValue();

            final String fullText;
            if (context.getProperty(PASSWORD).isSet()) {
                fullText = text + context.getProperty(PASSWORD).getValue();
            } else {
                fullText = text;
            }

            flowFile = session.write(flowFile, out -> out.write(fullText.getBytes(StandardCharsets.UTF_8)));
            session.transfer(flowFile, REL_SUCCESS);

            textLastWritten = fullText;
        }

        private String getTextLastWritten() {
            return textLastWritten;
        }
    }

    private ParameterContext createParameterContext(final String name) {
        return new StandardParameterContext.Builder()
                .id(UUID.randomUUID().toString())
                .name(name)
                .parameterReferenceManager(ParameterReferenceManager.EMPTY)
                .build();
    }
}
