// SPDX-License-Identifier: GPL-3.0-or-later

package smartctl

import (
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
	"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidwall/gjson"
)

var (
	dataConfigJSON, _ = os.ReadFile("testdata/config.json")
	dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")

	dataTypeSataScan, _         = os.ReadFile("testdata/type-sat/scan.json")
	dataTypeSataDeviceHDDSda, _ = os.ReadFile("testdata/type-sat/device-hdd-sda.json")
	dataTypeSataDeviceSSDSdc, _ = os.ReadFile("testdata/type-sat/device-ssd-sdc.json")

	dataTypeNvmeScan, _        = os.ReadFile("testdata/type-nvme/scan.json")
	dataTypeNvmeDeviceNvme0, _ = os.ReadFile("testdata/type-nvme/device-nvme0.json")
	dataTypeNvmeDeviceNvme1, _ = os.ReadFile("testdata/type-nvme/device-nvme1.json")

	dataTypeScsiScan, _      = os.ReadFile("testdata/type-scsi/scan.json")
	dataTypeScsiDeviceSda, _ = os.ReadFile("testdata/type-scsi/device-sda.json")
)

func Test_testDataIsValid(t *testing.T) {
	for name, data := range map[string][]byte{
		"dataConfigJSON": dataConfigJSON,
		"dataConfigYAML": dataConfigYAML,

		"dataTypeSataScan":         dataTypeSataScan,
		"dataTypeSataDeviceHDDSda": dataTypeSataDeviceHDDSda,
		"dataTypeSataDeviceSSDSdc": dataTypeSataDeviceSSDSdc,

		"dataTypeNvmeScan":        dataTypeNvmeScan,
		"dataTypeNvmeDeviceNvme0": dataTypeNvmeDeviceNvme0,
		"dataTypeNvmeDeviceNvme1": dataTypeNvmeDeviceNvme1,

		"dataTypeScsiScan":      dataTypeScsiScan,
		"dataTypeScsiDeviceSda": dataTypeScsiDeviceSda,
	} {
		require.NotNil(t, data, name)
	}
}

func TestSmartctl_ConfigurationSerialize(t *testing.T) {
	module.TestConfigurationSerialize(t, &Smartctl{}, dataConfigJSON, dataConfigYAML)
}

func TestSmartctl_Init(t *testing.T) {
	tests := map[string]struct {
		config   Config
		wantFail bool
	}{
		"fails if invalid power mode": {
			wantFail: true,
			config: func() Config {
				cfg := New().Config
				cfg.NoCheckPowerMode = "invalid"
				return cfg
			}(),
		},
		"fails if 'ndsudo' not found": {
			wantFail: true,
			config:   New().Config,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			smart := New()

			if test.wantFail {
				assert.Error(t, smart.Init())
			} else {
				assert.NoError(t, smart.Init())
			}
		})
	}
}

func TestSmartctl_Cleanup(t *testing.T) {
	tests := map[string]struct {
		prepare func() *Smartctl
	}{
		"not initialized exec": {
			prepare: func() *Smartctl {
				return New()
			},
		},
		"after check": {
			prepare: func() *Smartctl {
				smart := New()
				smart.exec = prepareMockOkTypeSata()
				_ = smart.Check()
				return smart
			},
		},
		"after collect": {
			prepare: func() *Smartctl {
				smart := New()
				smart.exec = prepareMockOkTypeSata()
				_ = smart.Collect()
				return smart
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			smart := test.prepare()

			assert.NotPanics(t, smart.Cleanup)
		})
	}
}

func TestSmartctl_Check(t *testing.T) {
	tests := map[string]struct {
		prepareMock func() *mockSmartctlCliExec
		wantFail    bool
	}{
		"success type sata devices": {
			wantFail:    false,
			prepareMock: prepareMockOkTypeSata,
		},
		"success type nvme devices": {
			wantFail:    false,
			prepareMock: prepareMockOkTypeNvme,
		},
		"error on scan": {
			wantFail:    true,
			prepareMock: prepareMockErrOnScan,
		},
		"unexpected response on scan": {
			wantFail:    true,
			prepareMock: prepareMockUnexpectedResponse,
		},
		"empty response on scan": {
			wantFail:    true,
			prepareMock: prepareMockEmptyResponse,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			smart := New()
			mock := test.prepareMock()
			smart.exec = mock

			if test.wantFail {
				assert.Error(t, smart.Check())
			} else {
				assert.NoError(t, smart.Check())
			}
		})
	}
}

func TestSmartctl_Collect(t *testing.T) {
	tests := map[string]struct {
		prepareMock   func() *mockSmartctlCliExec
		prepareConfig func() Config
		wantMetrics   map[string]int64
		wantCharts    int
	}{
		"success type sata devices": {
			prepareMock: prepareMockOkTypeSata,
			wantCharts:  68,
			wantMetrics: map[string]int64{
				"device_sda_type_sat_ata_smart_error_log_summary_count":       0,
				"device_sda_type_sat_attr_current_pending_sector_decoded":     0,
				"device_sda_type_sat_attr_current_pending_sector_normalized":  100,
				"device_sda_type_sat_attr_current_pending_sector_raw":         0,
				"device_sda_type_sat_attr_load_cycle_count_decoded":           360,
				"device_sda_type_sat_attr_load_cycle_count_normalized":        100,
				"device_sda_type_sat_attr_load_cycle_count_raw":               360,
				"device_sda_type_sat_attr_offline_uncorrectable_decoded":      0,
				"device_sda_type_sat_attr_offline_uncorrectable_normalized":   100,
				"device_sda_type_sat_attr_offline_uncorrectable_raw":          0,
				"device_sda_type_sat_attr_power-off_retract_count_decoded":    360,
				"device_sda_type_sat_attr_power-off_retract_count_normalized": 100,
				"device_sda_type_sat_attr_power-off_retract_count_raw":        360,
				"device_sda_type_sat_attr_power_cycle_count_decoded":          12,
				"device_sda_type_sat_attr_power_cycle_count_normalized":       100,
				"device_sda_type_sat_attr_power_cycle_count_raw":              12,
				"device_sda_type_sat_attr_power_on_hours_decoded":             8244,
				"device_sda_type_sat_attr_power_on_hours_normalized":          99,
				"device_sda_type_sat_attr_power_on_hours_raw":                 8244,
				"device_sda_type_sat_attr_raw_read_error_rate_decoded":        0,
				"device_sda_type_sat_attr_raw_read_error_rate_normalized":     100,
				"device_sda_type_sat_attr_raw_read_error_rate_raw":            0,
				"device_sda_type_sat_attr_reallocated_event_count_decoded":    0,
				"device_sda_type_sat_attr_reallocated_event_count_normalized": 100,
				"device_sda_type_sat_attr_reallocated_event_count_raw":        0,
				"device_sda_type_sat_attr_reallocated_sector_ct_decoded":      0,
				"device_sda_type_sat_attr_reallocated_sector_ct_normalized":   100,
				"device_sda_type_sat_attr_reallocated_sector_ct_raw":          0,
				"device_sda_type_sat_attr_seek_error_rate_decoded":            0,
				"device_sda_type_sat_attr_seek_error_rate_normalized":         100,
				"device_sda_type_sat_attr_seek_error_rate_raw":                0,
				"device_sda_type_sat_attr_seek_time_performance_decoded":      15,
				"device_sda_type_sat_attr_seek_time_performance_normalized":   140,
				"device_sda_type_sat_attr_seek_time_performance_raw":          15,
				"device_sda_type_sat_attr_spin_retry_count_decoded":           0,
				"device_sda_type_sat_attr_spin_retry_count_normalized":        100,
				"device_sda_type_sat_attr_spin_retry_count_raw":               0,
				"device_sda_type_sat_attr_spin_up_time_decoded":               281,
				"device_sda_type_sat_attr_spin_up_time_normalized":            86,
				"device_sda_type_sat_attr_spin_up_time_raw":                   25788088601,
				"device_sda_type_sat_attr_start_stop_count_decoded":           12,
				"device_sda_type_sat_attr_start_stop_count_normalized":        100,
				"device_sda_type_sat_attr_start_stop_count_raw":               12,
				"device_sda_type_sat_attr_temperature_celsius_decoded":        49,
				"device_sda_type_sat_attr_temperature_celsius_normalized":     43,
				"device_sda_type_sat_attr_temperature_celsius_raw":            240519741489,
				"device_sda_type_sat_attr_throughput_performance_decoded":     48,
				"device_sda_type_sat_attr_throughput_performance_normalized":  148,
				"device_sda_type_sat_attr_throughput_performance_raw":         48,
				"device_sda_type_sat_attr_udma_crc_error_count_decoded":       0,
				"device_sda_type_sat_attr_udma_crc_error_count_normalized":    100,
				"device_sda_type_sat_attr_udma_crc_error_count_raw":           0,
				"device_sda_type_sat_attr_unknown_attribute_decoded":          100,
				"device_sda_type_sat_attr_unknown_attribute_normalized":       100,
				"device_sda_type_sat_attr_unknown_attribute_raw":              100,
				"device_sda_type_sat_power_cycle_count":                       12,
				"device_sda_type_sat_power_on_time":                           29678400,
				"device_sda_type_sat_smart_status_failed":                     0,
				"device_sda_type_sat_smart_status_passed":                     1,
				"device_sda_type_sat_temperature":                             49,
				"device_sdc_type_sat_ata_smart_error_log_summary_count":       0,
				"device_sdc_type_sat_attr_available_reservd_space_decoded":    100,
				"device_sdc_type_sat_attr_available_reservd_space_normalized": 100,
				"device_sdc_type_sat_attr_available_reservd_space_raw":        100,
				"device_sdc_type_sat_attr_command_timeout_decoded":            0,
				"device_sdc_type_sat_attr_command_timeout_normalized":         100,
				"device_sdc_type_sat_attr_command_timeout_raw":                0,
				"device_sdc_type_sat_attr_end-to-end_error_decoded":           0,
				"device_sdc_type_sat_attr_end-to-end_error_normalized":        100,
				"device_sdc_type_sat_attr_end-to-end_error_raw":               0,
				"device_sdc_type_sat_attr_media_wearout_indicator_decoded":    65406,
				"device_sdc_type_sat_attr_media_wearout_indicator_normalized": 100,
				"device_sdc_type_sat_attr_media_wearout_indicator_raw":        65406,
				"device_sdc_type_sat_attr_power_cycle_count_decoded":          13,
				"device_sdc_type_sat_attr_power_cycle_count_normalized":       100,
				"device_sdc_type_sat_attr_power_cycle_count_raw":              13,
				"device_sdc_type_sat_attr_power_on_hours_decoded":             8244,
				"device_sdc_type_sat_attr_power_on_hours_normalized":          100,
				"device_sdc_type_sat_attr_power_on_hours_raw":                 8244,
				"device_sdc_type_sat_attr_reallocated_sector_ct_decoded":      0,
				"device_sdc_type_sat_attr_reallocated_sector_ct_normalized":   100,
				"device_sdc_type_sat_attr_reallocated_sector_ct_raw":          0,
				"device_sdc_type_sat_attr_reported_uncorrect_decoded":         0,
				"device_sdc_type_sat_attr_reported_uncorrect_normalized":      100,
				"device_sdc_type_sat_attr_reported_uncorrect_raw":             0,
				"device_sdc_type_sat_attr_temperature_celsius_decoded":        27,
				"device_sdc_type_sat_attr_temperature_celsius_normalized":     73,
				"device_sdc_type_sat_attr_temperature_celsius_raw":            184684970011,
				"device_sdc_type_sat_attr_total_lbas_read_decoded":            76778,
				"device_sdc_type_sat_attr_total_lbas_read_normalized":         253,
				"device_sdc_type_sat_attr_total_lbas_read_raw":                76778,
				"device_sdc_type_sat_attr_total_lbas_written_decoded":         173833,
				"device_sdc_type_sat_attr_total_lbas_written_normalized":      253,
				"device_sdc_type_sat_attr_total_lbas_written_raw":             173833,
				"device_sdc_type_sat_attr_udma_crc_error_count_decoded":       0,
				"device_sdc_type_sat_attr_udma_crc_error_count_normalized":    100,
				"device_sdc_type_sat_attr_udma_crc_error_count_raw":           0,
				"device_sdc_type_sat_attr_unknown_attribute_decoded":          0,
				"device_sdc_type_sat_attr_unknown_attribute_normalized":       0,
				"device_sdc_type_sat_attr_unknown_attribute_raw":              0,
				"device_sdc_type_sat_attr_unknown_ssd_attribute_decoded":      4694419309637,
				"device_sdc_type_sat_attr_unknown_ssd_attribute_normalized":   4,
				"device_sdc_type_sat_attr_unknown_ssd_attribute_raw":          4694419309637,
				"device_sdc_type_sat_power_cycle_count":                       13,
				"device_sdc_type_sat_power_on_time":                           29678400,
				"device_sdc_type_sat_smart_status_failed":                     0,
				"device_sdc_type_sat_smart_status_passed":                     1,
				"device_sdc_type_sat_temperature":                             27,
			},
		},
		"success type nvme devices": {
			prepareMock: prepareMockOkTypeNvme,
			wantCharts:  4,
			wantMetrics: map[string]int64{
				"device_nvme0_type_nvme_power_cycle_count":   2,
				"device_nvme0_type_nvme_power_on_time":       11206800,
				"device_nvme0_type_nvme_smart_status_failed": 0,
				"device_nvme0_type_nvme_smart_status_passed": 1,
				"device_nvme0_type_nvme_temperature":         39,
			},
		},
		"success type nvme devices with extra": {
			prepareMock: prepareMockOkTypeNvme,
			prepareConfig: func() Config {
				cfg := New().Config
				cfg.ExtraDevices = []ConfigExtraDevice{
					{Name: "/dev/nvme1", Type: "nvme"},
				}
				return cfg
			},
			wantCharts: 8,
			wantMetrics: map[string]int64{
				"device_nvme0_type_nvme_power_cycle_count":   2,
				"device_nvme0_type_nvme_power_on_time":       11206800,
				"device_nvme0_type_nvme_smart_status_failed": 0,
				"device_nvme0_type_nvme_smart_status_passed": 1,
				"device_nvme0_type_nvme_temperature":         39,
				"device_nvme1_type_nvme_power_cycle_count":   5,
				"device_nvme1_type_nvme_power_on_time":       17038800,
				"device_nvme1_type_nvme_smart_status_failed": 0,
				"device_nvme1_type_nvme_smart_status_passed": 1,
				"device_nvme1_type_nvme_temperature":         36,
			},
		},
		"success type scsi devices": {
			prepareMock: prepareMockOkTypeScsi,
			wantCharts:  7,
			wantMetrics: map[string]int64{
				"device_sda_type_scsi_power_cycle_count":                              4,
				"device_sda_type_scsi_power_on_time":                                  5908920,
				"device_sda_type_scsi_scsi_error_log_read_total_errors_corrected":     647736,
				"device_sda_type_scsi_scsi_error_log_read_total_uncorrected_errors":   0,
				"device_sda_type_scsi_scsi_error_log_verify_total_errors_corrected":   0,
				"device_sda_type_scsi_scsi_error_log_verify_total_uncorrected_errors": 0,
				"device_sda_type_scsi_scsi_error_log_write_total_errors_corrected":    0,
				"device_sda_type_scsi_scsi_error_log_write_total_uncorrected_errors":  0,
				"device_sda_type_scsi_smart_status_failed":                            0,
				"device_sda_type_scsi_smart_status_passed":                            1,
				"device_sda_type_scsi_temperature":                                    34,
			},
		},
		"error on scan": {
			prepareMock: prepareMockErrOnScan,
		},
		"unexpected response on scan": {
			prepareMock: prepareMockUnexpectedResponse,
		},
		"empty response on scan": {
			prepareMock: prepareMockEmptyResponse,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			smart := New()
			if test.prepareConfig != nil {
				smart.Config = test.prepareConfig()
			}
			mock := test.prepareMock()
			smart.exec = mock
			smart.ScanEvery = web.Duration(time.Microsecond * 1)
			smart.PollDevicesEvery = web.Duration(time.Microsecond * 1)

			var mx map[string]int64
			for i := 0; i < 10; i++ {
				mx = smart.Collect()
			}

			assert.Equal(t, test.wantMetrics, mx)
			assert.Len(t, *smart.Charts(), test.wantCharts)
			testMetricsHasAllChartsDims(t, smart, mx)
		})
	}
}

func testMetricsHasAllChartsDims(t *testing.T, smart *Smartctl, mx map[string]int64) {
	for _, chart := range *smart.Charts() {
		if chart.Obsolete {
			continue
		}
		for _, dim := range chart.Dims {
			_, ok := mx[dim.ID]
			assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
		}
		for _, v := range chart.Vars {
			_, ok := mx[v.ID]
			assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID)
		}
	}
}

func prepareMockOkTypeSata() *mockSmartctlCliExec {
	return &mockSmartctlCliExec{
		errOnScan: false,
		scanData:  dataTypeSataScan,
		deviceDataFunc: func(deviceName, deviceType, powerMode string) ([]byte, error) {
			if deviceType != "sat" {
				return nil, fmt.Errorf("unexpected device type %s", deviceType)
			}
			switch deviceName {
			case "/dev/sda":
				return dataTypeSataDeviceHDDSda, nil
			case "/dev/sdc":
				return dataTypeSataDeviceSSDSdc, nil
			default:
				return nil, fmt.Errorf("unexpected device name %s", deviceName)
			}
		},
	}
}

func prepareMockOkTypeNvme() *mockSmartctlCliExec {
	return &mockSmartctlCliExec{
		errOnScan: false,
		scanData:  dataTypeNvmeScan,
		deviceDataFunc: func(deviceName, deviceType, powerMode string) ([]byte, error) {
			if deviceType != "nvme" {
				return nil, fmt.Errorf("unexpected device type %s", deviceType)
			}
			switch deviceName {
			case "/dev/nvme0":
				return dataTypeNvmeDeviceNvme0, nil
			case "/dev/nvme1":
				return dataTypeNvmeDeviceNvme1, nil
			default:
				return nil, fmt.Errorf("unexpected device name %s", deviceName)
			}
		},
	}
}

func prepareMockOkTypeScsi() *mockSmartctlCliExec {
	return &mockSmartctlCliExec{
		errOnScan: false,
		scanData:  dataTypeScsiScan,
		deviceDataFunc: func(deviceName, deviceType, powerMode string) ([]byte, error) {
			if deviceType != "scsi" {
				return nil, fmt.Errorf("unexpected device type %s", deviceType)
			}
			switch deviceName {
			case "/dev/sda":
				return dataTypeScsiDeviceSda, nil
			default:
				return nil, fmt.Errorf("unexpected device name %s", deviceName)
			}
		},
	}
}

func prepareMockErrOnScan() *mockSmartctlCliExec {
	return &mockSmartctlCliExec{
		errOnScan: true,
	}
}

func prepareMockUnexpectedResponse() *mockSmartctlCliExec {
	return &mockSmartctlCliExec{
		scanData:       []byte(randomJsonData),
		deviceDataFunc: func(_, _, _ string) ([]byte, error) { return []byte(randomJsonData), nil },
	}
}

func prepareMockEmptyResponse() *mockSmartctlCliExec {
	return &mockSmartctlCliExec{}
}

type mockSmartctlCliExec struct {
	errOnScan      bool
	scanData       []byte
	deviceDataFunc func(deviceName, deviceType, powerMode string) ([]byte, error)
}

func (m *mockSmartctlCliExec) scan(_ bool) (*gjson.Result, error) {
	if m.errOnScan {
		return nil, fmt.Errorf("mock.scan() error")
	}
	res := gjson.ParseBytes(m.scanData)
	return &res, nil
}

func (m *mockSmartctlCliExec) deviceInfo(deviceName, deviceType, powerMode string) (*gjson.Result, error) {
	if m.deviceDataFunc == nil {
		return nil, nil
	}
	bs, err := m.deviceDataFunc(deviceName, deviceType, powerMode)
	if err != nil {
		return nil, err
	}
	res := gjson.ParseBytes(bs)
	return &res, nil
}

var randomJsonData = `
{
    "elephant": {
        "burn": false,
        "mountain": true,
        "fog": false,
        "skin": -1561907625,
        "burst": "anyway",
        "shadow": 1558616893
    },
    "start": "ever",
    "base": 2093056027,
    "mission": -2007590351,
    "victory": 999053756,
    "die": false
}
`
