/*
 * 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.rocketmq.eventbridge.tools.pattern;

import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.rocketmq.eventbridge.config.AppConfig;
import org.apache.rocketmq.eventbridge.config.GlobalConfig;
import org.apache.rocketmq.eventbridge.tools.JsonUtil;
import org.junit.Before;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;

public class PatternEvaluatorTest {

    @Before
    public void before() {
        GlobalConfig globalConfig = new GlobalConfig();
        Set<String> eventExtensionKeys = Sets.newHashSet("aliyunregionid");
        globalConfig.setEventExtensionKeys(eventExtensionKeys);
        AppConfig.refreshGlobalConfig(globalConfig);
    }

    @Test
    public void evaluateWithEmptyPattern() {
        String pattern = "{}";
        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("source", "acs.aliyuncvc");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();
        specMap.put("eventbusname", "demo-bus");
        assertThat(evaluator.evaluateExtensionAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();
        assertThat(evaluator.evaluateData("{\"anyKey\":\"anyValue\"}")).isTrue();
    }

    @Test
    public void evaluateWithStringPattern() {
        String pattern = "{\"source\":[\"acs.aliyuncvc\"]}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("source", "acs.aliyuncvc");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();

        specMap.put("source", "acs.ecs");

        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isFalse();

        // This matches on events where the value for
        // the "source" field is either "acs.aliyuncvc" or "acs.ecs".
        String patternWithTwoValues = "{\"source\":[\"acs.aliyuncvc\", \"acs.ecs\"]}";
        evaluator = PatternEvaluatorBuilder.build(patternWithTwoValues);
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();
    }

    @Test
    public void evaluateWithAliyunAttr() {
        String pattern = "{\"aliyunregionid\":[\"cn-hangzhou\"]}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("aliyunregionid", "cn-hangzhou");
        assertThat(evaluator.evaluateExtensionAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();

        specMap.put("aliyunregionid", "cn-beijing");
        assertThat(evaluator.evaluateExtensionAttr(JsonUtil.convertToJsonElement(specMap))).isFalse();
    }

    @Test
    public void evaluateWithNumberPattern() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"risk-score\": [80, 20.5],\n" + "      \"trigger\": {\n" + "        \"risk\": [9.2, 8]\n"
            + "      }\n" + "    }\n" + "}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"risk-score\": 80,\n" + "    \"trigger\": {\n" + "        \"risk\": 8\n"
            + "    }\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"risk-score\": 80,\n" + "    \"trigger\": {\n" + "        \"risk\": 8.2\n" + "    }\n"
            + "}";
        assertThat(evaluator.evaluateData(jsonData)).isFalse();
    }

    @Test
    public void evaluateWithNotExistsField() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"subject\": [\"sub-value\"],\n"
            + "    \"type\": [\"type-value\"]\n" + "}";
        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("source", "acs.ecs");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isFalse();

        specMap.put("subject", "sub-value");

        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isFalse();

        specMap.put("type", "type-value");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();
    }

    @Test
    public void evaluateWithDiffNestingStructure() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"risk-score\": [80],\n" + "      \"trigger\": {\n" + "        \"risk\": [8]\n" + "      }\n"
            + "    }\n" + "}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"risk-score\": 80,\n" + "    \"risk\": 8" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();
    }

    @Test
    public void evaluateWithScientificNumber() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"risk-score\": [3.0e2]\n" + "    }\n" + "}";
        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"risk-score\": 300\n" + "}";
        assertThat(evaluator.evaluateData(jsonData)).isFalse();
    }

    @Test
    public void evaluateWithComplexPattern() {
        String pattern = "{\n" + "    \"time\": [{\"prefix\": \"2020-08-15\"}],\n"
            + "    \"source\": [\"acs.ecs\", \"acs.fc\"],\n" + "    \"data\": {\n"
            + "        \"state\": [ { \"anything-but\": \"initializing\" } ],\n"
            + "        \"c-count\": [ { \"numeric\": [ \">\", 0, \"<=\", 5 ] } ],\n"
            + "        \"d-count\": [ { \"numeric\": [ \"<\", 10 ] } ],\n"
            + "        \"x-limit\": [ { \"anything-but\": [ 100, 200, 300 ] } ],\n"
            + "        \"prod-type\": [\"rmq\", \"kafka\", \"mqtt\"]\n" + "    }\n" + "}";

        final PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("source", "acs.ecs");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isFalse();

        String jsonData = "{\n" + "    \"state\": \"terminated\",\n" + "    \"c-count\": 3.12,\n"
            + "    \"d-count\": 5,\n" + "    \"x-limit\": 120,\n" + "    \"prod-type\": [\"kafka\", \"eventbridge\"],\n"
            + "    \"other-fields\": {\n" + "        \"second-level\": {\n"
            + "            \"thrid-array\": [\"a\", \"b\", \"c\", \"d\"]\n" + "        }\n" + "    }\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();
    }

    @Test
    public void evaluateWithEmptyString() {
        String pattern = "{\"subject\":[\"\"]}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("subject", "");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();
    }

    @Test
    public void evaluateWithNullValue() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"risk-score\": [null]\n" + "    }\n" + "}";
        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"risk-score\": null\n" + "}";
        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"risk-score\": \"null\"\n" + "}";
        assertThat(evaluator.evaluateData(jsonData)).isFalse();
    }

    @Test
    public void evaluateWithArray() {
        // If the value in the event is an array,
        // then the pattern matches if the intersection of the pattern array and the event array is non-empty.
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"instance-list\": [\"instance1\", \"instance2\",\"instance3\"]\n" + "    }\n" + "}";
        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"instance-list\": [\"instance4\", \"instance3\"]\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"instance-list\": [\"instance4\", \"instance5\"]\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();
    }

    @Test
    public void evaluateWithPrefixCondition() {
        String pattern = "{\"time\":[{\"prefix\": \"2020-08-17\"}]}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("time", "2020-08-17T18:23:00");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();

        specMap.put("time", "2020-08-18T18:23:00");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isFalse();
    }

    @Test
    public void evaluateWithSuffixCondition() {
        String pattern = "{\"time\":[{\"suffix\": \"23:00\"}]}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        Map<String, String> specMap = new HashMap<>();
        specMap.put("time", "2020-08-17T18:23:00");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isTrue();

        specMap.put("time", "2020-08-17T18:23:06");
        assertThat(evaluator.evaluateSpecAttr(JsonUtil.convertToJsonElement(specMap))).isFalse();
    }

    @Test
    public void evaluateWithAnythingButCondition() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"state\": [{\"anything-but\": \"stopped\"}]\n" + "    }\n" + "}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"state\": \"started\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"state\": \"stopped\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        // Anything-but array
        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"state\": [{\"anything-but\": [\"stopped\", \"started\"]}]\n" + "    }\n" + "}";
        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"state\": \"started\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        jsonData = "{\n" + "    \"state\": \"stopped\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        jsonData = "{\n" + "    \"state\": \"initializing\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        // Anything-but with prefix
        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"state\": [{\"anything-but\": {\"prefix\": \"ini\"}}]\n" + "    }\n" + "}";
        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"state\": \"started\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"state\": \"initializing\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        // Anything-but with value array

    }

    @Test
    public void evaluateWithCIDRCondition() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"ipv4\": [{\"cidr\": \"10.0.0.0/24\"}]\n" + "    }\n" + "}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"ipv4\": \"10.0.0.251\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"ipv4\": \"10.0.2.251\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        // Illegal IPv4
        jsonData = "{\n" + "    \"ipv4\": \"10.0.0.1234\"\n" + "}";

        // Invalid IP address will return false
        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        // Exact IP address

        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"ipv4\": [{\"cidr\": \"10.0.0.1\"}]\n" + "    }\n" + "}";

        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"ipv4\": \"10.0.0.251\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        jsonData = "{\n" + "    \"ipv4\": \"10.0.0.1\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();
    }

    @Test
    public void evaluateWithExistsCondition() {
        String pattern = "{\n" + "    \"data\": {\n" + "      \"state\": [{\"exists\": true}]\n" + "    }\n" + "}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"state\": \"started\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"other-state\": \"stopped\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();
    }

    @Test
    public void evaluateWithExistsFalseCondition() {
        String pattern = "{\n" + "    \"data\": {\n" + "      \"state\": [{\"exists\": false}]\n" + "    }\n" + "}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"state\": \"started\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        jsonData = "{\n" + "    \"other-state\": \"stopped\"\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();
    }

    @Test
    public void evaluateWithInValidNumericCondition() {
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\">\", 1.0e10]}]\n" + "    }\n" + "}";

        try {
            PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);
            failBecauseExceptionWasNotThrown(InvalidEventPatternException.class);
        } catch (InvalidEventPatternException e) {
            assertThat(e.getMessage()).contains("between -1.0E9 and 1.0E9");
        }

        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\">\", -1.0e10]}]\n" + "    }\n" + "}";

        try {
            PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);
            failBecauseExceptionWasNotThrown(InvalidEventPatternException.class);
        } catch (InvalidEventPatternException e) {
            assertThat(e.getMessage()).contains("between -1.0E9 and 1.0E9");
        }
    }

    @Test
    public void evaluateWithNumericCondition() {
        // Case 1: Greater
        String pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\">\", 120]}]\n" + "    }\n" + "}";

        PatternEvaluator evaluator = PatternEvaluatorBuilder.build(pattern);

        String jsonData = "{\n" + "    \"c-count\": 122\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"c-count\": 110\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        // Case 2: Greater than
        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\">=\", 120]}]\n" + "    }\n" + "}";

        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"c-count\": 120\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        // Case 3: less

        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\"<\", 120]}]\n" + "    }\n" + "}";

        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"c-count\": 119\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        jsonData = "{\n" + "    \"c-count\": 120\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isFalse();

        // Case 4: less than
        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\"<=\", 120]}]\n" + "    }\n" + "}";

        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"c-count\": 120\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        // Case 5: Equal
        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\"=\", 1.1234567]}]\n" + "    }\n" + "}";

        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"c-count\": 1.12345678\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();

        // Case 6: Not Equal
        pattern = "{\n" + "    \"source\": [\"acs.ecs\"],\n" + "    \"data\": {\n"
            + "      \"c-count\": [{\"numeric\": [\"!=\", 1.123455]}]\n" + "    }\n" + "}";

        evaluator = PatternEvaluatorBuilder.build(pattern);

        jsonData = "{\n" + "    \"c-count\": 1.123456\n" + "}";

        assertThat(evaluator.evaluateData(jsonData)).isTrue();
    }
}
