// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package metrics

import (
	"errors"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/google/cadvisor/container"
	info "github.com/google/cadvisor/info/v1"
	"github.com/prometheus/client_golang/prometheus"
)

type testSubcontainersInfoProvider struct{}

func (p testSubcontainersInfoProvider) GetVersionInfo() (*info.VersionInfo, error) {
	return &info.VersionInfo{
		KernelVersion:      "4.1.6-200.fc22.x86_64",
		ContainerOsVersion: "Fedora 22 (Twenty Two)",
		DockerVersion:      "1.8.1",
		CadvisorVersion:    "0.16.0",
		CadvisorRevision:   "abcdef",
	}, nil
}

func (p testSubcontainersInfoProvider) GetMachineInfo() (*info.MachineInfo, error) {
	return &info.MachineInfo{
		NumCores:       4,
		MemoryCapacity: 1024,
	}, nil
}

var allMetrics = container.MetricSet{
	container.CpuUsageMetrics:         struct{}{},
	container.ProcessSchedulerMetrics: struct{}{},
	container.PerCpuUsageMetrics:      struct{}{},
	container.MemoryUsageMetrics:      struct{}{},
	container.CpuLoadMetrics:          struct{}{},
	container.DiskIOMetrics:           struct{}{},
	container.AcceleratorUsageMetrics: struct{}{},
	container.DiskUsageMetrics:        struct{}{},
	container.NetworkUsageMetrics:     struct{}{},
	container.NetworkTcpUsageMetrics:  struct{}{},
	container.NetworkUdpUsageMetrics:  struct{}{},
	container.ProcessMetrics:          struct{}{},
}

func (p testSubcontainersInfoProvider) SubcontainersInfo(string, *info.ContainerInfoRequest) ([]*info.ContainerInfo, error) {
	return []*info.ContainerInfo{
		{
			ContainerReference: info.ContainerReference{
				Name:    "testcontainer",
				Aliases: []string{"testcontaineralias"},
			},
			Spec: info.ContainerSpec{
				Image:  "test",
				HasCpu: true,
				Cpu: info.CpuSpec{
					Limit:  1000,
					Period: 100000,
					Quota:  10000,
				},
				Memory: info.MemorySpec{
					Limit:       2048,
					Reservation: 1024,
					SwapLimit:   4096,
				},
				HasProcesses: true,
				Processes: info.ProcessSpec{
					Limit: 100,
				},
				CreationTime: time.Unix(1257894000, 0),
				Labels: map[string]string{
					"foo.label": "bar",
				},
				Envs: map[string]string{
					"foo+env": "prod",
				},
			},
			Stats: []*info.ContainerStats{
				{
					Timestamp: time.Unix(1395066363, 0),
					Cpu: info.CpuStats{
						Usage: info.CpuUsage{
							Total:  1,
							PerCpu: []uint64{2, 3, 4, 5},
							User:   6,
							System: 7,
						},
						CFS: info.CpuCFS{
							Periods:          723,
							ThrottledPeriods: 18,
							ThrottledTime:    1724314000,
						},
						Schedstat: info.CpuSchedstat{
							RunTime:      53643567,
							RunqueueTime: 479424566378,
							RunPeriods:   984285,
						},
						LoadAverage: 2,
					},
					Memory: info.MemoryStats{
						Usage:      8,
						MaxUsage:   8,
						WorkingSet: 9,
						ContainerData: info.MemoryStatsMemoryData{
							Pgfault:    10,
							Pgmajfault: 11,
						},
						HierarchicalData: info.MemoryStatsMemoryData{
							Pgfault:    12,
							Pgmajfault: 13,
						},
						Cache:      14,
						RSS:        15,
						MappedFile: 16,
						Swap:       8192,
					},
					Network: info.NetworkStats{
						InterfaceStats: info.InterfaceStats{
							Name:      "eth0",
							RxBytes:   14,
							RxPackets: 15,
							RxErrors:  16,
							RxDropped: 17,
							TxBytes:   18,
							TxPackets: 19,
							TxErrors:  20,
							TxDropped: 21,
						},
						Interfaces: []info.InterfaceStats{
							{
								Name:      "eth0",
								RxBytes:   14,
								RxPackets: 15,
								RxErrors:  16,
								RxDropped: 17,
								TxBytes:   18,
								TxPackets: 19,
								TxErrors:  20,
								TxDropped: 21,
							},
						},
						Tcp: info.TcpStat{
							Established: 13,
							SynSent:     0,
							SynRecv:     0,
							FinWait1:    0,
							FinWait2:    0,
							TimeWait:    0,
							Close:       0,
							CloseWait:   0,
							LastAck:     0,
							Listen:      3,
							Closing:     0,
						},
						Tcp6: info.TcpStat{
							Established: 11,
							SynSent:     0,
							SynRecv:     0,
							FinWait1:    0,
							FinWait2:    0,
							TimeWait:    0,
							Close:       0,
							CloseWait:   0,
							LastAck:     0,
							Listen:      3,
							Closing:     0,
						},
						Udp: info.UdpStat{
							Listen:   0,
							Dropped:  0,
							RxQueued: 0,
							TxQueued: 0,
						},
						Udp6: info.UdpStat{
							Listen:   0,
							Dropped:  0,
							RxQueued: 0,
							TxQueued: 0,
						},
					},
					Filesystem: []info.FsStats{
						{
							Device:          "sda1",
							InodesFree:      524288,
							Inodes:          2097152,
							Limit:           22,
							Usage:           23,
							ReadsCompleted:  24,
							ReadsMerged:     25,
							SectorsRead:     26,
							ReadTime:        27,
							WritesCompleted: 28,
							WritesMerged:    39,
							SectorsWritten:  40,
							WriteTime:       41,
							IoInProgress:    42,
							IoTime:          43,
							WeightedIoTime:  44,
						},
						{
							Device:          "sda2",
							InodesFree:      262144,
							Inodes:          2097152,
							Limit:           37,
							Usage:           38,
							ReadsCompleted:  39,
							ReadsMerged:     40,
							SectorsRead:     41,
							ReadTime:        42,
							WritesCompleted: 43,
							WritesMerged:    44,
							SectorsWritten:  45,
							WriteTime:       46,
							IoInProgress:    47,
							IoTime:          48,
							WeightedIoTime:  49,
						},
					},
					Accelerators: []info.AcceleratorStats{
						{
							Make:        "nvidia",
							Model:       "tesla-p100",
							ID:          "GPU-deadbeef-1234-5678-90ab-feedfacecafe",
							MemoryTotal: 20304050607,
							MemoryUsed:  2030405060,
							DutyCycle:   12,
						},
						{
							Make:        "nvidia",
							Model:       "tesla-k80",
							ID:          "GPU-deadbeef-0123-4567-89ab-feedfacecafe",
							MemoryTotal: 10203040506,
							MemoryUsed:  1020304050,
							DutyCycle:   6,
						},
					},
					Processes: info.ProcessStats{
						ProcessCount:   1,
						FdCount:        5,
						SocketCount:    3,
						ThreadsCurrent: 5,
						ThreadsMax:     100,
					},
					TaskStats: info.LoadStats{
						NrSleeping:        50,
						NrRunning:         51,
						NrStopped:         52,
						NrUninterruptible: 53,
						NrIoWait:          54,
					},
				},
			},
		},
	}, nil
}

var (
	includeRe = regexp.MustCompile(`^(?:(?:# HELP |# TYPE )?container_|cadvisor_version_info\{)`)
	ignoreRe  = regexp.MustCompile(`^container_last_seen\{`)
)

func TestPrometheusCollector(t *testing.T) {
	c := NewPrometheusCollector(testSubcontainersInfoProvider{}, func(container *info.ContainerInfo) map[string]string {
		s := DefaultContainerLabels(container)
		s["zone.name"] = "hello"
		return s
	}, allMetrics)
	prometheus.MustRegister(c)
	defer prometheus.Unregister(c)

	testPrometheusCollector(t, c, "testdata/prometheus_metrics")
}

func testPrometheusCollector(t *testing.T, c *PrometheusCollector, metricsFile string) {
	rw := httptest.NewRecorder()
	prometheus.Handler().ServeHTTP(rw, &http.Request{})

	wantMetrics, err := ioutil.ReadFile(metricsFile)
	if err != nil {
		t.Fatalf("unable to read input test file %s", metricsFile)
	}

	wantLines := strings.Split(string(wantMetrics), "\n")
	gotLines := strings.Split(string(rw.Body.String()), "\n")

	// Until the Prometheus Go client library offers better testability
	// (https://github.com/prometheus/client_golang/issues/58), we simply compare
	// verbatim text-format metrics outputs, but ignore certain metric lines
	// whose value depends on the current time or local circumstances.
	for i, want := range wantLines {
		if !includeRe.MatchString(want) || ignoreRe.MatchString(want) {
			continue
		}
		if want != gotLines[i] {
			t.Fatalf("unexpected metric line\nwant: %s\nhave: %s", want, gotLines[i])
		}
	}
}

type erroringSubcontainersInfoProvider struct {
	successfulProvider testSubcontainersInfoProvider
	shouldFail         bool
}

func (p *erroringSubcontainersInfoProvider) GetVersionInfo() (*info.VersionInfo, error) {
	if p.shouldFail {
		return nil, errors.New("Oops 1")
	}
	return p.successfulProvider.GetVersionInfo()
}

func (p *erroringSubcontainersInfoProvider) GetMachineInfo() (*info.MachineInfo, error) {
	if p.shouldFail {
		return nil, errors.New("Oops 2")
	}
	return p.successfulProvider.GetMachineInfo()
}

func (p *erroringSubcontainersInfoProvider) SubcontainersInfo(
	a string, r *info.ContainerInfoRequest) ([]*info.ContainerInfo, error) {
	if p.shouldFail {
		return []*info.ContainerInfo{}, errors.New("Oops 3")
	}
	return p.successfulProvider.SubcontainersInfo(a, r)
}

func TestPrometheusCollector_scrapeFailure(t *testing.T) {
	provider := &erroringSubcontainersInfoProvider{
		successfulProvider: testSubcontainersInfoProvider{},
		shouldFail:         true,
	}

	c := NewPrometheusCollector(provider, func(container *info.ContainerInfo) map[string]string {
		s := DefaultContainerLabels(container)
		s["zone.name"] = "hello"
		return s
	}, allMetrics)
	prometheus.MustRegister(c)
	defer prometheus.Unregister(c)

	testPrometheusCollector(t, c, "testdata/prometheus_metrics_failure")

	provider.shouldFail = false

	testPrometheusCollector(t, c, "testdata/prometheus_metrics")
}
