package smartctl

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/plugins/inputs"
	"github.com/influxdata/telegraf/plugins/parsers/influx"
	"github.com/influxdata/telegraf/testutil"
)

func TestCasesScan(t *testing.T) {
	// Get all directories in testdata
	folders, err := os.ReadDir("testcases_scan")
	require.NoError(t, err)

	// Register the plugin
	inputs.Add("smartctl", func() telegraf.Input {
		return &Smartctl{}
	})

	for _, f := range folders {
		if !f.IsDir() {
			continue
		}
		testcasePath := filepath.Join("testcases_scan", f.Name())
		configFilename := filepath.Join(testcasePath, "telegraf.toml")
		scanFilename := filepath.Join(testcasePath, "response.json")
		expectedFilename := filepath.Join(testcasePath, "expected.out")

		t.Run(f.Name(), func(t *testing.T) {
			parser := &influx.Parser{}
			require.NoError(t, parser.Init())

			// Read the expected output if any
			var expected int
			if _, err := os.Stat(expectedFilename); err == nil {
				var err error
				expectedBytes, err := os.ReadFile(expectedFilename)
				require.NoError(t, err)
				expected, err = strconv.Atoi(strings.TrimSpace(string(expectedBytes)))
				require.NoError(t, err)
			}

			// Update exec to return fake data.
			execCommand = fakeScanExecCommand
			defer func() { execCommand = exec.Command }()

			// Configure the plugin
			cfg := config.NewConfig()
			require.NoError(t, cfg.LoadConfig(configFilename))
			require.Len(t, cfg.Inputs, 1)
			plugin := cfg.Inputs[0].Input.(*Smartctl)
			require.NoError(t, plugin.Init())

			scanArgs = append(scanArgs, scanFilename)
			devices, err := plugin.scan()
			require.NoError(t, err)
			require.Len(t, devices, expected)
		})
	}
}

func fakeScanExecCommand(command string, args ...string) *exec.Cmd {
	cs := make([]string, 0, len(args)+3)
	cs = append(cs, "-test.run=TestScanHelperProcess", "--", command)
	cs = append(cs, args...)
	cmd := exec.Command(os.Args[0], cs...)
	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
	return cmd
}

func TestScanHelperProcess(*testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
		return
	}
	args := os.Args

	scanBytes, err := os.ReadFile(args[len(args)-1])
	if err != nil {
		fmt.Fprint(os.Stdout, "unknown filename")
		//nolint:revive // os.Exit called intentionally
		os.Exit(42)
	}

	// Emit a warning to stderr to ensure it does not corrupt the JSON parsed
	// from stdout (see issue #19165)
	fmt.Fprintln(os.Stderr, "/dev/sda: warning emitted to stderr")
	fmt.Fprint(os.Stdout, string(scanBytes))
	//nolint:revive // os.Exit called intentionally
	os.Exit(0)
}

func TestCasesDevices(t *testing.T) {
	// Get all directories in testdata
	folders, err := os.ReadDir("testcases_device")
	require.NoError(t, err)

	// Register the plugin
	inputs.Add("smartctl", func() telegraf.Input {
		return &Smartctl{}
	})

	for _, f := range folders {
		if !f.IsDir() {
			continue
		}
		testcasePath := filepath.Join("testcases_device", f.Name())
		deviceFilename := filepath.Join(testcasePath, "device")
		deviceTypeFilename := filepath.Join(testcasePath, "deviceType")
		expectedFilename := filepath.Join(testcasePath, "expected.out")

		t.Run(f.Name(), func(t *testing.T) {
			parser := &influx.Parser{}
			require.NoError(t, parser.Init())

			// Read the expected output if any
			var expected []telegraf.Metric
			if _, err := os.Stat(expectedFilename); err == nil {
				var err error
				expected, err = testutil.ParseMetricsFromFile(expectedFilename, parser)
				require.NoError(t, err)
			}

			// Read the devices to scan
			deviceBytes, err := os.ReadFile(deviceFilename)
			require.NoError(t, err)
			deviceTypeBytes, err := os.ReadFile(deviceTypeFilename)
			require.NoError(t, err)

			// Update exec to return fake data.
			execCommand = fakeDeviceExecCommand
			defer func() { execCommand = exec.Command }()

			// Configure the plugin
			plugin := Smartctl{}
			require.NoError(t, plugin.Init())

			var acc testutil.Accumulator
			require.NoError(t,
				plugin.scanDevice(
					&acc,
					strings.TrimSpace(string(deviceBytes)),
					strings.TrimSpace(string(deviceTypeBytes)),
				),
			)

			// Check the metric nevertheless as we might get some metrics despite errors.
			actual := acc.GetTelegrafMetrics()
			testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
			acc.Lock()
			defer acc.Unlock()
			require.Empty(t, acc.Errors)
		})
	}
}

func fakeDeviceExecCommand(command string, args ...string) *exec.Cmd {
	cs := make([]string, 0, len(args)+3)
	cs = append(cs, "-test.run=TestDeviceHelperProcess", "--", command)
	cs = append(cs, args...)
	cmd := exec.Command(os.Args[0], cs...)
	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
	return cmd
}

func TestDeviceHelperProcess(t *testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
		return
	}
	args := os.Args

	var filename string
	if slices.Contains(args, "/dev/nvme0") {
		filename = "testcases_device/nvme/response.json"
	} else if slices.Contains(args, "/dev/sda") {
		filename = "testcases_device/usb/response.json"
	} else if slices.Contains(args, "/dev/bus/6") {
		filename = "testcases_device/megaraid/response.json"
	} else if slices.Contains(args, "/dev/sdb") {
		filename = "testcases_device/scsi/response.json"
	} else if slices.Contains(args, "/dev/sdaa") {
		filename = "testcases_device/scsi_extended/response.json"
	} else {
		fmt.Fprint(os.Stdout, "unknown filename")
		os.Exit(42) //nolint:revive // os.Exit called intentionally
	}

	scanBytes, err := os.ReadFile(filename)
	require.NoError(t, err)
	// Emit a warning to stderr to ensure it does not corrupt the JSON parsed
	// from stdout (see issue #19165)
	fmt.Fprintln(os.Stderr, "/dev/sda: warning emitted to stderr")
	fmt.Fprint(os.Stdout, string(scanBytes))
	os.Exit(0) //nolint:revive // os.Exit called intentionally
}
