package ipmi_sensor

import (
	"fmt"
	"os"
	"os/exec"
	"testing"
	"time"

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

func TestGather(t *testing.T) {
	i := &Ipmi{
		Servers:   []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
		Path:      "ipmitool",
		Privilege: "USER",
		Timeout:   config.Duration(time.Second * 5),
		HexKey:    "1234567F",
	}

	// overwriting exec commands with mock commands
	execCommand = fakeExecCommand
	var acc testutil.Accumulator

	err := acc.GatherError(i.Gather)

	require.NoError(t, err)

	require.EqualValues(t, acc.NFields(), 262, "non-numeric measurements should be ignored")

	conn := NewConnection(i.Servers[0], i.Privilege, i.HexKey)
	require.EqualValues(t, "USERID", conn.Username)
	require.EqualValues(t, "lan", conn.Interface)
	require.EqualValues(t, "1234567F", conn.HexKey)

	var testsWithServer = []struct {
		fields map[string]interface{}
		tags   map[string]string
	}{
		{
			map[string]interface{}{
				"value":  float64(20),
				"status": int(1),
			},
			map[string]string{
				"name":   "ambient_temp",
				"server": "192.168.1.1",
				"unit":   "degrees_c",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(80),
				"status": int(1),
			},
			map[string]string{
				"name":   "altitude",
				"server": "192.168.1.1",
				"unit":   "feet",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(210),
				"status": int(1),
			},
			map[string]string{
				"name":   "avg_power",
				"server": "192.168.1.1",
				"unit":   "watts",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(4.9),
				"status": int(1),
			},
			map[string]string{
				"name":   "planar_5v",
				"server": "192.168.1.1",
				"unit":   "volts",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(3.05),
				"status": int(1),
			},
			map[string]string{
				"name":   "planar_vbat",
				"server": "192.168.1.1",
				"unit":   "volts",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(2610),
				"status": int(1),
			},
			map[string]string{
				"name":   "fan_1a_tach",
				"server": "192.168.1.1",
				"unit":   "rpm",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(1775),
				"status": int(1),
			},
			map[string]string{
				"name":   "fan_1b_tach",
				"server": "192.168.1.1",
				"unit":   "rpm",
			},
		},
	}

	for _, test := range testsWithServer {
		acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
	}

	i = &Ipmi{
		Path:    "ipmitool",
		Timeout: config.Duration(time.Second * 5),
	}

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

	var testsWithoutServer = []struct {
		fields map[string]interface{}
		tags   map[string]string
	}{
		{
			map[string]interface{}{
				"value":  float64(20),
				"status": int(1),
			},
			map[string]string{
				"name": "ambient_temp",
				"unit": "degrees_c",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(80),
				"status": int(1),
			},
			map[string]string{
				"name": "altitude",
				"unit": "feet",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(210),
				"status": int(1),
			},
			map[string]string{
				"name": "avg_power",
				"unit": "watts",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(4.9),
				"status": int(1),
			},
			map[string]string{
				"name": "planar_5v",
				"unit": "volts",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(3.05),
				"status": int(1),
			},
			map[string]string{
				"name": "planar_vbat",
				"unit": "volts",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(2610),
				"status": int(1),
			},
			map[string]string{
				"name": "fan_1a_tach",
				"unit": "rpm",
			},
		},
		{
			map[string]interface{}{
				"value":  float64(1775),
				"status": int(1),
			},
			map[string]string{
				"name": "fan_1b_tach",
				"unit": "rpm",
			},
		},
	}

	for _, test := range testsWithoutServer {
		acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
	}
}

// fackeExecCommand is a helper function that mock
// the exec.Command call (and call the test binary)
func fakeExecCommand(command string, args ...string) *exec.Cmd {
	cs := []string{"-test.run=TestHelperProcess", "--", command}
	cs = append(cs, args...)
	cmd := exec.Command(os.Args[0], cs...)
	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
	return cmd
}

// TestHelperProcess isn't a real test. It's used to mock exec.Command
// For example, if you run:
// GO_WANT_HELPER_PROCESS=1 go test -test.run=TestHelperProcess -- chrony tracking
// it returns below mockData.
func TestHelperProcess(_ *testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
		return
	}

	mockData := `Ambient Temp     | 20 degrees C      | ok
Altitude         | 80 feet           | ok
Avg Power        | 210 Watts         | ok
Planar 3.3V      | 3.29 Volts        | ok
Planar 5V        | 4.90 Volts        | ok
Planar 12V       | 12.04 Volts       | ok
Planar VBAT      | 3.05 Volts        | ok
Fan 1A Tach      | 2610 RPM          | ok
Fan 1B Tach      | 1775 RPM          | ok
Fan 2A Tach      | 2001 RPM          | ok
Fan 2B Tach      | 1275 RPM          | ok
Fan 3A Tach      | 2929 RPM          | ok
Fan 3B Tach      | 2125 RPM          | ok
Fan 1            | 0x00              | ok
Fan 2            | 0x00              | ok
Fan 3            | 0x00              | ok
Front Panel      | 0x00              | ok
Video USB        | 0x00              | ok
DASD Backplane 1 | 0x00              | ok
SAS Riser        | 0x00              | ok
PCI Riser 1      | 0x00              | ok
PCI Riser 2      | 0x00              | ok
CPU 1            | 0x00              | ok
CPU 2            | 0x00              | ok
All CPUs         | 0x00              | ok
One of The CPUs  | 0x00              | ok
IOH Temp Status  | 0x00              | ok
CPU 1 OverTemp   | 0x00              | ok
CPU 2 OverTemp   | 0x00              | ok
CPU Fault Reboot | 0x00              | ok
Aux Log          | 0x00              | ok
NMI State        | 0x00              | ok
ABR Status       | 0x00              | ok
Firmware Error   | 0x00              | ok
PCIs             | 0x00              | ok
CPUs             | 0x00              | ok
DIMMs            | 0x00              | ok
Sys Board Fault  | 0x00              | ok
Power Supply 1   | 0x00              | ok
Power Supply 2   | 0x00              | ok
PS 1 Fan Fault   | 0x00              | ok
PS 2 Fan Fault   | 0x00              | ok
VT Fault         | 0x00              | ok
Pwr Rail A Fault | 0x00              | ok
Pwr Rail B Fault | 0x00              | ok
Pwr Rail C Fault | 0x00              | ok
Pwr Rail D Fault | 0x00              | ok
Pwr Rail E Fault | 0x00              | ok
PS 1 Therm Fault | 0x00              | ok
PS 2 Therm Fault | 0x00              | ok
PS1 12V OV Fault | 0x00              | ok
PS2 12V OV Fault | 0x00              | ok
PS1 12V UV Fault | 0x00              | ok
PS2 12V UV Fault | 0x00              | ok
PS1 12V OC Fault | 0x00              | ok
PS2 12V OC Fault | 0x00              | ok
PS 1 VCO Fault   | 0x00              | ok
PS 2 VCO Fault   | 0x00              | ok
Power Unit       | 0x00              | ok
Cooling Zone 1   | 0x00              | ok
Cooling Zone 2   | 0x00              | ok
Cooling Zone 3   | 0x00              | ok
Drive 0          | 0x00              | ok
Drive 1          | 0x00              | ok
Drive 2          | 0x00              | ok
Drive 3          | 0x00              | ok
Drive 4          | 0x00              | ok
Drive 5          | 0x00              | ok
Drive 6          | 0x00              | ok
Drive 7          | 0x00              | ok
Drive 8          | 0x00              | ok
Drive 9          | 0x00              | ok
Drive 10         | 0x00              | ok
Drive 11         | 0x00              | ok
Drive 12         | 0x00              | ok
Drive 13         | 0x00              | ok
Drive 14         | 0x00              | ok
Drive 15         | 0x00              | ok
All DIMMS        | 0x00              | ok
One of the DIMMs | 0x00              | ok
DIMM 1           | 0x00              | ok
DIMM 2           | 0x00              | ok
DIMM 3           | 0x00              | ok
DIMM 4           | 0x00              | ok
DIMM 5           | 0x00              | ok
DIMM 6           | 0x00              | ok
DIMM 7           | 0x00              | ok
DIMM 8           | 0x00              | ok
DIMM 9           | 0x00              | ok
DIMM 10          | 0x00              | ok
DIMM 11          | 0x00              | ok
DIMM 12          | 0x00              | ok
DIMM 13          | 0x00              | ok
DIMM 14          | 0x00              | ok
DIMM 15          | 0x00              | ok
DIMM 16          | 0x00              | ok
DIMM 17          | 0x00              | ok
DIMM 18          | 0x00              | ok
DIMM 1 Temp      | 0x00              | ok
DIMM 2 Temp      | 0x00              | ok
DIMM 3 Temp      | 0x00              | ok
DIMM 4 Temp      | 0x00              | ok
DIMM 5 Temp      | 0x00              | ok
DIMM 6 Temp      | 0x00              | ok
DIMM 7 Temp      | 0x00              | ok
DIMM 8 Temp      | 0x00              | ok
DIMM 9 Temp      | 0x00              | ok
DIMM 10 Temp     | 0x00              | ok
DIMM 11 Temp     | 0x00              | ok
DIMM 12 Temp     | 0x00              | ok
DIMM 13 Temp     | 0x00              | ok
DIMM 14 Temp     | 0x00              | ok
DIMM 15 Temp     | 0x00              | ok
DIMM 16 Temp     | 0x00              | ok
DIMM 17 Temp     | 0x00              | ok
DIMM 18 Temp     | 0x00              | ok
PCI 1            | 0x00              | ok
PCI 2            | 0x00              | ok
PCI 3            | 0x00              | ok
PCI 4            | 0x00              | ok
All PCI Error    | 0x00              | ok
One of PCI Error | 0x00              | ok
IPMI Watchdog    | 0x00              | ok
Host Power       | 0x00              | ok
DASD Backplane 2 | 0x00              | ok
DASD Backplane 3 | Not Readable      | ns
DASD Backplane 4 | Not Readable      | ns
Backup Memory    | 0x00              | ok
Progress         | 0x00              | ok
Planar Fault     | 0x00              | ok
SEL Fullness     | 0x00              | ok
PCI 5            | 0x00              | ok
OS RealTime Mod  | 0x00              | ok
`

	args := os.Args

	// Previous arguments are tests stuff, that looks like :
	// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
	cmd, args := args[3], args[4:]

	// Ignore the returned errors for the mocked interface as tests will fail anyway
	if cmd == "ipmitool" {
		//nolint:errcheck,revive
		fmt.Fprint(os.Stdout, mockData)
	} else {
		//nolint:errcheck,revive
		fmt.Fprint(os.Stdout, "command not found")
		os.Exit(1)
	}
	os.Exit(0)
}

func TestGatherV2(t *testing.T) {
	i := &Ipmi{
		Servers:       []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
		Path:          "ipmitool",
		Privilege:     "USER",
		Timeout:       config.Duration(time.Second * 5),
		MetricVersion: 2,
		HexKey:        "0000000F",
	}
	// overwriting exec commands with mock commands
	execCommand = fakeExecCommandV2
	var acc testutil.Accumulator

	err := acc.GatherError(i.Gather)

	require.NoError(t, err)

	conn := NewConnection(i.Servers[0], i.Privilege, i.HexKey)
	require.EqualValues(t, "USERID", conn.Username)
	require.EqualValues(t, "lan", conn.Interface)
	require.EqualValues(t, "0000000F", conn.HexKey)

	var testsWithServer = []struct {
		fields map[string]interface{}
		tags   map[string]string
	}{
		//SEL              | 72h | ns  |  7.1 | No Reading
		{
			map[string]interface{}{
				"value": float64(0),
			},
			map[string]string{
				"name":        "sel",
				"entity_id":   "7.1",
				"status_code": "ns",
				"status_desc": "no_reading",
				"server":      "192.168.1.1",
			},
		},
	}

	for _, test := range testsWithServer {
		acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
	}

	i = &Ipmi{
		Path:          "ipmitool",
		Timeout:       config.Duration(time.Second * 5),
		MetricVersion: 2,
	}

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

	var testsWithoutServer = []struct {
		fields map[string]interface{}
		tags   map[string]string
	}{
		//SEL              | 72h | ns  |  7.1 | No Reading
		{
			map[string]interface{}{
				"value": float64(0),
			},
			map[string]string{
				"name":        "sel",
				"entity_id":   "7.1",
				"status_code": "ns",
				"status_desc": "no_reading",
			},
		},
		//Intrusion        | 73h | ok  |  7.1 |
		{
			map[string]interface{}{
				"value": float64(0),
			},
			map[string]string{
				"name":        "intrusion",
				"entity_id":   "7.1",
				"status_code": "ok",
				"status_desc": "ok",
			},
		},
		//Fan1             | 30h | ok  |  7.1 | 5040 RPM
		{
			map[string]interface{}{
				"value": float64(5040),
			},
			map[string]string{
				"name":        "fan1",
				"entity_id":   "7.1",
				"status_code": "ok",
				"unit":        "rpm",
			},
		},
		//Inlet Temp       | 04h | ok  |  7.1 | 25 degrees C
		{
			map[string]interface{}{
				"value": float64(25),
			},
			map[string]string{
				"name":        "inlet_temp",
				"entity_id":   "7.1",
				"status_code": "ok",
				"unit":        "degrees_c",
			},
		},
		//USB Cable Pres   | 50h | ok  |  7.1 | Connected
		{
			map[string]interface{}{
				"value": float64(0),
			},
			map[string]string{
				"name":        "usb_cable_pres",
				"entity_id":   "7.1",
				"status_code": "ok",
				"status_desc": "connected",
			},
		},
		//Current 1        | 6Ah | ok  | 10.1 | 7.20 Amps
		{
			map[string]interface{}{
				"value": float64(7.2),
			},
			map[string]string{
				"name":        "current_1",
				"entity_id":   "10.1",
				"status_code": "ok",
				"unit":        "amps",
			},
		},
		//Power Supply 1   | 03h | ok  | 10.1 | 110 Watts, Presence detected
		{
			map[string]interface{}{
				"value": float64(110),
			},
			map[string]string{
				"name":        "power_supply_1",
				"entity_id":   "10.1",
				"status_code": "ok",
				"unit":        "watts",
				"status_desc": "presence_detected",
			},
		},
	}

	for _, test := range testsWithoutServer {
		acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
	}
}

// fackeExecCommandV2 is a helper function that mock
// the exec.Command call (and call the test binary)
func fakeExecCommandV2(command string, args ...string) *exec.Cmd {
	cs := []string{"-test.run=TestHelperProcessV2", "--", command}
	cs = append(cs, args...)
	cmd := exec.Command(os.Args[0], cs...)
	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
	return cmd
}

// TestHelperProcessV2 isn't a real test. It's used to mock exec.Command
// For example, if you run:
// GO_WANT_HELPER_PROCESS=1 go test -test.run=TestHelperProcessV2 -- chrony tracking
// it returns below mockData.
func TestHelperProcessV2(_ *testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
		return
	}

	// Curated list of use cases instead of full dumps
	mockData := `SEL              | 72h | ns  |  7.1 | No Reading
Intrusion        | 73h | ok  |  7.1 |
Fan1             | 30h | ok  |  7.1 | 5040 RPM
Inlet Temp       | 04h | ok  |  7.1 | 25 degrees C
USB Cable Pres   | 50h | ok  |  7.1 | Connected
Current 1        | 6Ah | ok  | 10.1 | 7.20 Amps
Power Supply 1   | 03h | ok  | 10.1 | 110 Watts, Presence detected
`

	args := os.Args

	// Previous arguments are tests stuff, that looks like :
	// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
	cmd, args := args[3], args[4:]

	// Ignore the returned errors for the mocked interface as tests will fail anyway
	if cmd == "ipmitool" {
		//nolint:errcheck,revive
		fmt.Fprint(os.Stdout, mockData)
	} else {
		//nolint:errcheck,revive
		fmt.Fprint(os.Stdout, "command not found")
		os.Exit(1)
	}
	os.Exit(0)
}

func TestExtractFields(t *testing.T) {
	v1Data := `Ambient Temp     | 20 degrees C      | ok
Altitude         | 80 feet           | ok
Avg Power        | 210 Watts         | ok
Planar 3.3V      | 3.29 Volts        | ok
Planar 5V        | 4.90 Volts        | ok
Planar 12V       | 12.04 Volts       | ok
B                | 0x00              | ok
Unable to send command: Invalid argument
ECC Corr Err     | Not Readable      | ns
Unable to send command: Invalid argument
ECC Uncorr Err   | Not Readable      | ns
Unable to send command: Invalid argument
`

	v2Data := `SEL              | 72h | ns  |  7.1 | No Reading
Intrusion        | 73h | ok  |  7.1 |
Fan1             | 30h | ok  |  7.1 | 5040 RPM
Inlet Temp       | 04h | ok  |  7.1 | 25 degrees C
USB Cable Pres   | 50h | ok  |  7.1 | Connected
Unable to send command: Invalid argument
Current 1        | 6Ah | ok  | 10.1 | 7.20 Amps
Unable to send command: Invalid argument
Power Supply 1   | 03h | ok  | 10.1 | 110 Watts, Presence detected
`

	tests := []string{
		v1Data,
		v2Data,
	}

	for i := range tests {
		t.Logf("Checking v%d data...", i+1)
		extractFieldsFromRegex(reV1ParseLine, tests[i])
		extractFieldsFromRegex(reV2ParseLine, tests[i])
	}
}

func Test_parseV1(t *testing.T) {
	type args struct {
		hostname   string
		cmdOut     []byte
		measuredAt time.Time
	}
	tests := []struct {
		name       string
		args       args
		wantFields map[string]interface{}
		wantErr    bool
	}{
		{
			name: "Test correct V1 parsing with hex code",
			args: args{
				hostname:   "host",
				measuredAt: time.Now(),
				cmdOut:     []byte("PS1 Status       | 0x02              | ok"),
			},
			wantFields: map[string]interface{}{"value": float64(2), "status": 1},
			wantErr:    false,
		},
		{
			name: "Test correct V1 parsing with value with unit",
			args: args{
				hostname:   "host",
				measuredAt: time.Now(),
				cmdOut:     []byte("Avg Power        | 210 Watts         | ok"),
			},
			wantFields: map[string]interface{}{"value": float64(210), "status": 1},
			wantErr:    false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator

			if err := parseV1(&acc, tt.args.hostname, tt.args.cmdOut, tt.args.measuredAt); (err != nil) != tt.wantErr {
				t.Errorf("parseV1() error = %v, wantErr %v", err, tt.wantErr)
			}

			acc.AssertContainsFields(t, "ipmi_sensor", tt.wantFields)
		})
	}
}

func Test_parseV2(t *testing.T) {
	type args struct {
		hostname   string
		cmdOut     []byte
		measuredAt time.Time
	}
	tests := []struct {
		name     string
		args     args
		expected []telegraf.Metric
		wantErr  bool
	}{
		{
			name: "Test correct V2 parsing with analog value with unit",
			args: args{
				hostname:   "host",
				cmdOut:     []byte("Power Supply 1   | 03h | ok  | 10.1 | 110 Watts, Presence detected"),
				measuredAt: time.Now(),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("ipmi_sensor",
					map[string]string{
						"name":        "power_supply_1",
						"status_code": "ok",
						"server":      "host",
						"entity_id":   "10.1",
						"unit":        "watts",
						"status_desc": "presence_detected",
					},
					map[string]interface{}{"value": 110.0},
					time.Unix(0, 0),
				),
			},
			wantErr: false,
		},
		{
			name: "Test correct V2 parsing without analog value",
			args: args{
				hostname:   "host",
				cmdOut:     []byte("Intrusion        | 73h | ok  |  7.1 |"),
				measuredAt: time.Now(),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("ipmi_sensor",
					map[string]string{
						"name":        "intrusion",
						"status_code": "ok",
						"server":      "host",
						"entity_id":   "7.1",
						"status_desc": "ok",
					},
					map[string]interface{}{"value": 0.0},
					time.Unix(0, 0),
				),
			},
			wantErr: false,
		},
		{
			name: "parse negative value",
			args: args{
				hostname:   "host",
				cmdOut:     []byte("DIMM Thrm Mrgn 1 | B0h | ok  |  8.1 | -55 degrees C"),
				measuredAt: time.Now(),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("ipmi_sensor",
					map[string]string{
						"name":        "dimm_thrm_mrgn_1",
						"status_code": "ok",
						"server":      "host",
						"entity_id":   "8.1",
						"unit":        "degrees_c",
					},
					map[string]interface{}{"value": -55.0},
					time.Unix(0, 0),
				),
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var acc testutil.Accumulator
			if err := parseV2(&acc, tt.args.hostname, tt.args.cmdOut, tt.args.measuredAt); (err != nil) != tt.wantErr {
				t.Errorf("parseV2() error = %v, wantErr %v", err, tt.wantErr)
			}
			testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
		})
	}
}

func TestSanitizeIPMICmd(t *testing.T) {
	tests := []struct {
		name     string
		args     []string
		expected []string
	}{
		{
			name: "default args",
			args: []string{
				"-H", "localhost",
				"-U", "username",
				"-P", "password",
				"-I", "lan",
			},
			expected: []string{
				"-H", "localhost",
				"-U", "username",
				"-P", "REDACTED",
				"-I", "lan",
			},
		},
		{
			name: "no password",
			args: []string{
				"-H", "localhost",
				"-U", "username",
				"-I", "lan",
			},
			expected: []string{
				"-H", "localhost",
				"-U", "username",
				"-I", "lan",
			},
		},
		{
			name:     "empty args",
			args:     []string{},
			expected: []string{},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var sanitizedArgs []string = sanitizeIPMICmd(tt.args)
			require.Equal(t, tt.expected, sanitizedArgs)
		})
	}
}
