package filecount

import (
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/testutil"
	"github.com/stretchr/testify/require"
)

func TestNoFilters(t *testing.T) {
	fc := getNoFilterFileCount()
	matches := []string{"foo", "bar", "baz", "qux",
		"subdir/", "subdir/quux", "subdir/quuz",
		"subdir/nested2", "subdir/nested2/qux"}
	fileCountEquals(t, fc, len(matches), 5096)
}

func TestNoFiltersOnChildDir(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.Directories = []string{getTestdataDir() + "/*"}
	matches := []string{"subdir/quux", "subdir/quuz",
		"subdir/nested2/qux", "subdir/nested2"}

	tags := map[string]string{"directory": getTestdataDir() + "/subdir"}
	acc := testutil.Accumulator{}
	acc.GatherError(fc.Gather)
	require.True(t, acc.HasPoint("filecount", tags, "count", int64(len(matches))))
	require.True(t, acc.HasPoint("filecount", tags, "size_bytes", int64(600)))
}

func TestNoRecursiveButSuperMeta(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.Recursive = false
	fc.Directories = []string{getTestdataDir() + "/**"}
	matches := []string{"subdir/quux", "subdir/quuz", "subdir/nested2"}

	tags := map[string]string{"directory": getTestdataDir() + "/subdir"}
	acc := testutil.Accumulator{}
	acc.GatherError(fc.Gather)

	require.True(t, acc.HasPoint("filecount", tags, "count", int64(len(matches))))
	require.True(t, acc.HasPoint("filecount", tags, "size_bytes", int64(200)))
}

func TestNameFilter(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.Name = "ba*"
	matches := []string{"bar", "baz"}
	fileCountEquals(t, fc, len(matches), 0)
}

func TestNonRecursive(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.Recursive = false
	matches := []string{"foo", "bar", "baz", "qux", "subdir"}

	fileCountEquals(t, fc, len(matches), 4496)
}

func TestDoubleAndSimpleStar(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.Directories = []string{getTestdataDir() + "/**/*"}
	matches := []string{"qux"}

	tags := map[string]string{"directory": getTestdataDir() + "/subdir/nested2"}

	acc := testutil.Accumulator{}
	acc.GatherError(fc.Gather)

	require.True(t, acc.HasPoint("filecount", tags, "count", int64(len(matches))))
	require.True(t, acc.HasPoint("filecount", tags, "size_bytes", int64(400)))
}

func TestRegularOnlyFilter(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.RegularOnly = true
	matches := []string{
		"foo", "bar", "baz", "qux", "subdir/quux", "subdir/quuz",
		"subdir/nested2/qux"}

	fileCountEquals(t, fc, len(matches), 800)
}

func TestSizeFilter(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.Size = internal.Size{Size: -100}
	matches := []string{"foo", "bar", "baz",
		"subdir/quux", "subdir/quuz"}
	fileCountEquals(t, fc, len(matches), 0)

	fc.Size = internal.Size{Size: 100}
	matches = []string{"qux", "subdir/nested2//qux"}

	fileCountEquals(t, fc, len(matches), 800)
}

func TestMTimeFilter(t *testing.T) {
	mtime := time.Date(2011, time.December, 14, 18, 25, 5, 0, time.UTC)
	fileAge := time.Since(mtime) - (60 * time.Second)

	fc := getNoFilterFileCount()
	fc.MTime = internal.Duration{Duration: -fileAge}
	matches := []string{"foo", "bar", "qux",
		"subdir/", "subdir/quux", "subdir/quuz",
		"subdir/nested2", "subdir/nested2/qux"}

	fileCountEquals(t, fc, len(matches), 5096)

	fc.MTime = internal.Duration{Duration: fileAge}
	matches = []string{"baz"}
	fileCountEquals(t, fc, len(matches), 0)
}

// The library dependency karrick/godirwalk completely abstracts out the
// behavior of the FollowSymlinks plugin input option. However, it should at
// least behave identically when enabled on a filesystem with no symlinks.
func TestFollowSymlinks(t *testing.T) {
	fc := getNoFilterFileCount()
	fc.FollowSymlinks = true
	matches := []string{"foo", "bar", "baz", "qux",
		"subdir/", "subdir/quux", "subdir/quuz",
		"subdir/nested2", "subdir/nested2/qux"}

	fileCountEquals(t, fc, len(matches), 5096)
}

// Paths with a trailing slash will not exactly match paths produced during the
// walk as these paths are cleaned before being returned from godirwalk. #6329
func TestDirectoryWithTrailingSlash(t *testing.T) {
	plugin := &FileCount{
		Directories: []string{getTestdataDir() + string(filepath.Separator)},
		Name:        "*",
		Recursive:   true,
		Fs:          getFakeFileSystem(getTestdataDir()),
	}

	var acc testutil.Accumulator
	err := plugin.Gather(&acc)
	require.NoError(t, err)

	expected := []telegraf.Metric{
		testutil.MustMetric(
			"filecount",
			map[string]string{
				"directory": getTestdataDir(),
			},
			map[string]interface{}{
				"count":      9,
				"size_bytes": 5096,
			},
			time.Unix(0, 0),
			telegraf.Gauge,
		),
	}

	testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
}

func getNoFilterFileCount() FileCount {
	return FileCount{
		Log:         testutil.Logger{},
		Directories: []string{getTestdataDir()},
		Name:        "*",
		Recursive:   true,
		RegularOnly: false,
		Size:        internal.Size{Size: 0},
		MTime:       internal.Duration{Duration: 0},
		fileFilters: nil,
		Fs:          getFakeFileSystem(getTestdataDir()),
	}
}

func getTestdataDir() string {
	dir, err := os.Getwd()
	if err != nil {
		// if we cannot even establish the test directory, further progress is meaningless
		panic(err)
	}

	var chunks []string
	var testDirectory string

	if runtime.GOOS == "windows" {
		chunks = strings.Split(dir, "\\")
		testDirectory = strings.Join(chunks[:], "\\") + "\\testdata"
	} else {
		chunks = strings.Split(dir, "/")
		testDirectory = strings.Join(chunks[:], "/") + "/testdata"
	}
	return testDirectory
}

func getFakeFileSystem(basePath string) fakeFileSystem {
	// create our desired "filesystem" object, complete with an internal map allowing our funcs to return meta data as requested

	mtime := time.Date(2015, time.December, 14, 18, 25, 5, 0, time.UTC)
	olderMtime := time.Date(2010, time.December, 14, 18, 25, 5, 0, time.UTC)

	// set file permisions
	var fmask uint32 = 0666
	var dmask uint32 = 0666

	// set directory bit
	dmask |= (1 << uint(32-1))

	// create a lookup map for getting "files" from the "filesystem"
	fileList := map[string]fakeFileInfo{
		basePath:                         {name: "testdata", size: int64(4096), filemode: uint32(dmask), modtime: mtime, isdir: true},
		basePath + "/foo":                {name: "foo", filemode: uint32(fmask), modtime: mtime},
		basePath + "/bar":                {name: "bar", filemode: uint32(fmask), modtime: mtime},
		basePath + "/baz":                {name: "baz", filemode: uint32(fmask), modtime: olderMtime},
		basePath + "/qux":                {name: "qux", size: int64(400), filemode: uint32(fmask), modtime: mtime},
		basePath + "/subdir":             {name: "subdir", size: int64(4096), filemode: uint32(dmask), modtime: mtime, isdir: true},
		basePath + "/subdir/quux":        {name: "quux", filemode: uint32(fmask), modtime: mtime},
		basePath + "/subdir/quuz":        {name: "quuz", filemode: uint32(fmask), modtime: mtime},
		basePath + "/subdir/nested2":     {name: "nested2", size: int64(200), filemode: uint32(dmask), modtime: mtime, isdir: true},
		basePath + "/subdir/nested2/qux": {name: "qux", filemode: uint32(fmask), modtime: mtime, size: int64(400)},
	}

	fs := fakeFileSystem{files: fileList}
	return fs

}

func fileCountEquals(t *testing.T, fc FileCount, expectedCount int, expectedSize int) {
	tags := map[string]string{"directory": getTestdataDir()}
	acc := testutil.Accumulator{}
	acc.GatherError(fc.Gather)
	require.True(t, acc.HasPoint("filecount", tags, "count", int64(expectedCount)))
	require.True(t, acc.HasPoint("filecount", tags, "size_bytes", int64(expectedSize)))
}
