package docker

import (
	"context"
	"crypto/tls"
	"sort"
	"testing"

	"github.com/influxdata/telegraf/testutil"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/swarm"
	"github.com/stretchr/testify/require"
)

type MockClient struct {
	InfoF             func(ctx context.Context) (types.Info, error)
	ContainerListF    func(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error)
	ContainerStatsF   func(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error)
	ContainerInspectF func(ctx context.Context, containerID string) (types.ContainerJSON, error)
	ServiceListF      func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error)
	TaskListF         func(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error)
	NodeListF         func(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error)
}

func (c *MockClient) Info(ctx context.Context) (types.Info, error) {
	return c.InfoF(ctx)
}

func (c *MockClient) ContainerList(
	ctx context.Context,
	options types.ContainerListOptions,
) ([]types.Container, error) {
	return c.ContainerListF(ctx, options)
}

func (c *MockClient) ContainerStats(
	ctx context.Context,
	containerID string,
	stream bool,
) (types.ContainerStats, error) {
	return c.ContainerStatsF(ctx, containerID, stream)
}

func (c *MockClient) ContainerInspect(
	ctx context.Context,
	containerID string,
) (types.ContainerJSON, error) {
	return c.ContainerInspectF(ctx, containerID)
}

func (c *MockClient) ServiceList(
	ctx context.Context,
	options types.ServiceListOptions,
) ([]swarm.Service, error) {
	return c.ServiceListF(ctx, options)
}

func (c *MockClient) TaskList(
	ctx context.Context,
	options types.TaskListOptions,
) ([]swarm.Task, error) {
	return c.TaskListF(ctx, options)
}

func (c *MockClient) NodeList(
	ctx context.Context,
	options types.NodeListOptions,
) ([]swarm.Node, error) {
	return c.NodeListF(ctx, options)
}

var baseClient = MockClient{
	InfoF: func(context.Context) (types.Info, error) {
		return info, nil
	},
	ContainerListF: func(context.Context, types.ContainerListOptions) ([]types.Container, error) {
		return containerList, nil
	},
	ContainerStatsF: func(context.Context, string, bool) (types.ContainerStats, error) {
		return containerStats(), nil
	},
	ContainerInspectF: func(context.Context, string) (types.ContainerJSON, error) {
		return containerInspect, nil
	},
	ServiceListF: func(context.Context, types.ServiceListOptions) ([]swarm.Service, error) {
		return ServiceList, nil
	},
	TaskListF: func(context.Context, types.TaskListOptions) ([]swarm.Task, error) {
		return TaskList, nil
	},
	NodeListF: func(context.Context, types.NodeListOptions) ([]swarm.Node, error) {
		return NodeList, nil
	},
}

func newClient(host string, tlsConfig *tls.Config) (Client, error) {
	return &baseClient, nil
}

func TestDockerGatherContainerStats(t *testing.T) {
	var acc testutil.Accumulator
	stats := testStats()

	tags := map[string]string{
		"container_name":  "redis",
		"container_image": "redis/image",
	}

	gatherContainerStats(stats, &acc, tags, "123456789", true, true, "linux")

	// test docker_container_net measurement
	netfields := map[string]interface{}{
		"rx_dropped":   uint64(1),
		"rx_bytes":     uint64(2),
		"rx_errors":    uint64(3),
		"tx_packets":   uint64(4),
		"tx_dropped":   uint64(1),
		"rx_packets":   uint64(2),
		"tx_errors":    uint64(3),
		"tx_bytes":     uint64(4),
		"container_id": "123456789",
	}
	nettags := copyTags(tags)
	nettags["network"] = "eth0"
	acc.AssertContainsTaggedFields(t, "docker_container_net", netfields, nettags)

	netfields = map[string]interface{}{
		"rx_dropped":   uint64(6),
		"rx_bytes":     uint64(8),
		"rx_errors":    uint64(10),
		"tx_packets":   uint64(12),
		"tx_dropped":   uint64(6),
		"rx_packets":   uint64(8),
		"tx_errors":    uint64(10),
		"tx_bytes":     uint64(12),
		"container_id": "123456789",
	}
	nettags = copyTags(tags)
	nettags["network"] = "total"
	acc.AssertContainsTaggedFields(t, "docker_container_net", netfields, nettags)

	// test docker_blkio measurement
	blkiotags := copyTags(tags)
	blkiotags["device"] = "6:0"
	blkiofields := map[string]interface{}{
		"io_service_bytes_recursive_read": uint64(100),
		"io_serviced_recursive_write":     uint64(101),
		"container_id":                    "123456789",
	}
	acc.AssertContainsTaggedFields(t, "docker_container_blkio", blkiofields, blkiotags)

	blkiotags = copyTags(tags)
	blkiotags["device"] = "total"
	blkiofields = map[string]interface{}{
		"io_service_bytes_recursive_read": uint64(100),
		"io_serviced_recursive_write":     uint64(302),
		"container_id":                    "123456789",
	}
	acc.AssertContainsTaggedFields(t, "docker_container_blkio", blkiofields, blkiotags)

	// test docker_container_mem measurement
	memfields := map[string]interface{}{
		"active_anon":               uint64(0),
		"active_file":               uint64(1),
		"cache":                     uint64(0),
		"container_id":              "123456789",
		"fail_count":                uint64(1),
		"hierarchical_memory_limit": uint64(0),
		"inactive_anon":             uint64(0),
		"inactive_file":             uint64(3),
		"limit":                     uint64(2000),
		"mapped_file":               uint64(0),
		"max_usage":                 uint64(1001),
		"pgfault":                   uint64(2),
		"pgmajfault":                uint64(0),
		"pgpgin":                    uint64(0),
		"pgpgout":                   uint64(0),
		"rss_huge":                  uint64(0),
		"rss":                       uint64(0),
		"total_active_anon":         uint64(0),
		"total_active_file":         uint64(0),
		"total_cache":               uint64(0),
		"total_inactive_anon":       uint64(0),
		"total_inactive_file":       uint64(0),
		"total_mapped_file":         uint64(0),
		"total_pgfault":             uint64(0),
		"total_pgmajfault":          uint64(0),
		"total_pgpgin":              uint64(4),
		"total_pgpgout":             uint64(0),
		"total_rss_huge":            uint64(444),
		"total_rss":                 uint64(44),
		"total_unevictable":         uint64(0),
		"total_writeback":           uint64(55),
		"unevictable":               uint64(0),
		"usage_percent":             float64(55.55),
		"usage":                     uint64(1111),
		"writeback":                 uint64(0),
	}

	acc.AssertContainsTaggedFields(t, "docker_container_mem", memfields, tags)

	// test docker_container_cpu measurement
	cputags := copyTags(tags)
	cputags["cpu"] = "cpu-total"
	cpufields := map[string]interface{}{
		"usage_total":                  uint64(500),
		"usage_in_usermode":            uint64(100),
		"usage_in_kernelmode":          uint64(200),
		"usage_system":                 uint64(100),
		"throttling_periods":           uint64(1),
		"throttling_throttled_periods": uint64(0),
		"throttling_throttled_time":    uint64(0),
		"usage_percent":                float64(400.0),
		"container_id":                 "123456789",
	}
	acc.AssertContainsTaggedFields(t, "docker_container_cpu", cpufields, cputags)

	cputags["cpu"] = "cpu0"
	cpu0fields := map[string]interface{}{
		"usage_total":  uint64(1),
		"container_id": "123456789",
	}
	acc.AssertContainsTaggedFields(t, "docker_container_cpu", cpu0fields, cputags)

	cputags["cpu"] = "cpu1"
	cpu1fields := map[string]interface{}{
		"usage_total":  uint64(1002),
		"container_id": "123456789",
	}
	acc.AssertContainsTaggedFields(t, "docker_container_cpu", cpu1fields, cputags)

	// Those tagged filed should not be present because of offline CPUs
	cputags["cpu"] = "cpu2"
	cpu2fields := map[string]interface{}{
		"usage_total":  uint64(0),
		"container_id": "123456789",
	}
	acc.AssertDoesNotContainsTaggedFields(t, "docker_container_cpu", cpu2fields, cputags)

	cputags["cpu"] = "cpu3"
	cpu3fields := map[string]interface{}{
		"usage_total":  uint64(0),
		"container_id": "123456789",
	}
	acc.AssertDoesNotContainsTaggedFields(t, "docker_container_cpu", cpu3fields, cputags)
}

func TestDocker_WindowsMemoryContainerStats(t *testing.T) {
	var acc testutil.Accumulator

	d := Docker{
		newClient: func(string, *tls.Config) (Client, error) {
			return &MockClient{
				InfoF: func(ctx context.Context) (types.Info, error) {
					return info, nil
				},
				ContainerListF: func(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
					return containerList, nil
				},
				ContainerStatsF: func(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error) {
					return containerStatsWindows(), nil
				},
				ContainerInspectF: func(ctx context.Context, containerID string) (types.ContainerJSON, error) {
					return containerInspect, nil
				},
				ServiceListF: func(context.Context, types.ServiceListOptions) ([]swarm.Service, error) {
					return ServiceList, nil
				},
				TaskListF: func(context.Context, types.TaskListOptions) ([]swarm.Task, error) {
					return TaskList, nil
				},
				NodeListF: func(context.Context, types.NodeListOptions) ([]swarm.Node, error) {
					return NodeList, nil
				},
			}, nil
		},
	}
	err := d.Gather(&acc)
	require.NoError(t, err)
}

func TestContainerLabels(t *testing.T) {
	var tests = []struct {
		name      string
		container types.Container
		include   []string
		exclude   []string
		expected  map[string]string
	}{
		{
			name: "Nil filters matches all",
			container: types.Container{
				Labels: map[string]string{
					"a": "x",
				},
			},
			include: nil,
			exclude: nil,
			expected: map[string]string{
				"a": "x",
			},
		},
		{
			name: "Empty filters matches all",
			container: types.Container{
				Labels: map[string]string{
					"a": "x",
				},
			},
			include: []string{},
			exclude: []string{},
			expected: map[string]string{
				"a": "x",
			},
		},
		{
			name: "Must match include",
			container: types.Container{
				Labels: map[string]string{
					"a": "x",
					"b": "y",
				},
			},
			include: []string{"a"},
			exclude: []string{},
			expected: map[string]string{
				"a": "x",
			},
		},
		{
			name: "Must not match exclude",
			container: types.Container{
				Labels: map[string]string{
					"a": "x",
					"b": "y",
				},
			},
			include: []string{},
			exclude: []string{"b"},
			expected: map[string]string{
				"a": "x",
			},
		},
		{
			name: "Include Glob",
			container: types.Container{
				Labels: map[string]string{
					"aa": "x",
					"ab": "y",
					"bb": "z",
				},
			},
			include: []string{"a*"},
			exclude: []string{},
			expected: map[string]string{
				"aa": "x",
				"ab": "y",
			},
		},
		{
			name: "Exclude Glob",
			container: types.Container{
				Labels: map[string]string{
					"aa": "x",
					"ab": "y",
					"bb": "z",
				},
			},
			include: []string{},
			exclude: []string{"a*"},
			expected: map[string]string{
				"bb": "z",
			},
		},
		{
			name: "Excluded Includes",
			container: types.Container{
				Labels: map[string]string{
					"aa": "x",
					"ab": "y",
					"bb": "z",
				},
			},
			include: []string{"a*"},
			exclude: []string{"*b"},
			expected: map[string]string{
				"aa": "x",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator

			newClientFunc := func(host string, tlsConfig *tls.Config) (Client, error) {
				client := baseClient
				client.ContainerListF = func(context.Context, types.ContainerListOptions) ([]types.Container, error) {
					return []types.Container{tt.container}, nil
				}
				return &client, nil
			}

			d := Docker{
				newClient:    newClientFunc,
				LabelInclude: tt.include,
				LabelExclude: tt.exclude,
			}

			err := d.Gather(&acc)
			require.NoError(t, err)

			// Grab tags from a container metric
			var actual map[string]string
			for _, metric := range acc.Metrics {
				if metric.Measurement == "docker_container_cpu" {
					actual = metric.Tags
				}
			}

			for k, v := range tt.expected {
				require.Equal(t, v, actual[k])
			}
		})
	}
}

func TestContainerNames(t *testing.T) {
	var tests = []struct {
		name       string
		containers [][]string
		include    []string
		exclude    []string
		expected   []string
	}{
		{
			name: "Nil filters matches all",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  nil,
			exclude:  nil,
			expected: []string{"etcd", "etcd2"},
		},
		{
			name: "Empty filters matches all",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{},
			exclude:  []string{},
			expected: []string{"etcd", "etcd2"},
		},
		{
			name: "Match all containers",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{"*"},
			exclude:  []string{},
			expected: []string{"etcd", "etcd2"},
		},
		{
			name: "Include prefix match",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{"etc*"},
			exclude:  []string{},
			expected: []string{"etcd", "etcd2"},
		},
		{
			name: "Exact match",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{"etcd"},
			exclude:  []string{},
			expected: []string{"etcd"},
		},
		{
			name: "Star matches zero length",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{"etcd2*"},
			exclude:  []string{},
			expected: []string{"etcd2"},
		},
		{
			name: "Exclude matches all",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{},
			exclude:  []string{"etc*"},
			expected: []string{},
		},
		{
			name: "Exclude single",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{},
			exclude:  []string{"etcd"},
			expected: []string{"etcd2"},
		},
		{
			name: "Exclude all",
			containers: [][]string{
				{"/etcd"},
				{"/etcd2"},
			},
			include:  []string{"*"},
			exclude:  []string{"*"},
			expected: []string{},
		},
		{
			name: "Exclude item matching include",
			containers: [][]string{
				{"acme"},
				{"foo"},
				{"acme-test"},
			},
			include:  []string{"acme*"},
			exclude:  []string{"*test*"},
			expected: []string{"acme"},
		},
		{
			name: "Exclude item no wildcards",
			containers: [][]string{
				{"acme"},
				{"acme-test"},
			},
			include:  []string{"acme*"},
			exclude:  []string{"test"},
			expected: []string{"acme", "acme-test"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator

			newClientFunc := func(host string, tlsConfig *tls.Config) (Client, error) {
				client := baseClient
				client.ContainerListF = func(context.Context, types.ContainerListOptions) ([]types.Container, error) {
					var containers []types.Container
					for _, names := range tt.containers {
						containers = append(containers, types.Container{
							Names: names,
						})
					}
					return containers, nil
				}
				return &client, nil
			}

			d := Docker{
				newClient:        newClientFunc,
				ContainerInclude: tt.include,
				ContainerExclude: tt.exclude,
			}

			err := d.Gather(&acc)
			require.NoError(t, err)

			// Set of expected names
			var expected = make(map[string]bool)
			for _, v := range tt.expected {
				expected[v] = true
			}

			// Set of actual names
			var actual = make(map[string]bool)
			for _, metric := range acc.Metrics {
				if name, ok := metric.Tags["container_name"]; ok {
					actual[name] = true
				}
			}

			require.Equal(t, expected, actual)
		})
	}
}

func TestDockerGatherInfo(t *testing.T) {
	var acc testutil.Accumulator
	d := Docker{
		newClient: newClient,
		TagEnvironment: []string{"ENVVAR1", "ENVVAR2", "ENVVAR3", "ENVVAR5",
			"ENVVAR6", "ENVVAR7", "ENVVAR8", "ENVVAR9"},
	}

	err := acc.GatherError(d.Gather)
	require.NoError(t, err)

	acc.AssertContainsTaggedFields(t,
		"docker",
		map[string]interface{}{
			"n_listener_events":       int(0),
			"n_cpus":                  int(4),
			"n_used_file_descriptors": int(19),
			"n_containers":            int(108),
			"n_containers_running":    int(98),
			"n_containers_stopped":    int(6),
			"n_containers_paused":     int(3),
			"n_images":                int(199),
			"n_goroutines":            int(39),
		},
		map[string]string{"engine_host": "absol"},
	)

	acc.AssertContainsTaggedFields(t,
		"docker_data",
		map[string]interface{}{
			"used":      int64(17300000000),
			"total":     int64(107400000000),
			"available": int64(36530000000),
		},
		map[string]string{
			"unit":        "bytes",
			"engine_host": "absol",
		},
	)
	acc.AssertContainsTaggedFields(t,
		"docker_container_cpu",
		map[string]interface{}{
			"usage_total":  uint64(1231652),
			"container_id": "b7dfbb9478a6ae55e237d4d74f8bbb753f0817192b5081334dc78476296e2173",
		},
		map[string]string{
			"container_name":    "etcd2",
			"container_image":   "quay.io:4443/coreos/etcd",
			"cpu":               "cpu3",
			"container_version": "v2.2.2",
			"engine_host":       "absol",
			"ENVVAR1":           "loremipsum",
			"ENVVAR2":           "dolorsitamet",
			"ENVVAR3":           "=ubuntu:10.04",
			"ENVVAR7":           "ENVVAR8=ENVVAR9",
			"label1":            "test_value_1",
			"label2":            "test_value_2",
		},
	)
	acc.AssertContainsTaggedFields(t,
		"docker_container_mem",
		map[string]interface{}{
			"container_id":  "b7dfbb9478a6ae55e237d4d74f8bbb753f0817192b5081334dc78476296e2173",
			"limit":         uint64(18935443456),
			"max_usage":     uint64(0),
			"usage":         uint64(0),
			"usage_percent": float64(0),
		},
		map[string]string{
			"engine_host":       "absol",
			"container_name":    "etcd2",
			"container_image":   "quay.io:4443/coreos/etcd",
			"container_version": "v2.2.2",
			"ENVVAR1":           "loremipsum",
			"ENVVAR2":           "dolorsitamet",
			"ENVVAR3":           "=ubuntu:10.04",
			"ENVVAR7":           "ENVVAR8=ENVVAR9",
			"label1":            "test_value_1",
			"label2":            "test_value_2",
		},
	)
}

func TestDockerGatherSwarmInfo(t *testing.T) {
	var acc testutil.Accumulator
	d := Docker{
		newClient: newClient,
	}

	err := acc.GatherError(d.Gather)
	require.NoError(t, err)

	d.gatherSwarmInfo(&acc)

	// test docker_container_net measurement
	acc.AssertContainsTaggedFields(t,
		"docker_swarm",
		map[string]interface{}{
			"tasks_running": int(2),
			"tasks_desired": uint64(2),
		},
		map[string]string{
			"service_id":   "qolkls9g5iasdiuihcyz9rnx2",
			"service_name": "test1",
			"service_mode": "replicated",
		},
	)

	acc.AssertContainsTaggedFields(t,
		"docker_swarm",
		map[string]interface{}{
			"tasks_running": int(1),
			"tasks_desired": int(1),
		},
		map[string]string{
			"service_id":   "qolkls9g5iasdiuihcyz9rn3",
			"service_name": "test2",
			"service_mode": "global",
		},
	)
}

func TestContainerStateFilter(t *testing.T) {
	var tests = []struct {
		name     string
		include  []string
		exclude  []string
		expected map[string][]string
	}{
		{
			name: "default",
			expected: map[string][]string{
				"status": []string{"running"},
			},
		},
		{
			name:    "include running",
			include: []string{"running"},
			expected: map[string][]string{
				"status": []string{"running"},
			},
		},
		{
			name:    "include glob",
			include: []string{"r*"},
			expected: map[string][]string{
				"status": []string{"restarting", "running", "removing"},
			},
		},
		{
			name:    "include all",
			include: []string{"*"},
			expected: map[string][]string{
				"status": []string{"created", "restarting", "running", "removing", "paused", "exited", "dead"},
			},
		},
		{
			name:    "exclude all",
			exclude: []string{"*"},
			expected: map[string][]string{
				"status": []string{},
			},
		},
		{
			name:    "exclude all",
			include: []string{"*"},
			exclude: []string{"exited"},
			expected: map[string][]string{
				"status": []string{"created", "restarting", "running", "removing", "paused", "dead"},
			},
		},
	}

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

			newClientFunc := func(host string, tlsConfig *tls.Config) (Client, error) {
				client := baseClient
				client.ContainerListF = func(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
					for k, v := range tt.expected {
						actual := options.Filters.Get(k)
						sort.Strings(actual)
						sort.Strings(v)
						require.Equal(t, v, actual)
					}

					return nil, nil
				}
				return &client, nil
			}

			d := Docker{
				newClient:             newClientFunc,
				ContainerStateInclude: tt.include,
				ContainerStateExclude: tt.exclude,
			}

			err := d.Gather(&acc)
			require.NoError(t, err)
		})
	}
}
