/*
 * 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.jmeter.extractor.json.jmespath;

import static org.hamcrest.MatcherAssert.assertThat;

import java.util.function.Consumer;
import java.util.stream.Stream;

import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.AbstractScopedTestElement;
import org.apache.jmeter.threads.JMeterContext;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterVariables;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class TestJMESPathExtractor {
    private static final String DEFAULT_VALUE = "NONE"; // $NON-NLS-1$
    private static final String REFERENCE_NAME = "varname"; // $NON-NLS-1$
    private static final String REFERENCE_NAME_MATCH_NUMBER = "varname_matchNr"; // $NON-NLS-1$

    private static JMESPathExtractor setupProcessor(JMeterVariables vars, SampleResult sampleResult, String data, boolean isSourceVars, String matchNumbers) {
        JMeterContext jmctx = JMeterContextService.getContext();
        jmctx.setVariables(vars);
        jmctx.setPreviousResult(sampleResult);
        JMESPathExtractor processor = new JMESPathExtractor();
        processor.setThreadContext(jmctx);
        processor.setRefName(REFERENCE_NAME);
        processor.setMatchNumber(matchNumbers);
        processor.setDefaultValue(DEFAULT_VALUE);
        if (isSourceVars) {
            vars.put("contentvar", data);
            processor.setScopeVariable("contentvar");
        } else {
            sampleResult.setResponseData(data, null);
            processor.setScopeAll();
        }
        return processor;
    }

    @Test
    void testNoMatchNumberSet() {
        JMeterVariables vars = new JMeterVariables();
        SampleResult sampleResult = new SampleResult();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "[1]", false, "");
        processor.setJmesPathExpression("[*]");
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is("1"));
    }

    private static Stream<Arguments> dataOneMatch() {
        return Stream.of(
            Arguments.of("[\"one\"]", "[*]", "one", "1"),
            Arguments.of("{\"a\": {\"b\": {\"c\": {\"d\": \"value\"}}}}", "a.b.c.d", "value", "1"),
            Arguments.of("{\r\n" + "  \"people\": [\r\n" + "    {\"first\": \"James\", \"last\": \"d\"},\r\n"
                    + "    {\"first\": \"Jacob\", \"last\": \"e\"},\r\n"
                    + "    {\"first\": \"Jayden\", \"last\": \"f\"},\r\n" + "    {\"missing\": \"different\"}\r\n"
                    + "  ],\r\n" + "  \"foo\": {\"bar\": \"baz\"}\r\n" + "}", "people[2]",
                    "{\"first\":\"Jayden\",\"last\":\"f\"}",
                    "1")
        );
    }

    @ParameterizedTest(name = "TestFromVars: {index} Extract from {0} with path {1} should result in {2} for match {3}")
    @MethodSource("dataOneMatch")
    void testOneMatchFromVars(String data, String jmesPath, String expectedResult, String expectedMatchNumber) {
        testOneMatchOnAllExtractedValues(true, data, jmesPath, expectedResult, expectedMatchNumber);
    }

    @ParameterizedTest(name = "TestFromSampleResult: {index} Extract from {0} with path {1} should result in {2} for match {3}")
    @MethodSource("dataOneMatch")
    void testOneFromSampleResult(String data, String jmesPath, String expectedResult, String expectedMatchNumber) {
        testOneMatchOnAllExtractedValues(false, data, jmesPath, expectedResult, expectedMatchNumber);
    }

    private void testOneMatchOnAllExtractedValues(boolean fromVars, String data, String jmesPath, String expectedResult, String expectedMatchNumber) {
        JMeterVariables vars = new JMeterVariables();
        SampleResult sampleResult = new SampleResult();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, data, fromVars, "-1");
        processor.setJmesPathExpression(jmesPath);
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(CoreMatchers.nullValue()));
        assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(expectedResult));
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(expectedMatchNumber));

        processor.clearOldRefVars(vars, REFERENCE_NAME);
        assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(CoreMatchers.nullValue()));
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(CoreMatchers.nullValue()));
    }

    private static Stream<Arguments> dataMultipleMatches() {
        return Stream.of(
            Arguments.of("[\"one\", \"two\"]", "[*]", new String[] {"one", "two"}, "2"),
            Arguments.of("[\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"]", "[0:3]", new String[] {"a", "b","c"}, "3"),
            Arguments.of("{\r\n" + "  \"people\": [\r\n" + "    {\"first\": \"James\", \"last\": \"d\"},\r\n"
                    + "    {\"first\": \"Jacob\", \"last\": \"e\"},\r\n"
                    + "    {\"first\": \"Jayden\", \"last\": \"f\"},\r\n" + "    {\"missing\": \"different\"}\r\n"
                    + "  ],\r\n" + "  \"foo\": {\"bar\": \"baz\"}\r\n" + "}", "people[:2].first", new String[] {"James", "Jacob"}, "2")
        );
    }

    @ParameterizedTest
    @MethodSource("dataMultipleMatches")
    void testMultipleMatchesOnAllExtractedValuesFromVars(String data, String jmesPath, String[] expectedResults, String expectedMatchNumber) {
        testMultipleMatchesOnAllExtractedValues(true, data, jmesPath, expectedResults, expectedMatchNumber);
    }

    @ParameterizedTest
    @MethodSource("dataMultipleMatches")
    void testMultipleMatchesOnAllExtractedValuesFromSampleResult(String data, String jmesPath, String[] expectedResults, String expectedMatchNumber) {
        testMultipleMatchesOnAllExtractedValues(false, data, jmesPath, expectedResults, expectedMatchNumber);
    }

    private void testMultipleMatchesOnAllExtractedValues(boolean fromVars, String data, String jmesPath, String[] expectedResults, String expectedMatchNumber) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, data, fromVars, "-1");
        // test1
        processor.setJmesPathExpression(jmesPath);
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(CoreMatchers.nullValue()));
        for (int i = 0; i < expectedResults.length; i++) {
            assertThat(vars.get(REFERENCE_NAME + "_"+(i+1)), CoreMatchers.is(expectedResults[i]));
        }
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(expectedMatchNumber));
    }

    private static final String TEST_DATA = "{\r\n" + "  \"people\": [\r\n" + "    {\"first\": \"James\", \"last\": \"d\", \"age\":10},\r\n"
            + "    {\"first\": \"Jacob\", \"last\": \"e\", \"age\":20},\r\n"
            + "    {\"first\": \"Jayden\", \"last\": \"f\", \"age\":30},\r\n"
            + "    {\"missing\": \"different\"}\r\n" + "  ],\r\n" + "  \"foo\": {\"bar\": \"baz\"}\r\n"
            + "}";

    private static Stream<Arguments> dataMatchNumberMoreThanZero() {
        return Stream.of(
            Arguments.of(TEST_DATA, "people[:3].first", "1", "James", "3"),
            Arguments.of(TEST_DATA, "people[:3].first", "2", "Jacob", "3"),
            Arguments.of(TEST_DATA, "people[:3].first", "3", "Jayden", "3"),
            Arguments.of(TEST_DATA, "people[:3].age", "3", "30", "3"),
            Arguments.of(TEST_DATA, "people[:3].first", "4", DEFAULT_VALUE, "3")
        );
    }

    @ParameterizedTest
    @MethodSource("dataMatchNumberMoreThanZero")
    void testFromVars(String data, String jmesPath,
            String matchNumber, String expectedResult, String expectedMatchNumber) {
        testMatchNumberMoreThanZeroOn1ExtractedValue(true, data, jmesPath, matchNumber, expectedResult, expectedMatchNumber);
    }

    @ParameterizedTest
    @MethodSource("dataMatchNumberMoreThanZero")
    void testFromSampleResult(String data, String jmesPath,
            String matchNumber, String expectedResult, String expectedMatchNumber) {
        testMatchNumberMoreThanZeroOn1ExtractedValue(false, data, jmesPath, matchNumber, expectedResult, expectedMatchNumber);
    }

    private void testMatchNumberMoreThanZeroOn1ExtractedValue(boolean fromVars, String data, String jmesPath,
            String matchNumber, String expectedResult, String expectedMatchNumber) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, data, fromVars, "1");
        processor.setMatchNumber(matchNumber);
        processor.setJmesPathExpression(jmesPath);
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(expectedResult));
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(expectedMatchNumber));
    }

    enum AccessMode {
        ALL(AbstractScopedTestElement::setScopeAll),
        PARENT(AbstractScopedTestElement::setScopeParent),
        CHILDREN(AbstractScopedTestElement::setScopeChildren);

        @SuppressWarnings("ImmutableEnumChecker")
        private final Consumer<AbstractScopedTestElement> applier;

        AccessMode(Consumer<AbstractScopedTestElement> applier) {
            this.applier = applier;
        }

        void configure(AbstractScopedTestElement element) {
            applier.accept(element);
        }
    }

    private static Stream<Arguments> dataScopedSamples() {
        return Stream.of(
                Arguments.of(AccessMode.ALL, "a", "1", "23", "2"),
                Arguments.of(AccessMode.ALL, "a", "2", "42", "2"),
                Arguments.of(AccessMode.ALL, "b", "0", "parent_only", "1"),
                Arguments.of(AccessMode.ALL, "c", "0", "child_only", "1"),
                Arguments.of(AccessMode.PARENT, "a", "1", "23", "1"),
                Arguments.of(AccessMode.PARENT, "b", "0", "parent_only", "1"),
                Arguments.of(AccessMode.PARENT, "c", "0", "NONE", "0"),
                Arguments.of(AccessMode.CHILDREN, "a", "1", "42", "1"),
                Arguments.of(AccessMode.CHILDREN, "b", "0", "NONE", "0"),
                Arguments.of(AccessMode.CHILDREN, "c", "0", "child_only", "1")
        );
    }

    @ParameterizedTest(name = "{index}: Mode: {0} Path: {1} MatchNr: {2} Result: {3} Count: {4}")
    @MethodSource("dataScopedSamples")
    void testRandomElementAllMatches(AccessMode accessMode, String path, String matchNumber, String resultObject, String resultCount) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "{\"a\": 23, \"b\": \"parent_only\"}", false, matchNumber);
        SampleResult subSample = new SampleResult();
        subSample.setResponseData("{\"a\": 42, \"c\": \"child_only\"}", null);
        sampleResult.addSubResult(subSample);

        processor.setJmesPathExpression(path);
        accessMode.configure(processor);

        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(resultObject));
        assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(CoreMatchers.nullValue()));
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(resultCount));
    }

    private static Stream<Arguments> dataSourceVarOrResponse() {
        return Stream.of(Arguments.of(Boolean.TRUE), Arguments.of(Boolean.FALSE));
    }

    @ParameterizedTest
    @MethodSource("dataSourceVarOrResponse")
    void testRandomElementOneMatch(boolean fromVariables) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "{\"a\": {\"b\": {\"c\": {\"d\": \"value\"}}}}", fromVariables, "0");

        processor.setJmesPathExpression("a.b.c.d");
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is("value"));
        assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(CoreMatchers.nullValue()));
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is("1"));
    }

    @ParameterizedTest
    @MethodSource("dataSourceVarOrResponse")
    void testRandomElementMultipleMatches(boolean fromVariables) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "[\"one\", \"two\"]", fromVariables, "0");

        processor.setJmesPathExpression("[*]");
        processor.process();
        assertThat(vars.get(REFERENCE_NAME),
                CoreMatchers.is(CoreMatchers.anyOf(CoreMatchers.is("one"), CoreMatchers.is("two"))));
        assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(CoreMatchers.nullValue()));
        assertThat(vars.get(REFERENCE_NAME + "_2"), CoreMatchers.is(CoreMatchers.nullValue()));
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is("2"));
    }

    @ParameterizedTest
    @MethodSource("dataSourceVarOrResponse")
    void testEmptySourceData(boolean fromVariables) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "", fromVariables, "-1");

        processor.setJmesPathExpression("[*]");
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(DEFAULT_VALUE));
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(CoreMatchers.nullValue()));
    }

    @ParameterizedTest
    @MethodSource("dataSourceVarOrResponse")
    void testErrorInJMESPath(boolean fromVariables) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "{\"a\": {\"b\": {\"c\": {\"d\": \"value\"}}}}", fromVariables, "-1");

        processor.setJmesPathExpression("$.k");
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(DEFAULT_VALUE));
        assertThat(vars.get(REFERENCE_NAME+ "_1"), CoreMatchers.nullValue());
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.nullValue());
    }

    @ParameterizedTest
    @MethodSource("dataSourceVarOrResponse")
    void testNoMatch(boolean fromVariables) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "{\"a\": {\"b\": {\"c\": {\"d\": \"value\"}}}}", fromVariables, "-1");

        processor.setJmesPathExpression("a.b.c.f");
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(DEFAULT_VALUE));
        assertThat(vars.get(REFERENCE_NAME+ "_1"), CoreMatchers.nullValue());
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is("0"));
    }

    @ParameterizedTest
    @MethodSource("dataSourceVarOrResponse")
    void testNoInput(boolean fromVariables) {
        SampleResult sampleResult = new SampleResult();
        JMeterVariables vars = new JMeterVariables();
        JMESPathExtractor processor = setupProcessor(vars, sampleResult, "", fromVariables, "0");

        processor.setJmesPathExpression("a.b");
        processor.process();
        assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(DEFAULT_VALUE));
        assertThat(vars.get(REFERENCE_NAME+ "_1"), CoreMatchers.nullValue());
        assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.nullValue());
    }
}
