package example

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/metric"
	"github.com/influxdata/telegraf/testutil"
)

// This file should contain a set of unit-tests to cover your plugin. This will ease
// spotting bugs and mistakes when later modifying or extending the functionality.
// To do so, please write one 'TestXYZ' function per 'case' e.g. default init,
// things that should fail or expected values from a mockup.

func TestInitDefault(t *testing.T) {
	// This test should succeed with the default initialization.

	// Use whatever you use in the init() function plus the mandatory options.
	// ATTENTION: Always initialize the "Log" as you will get SIGSEGV otherwise.
	plugin := &Example{
		DeviceName: "test",
		Timeout:    config.Duration(100 * time.Millisecond),
		Log:        testutil.Logger{},
	}

	// Test the initialization succeeds
	require.NoError(t, plugin.Init())

	// Also test that default values are set correctly
	require.Equal(t, config.Duration(100*time.Millisecond), plugin.Timeout)
	require.Equal(t, "test", plugin.DeviceName)
	require.Equal(t, int64(2), plugin.NumberFields)
}

func TestInitFail(t *testing.T) {
	// You should also test for your safety nets to work i.e. you get errors for
	// invalid configuration-option values. So check your error paths in Init()
	// and check if you reach them

	// We setup a table-test here to specify "setting" - "expected error" values.
	// Even though it seems overkill here for the example plugin, we reuse this structure
	// later for checking the metrics
	tests := []struct {
		name     string
		plugin   *Example
		expected string
	}{
		{
			name:     "all empty",
			plugin:   &Example{},
			expected: "device name cannot be empty",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Always initialize the logger to avoid SIGSEGV. This is done automatically by
			// telegraf during normal operation.
			tt.plugin.Log = testutil.Logger{}
			err := tt.plugin.Init()
			require.Error(t, err)
			require.EqualError(t, err, tt.expected)
		})
	}
}

func TestFixedValue(t *testing.T) {
	// You can organize the test e.g. by operation mode (like we do here random vs. fixed), by features or
	// by different metrics gathered. Please choose the partitioning most suited for your plugin

	// We again setup a table-test here to specify "setting" - "expected output metric" pairs.
	tests := []struct {
		name     string
		plugin   *Example
		expected []telegraf.Metric
	}{
		{
			name: "count only",
			plugin: &Example{
				DeviceName:   "test",
				NumberFields: 1,
			},
			expected: []telegraf.Metric{
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count": 1,
					},
					time.Unix(0, 0),
				),
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count": 2,
					},
					time.Unix(0, 0),
				),
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count": 3,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "default settings",
			plugin: &Example{
				DeviceName: "test",
			},
			expected: []telegraf.Metric{
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count":  1,
						"field1": float64(0),
					},
					time.Unix(0, 0),
				),
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count":  2,
						"field1": float64(0),
					},
					time.Unix(0, 0),
				),
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count":  3,
						"field1": float64(0),
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "more fields",
			plugin: &Example{
				DeviceName:   "test",
				NumberFields: 4,
			},
			expected: []telegraf.Metric{
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count":  1,
						"field1": float64(0),
						"field2": float64(0),
						"field3": float64(0),
					},
					time.Unix(0, 0),
				),
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count":  2,
						"field1": float64(0),
						"field2": float64(0),
						"field3": float64(0),
					},
					time.Unix(0, 0),
				),
				metric.New(
					"example",
					map[string]string{
						"device": "test",
					},
					map[string]interface{}{
						"count":  3,
						"field1": float64(0),
						"field2": float64(0),
						"field3": float64(0),
					},
					time.Unix(0, 0),
				),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator

			tt.plugin.Log = testutil.Logger{}
			require.NoError(t, tt.plugin.Init())

			// Call gather and check no error occurs. In case you use acc.AddError() somewhere
			// in your code, it is not sufficient to only check the return value of Gather().
			require.NoError(t, tt.plugin.Gather(&acc))
			require.Empty(t, acc.Errors, "found errors accumulated by acc.AddError()")

			// Wait for the expected number of metrics to avoid flaky tests due to
			// race conditions.
			acc.Wait(len(tt.expected))

			// Compare the metrics in a convenient way. Here we ignore
			// the metric time during comparison as we cannot inject the time
			// during test. For more comparison options check testutil package.
			testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
		})
	}
}

func TestRandomValue(t *testing.T) {
	// Sometimes, you cannot know the exact outcome of the gather cycle e.g. if the gathering involves random data.
	// However, you should check the result nevertheless, applying as many conditions as you can.

	// We again setup a table-test here to specify "setting" - "expected output metric" pairs.
	tests := []struct {
		name     string
		plugin   *Example
		template telegraf.Metric
	}{
		{
			name: "count only",
			plugin: &Example{
				DeviceName:           "test",
				NumberFields:         1,
				EnableRandomVariable: true,
			},
			template: metric.New(
				"example",
				map[string]string{
					"device": "test",
				},
				map[string]interface{}{
					"count": 1,
				},
				time.Unix(0, 0),
			),
		},
		{
			name: "default settings",
			plugin: &Example{
				DeviceName:           "test",
				EnableRandomVariable: true,
			},
			template: metric.New(
				"example",
				map[string]string{
					"device": "test",
				},
				map[string]interface{}{
					"count":  1,
					"field1": float64(0),
				},
				time.Unix(0, 0),
			),
		},
		{
			name: "more fields",
			plugin: &Example{
				DeviceName:           "test",
				NumberFields:         4,
				EnableRandomVariable: true,
			},
			template: metric.New(
				"example",
				map[string]string{
					"device": "test",
				},
				map[string]interface{}{
					"count":  1,
					"field1": float64(0),
					"field2": float64(0),
					"field3": float64(0),
				},
				time.Unix(0, 0),
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator

			tt.plugin.Log = testutil.Logger{}
			require.NoError(t, tt.plugin.Init())

			// Call gather and check no error occurs. In case you use acc.AddError() somewhere
			// in your code, it is not sufficient to only check the return value of Gather().
			require.NoError(t, tt.plugin.Gather(&acc))
			require.Empty(t, acc.Errors, "found errors accumulated by acc.AddError()")

			// Wait for the expected number of metrics to avoid flaky tests due to
			// race conditions.
			acc.Wait(3)

			// Compare all aspects of the metric that are known to you
			for i, m := range acc.GetTelegrafMetrics() {
				require.Equal(t, m.Name(), tt.template.Name())
				require.Equal(t, m.Tags(), tt.template.Tags())

				// Check if all expected fields are there
				fields := m.Fields()
				for k := range tt.template.Fields() {
					if k == "count" {
						require.Equal(t, fields["count"], int64(i+1))
						continue
					}
					_, found := fields[k]
					require.Truef(t, found, "field %q not found", k)
				}
			}
		})
	}
}

func TestGatherFail(t *testing.T) {
	// You should also test for error conditions in your Gather() method. Try to cover all error paths.

	// We again setup a table-test here to specify "setting" - "expected error" pair.
	tests := []struct {
		name     string
		plugin   *Example
		expected string
	}{
		{
			name: "too many fields",
			plugin: &Example{
				DeviceName:   "test",
				NumberFields: 11,
			},
			expected: "too many fields",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator

			tt.plugin.Log = testutil.Logger{}
			require.NoError(t, tt.plugin.Init())

			err := tt.plugin.Gather(&acc)
			require.Error(t, err)
			require.EqualError(t, err, tt.expected)
		})
	}
}

func TestRandomValueFailPartial(t *testing.T) {
	// You should also test for error conditions in your Gather() with partial output. This is required when
	// using acc.AddError() as Gather() might succeed (return nil) but there are some metrics missing.

	// We again setup a table-test here to specify "setting" - "expected output metric" and "errors".
	tests := []struct {
		name        string
		plugin      *Example
		expected    []telegraf.Metric
		expectedErr string
	}{
		{
			name: "flappy gather",
			plugin: &Example{
				DeviceName:           "flappy",
				NumberFields:         1,
				EnableRandomVariable: true,
			},
			expected: []telegraf.Metric{
				metric.New(
					"example",
					map[string]string{
						"device": "flappy",
					},
					map[string]interface{}{
						"count": 1,
					},
					time.Unix(0, 0),
				),
				metric.New(
					"example",
					map[string]string{
						"device": "flappy",
					},
					map[string]interface{}{
						"count": 2,
					},
					time.Unix(0, 0),
				),
			},
			expectedErr: "too many runs for random values",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator

			tt.plugin.Log = testutil.Logger{}
			require.NoError(t, tt.plugin.Init())

			// Call gather and check no error occurs. However, we expect an error accumulated by acc.AddError()
			require.NoError(t, tt.plugin.Gather(&acc))

			// Wait for the expected number of metrics to avoid flaky tests due to
			// race conditions.
			acc.Wait(len(tt.expected))

			// Check the accumulated errors
			require.Len(t, acc.Errors, 1)
			require.EqualError(t, acc.Errors[0], tt.expectedErr)

			// Compare the expected partial metrics.
			testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
		})
	}
}
