/*
 * MIT License
 *
 * NiFi Protobuf Processor
 * Copyright (c) 2017 William Hiver
 * https://github.com/whiver/nifi-protobuf-processor
 *
 * 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 com.github.whiver.nifi.processor;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
import org.junit.Assert;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;


/**
 * A test class mocking a NiFi flow
 */
public class ProtobufDecoderTest {
    private final String[] validTestFiles = {"AddressBook_basic", "AddressBook_several"};

    /**
     * Test decoding valid files given a .desc schema
     * @throws IOException
     */
    @Test
    public void onTriggerDecodeValidFiles() throws IOException {
        TestRunner runner = TestRunners.newTestRunner(new ProtobufDecoder());

        // AddressBook test
        HashMap<String, String> addressBookProperties = new HashMap<>();
        addressBookProperties.put("protobuf.schemaPath", ProtobufDecoderTest.class.getResource("/schemas/AddressBook.desc").getPath());
        addressBookProperties.put("protobuf.messageType", "AddressBook");

        // AddressBook test
        for (String filename: validTestFiles) {
            InputStream jsonFile = ProtobufDecoderTest.class.getResourceAsStream("/data/" + filename + ".data");
            addressBookProperties.put("testfile", filename);
            runner.enqueue(jsonFile, addressBookProperties);
        }

        // Ensure the configuration is valid as-is
        runner.assertValid();

        // Run the enqueued content, it also takes an int = number of contents queued
        runner.run(validTestFiles.length);
        runner.assertQueueEmpty();

        // Check if the data was processed without failure
        List<MockFlowFile> results = runner.getFlowFilesForRelationship(ProtobufDecoder.SUCCESS);
        Assert.assertEquals("All flowfiles should be returned to success", validTestFiles.length, results.size());

        // Check if the content of the flowfile is as expected
        ObjectMapper mapper = new ObjectMapper();

        for (MockFlowFile result: results) {
            JsonNode expected = mapper.readTree(this.getClass().getResourceAsStream("/data/" + result.getAttribute("testfile") + ".json"));
            JsonNode given = mapper.readTree(runner.getContentAsByteArray(result));
            Assert.assertEquals("The parsing result of " + result.getAttribute("testfile") + ".data is not as expected", expected, given);
        }
    }

    /**
     * Test encoding valid files given an already compiled schema specified at processor level
     * @throws Exception
     */
    @Test
    public void onTriggerDecodeValidFilesWithSchemaAtProcessorLevel() throws Exception {
        TestRunner runner = TestRunners.newTestRunner(new ProtobufDecoder());
        runner.setProperty(ProtobufProcessor.COMPILE_SCHEMA, "false");
        runner.setProperty(ProtobufProcessor.PROTOBUF_SCHEMA, ProtobufDecoderTest.class.getResource("/schemas/Person.desc").getPath());

        InputStream dataFile = ProtobufDecoderTest.class.getResourceAsStream("/data/Person.data");
        HashMap<String, String> personProperties = new HashMap<>();
        personProperties.put("protobuf.messageType", "Person");
        runner.enqueue(dataFile, personProperties);

        runner.assertValid();
        runner.run(1);
        runner.assertQueueEmpty();

        runner.assertAllFlowFilesTransferred(ProtobufDecoder.SUCCESS);
        MockFlowFile result = runner.getFlowFilesForRelationship(ProtobufDecoder.SUCCESS).get(0);

        ObjectMapper mapper = new ObjectMapper();
        JsonNode expected = mapper.readTree(this.getClass().getResourceAsStream("/data/Person.json"));
        JsonNode given = mapper.readTree(runner.getContentAsByteArray(result));
        Assert.assertEquals("The parsing result of Person.data is not as expected", expected, given);
    }

    /**
     * Test decoding valid files given an uncompiled .proto schema specified at flowfile level
     * @throws Exception
     */
    @Test
    public void onTriggerCompileFlowfileSchemaAndDecodeValidFiles() throws Exception {
        TestRunner runner = TestRunners.newTestRunner(new ProtobufDecoder());
        runner.setProperty(ProtobufProcessor.COMPILE_SCHEMA, "true");

        InputStream dataFile = ProtobufDecoderTest.class.getResourceAsStream("/data/Person.data");
        HashMap<String, String> personProperties = new HashMap<>();
        personProperties.put("protobuf.schemaPath", ProtobufDecoderTest.class.getResource("/schemas/Person.proto").getPath());
        personProperties.put("protobuf.messageType", "Person");
        runner.enqueue(dataFile, personProperties);

        runner.assertValid();
        runner.run(1);
        runner.assertQueueEmpty();

        runner.assertAllFlowFilesTransferred(ProtobufDecoder.SUCCESS);
        MockFlowFile result = runner.getFlowFilesForRelationship(ProtobufDecoder.SUCCESS).get(0);

        ObjectMapper mapper = new ObjectMapper();
        JsonNode expected = mapper.readTree(this.getClass().getResourceAsStream("/data/Person.json"));
        JsonNode given = mapper.readTree(runner.getContentAsByteArray(result));
        Assert.assertEquals("The parsing result of Person.data is not as expected", expected, given);
    }

    /**
     * Test decoding valid files given an uncompiled .proto schema specified at processor level
     * @throws Exception
     */
    @Test
    public void onTriggerCompileProcessorSchemaAndDecodeValidFiles() throws Exception {
        TestRunner runner = TestRunners.newTestRunner(new ProtobufDecoder());
        runner.setProperty(ProtobufProcessor.COMPILE_SCHEMA, "true");
        runner.setProperty(ProtobufProcessor.PROTOBUF_SCHEMA, ProtobufDecoderTest.class.getResource("/schemas/Person.proto").getPath());

        InputStream dataFile = ProtobufDecoderTest.class.getResourceAsStream("/data/Person.data");
        HashMap<String, String> personProperties = new HashMap<>();
        personProperties.put("protobuf.messageType", "Person");
        runner.enqueue(dataFile, personProperties);

        runner.assertValid();
        runner.run(1);
        runner.assertQueueEmpty();

        runner.assertAllFlowFilesTransferred(ProtobufDecoder.SUCCESS);
        MockFlowFile result = runner.getFlowFilesForRelationship(ProtobufDecoder.SUCCESS).get(0);

        ObjectMapper mapper = new ObjectMapper();
        JsonNode expected = mapper.readTree(this.getClass().getResourceAsStream("/data/Person.json"));
        JsonNode given = mapper.readTree(runner.getContentAsByteArray(result));
        Assert.assertEquals("The parsing result of Person.data is not as expected", expected, given);
    }

    /**
     * Test if the per-flowfile schema have priority on the processor-wide one
     * @throws IOException
     */
    @Test
    public void onTriggerUsePerFlowfileSchemaIfAvailable() throws IOException {
        TestRunner runner = TestRunners.newTestRunner(new ProtobufDecoder());
        runner.setProperty("protobuf.schemaPath", ProtobufEncoderTest.class.getResource("/schemas/AddressBook.desc").getPath());

        InputStream dataFile = ProtobufDecoderTest.class.getResourceAsStream("/data/Person.data");
        HashMap<String, String> personProperties = new HashMap<>();
        personProperties.put("protobuf.schemaPath", ProtobufDecoderTest.class.getResource("/schemas/Person.desc").getPath());
        personProperties.put("protobuf.messageType", "Person");
        runner.enqueue(dataFile, personProperties);

        runner.assertValid();
        runner.run(1);
        runner.assertQueueEmpty();

        runner.assertAllFlowFilesTransferred(ProtobufDecoder.SUCCESS);
        MockFlowFile result = runner.getFlowFilesForRelationship(ProtobufDecoder.SUCCESS).get(0);

        ObjectMapper mapper = new ObjectMapper();
        JsonNode expected = mapper.readTree(this.getClass().getResourceAsStream("/data/Person.json"));
        JsonNode given = mapper.readTree(runner.getContentAsByteArray(result));
        Assert.assertEquals("The decoder should use the schema from flowfile instead of processor if given", expected, given);
    }

    /**
     * Test decoding using a processor-wide schema and switches between flowfile schema and processor schema
     * @throws Exception
     */
    @Test
    public void onPropertyModified() throws Exception {
        TestRunner runner = TestRunners.newTestRunner(new ProtobufDecoder());

        HashMap<String, String> addressBookProperties = new HashMap<>();
        addressBookProperties.put("protobuf.messageType", "AddressBook");


        /*
            First try to decode using a schema set in a processor property
         */

        runner.setProperty("protobuf.schemaPath", ProtobufDecoderTest.class.getResource("/schemas/AddressBook.desc").getPath());
        runner.assertValid();

        runner.enqueue(ProtobufDecoderTest.class.getResourceAsStream("/data/AddressBook_basic.data"), addressBookProperties);

        runner.run(1);

        // Check if the flowfile has been successfully processed
        runner.assertQueueEmpty();
        runner.assertAllFlowFilesTransferred(ProtobufDecoder.SUCCESS);

        // Finally check the content
        MockFlowFile result = runner.getFlowFilesForRelationship(ProtobufDecoder.SUCCESS).get(0);
        ObjectMapper mapper = new ObjectMapper();
        JsonNode expected = mapper.readTree(this.getClass().getResourceAsStream("/data/AddressBook_basic.json"));
        JsonNode given = mapper.readTree(runner.getContentAsByteArray(result));
        Assert.assertEquals("The parsing result of AddressBook_basic.data is not as expected", expected, given);


        /*
            Then try to remove the schema from the processor property and see if it still parse
         */

        runner.clearTransferState();
        runner.removeProperty(runner.getProcessor().getPropertyDescriptor("protobuf.schemaPath"));
        Assert.assertFalse("The schema property should now be null", runner.getProcessContext().getProperty("protobuf.schemaPath").isSet());
        runner.assertValid();

        runner.enqueue(ProtobufDecoderTest.class.getResourceAsStream("/data/AddressBook_basic.data"), addressBookProperties);

        runner.run(1);

        // Check if the flowfile has been successfully processed
        runner.assertQueueEmpty();
        runner.assertAllFlowFilesTransferred(ProtobufDecoder.INVALID_SCHEMA);


        /*
            Finally add the property again to see if it works again
         */

        runner.clearTransferState();
        runner.setProperty("protobuf.schemaPath", ProtobufDecoderTest.class.getResource("/schemas/AddressBook.desc").getPath());
        runner.assertValid();

        runner.enqueue(ProtobufDecoderTest.class.getResourceAsStream("/data/AddressBook_basic.data"), addressBookProperties);

        runner.run(1);

        // Check if the flowfile has been successfully processed
        runner.assertQueueEmpty();
        runner.assertAllFlowFilesTransferred(ProtobufDecoder.SUCCESS);

        // Finally check the content
        result = runner.getFlowFilesForRelationship(ProtobufDecoder.SUCCESS).get(0);
        expected = mapper.readTree(this.getClass().getResourceAsStream("/data/AddressBook_basic.json"));
        given = mapper.readTree(runner.getContentAsByteArray(result));
        Assert.assertEquals("The parsing result of AddressBook_basic.data is not as expected", expected, given);

    }
}