//go:build linux || freebsd || netbsd || openbsd || solaris || !windows

package csplugin

import (
	"encoding/json"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"testing"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/tomb.v2"
	"gopkg.in/yaml.v2"

	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
	"github.com/crowdsecurity/crowdsec/pkg/cstest"
	"github.com/crowdsecurity/crowdsec/pkg/models"
)

var testPath string

func setPluginPermTo744(t *testing.T) {
	setPluginPermTo(t, "744")
}

func setPluginPermTo722(t *testing.T) {
	setPluginPermTo(t, "722")
}

func setPluginPermTo724(t *testing.T) {
	setPluginPermTo(t, "724")
}
func TestGetPluginNameAndTypeFromPath(t *testing.T) {
	setUp(t)
	defer tearDown(t)
	type args struct {
		path string
	}
	tests := []struct {
		name    string
		args    args
		want    string
		want1   string
		expectedErr string
	}{
		{
			name: "valid plugin name, single dash",
			args: args{
				path: path.Join(testPath, "notification-gitter"),
			},
			want:    "notification",
			want1:   "gitter",
		},
		{
			name: "invalid plugin name",
			args: args{
				path: "./tests/gitter",
			},
			expectedErr: "plugin name ./tests/gitter is invalid. Name should be like {type-name}",
		},
		{
			name: "valid plugin name, multiple dash",
			args: args{
				path: "./tests/notification-instant-slack",
			},
			want:    "notification-instant",
			want1:   "slack",
		},
	}
	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			got, got1, err := getPluginTypeAndSubtypeFromPath(tc.args.path)
			cstest.RequireErrorContains(t, err, tc.expectedErr)

			assert.Equal(t, tc.want, got)
			assert.Equal(t, tc.want1, got1)
		})
	}
}

func TestListFilesAtPath(t *testing.T) {
	setUp(t)
	defer tearDown(t)
	type args struct {
		path string
	}
	tests := []struct {
		name    string
		args    args
		want    []string
		expectedErr string
	}{
		{
			name: "valid directory",
			args: args{
				path: testPath,
			},
			want: []string{
				filepath.Join(testPath, "notification-gitter"),
				filepath.Join(testPath, "slack"),
			},
		},
		{
			name: "invalid directory",
			args: args{
				path: "./foo/bar/",
			},
			expectedErr: "open ./foo/bar/: " + cstest.FileNotFoundMessage,
		},
	}
	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			got, err := listFilesAtPath(tc.args.path)
			cstest.RequireErrorContains(t, err, tc.expectedErr)

			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("listFilesAtPath() = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestBrokerInit(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Skipping test on windows")
	}

	tests := []struct {
		name        string
		action      func(*testing.T)
		procCfg     csconfig.PluginCfg
		expectedErr string
	}{
		{
			name:   "valid config",
			action: setPluginPermTo744,
		},
		{
			name:        "group writable binary",
			expectedErr: "notification-dummy is world writable",
			action:      setPluginPermTo722,
		},
		{
			name:        "group writable binary",
			expectedErr: "notification-dummy is group writable",
			action:      setPluginPermTo724,
		},
		{
			name:        "no plugin dir",
			expectedErr: cstest.FileNotFoundMessage,
			action:      tearDown,
		},
		{
			name:        "no plugin binary",
			expectedErr: "binary for plugin dummy_default not found",
			action: func(t *testing.T) {
				err := os.Remove(path.Join(testPath, "notification-dummy"))
				require.NoError(t, err)
			},
		},
		{
			name:        "only specify user",
			expectedErr: "both plugin user and group must be set",
			procCfg: csconfig.PluginCfg{
				User: "123445555551122toto",
			},
			action: setPluginPermTo744,
		},
		{
			name:        "only specify group",
			expectedErr: "both plugin user and group must be set",
			procCfg: csconfig.PluginCfg{
				Group: "123445555551122toto",
			},
			action: setPluginPermTo744,
		},
		{
			name:        "Fails to run as root",
			expectedErr: "operation not permitted",
			procCfg: csconfig.PluginCfg{
				User:  "root",
				Group: "root",
			},
			action: setPluginPermTo744,
		},
		{
			name:        "Invalid user and group",
			expectedErr: "unknown user toto1234",
			procCfg: csconfig.PluginCfg{
				User:  "toto1234",
				Group: "toto1234",
			},
			action: setPluginPermTo744,
		},
		{
			name:        "Valid user and invalid group",
			expectedErr: "unknown group toto1234",
			procCfg: csconfig.PluginCfg{
				User:  "nobody",
				Group: "toto1234",
			},
			action: setPluginPermTo744,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			defer tearDown(t)
			buildDummyPlugin(t)
			if tc.action != nil {
				tc.action(t)
			}
			pb := PluginBroker{}
			profiles := csconfig.NewDefaultConfig().API.Server.Profiles
			profiles = append(profiles, &csconfig.ProfileCfg{
				Notifications: []string{"dummy_default"},
			})
			err := pb.Init(&tc.procCfg, profiles, &csconfig.ConfigurationPaths{
				PluginDir:       testPath,
				NotificationDir: "./tests/notifications",
			})
			defer pb.Kill()
			cstest.RequireErrorContains(t, err, tc.expectedErr)
		})
	}
}

func readconfig(t *testing.T, path string) ([]byte, PluginConfig) {
	var config PluginConfig
	orig, err := os.ReadFile("tests/notifications/dummy.yaml")
	require.NoError(t, err,"unable to read config file %s", path)

	err = yaml.Unmarshal(orig, &config)
	require.NoError(t, err,"unable to unmarshal config file")
	
	return orig, config
}

func writeconfig(t *testing.T, config PluginConfig, path string) {
	data, err := yaml.Marshal(&config)
	require.NoError(t, err,"unable to marshal config file")

	err = os.WriteFile(path, data, 0644)
	require.NoError(t, err,"unable to write config file %s", path)
}

func TestBrokerNoThreshold(t *testing.T) {
	var alerts []models.Alert
	DefaultEmptyTicker = 50 * time.Millisecond

	buildDummyPlugin(t)
	setPluginPermTo744(t)
	defer tearDown(t)

	// init
	pluginCfg := csconfig.PluginCfg{}
	pb := PluginBroker{}
	profiles := csconfig.NewDefaultConfig().API.Server.Profiles
	profiles = append(profiles, &csconfig.ProfileCfg{
		Notifications: []string{"dummy_default"},
	})

	// default config
	err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
		PluginDir:       testPath,
		NotificationDir: "./tests/notifications",
	})

	assert.NoError(t, err)
	tomb := tomb.Tomb{}

	go pb.Run(&tomb)
	defer pb.Kill()

	// send one item, it should be processed right now
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(200 * time.Millisecond)

	// we expect one now
	content, err := os.ReadFile("./out")
	require.NoError(t, err, "Error reading file")

	err = json.Unmarshal(content, &alerts)
	assert.NoError(t, err)
	assert.Len(t, alerts, 1)

	// remove it
	os.Remove("./out")

	// and another one
	log.Printf("second send")
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(200 * time.Millisecond)

	// we expect one again, as we cleaned the file
	content, err = os.ReadFile("./out")
	require.NoError(t, err, "Error reading file")

	err = json.Unmarshal(content, &alerts)
	log.Printf("content-> %s", content)
	assert.NoError(t, err)
	assert.Len(t, alerts, 1)
}

func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) {
	// test grouping by "time"
	DefaultEmptyTicker = 50 * time.Millisecond
	buildDummyPlugin(t)
	setPluginPermTo744(t)
	defer tearDown(t)

	// init
	pluginCfg := csconfig.PluginCfg{}
	pb := PluginBroker{}
	profiles := csconfig.NewDefaultConfig().API.Server.Profiles
	profiles = append(profiles, &csconfig.ProfileCfg{
		Notifications: []string{"dummy_default"},
	})
	// set groupwait and groupthreshold, should honor whichever comes first
	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
	cfg.GroupThreshold = 4
	cfg.GroupWait = 1 * time.Second
	writeconfig(t, cfg, "tests/notifications/dummy.yaml")
	err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
		PluginDir:       testPath,
		NotificationDir: "./tests/notifications",
	})
	assert.NoError(t, err)
	tomb := tomb.Tomb{}

	go pb.Run(&tomb)
	defer pb.Kill()
	// send data
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(500 * time.Millisecond)
	// because of group threshold, we shouldn't have data yet
	assert.NoFileExists(t, "./out")
	time.Sleep(1 * time.Second)
	// after 1 seconds, we should have data
	content, err := os.ReadFile("./out")
	assert.NoError(t, err)

	var alerts []models.Alert
	err = json.Unmarshal(content, &alerts)
	assert.NoError(t, err)
	assert.Len(t, alerts, 3)

	// restore config
	err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
	require.NoError(t, err,"unable to write config file")
}

func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) {
	DefaultEmptyTicker = 50 * time.Millisecond
	buildDummyPlugin(t)
	setPluginPermTo(t, "744")
	defer tearDown(t)

	// init
	pluginCfg := csconfig.PluginCfg{}
	pb := PluginBroker{}
	profiles := csconfig.NewDefaultConfig().API.Server.Profiles
	profiles = append(profiles, &csconfig.ProfileCfg{
		Notifications: []string{"dummy_default"},
	})

	// set groupwait and groupthreshold, should honor whichever comes first
	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
	cfg.GroupThreshold = 4
	cfg.GroupWait = 4 * time.Second
	writeconfig(t, cfg, "tests/notifications/dummy.yaml")
	err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
		PluginDir:       testPath,
		NotificationDir: "./tests/notifications",
	})
	assert.NoError(t, err)
	tomb := tomb.Tomb{}

	go pb.Run(&tomb)
	defer pb.Kill()

	// send data
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(100 * time.Millisecond)

	// because of group threshold, we shouldn't have data yet
	assert.NoFileExists(t, "./out")
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(100 * time.Millisecond)

	// and now we should
	content, err := os.ReadFile("./out")
	require.NoError(t, err, "Error reading file")

	var alerts []models.Alert
	err = json.Unmarshal(content, &alerts)
	assert.NoError(t, err)
	assert.Len(t, alerts, 4)

	// restore config
	err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
	require.NoError(t, err,"unable to write config file")
}

func TestBrokerRunGroupThreshold(t *testing.T) {
	// test grouping by "size"
	DefaultEmptyTicker = 50 * time.Millisecond
	buildDummyPlugin(t)
	setPluginPermTo(t, "744")
	defer tearDown(t)

	// init
	pluginCfg := csconfig.PluginCfg{}
	pb := PluginBroker{}
	profiles := csconfig.NewDefaultConfig().API.Server.Profiles
	profiles = append(profiles, &csconfig.ProfileCfg{
		Notifications: []string{"dummy_default"},
	})

	// set groupwait
	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
	cfg.GroupThreshold = 4
	writeconfig(t, cfg, "tests/notifications/dummy.yaml")
	err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
		PluginDir:       testPath,
		NotificationDir: "./tests/notifications",
	})

	assert.NoError(t, err)
	tomb := tomb.Tomb{}

	go pb.Run(&tomb)
	defer pb.Kill()

	// send data
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(100 * time.Millisecond)

	// because of group threshold, we shouldn't have data yet
	assert.NoFileExists(t, "./out")
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(100 * time.Millisecond)

	// and now we should
	content, err := os.ReadFile("./out")
	require.NoError(t, err, "Error reading file")

	var alerts []models.Alert
	err = json.Unmarshal(content, &alerts)
	assert.NoError(t, err)
	assert.Len(t, alerts, 4)

	// restore config
	err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
	require.NoError(t, err, "unable to write config file")
}

func TestBrokerRunTimeThreshold(t *testing.T) {
	DefaultEmptyTicker = 50 * time.Millisecond
	buildDummyPlugin(t)
	setPluginPermTo(t, "744")
	defer tearDown(t)

	// init
	pluginCfg := csconfig.PluginCfg{}
	pb := PluginBroker{}
	profiles := csconfig.NewDefaultConfig().API.Server.Profiles
	profiles = append(profiles, &csconfig.ProfileCfg{
		Notifications: []string{"dummy_default"},
	})

	// set groupwait
	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
	cfg.GroupWait = 1 * time.Second
	writeconfig(t, cfg, "tests/notifications/dummy.yaml")
	err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
		PluginDir:       testPath,
		NotificationDir: "./tests/notifications",
	})
	assert.NoError(t, err)
	tomb := tomb.Tomb{}

	go pb.Run(&tomb)
	defer pb.Kill()

	// send data
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(200 * time.Millisecond)

	// we shouldn't have data yet
	assert.NoFileExists(t, "./out")
	time.Sleep(1 * time.Second)

	// and now we should
	content, err := os.ReadFile("./out")
	require.NoError(t, err, "Error reading file")

	var alerts []models.Alert
	err = json.Unmarshal(content, &alerts)
	assert.NoError(t, err)
	assert.Len(t, alerts, 1)

	// restore config
	err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
	require.NoError(t, err, "unable to write config file %s", err)
}

func TestBrokerRunSimple(t *testing.T) {
	DefaultEmptyTicker = 50 * time.Millisecond
	buildDummyPlugin(t)
	setPluginPermTo(t, "744")
	defer tearDown(t)
	pluginCfg := csconfig.PluginCfg{}
	pb := PluginBroker{}
	profiles := csconfig.NewDefaultConfig().API.Server.Profiles
	profiles = append(profiles, &csconfig.ProfileCfg{
		Notifications: []string{"dummy_default"},
	})
	err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
		PluginDir:       testPath,
		NotificationDir: "./tests/notifications",
	})
	assert.NoError(t, err)
	tomb := tomb.Tomb{}

	go pb.Run(&tomb)
	defer pb.Kill()

	assert.NoFileExists(t, "./out")

	defer os.Remove("./out")

	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
	time.Sleep(time.Millisecond * 200)

	content, err := os.ReadFile("./out")
	require.NoError(t, err, "Error reading file")

	var alerts []models.Alert
	err = json.Unmarshal(content, &alerts)
	assert.NoError(t, err)
	assert.Len(t, alerts, 2)
}

func buildDummyPlugin(t *testing.T) {
	dir, err := os.MkdirTemp("./tests", "cs_plugin_test")
	require.NoError(t, err)

	cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy"), "../../plugins/notifications/dummy/")
	err = cmd.Run()
	require.NoError(t, err, "while building dummy plugin")

	testPath = dir
	os.Remove("./out")
}

func setPluginPermTo(t *testing.T, perm string) {
	if runtime.GOOS != "windows" {
		err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run()
		require.NoError(t, err, "chmod 744 %s", path.Join(testPath, "notification-dummy"))
	}
}

func setUp(t *testing.T) {
	dir, err := os.MkdirTemp("./", "cs_plugin_test")
	require.NoError(t, err)

	f, err := os.Create(path.Join(dir, "slack"))
	require.NoError(t, err)

	f.Close()
	f, err = os.Create(path.Join(dir, "notification-gitter"))
	require.NoError(t, err)

	f.Close()
	err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666)
	require.NoError(t, err)

	testPath = dir
}

func tearDown(t *testing.T) {
	err := os.RemoveAll(testPath)
	require.NoError(t, err)

	os.Remove("./out")
}
