// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 The Falco Authors
//
// Licensed 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 unit

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"github.com/gruntwork-io/terratest/modules/helm"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	"slices"
)

const chartPath = "../../"

// Using the default values we want to test that all the expected resources for the k8s-metacollector are rendered.
func TestRenderedResourcesWithDefaultValues(t *testing.T) {
	t.Parallel()

	helmChartPath, err := filepath.Abs(chartPath)
	require.NoError(t, err)

	options := &helm.Options{}
	// Template the chart using the default values.yaml file.
	output, err := helm.RenderTemplateE(t, options, helmChartPath, releaseName, nil)
	require.NoError(t, err)

	// Extract all rendered files from the output.
	re := regexp.MustCompile(patternK8sMetacollectorFiles)
	matches := re.FindAllStringSubmatch(output, -1)
	require.Len(t, matches, 0)

}

func TestRenderedResourcesWhenNotEnabled(t *testing.T) {
	t.Parallel()

	helmChartPath, err := filepath.Abs(chartPath)
	require.NoError(t, err)

	// Template files that we expect to be rendered.
	templateFiles := []string{
		"clusterrole.yaml",
		"clusterrolebinding.yaml",
		"deployment.yaml",
		"service.yaml",
		"serviceaccount.yaml",
	}

	require.NoError(t, err)

	options := &helm.Options{SetValues: map[string]string{
		"collectors.kubernetes.enabled": "true",
	}}

	// Template the chart using the default values.yaml file.
	output, err := helm.RenderTemplateE(t, options, helmChartPath, releaseName, nil)
	require.NoError(t, err)

	// Extract all rendered files from the output.
	re := regexp.MustCompile(patternK8sMetacollectorFiles)
	matches := re.FindAllStringSubmatch(output, -1)

	var renderedTemplates []string
	for _, match := range matches {
		// Filter out test templates.
		if !strings.Contains(match[1], "test-") {
			renderedTemplates = append(renderedTemplates, match[1])
		}
	}

	// Assert that the rendered resources are equal tho the expected ones.
	require.Equal(t, len(renderedTemplates), len(templateFiles), "should be equal")

	for _, rendered := range renderedTemplates {
		require.True(t, slices.Contains(templateFiles, rendered), "template files should contain all the rendered files")
	}
}

func TestPluginConfigurationInFalcoConfig(t *testing.T) {
	t.Parallel()

	helmChartPath, err := filepath.Abs(chartPath)
	require.NoError(t, err)

	testCases := []struct {
		name     string
		values   map[string]string
		expected func(t *testing.T, config any)
	}{
		{
			"defaultValues",
			nil,
			func(t *testing.T, config any) {
				plugin := config.(map[string]interface{})
				// Get init config.
				initConfig, ok := plugin["init_config"]
				require.True(t, ok)
				initConfigMap := initConfig.(map[string]interface{})
				// Check that the collector port is correctly set.
				port := initConfigMap["collectorPort"]
				require.Equal(t, float64(45000), port.(float64))
				// Check that the collector nodeName is correctly set.
				nodeName := initConfigMap["nodeName"]
				require.Equal(t, "${FALCO_K8S_NODE_NAME}", nodeName.(string))
				// Check that the collector hostname is correctly set.
				hostName := initConfigMap["collectorHostname"]
				require.Equal(t, fmt.Sprintf("%s-k8s-metacollector.default.svc", releaseName), hostName.(string))

				// Check that the library path is set.
				libPath := plugin["library_path"]
				require.Equal(t, "libk8smeta.so", libPath)
			},
		},
		{
			"overrideK8s-metacollectorNamespace",
			map[string]string{
				"k8s-metacollector.namespaceOverride": "test",
			},
			func(t *testing.T, config any) {
				plugin := config.(map[string]interface{})
				// Get init config.
				initConfig, ok := plugin["init_config"]
				require.True(t, ok)
				initConfigMap := initConfig.(map[string]interface{})
				// Check that the collector port is correctly set.
				port := initConfigMap["collectorPort"]
				require.Equal(t, float64(45000), port.(float64))
				// Check that the collector nodeName is correctly set.
				nodeName := initConfigMap["nodeName"]
				require.Equal(t, "${FALCO_K8S_NODE_NAME}", nodeName.(string))
				// Check that the collector hostname is correctly set.
				hostName := initConfigMap["collectorHostname"]
				require.Equal(t, fmt.Sprintf("%s-k8s-metacollector.test.svc", releaseName), hostName.(string))

				// Check that the library path is set.
				libPath := plugin["library_path"]
				require.Equal(t, "libk8smeta.so", libPath)
			},
		},
		{
			"overrideK8s-metacollectorName",
			map[string]string{
				"k8s-metacollector.fullnameOverride": "collector",
			},
			func(t *testing.T, config any) {
				plugin := config.(map[string]interface{})
				// Get init config.
				initConfig, ok := plugin["init_config"]
				require.True(t, ok)
				initConfigMap := initConfig.(map[string]interface{})
				// Check that the collector port is correctly set.
				port := initConfigMap["collectorPort"]
				require.Equal(t, float64(45000), port.(float64))
				// Check that the collector nodeName is correctly set.
				nodeName := initConfigMap["nodeName"]
				require.Equal(t, "${FALCO_K8S_NODE_NAME}", nodeName.(string))
				// Check that the collector hostname is correctly set.
				hostName := initConfigMap["collectorHostname"]
				require.Equal(t, "collector.default.svc", hostName.(string))

				// Check that the library path is set.
				libPath := plugin["library_path"]
				require.Equal(t, "libk8smeta.so", libPath)
			},
		},

		{
			"overrideK8s-metacollectorNamespaceAndName",
			map[string]string{
				"k8s-metacollector.namespaceOverride": "test",
				"k8s-metacollector.fullnameOverride":  "collector",
			},
			func(t *testing.T, config any) {
				plugin := config.(map[string]interface{})
				// Get init config.
				initConfig, ok := plugin["init_config"]
				require.True(t, ok)
				initConfigMap := initConfig.(map[string]interface{})
				// Check that the collector port is correctly set.
				port := initConfigMap["collectorPort"]
				require.Equal(t, float64(45000), port.(float64))
				// Check that the collector nodeName is correctly set.
				nodeName := initConfigMap["nodeName"]
				require.Equal(t, "${FALCO_K8S_NODE_NAME}", nodeName.(string))
				// Check that the collector hostname is correctly set.
				hostName := initConfigMap["collectorHostname"]
				require.Equal(t, "collector.test.svc", hostName.(string))

				// Check that the library path is set.
				libPath := plugin["library_path"]
				require.Equal(t, "libk8smeta.so", libPath)
			},
		},
		{
			"set CollectorHostname",
			map[string]string{
				"collectors.kubernetes.collectorHostname": "test",
			},
			func(t *testing.T, config any) {
				plugin := config.(map[string]interface{})
				// Get init config.
				initConfig, ok := plugin["init_config"]
				require.True(t, ok)
				initConfigMap := initConfig.(map[string]interface{})
				// Check that the collector port is correctly set.
				port := initConfigMap["collectorPort"]
				require.Equal(t, float64(45000), port.(float64))
				// Check that the collector nodeName is correctly set.
				nodeName := initConfigMap["nodeName"]
				require.Equal(t, "${FALCO_K8S_NODE_NAME}", nodeName.(string))
				// Check that the collector hostname is correctly set.
				hostName := initConfigMap["collectorHostname"]
				require.Equal(t, "test", hostName.(string))

				// Check that the library path is set.
				libPath := plugin["library_path"]
				require.Equal(t, "libk8smeta.so", libPath)
			},
		},

		{
			"set CollectorHostname and namespace name",
			map[string]string{
				"collectors.kubernetes.collectorHostname": "test-with-override",
				"k8s-metacollector.namespaceOverride":     "test",
				"k8s-metacollector.fullnameOverride":      "collector",
			},
			func(t *testing.T, config any) {
				plugin := config.(map[string]interface{})
				// Get init config.
				initConfig, ok := plugin["init_config"]
				require.True(t, ok)
				initConfigMap := initConfig.(map[string]interface{})
				// Check that the collector port is correctly set.
				port := initConfigMap["collectorPort"]
				require.Equal(t, float64(45000), port.(float64))
				// Check that the collector nodeName is correctly set.
				nodeName := initConfigMap["nodeName"]
				require.Equal(t, "${FALCO_K8S_NODE_NAME}", nodeName.(string))
				// Check that the collector hostname is correctly set.
				hostName := initConfigMap["collectorHostname"]
				require.Equal(t, "test-with-override", hostName.(string))

				// Check that the library path is set.
				libPath := plugin["library_path"]
				require.Equal(t, "libk8smeta.so", libPath)
			},
		},

		{
			"set collectorPort",
			map[string]string{
				"collectors.kubernetes.collectorPort": "8888",
			},
			func(t *testing.T, config any) {
				plugin := config.(map[string]interface{})
				// Get init config.
				initConfig, ok := plugin["init_config"]
				require.True(t, ok)
				initConfigMap := initConfig.(map[string]interface{})
				// Check that the collector port is correctly set.
				port := initConfigMap["collectorPort"]
				require.Equal(t, float64(8888), port.(float64))
				// Check that the collector nodeName is correctly set.
				nodeName := initConfigMap["nodeName"]
				require.Equal(t, "${FALCO_K8S_NODE_NAME}", nodeName.(string))
				// Check that the collector hostname is correctly set.
				hostName := initConfigMap["collectorHostname"]
				require.Equal(t, fmt.Sprintf("%s-k8s-metacollector.default.svc", releaseName), hostName.(string))

				// Check that the library path is set.
				libPath := plugin["library_path"]
				require.Equal(t, "libk8smeta.so", libPath)
			},
		},
		{
			"drive disabled",
			map[string]string{
				"driver.enabled": "false",
			},
			func(t *testing.T, config any) {
				require.Nil(t, config)
			},
		},
	}

	for _, testCase := range testCases {
		testCase := testCase

		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			// Enable the collector.
			if testCase.values != nil {
				testCase.values["collectors.kubernetes.enabled"] = "true"
			} else {
				testCase.values = map[string]string{"collectors.kubernetes.enabled": "true"}
			}

			options := &helm.Options{SetValues: testCase.values}
			output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/configmap.yaml"})

			var cm corev1.ConfigMap
			helm.UnmarshalK8SYaml(t, output, &cm)
			var config map[string]interface{}

			helm.UnmarshalK8SYaml(t, cm.Data["falco.yaml"], &config)
			plugins := config["plugins"]
			pluginsArray := plugins.([]interface{})
			found := false
			// Find the k8smeta plugin configuration.
			for _, plugin := range pluginsArray {
				if name, ok := plugin.(map[string]interface{})["name"]; ok && name == k8sMetaPluginName {
					testCase.expected(t, plugin)
					found = true
				}
			}
			if found {
				// Check that the plugin has been added to the ones that need to be loaded.
				loadplugins := config["load_plugins"]
				require.True(t, slices.Contains(loadplugins.([]interface{}), k8sMetaPluginName))
			} else {
				testCase.expected(t, nil)
				loadplugins := config["load_plugins"]
				require.True(t, !slices.Contains(loadplugins.([]interface{}), k8sMetaPluginName))
			}
		})
	}
}

// Test that the helper does not overwrite user's configuration.
func TestPluginConfigurationUniqueEntries(t *testing.T) {
	t.Parallel()

	pluginsJSON := `[
    {
        "init_config": null,
        "library_path": "libk8saudit.so",
        "name": "k8saudit",
        "open_params": "http://:9765/k8s-audit"
    },
    {
        "library_path": "libcloudtrail.so",
        "name": "cloudtrail"
    },
    {
        "init_config": "",
        "library_path": "libjson.so",
        "name": "json"
    },
    {
        "init_config": {
            "collectorHostname": "rendered-resources-k8s-metacollector.default.svc",
            "collectorPort": 45000,
            "nodeName": "${FALCO_K8S_NODE_NAME}"
        },
        "library_path": "libk8smeta.so",
        "name": "k8smeta"
    }
]`

	loadPluginsJSON := `[
    "k8smeta",
	"k8saudit"
]`
	helmChartPath, err := filepath.Abs(chartPath)
	require.NoError(t, err)

	options := &helm.Options{SetJsonValues: map[string]string{
		"falco.plugins":      pluginsJSON,
		"falco.load_plugins": loadPluginsJSON,
	}, SetValues: map[string]string{"collectors.kubernetes.enabled": "true"}}
	output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/configmap.yaml"})

	var cm corev1.ConfigMap
	helm.UnmarshalK8SYaml(t, output, &cm)
	var config map[string]interface{}

	helm.UnmarshalK8SYaml(t, cm.Data["falco.yaml"], &config)
	plugins := config["plugins"]

	out, err := json.MarshalIndent(plugins, "", "    ")
	require.NoError(t, err)
	require.Equal(t, pluginsJSON, string(out))
	pluginsArray := plugins.([]interface{})
	// Find the k8smeta plugin configuration.
	numConfigK8smeta := 0
	for _, plugin := range pluginsArray {
		if name, ok := plugin.(map[string]interface{})["name"]; ok && name == k8sMetaPluginName {
			numConfigK8smeta++
		}
	}

	require.Equal(t, 1, numConfigK8smeta)

	// Check that the plugin has been added to the ones that need to be loaded.
	loadplugins := config["load_plugins"]
	require.Len(t, loadplugins.([]interface{}), 2)
	require.True(t, slices.Contains(loadplugins.([]interface{}), k8sMetaPluginName))
}

// Test that the helper does not overwrite user's configuration.
func TestFalcoctlRefs(t *testing.T) {
	t.Parallel()

	pluginsJSON := `[
	    {
	        "init_config": null,
	        "library_path": "libk8saudit.so",
	        "name": "k8saudit",
	        "open_params": "http://:9765/k8s-audit"
	    },
	    {
	        "library_path": "libcloudtrail.so",
	        "name": "cloudtrail"
	    },
	    {
	        "init_config": "",
	        "library_path": "libjson.so",
	        "name": "json"
	    },
	    {
	        "init_config": {
	            "collectorHostname": "rendered-resources-k8s-metacollector.default.svc",
	            "collectorPort": 45000,
	            "nodeName": "${FALCO_K8S_NODE_NAME}"
	        },
	        "library_path": "libk8smeta.so",
	        "name": "k8smeta"
	    }
	]`

	testFunc := func(t *testing.T, config any) {
		// Get artifact configuration map.
		configMap := config.(map[string]interface{})
		artifactConfig := (configMap["artifact"]).(map[string]interface{})
		// Test allowed types.
		allowedTypes := artifactConfig["allowedTypes"]
		require.Len(t, allowedTypes, 2)
		require.True(t, slices.Contains(allowedTypes.([]interface{}), "plugin"))
		require.True(t, slices.Contains(allowedTypes.([]interface{}), "rulesfile"))
		// Test plugin reference.
		refs := artifactConfig["install"].(map[string]interface{})["refs"].([]interface{})
		require.Len(t, refs, 2)
		require.True(t, slices.Contains(refs, "falco-rules:3"))
		require.True(t, slices.Contains(refs, "ghcr.io/falcosecurity/plugins/plugin/k8smeta:0.1.0"))
	}

	testCases := []struct {
		name       string
		valuesJSON map[string]string
		expected   func(t *testing.T, config any)
	}{
		{
			"defaultValues",
			nil,
			testFunc,
		},
		{
			"setPluginConfiguration",
			map[string]string{
				"falco.plugins": pluginsJSON,
			},
			testFunc,
		},
		{
			"driver disabled",
			map[string]string{
				"driver.enabled": "false",
			},
			func(t *testing.T, config any) {
				// Get artifact configuration map.
				configMap := config.(map[string]interface{})
				artifactConfig := (configMap["artifact"]).(map[string]interface{})
				// Test plugin reference.
				refs := artifactConfig["install"].(map[string]interface{})["refs"].([]interface{})
				require.True(t, !slices.Contains(refs, "ghcr.io/falcosecurity/plugins/plugin/k8smeta:0.1.0"))
			},
		},
	}

	helmChartPath, err := filepath.Abs(chartPath)
	require.NoError(t, err)

	for _, testCase := range testCases {
		testCase := testCase

		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			options := &helm.Options{SetJsonValues: testCase.valuesJSON, SetValues: map[string]string{"collectors.kubernetes.enabled": "true"}}
			output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/falcoctl-configmap.yaml"})

			var cm corev1.ConfigMap
			helm.UnmarshalK8SYaml(t, output, &cm)
			var config map[string]interface{}
			helm.UnmarshalK8SYaml(t, cm.Data["falcoctl.yaml"], &config)
			testCase.expected(t, config)
		})
	}
}
