package fileacquisition

import (
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/crowdsecurity/crowdsec/pkg/types"
	log "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus/hooks/test"
	"github.com/stretchr/testify/assert"
	"gopkg.in/tomb.v2"
)

func TestBadConfiguration(t *testing.T) {
	tests := []struct {
		config      string
		expectedErr string
	}{
		{
			config:      `foobar: asd.log`,
			expectedErr: "line 1: field foobar not found in type fileacquisition.FileConfiguration",
		},
		{
			config:      `mode: tail`,
			expectedErr: "no filename or filenames configuration provided",
		},
		{
			config:      `filename: "[asd-.log"`,
			expectedErr: "Glob failure: syntax error in pattern",
		},
	}

	subLogger := log.WithFields(log.Fields{
		"type": "file",
	})
	for _, test := range tests {
		f := FileSource{}
		err := f.Configure([]byte(test.config), subLogger)
		assert.Contains(t, err.Error(), test.expectedErr)
	}
}

func TestConfigureDSN(t *testing.T) {
	tests := []struct {
		dsn         string
		expectedErr string
	}{
		{
			dsn:         "asd://",
			expectedErr: "invalid DSN asd:// for file source, must start with file://",
		},
		{
			dsn:         "file://",
			expectedErr: "empty file:// DSN",
		},
		{
			dsn:         "file:///etc/passwd?log_level=warn",
			expectedErr: "",
		},
		{
			dsn:         "file:///etc/passwd?log_level=foobar",
			expectedErr: "unknown level foobar: not a valid logrus Level:",
		},
	}
	subLogger := log.WithFields(log.Fields{
		"type": "file",
	})
	for _, test := range tests {
		f := FileSource{}
		err := f.ConfigureByDSN(test.dsn, "testtype", subLogger)
		if test.expectedErr != "" {
			assert.Contains(t, err.Error(), test.expectedErr)
		} else {
			assert.Equal(t, err, nil)
		}
	}
}

func TestOneShot(t *testing.T) {
	tests := []struct {
		config         string
		expectedErr    string
		expectedOutput string
		expectedLines  int
		logLevel       log.Level
		setup          func()
		afterConfigure func()
		teardown       func()
	}{
		{
			config: `
mode: cat
filename: /etc/shadow`,
			expectedErr:    "failed opening /etc/shadow: open /etc/shadow: permission denied",
			expectedOutput: "",
			logLevel:       log.WarnLevel,
			expectedLines:  0,
		},
		{
			config: `
mode: cat
filename: /`,
			expectedErr:    "",
			expectedOutput: "/ is a directory, ignoring it",
			logLevel:       log.WarnLevel,
			expectedLines:  0,
		},
		{
			config: `
mode: cat
filename: "[*-.log"`,
			expectedErr:    "Glob failure: syntax error in pattern",
			expectedOutput: "",
			logLevel:       log.WarnLevel,
			expectedLines:  0,
		},
		{
			config: `
mode: cat
filename: /do/not/exist`,
			expectedErr:    "",
			expectedOutput: "No matching files for pattern /do/not/exist",
			logLevel:       log.WarnLevel,
			expectedLines:  0,
		},
		{
			config: `
mode: cat
filename: test_files/test.log`,
			expectedErr:    "",
			expectedOutput: "",
			expectedLines:  5,
			logLevel:       log.WarnLevel,
		},
		{
			config: `
mode: cat
filename: test_files/test.log.gz`,
			expectedErr:    "",
			expectedOutput: "",
			expectedLines:  5,
			logLevel:       log.WarnLevel,
		},
		{
			config: `
mode: cat
filename: test_files/bad.gz`,
			expectedErr:    "failed to read gz test_files/bad.gz: unexpected EOF",
			expectedOutput: "",
			expectedLines:  0,
			logLevel:       log.WarnLevel,
		},
		{
			config: `
mode: cat
filename: test_files/test_delete.log`,
			setup: func() {
				os.Create("test_files/test_delete.log")
			},
			afterConfigure: func() {
				os.Remove("test_files/test_delete.log")
			},
			expectedErr: "could not stat file test_files/test_delete.log : stat test_files/test_delete.log: no such file or directory",
		},
	}

	for _, ts := range tests {
		logger, hook := test.NewNullLogger()
		logger.SetLevel(ts.logLevel)
		subLogger := logger.WithFields(log.Fields{
			"type": "file",
		})
		tomb := tomb.Tomb{}
		out := make(chan types.Event)
		f := FileSource{}
		if ts.setup != nil {
			ts.setup()
		}
		err := f.Configure([]byte(ts.config), subLogger)
		if err != nil && ts.expectedErr != "" {
			assert.Contains(t, err.Error(), ts.expectedErr)
			continue
		} else if err != nil && ts.expectedErr == "" {
			t.Fatalf("Unexpected error : %s", err)
		}
		if ts.afterConfigure != nil {
			ts.afterConfigure()
		}
		actualLines := 0
		if ts.expectedLines != 0 {
			go func() {
			READLOOP:
				for {
					select {
					case <-out:
						actualLines++
					case <-time.After(1 * time.Second):
						break READLOOP
					}
				}
			}()
		}
		err = f.OneShotAcquisition(out, &tomb)
		if ts.expectedLines != 0 {
			assert.Equal(t, actualLines, ts.expectedLines)
		}
		if ts.expectedErr != "" {
			if err == nil {
				t.Fatalf("Expected error but got nothing ! %+v", ts)
			}
			assert.Contains(t, err.Error(), ts.expectedErr)
		}
		if ts.expectedOutput != "" {
			assert.Contains(t, hook.LastEntry().Message, ts.expectedOutput)
			hook.Reset()
		}
		if ts.teardown != nil {
			ts.teardown()
		}
	}
}

func TestLiveAcquisition(t *testing.T) {
	tests := []struct {
		config         string
		expectedErr    string
		expectedOutput string
		expectedLines  int
		logLevel       log.Level
		setup          func()
		afterConfigure func()
		teardown       func()
	}{
		{
			config: `
mode: tail
filename: /etc/shadow`,
			expectedErr:    "",
			expectedOutput: "unable to read /etc/shadow : permission denied",
			logLevel:       log.InfoLevel,
			expectedLines:  0,
		},
		{
			config: `
mode: tail
filename: /`,
			expectedErr:    "",
			expectedOutput: "/ is a directory, ignoring it",
			logLevel:       log.WarnLevel,
			expectedLines:  0,
		},
		{
			config: `
mode: tail
filename: /do/not/exist`,
			expectedErr:    "",
			expectedOutput: "No matching files for pattern /do/not/exist",
			logLevel:       log.WarnLevel,
			expectedLines:  0,
		},
		{
			config: `
mode: tail
filenames:
 - test_files/*.log
force_inotify: true`,
			expectedErr:    "",
			expectedOutput: "",
			expectedLines:  5,
			logLevel:       log.DebugLevel,
		},
		{
			config: `
mode: tail
filenames:
 - test_files/*.log
force_inotify: true`,
			expectedErr:    "",
			expectedOutput: "",
			expectedLines:  0,
			logLevel:       log.DebugLevel,
			afterConfigure: func() {
				os.Create("test_files/a.log")
				os.Remove("test_files/a.log")
			},
		},
		{
			config: `
mode: tail
filenames:
 - test_files/*.log
force_inotify: true`,
			expectedErr:    "",
			expectedOutput: "",
			expectedLines:  5,
			logLevel:       log.DebugLevel,
			afterConfigure: func() {
				os.Create("test_files/a.log")
				time.Sleep(1 * time.Second)
				os.Chmod("test_files/a.log", 0000)
			},
			teardown: func() {
				os.Chmod("test_files/a.log", 0644)
				os.Remove("test_files/a.log")
			},
		},
		{
			config: `
mode: tail
filenames:
 - test_files/*.log
force_inotify: true`,
			expectedErr:    "",
			expectedOutput: "",
			expectedLines:  5,
			logLevel:       log.DebugLevel,
			afterConfigure: func() {
				os.Mkdir("test_files/pouet/", 0700)
			},
			teardown: func() {
				os.Remove("test_files/pouet/")
			},
		},
	}

	for _, ts := range tests {
		logger, hook := test.NewNullLogger()
		logger.SetLevel(ts.logLevel)
		subLogger := logger.WithFields(log.Fields{
			"type": "file",
		})
		tomb := tomb.Tomb{}
		out := make(chan types.Event)
		f := FileSource{}
		if ts.setup != nil {
			ts.setup()
		}
		err := f.Configure([]byte(ts.config), subLogger)
		if err != nil {
			t.Fatalf("Unexpected error : %s", err)
		}
		if ts.afterConfigure != nil {
			ts.afterConfigure()
		}
		actualLines := 0
		if ts.expectedLines != 0 {
			go func() {
			READLOOP:
				for {
					select {
					case <-out:
						actualLines++
					case <-time.After(2 * time.Second):
						break READLOOP
					}
				}
			}()
		}
		err = f.StreamingAcquisition(out, &tomb)

		if ts.expectedErr != "" {
			if err == nil {
				t.Fatalf("Expected error but got nothing ! %+v", ts)
			}
			assert.Contains(t, err.Error(), ts.expectedErr)
		}

		if ts.expectedLines != 0 {
			fd, err := os.Create("test_files/stream.log")
			if err != nil {
				t.Fatalf("could not create test file : %s", err)
			}
			for i := 0; i < 5; i++ {
				_, err = fd.WriteString(fmt.Sprintf("%d\n", i))
				if err != nil {
					t.Fatalf("could not write test file : %s", err)
					os.Remove("test_files/stream.log")
				}
			}
			fd.Close()
			//we sleep to make sure we detect the new file
			time.Sleep(1 * time.Second)
			os.Remove("test_files/stream.log")
			assert.Equal(t, actualLines, ts.expectedLines)
		}

		if ts.expectedOutput != "" {
			if hook.LastEntry() == nil {
				t.Fatalf("expected output %s, but got nothing", ts.expectedOutput)
			}
			assert.Contains(t, hook.LastEntry().Message, ts.expectedOutput)
			hook.Reset()
		}

		if ts.teardown != nil {
			ts.teardown()
		}

		tomb.Kill(nil)
	}
}
