package rule

import (
	"fmt"
	"io"
	"math/rand"
	"os"
	"testing"
	"time"
)

var tmpDir string

func TestMain(m *testing.M) {
	tmpDir = "/tmp/ostest_" + randString()
	os.Mkdir(tmpDir, 0777)
	defer os.RemoveAll(tmpDir)
	os.Exit(m.Run())
}

func TestRuleLoader(t *testing.T) {
	t.Parallel()
	t.Log("Test rules loader")

	var list []Operator
	dur1s := Duration("1s")
	dummyOper, _ := NewOperator(Simple, false, OpTrue, "", list)
	dummyOper.Compile()
	inMem1sRule := Create("000-xxx-name", "rule description xxx", true, false, false, Allow, dur1s, dummyOper)
	inMemUntilRestartRule := Create("000-aaa-name", "rule description aaa", true, false, false, Allow, Restart, dummyOper)

	l, err := NewLoader(false)
	if err != nil {
		t.Fail()
	}
	if err = l.Load("/non/existent/path/"); err == nil {
		t.Error("non existent path test: err should not be nil")
	}

	if err = l.Load("testdata/"); err != nil {
		t.Error("Error loading test rules: ", err)
	}
	// we expect 6 valid rules (2 invalid), loaded from testdata/
	testNumRules(t, l, 6)

	if err = l.Add(inMem1sRule, false); err != nil {
		t.Error("Error adding temporary rule")
	}
	testNumRules(t, l, 7)

	// test auto deletion of temporary rule
	time.Sleep(time.Second * 2)
	testNumRules(t, l, 6)

	if err = l.Add(inMemUntilRestartRule, false); err != nil {
		t.Error("Error adding temporary rule (2)")
	}
	testNumRules(t, l, 7)
	testRulesOrder(t, l)
	testSortRules(t, l)
	testFindMatch(t, l)
	testFindEnabled(t, l)
	testDurationChange(t, l)
}

func TestRuleLoaderInvalidRegexp(t *testing.T) {
	t.Parallel()
	t.Log("Test rules loader: invalid regexp")

	l, err := NewLoader(true)
	if err != nil {
		t.Fail()
	}
	t.Run("loadRule() from disk test (simple)", func(t *testing.T) {
		if err := l.loadRule("testdata/invalid-regexp.json"); err == nil {
			t.Error("invalid regexp rule loaded: loadRule()")
		}
	})

	t.Run("loadRule() from disk test (list)", func(t *testing.T) {
		if err := l.loadRule("testdata/invalid-regexp-list.json"); err == nil {
			t.Error("invalid regexp rule loaded: loadRule()")
		}
	})

	var list []Operator
	dur30m := Duration("30m")
	opListData := `[{"type": "regexp", "operand": "process.path", "sensitive": false, "data": "^(/di(rmngr)$"}, {"type": "simple", "operand": "dest.port", "data": "53", "sensitive": false}]`
	invalidRegexpOp, _ := NewOperator(List, false, OpList, opListData, list)
	invalidRegexpRule := Create("invalid-regexp", "invalid rule description", true, false, false, Allow, dur30m, invalidRegexpOp)

	t.Run("replaceUserRule() test list", func(t *testing.T) {
		if err := l.replaceUserRule(invalidRegexpRule); err == nil {
			t.Error("invalid regexp rule loaded: replaceUserRule()")
		}
	})
}

// Test rules of type operator.list. There're these scenarios:
// - Enabled rules:
//    * operator Data field is ignored if it contains the list of operators as json string.
//    * the operarots list is expanded as json objecs under "list": []
// For new rules (> v1.6.3), Data field will be empty.
//
// - Disabled rules
//    * (old) the Data field contains the list of operators as json string, and the list of operarots is empty.
//    * Data field empty, and the list of operators expanded.
// In all cases the list of operators must be loaded.
func TestRuleLoaderList(t *testing.T) {
	l, err := NewLoader(true)
	if err != nil {
		t.Fail()
	}

	testRules := map[string]string{
		"rule-with-operator-list":                          "testdata/rule-operator-list.json",
		"rule-disabled-with-operators-list-as-json-string": "testdata/rule-disabled-operator-list.json",
		"rule-disabled-with-operators-list-expanded":       "testdata/rule-disabled-operator-list-expanded.json",
		"rule-with-operator-list-data-empty":               "testdata/rule-operator-list-data-empty.json",
	}

	for name, path := range testRules {
		t.Run(fmt.Sprint("loadRule() ", path), func(t *testing.T) {
			if err := l.loadRule(path); err != nil {
				t.Error(fmt.Sprint("loadRule() ", path, " error:"), err)
			}
			t.Log("Test: List rule:", name, path)
			r, found := l.rules[name]
			if !found {
				t.Error(fmt.Sprint("loadRule() ", path, " not in the list:"), l.rules)
			}
			// Starting from > v1.6.3, after loading a rule of type List, the field Operator.Data is emptied, if the Data contained the list of operators as json.
			if len(r.Operator.List) != 2 {
				t.Error(fmt.Sprint("loadRule() ", path, " operator List not loaded:"), r)
			}
			if r.Operator.List[0].Type != Simple ||
				r.Operator.List[0].Operand != OpProcessPath ||
				r.Operator.List[0].Data != "/usr/bin/telnet" {
				t.Error(fmt.Sprint("loadRule() ", path, " operator List 0 not loaded:"), r)
			}
			if r.Operator.List[1].Type != Simple ||
				r.Operator.List[1].Operand != OpDstPort ||
				r.Operator.List[1].Data != "53" {
				t.Error(fmt.Sprint("loadRule() ", path, " operator List 1 not loaded:"), r)
			}
		})
	}
}

func TestLiveReload(t *testing.T) {
	t.Parallel()
	t.Log("Test rules loader with live reload")
	l, err := NewLoader(true)
	if err != nil {
		t.Fail()
	}
	if err = Copy("testdata/000-allow-chrome.json", tmpDir+"/000-allow-chrome.json"); err != nil {
		t.Error("Error copying rule into a temp dir")
	}
	if err = Copy("testdata/001-deny-chrome.json", tmpDir+"/001-deny-chrome.json"); err != nil {
		t.Error("Error copying rule into a temp dir")
	}
	if err = l.Load(tmpDir); err != nil {
		t.Error("Error loading test rules: ", err)
	}
	//wait for watcher to activate
	time.Sleep(time.Second)
	if err = Copy("testdata/live_reload/test-live-reload-remove.json", tmpDir+"/test-live-reload-remove.json"); err != nil {
		t.Error("Error copying rules into temp dir")
	}
	if err = Copy("testdata/live_reload/test-live-reload-delete.json", tmpDir+"/test-live-reload-delete.json"); err != nil {
		t.Error("Error copying rules into temp dir")
	}
	//wait for watcher to pick up the changes
	time.Sleep(time.Second)
	testNumRules(t, l, 4)
	if err = os.Remove(tmpDir + "/test-live-reload-remove.json"); err != nil {
		t.Error("Error Remove()ing file from temp dir")
	}
	if err = l.Delete("test-live-reload-delete"); err != nil {
		t.Error("Error Delete()ing file from temp dir")
	}
	//wait for watcher to pick up the changes
	time.Sleep(time.Second)
	testNumRules(t, l, 2)
}

func randString() string {
	rand.Seed(time.Now().UnixNano())
	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
	b := make([]rune, 10)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func Copy(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, in)
	if err != nil {
		return err
	}
	return out.Close()
}

func testNumRules(t *testing.T, l *Loader, num int) {
	if l.NumRules() != num {
		t.Error("rules number should be (2): ", num)
	}
}

func testRulesOrder(t *testing.T, l *Loader) {
	if l.activeRules[0] != "000-aaa-name" {
		t.Error("Rules not in order (0): ", l.activeRules)
	}
	if l.activeRules[1] != "000-allow-chrome" {
		t.Error("Rules not in order (1): ", l.activeRules)
	}
	if l.activeRules[2] != "001-deny-chrome" {
		t.Error("Rules not in order (2): ", l.activeRules)
	}
}

func testSortRules(t *testing.T, l *Loader) {
	l.activeRules[1] = "001-deny-chrome"
	l.activeRules[2] = "000-allow-chrome"
	l.sortRules()
	if l.activeRules[1] != "000-allow-chrome" {
		t.Error("Rules not in order (1): ", l.activeRules)
	}
	if l.activeRules[2] != "001-deny-chrome" {
		t.Error("Rules not in order (2): ", l.activeRules)
	}
}

func testFindMatch(t *testing.T, l *Loader) {
	conn.Process.Path = "/opt/google/chrome/chrome"

	testFindPriorityMatch(t, l)
	testFindDenyMatch(t, l)
	testFindAllowMatch(t, l)

	restoreConnection()
}

func testFindPriorityMatch(t *testing.T, l *Loader) {
	match := l.FindFirstMatch(conn)
	if match == nil {
		t.Error("FindPriorityMatch didn't match")
	}
	// test 000-allow-chrome, priority == true
	if match.Name != "000-allow-chrome" {
		t.Error("findPriorityMatch: priority rule failed: ", match)
	}

}

func testFindDenyMatch(t *testing.T, l *Loader) {
	l.rules["000-allow-chrome"].Precedence = false
	// test 000-allow-chrome, priority == false
	// 001-deny-chrome must match
	match := l.FindFirstMatch(conn)
	if match == nil {
		t.Error("FindDenyMatch deny didn't match")
	}
	if match.Name != "001-deny-chrome" {
		t.Error("findDenyMatch: deny rule failed: ", match)
	}
}

func testFindAllowMatch(t *testing.T, l *Loader) {
	l.rules["000-allow-chrome"].Precedence = false
	l.rules["001-deny-chrome"].Action = Allow
	// test 000-allow-chrome, priority == false
	// 001-deny-chrome must match
	match := l.FindFirstMatch(conn)
	if match == nil {
		t.Error("FindAllowMatch allow didn't match")
	}
	if match.Name != "001-deny-chrome" {
		t.Error("findAllowMatch: allow rule failed: ", match)
	}
}

func testFindEnabled(t *testing.T, l *Loader) {
	l.rules["000-allow-chrome"].Precedence = false
	l.rules["001-deny-chrome"].Action = Allow
	l.rules["001-deny-chrome"].Enabled = false
	// test 000-allow-chrome, priority == false
	// 001-deny-chrome must match
	match := l.FindFirstMatch(conn)
	if match == nil {
		t.Error("FindEnabledMatch, match nil")
	}
	if match.Name == "001-deny-chrome" {
		t.Error("findEnabledMatch: deny rule shouldn't have matched: ", match)
	}
}

// test that changing the Duration of a temporary rule doesn't delete
// the new one, ignoring the old timer.
func testDurationChange(t *testing.T, l *Loader) {
	l.rules["000-aaa-name"].Duration = "2s"
	if err := l.replaceUserRule(l.rules["000-aaa-name"]); err != nil {
		t.Error("testDurationChange, error replacing rule: ", err)
	}
	l.rules["000-aaa-name"].Duration = "1h"
	if err := l.replaceUserRule(l.rules["000-aaa-name"]); err != nil {
		t.Error("testDurationChange, error replacing rule: ", err)
	}
	time.Sleep(time.Second * 4)
	if _, found := l.rules["000-aaa-name"]; !found {
		t.Error("testDurationChange, error: rule has been deleted")
	}
}
