// Copyright 2018 The Prometheus Authors
// 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 procfs

import (
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"
)

func TestMountStats(t *testing.T) {
	tests := []struct {
		name    string
		s       string
		mounts  []*Mount
		invalid bool
	}{
		{
			name: "no devices",
			s:    `hello`,
		},
		{
			name:    "device has too few fields",
			s:       `device foo`,
			invalid: true,
		},
		{
			name:    "device incorrect format",
			s:       `device rootfs BAD on / with fstype rootfs`,
			invalid: true,
		},
		{
			name:    "device incorrect format",
			s:       `device rootfs mounted BAD / with fstype rootfs`,
			invalid: true,
		},
		{
			name:    "device incorrect format",
			s:       `device rootfs mounted on / BAD fstype rootfs`,
			invalid: true,
		},
		{
			name:    "device incorrect format",
			s:       `device rootfs mounted on / with BAD rootfs`,
			invalid: true,
		},
		{
			name:    "device rootfs cannot have stats",
			s:       `device rootfs mounted on / with fstype rootfs stats`,
			invalid: true,
		},
		{
			name:    "NFSv4 device with too little info",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nhello",
			invalid: true,
		},
		{
			name:    "NFSv4 device with bad bytes",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nbytes: 0",
			invalid: true,
		},
		{
			name:    "NFSv4 device with bad events",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nevents: 0",
			invalid: true,
		},
		{
			name:    "NFSv4 device with bad per-op stats",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nper-op statistics\nFOO 0",
			invalid: true,
		},
		{
			name:    "NFSv4 device with bad transport stats",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nxprt: tcp",
			invalid: true,
		},
		{
			name:    "NFSv4 device with bad transport version",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=foo\nxprt: tcp 0",
			invalid: true,
		},
		{
			name:    "NFSv4 device with bad transport stats version 1.0",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.0\nxprt: tcp 0 0 0 0 0 0 0 0 0 0 0 0 0",
			invalid: true,
		},
		{
			name:    "NFSv4 device with bad transport stats version 1.1",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nxprt: tcp 0 0 0 0 0 0 0 0 0 0",
			invalid: true,
		},
		{
			name:    "NFSv3 device with bad transport protocol",
			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nxprt: tcpx 0 0 0 0 0 0 0 0 0 0",
			invalid: true,
		},
		{
			name: "NFSv3 device using TCP with transport stats version 1.0 OK",
			s:    "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs statvers=1.0\nxprt: tcp 1 2 3 4 5 6 7 8 9 10",
			mounts: []*Mount{{
				Device: "192.168.1.1:/srv",
				Mount:  "/mnt/nfs",
				Type:   "nfs",
				Stats: &MountStatsNFS{
					StatVersion: "1.0",
					Transport: NFSTransportStats{
						Protocol:                 "tcp",
						Port:                     1,
						Bind:                     2,
						Connect:                  3,
						ConnectIdleTime:          4,
						IdleTime:                 5 * time.Second,
						Sends:                    6,
						Receives:                 7,
						BadTransactionIDs:        8,
						CumulativeActiveRequests: 9,
						CumulativeBacklog:        10,
						MaximumRPCSlotsUsed:      0, // these three are not
						CumulativeSendingQueue:   0, // present in statvers=1.0
						CumulativePendingQueue:   0, //
					},
				},
			}},
		},
		{
			name: "NFSv3 device using UDP with transport stats version 1.0 OK",
			s:    "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs statvers=1.0\nxprt: udp 1 2 3 4 5 6 7",
			mounts: []*Mount{{
				Device: "192.168.1.1:/srv",
				Mount:  "/mnt/nfs",
				Type:   "nfs",
				Stats: &MountStatsNFS{
					StatVersion: "1.0",
					Transport: NFSTransportStats{
						Protocol:                 "udp",
						Port:                     1,
						Bind:                     2,
						Connect:                  0,
						ConnectIdleTime:          0,
						IdleTime:                 0,
						Sends:                    3,
						Receives:                 4,
						BadTransactionIDs:        5,
						CumulativeActiveRequests: 6,
						CumulativeBacklog:        7,
						MaximumRPCSlotsUsed:      0, // these three are not
						CumulativeSendingQueue:   0, // present in statvers=1.0
						CumulativePendingQueue:   0, //
					},
				},
			}},
		},
		{
			name: "NFSv3 device using TCP with transport stats version 1.1 OK",
			s:    "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs statvers=1.1\nxprt: tcp 1 2 3 4 5 6 7 8 9 10 11 12 13",
			mounts: []*Mount{{
				Device: "192.168.1.1:/srv",
				Mount:  "/mnt/nfs",
				Type:   "nfs",
				Stats: &MountStatsNFS{
					StatVersion: "1.1",
					Transport: NFSTransportStats{
						Protocol:                 "tcp",
						Port:                     1,
						Bind:                     2,
						Connect:                  3,
						ConnectIdleTime:          4,
						IdleTime:                 5 * time.Second,
						Sends:                    6,
						Receives:                 7,
						BadTransactionIDs:        8,
						CumulativeActiveRequests: 9,
						CumulativeBacklog:        10,
						MaximumRPCSlotsUsed:      11,
						CumulativeSendingQueue:   12,
						CumulativePendingQueue:   13,
					},
				},
			}},
		},
		{
			name: "NFSv3 device using UDP with transport stats version 1.1 OK",
			s:    "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs statvers=1.1\nxprt: udp 1 2 3 4 5 6 7 8 9 10",
			mounts: []*Mount{{
				Device: "192.168.1.1:/srv",
				Mount:  "/mnt/nfs",
				Type:   "nfs",
				Stats: &MountStatsNFS{
					StatVersion: "1.1",
					Transport: NFSTransportStats{
						Protocol:                 "udp",
						Port:                     1,
						Bind:                     2,
						Connect:                  0, // these three are not
						ConnectIdleTime:          0, // present for UDP
						IdleTime:                 0, //
						Sends:                    3,
						Receives:                 4,
						BadTransactionIDs:        5,
						CumulativeActiveRequests: 6,
						CumulativeBacklog:        7,
						MaximumRPCSlotsUsed:      8,
						CumulativeSendingQueue:   9,
						CumulativePendingQueue:   10,
					},
				},
			}},
		},
		{
			name: "NFSv3 device with mountaddr OK",
			s:    "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs statvers=1.1\nopts: rw,vers=3,mountaddr=192.168.1.1,proto=udp\n",
			mounts: []*Mount{{
				Device: "192.168.1.1:/srv",
				Mount:  "/mnt/nfs",
				Type:   "nfs",
				Stats: &MountStatsNFS{
					StatVersion:  "1.1",
					MountAddress: "192.168.1.1",
				},
			}},
		},
		{
			name: "device rootfs OK",
			s:    `device rootfs mounted on / with fstype rootfs`,
			mounts: []*Mount{{
				Device: "rootfs",
				Mount:  "/",
				Type:   "rootfs",
			}},
		},
		{
			name: "NFSv3 device with minimal stats OK",
			s:    `device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs statvers=1.1`,
			mounts: []*Mount{{
				Device: "192.168.1.1:/srv",
				Mount:  "/mnt/nfs",
				Type:   "nfs",
				Stats: &MountStatsNFS{
					StatVersion: "1.1",
				},
			}},
		},
		{
			name: "fixtures/proc OK",
			mounts: []*Mount{
				{
					Device: "rootfs",
					Mount:  "/",
					Type:   "rootfs",
				},
				{
					Device: "sysfs",
					Mount:  "/sys",
					Type:   "sysfs",
				},
				{
					Device: "proc",
					Mount:  "/proc",
					Type:   "proc",
				},
				{
					Device: "/dev/sda1",
					Mount:  "/",
					Type:   "ext4",
				},
				{
					Device: "192.168.1.1:/srv/test",
					Mount:  "/mnt/nfs/test",
					Type:   "nfs4",
					Stats: &MountStatsNFS{
						StatVersion: "1.1",
						Age:         13968 * time.Second,
						Bytes: NFSBytesStats{
							Read:      1207640230,
							ReadTotal: 1210214218,
							ReadPages: 295483,
						},
						Events: NFSEventsStats{
							InodeRevalidate: 52,
							DnodeRevalidate: 226,
							VFSOpen:         1,
							VFSLookup:       13,
							VFSAccess:       398,
							VFSReadPages:    331,
							VFSWritePages:   47,
							VFSFlush:        77,
							VFSFileRelease:  77,
						},
						Operations: []NFSOperationStats{
							{
								Operation: "NULL",
							},
							{
								Operation:                   "READ",
								Requests:                    1298,
								Transmissions:               1298,
								BytesSent:                   207680,
								BytesReceived:               1210292152,
								CumulativeQueueTime:         6 * time.Millisecond,
								CumulativeTotalResponseTime: 79386 * time.Millisecond,
								CumulativeTotalRequestTime:  79407 * time.Millisecond,
							},
							{
								Operation: "WRITE",
							},
						},
						Transport: NFSTransportStats{
							Protocol:                 "tcp",
							Port:                     832,
							Connect:                  1,
							IdleTime:                 11 * time.Second,
							Sends:                    6428,
							Receives:                 6428,
							CumulativeActiveRequests: 12154,
							MaximumRPCSlotsUsed:      24,
							CumulativeSendingQueue:   26,
							CumulativePendingQueue:   5726,
						},
					},
				},
			},
		},
	}

	for i, tt := range tests {
		t.Logf("[%02d] test %q", i, tt.name)

		var mounts []*Mount
		var err error

		if tt.s != "" {
			mounts, err = parseMountStats(strings.NewReader(tt.s))
		} else {
			proc, e := getProcFixtures(t).NewProc(26231)
			if e != nil {
				t.Fatalf("failed to create proc: %v", err)
			}

			mounts, err = proc.MountStats()
		}

		if tt.invalid && err == nil {
			t.Error("expected an error, but none occurred")
		}
		if !tt.invalid && err != nil {
			t.Errorf("unexpected error: %v", err)
		}

		if want, have := tt.mounts, mounts; !reflect.DeepEqual(want, have) {
			t.Errorf("mounts:\nwant:\n%v\nhave:\n%v", mountsStr(want), mountsStr(have))
		}
	}
}

func mountsStr(mounts []*Mount) string {
	var out string
	for i, m := range mounts {
		out += fmt.Sprintf("[%d] %q on %q (%q)", i, m.Device, m.Mount, m.Type)

		stats, ok := m.Stats.(*MountStatsNFS)
		if !ok {
			out += "\n"
			continue
		}

		out += fmt.Sprintf("\n\t- mountaddr: %s", stats.MountAddress)
		out += fmt.Sprintf("\n\t- v%s, age: %s", stats.StatVersion, stats.Age)
		out += fmt.Sprintf("\n\t- bytes: %v", stats.Bytes)
		out += fmt.Sprintf("\n\t- events: %v", stats.Events)
		out += fmt.Sprintf("\n\t- transport: %v", stats.Transport)
		out += fmt.Sprintf("\n\t- per-operation stats:")

		for _, o := range stats.Operations {
			out += fmt.Sprintf("\n\t\t- %v", o)
		}

		out += "\n"
	}

	return out
}
