package cpu

import (
	"testing"

	"github.com/shirou/gopsutil/v4/cpu"
	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf/plugins/common/psutil"
	"github.com/influxdata/telegraf/testutil"
)

func newCPUStats(ps psutil.PS) *CPUStats {
	return &CPUStats{
		ps:             ps,
		CollectCPUTime: true,
		ReportActive:   true,
	}
}

func TestCPUStats(t *testing.T) {
	var mps psutil.MockPS
	defer mps.AssertExpectations(t)
	var acc testutil.Accumulator

	cts := cpu.TimesStat{
		CPU:       "cpu0",
		User:      8.8,
		System:    8.2,
		Idle:      80.1,
		Nice:      1.3,
		Iowait:    0.8389,
		Irq:       0.6,
		Softirq:   0.11,
		Steal:     0.0511,
		Guest:     3.1,
		GuestNice: 0.324,
	}

	cts2 := cpu.TimesStat{
		CPU:       "cpu0",
		User:      24.9,     // increased by 16.1
		System:    10.9,     // increased by 2.7
		Idle:      157.9798, // increased by 77.8798 (for total increase of 100)
		Nice:      3.5,      // increased by 2.2
		Iowait:    0.929,    // increased by 0.0901
		Irq:       1.2,      // increased by 0.6
		Softirq:   0.31,     // increased by 0.2
		Steal:     0.2812,   // increased by 0.2301
		Guest:     11.4,     // increased by 8.3
		GuestNice: 2.524,    // increased by 2.2
	}

	mps.On("CPUTimes").Return([]cpu.TimesStat{cts}, nil)

	cs := newCPUStats(&mps)

	err := cs.Gather(&acc)
	require.NoError(t, err)

	// Computed values are checked with delta > 0 because of floating point arithmetic
	// imprecision
	assertContainsTaggedFloat(t, &acc, "time_user", 8.8, 0)
	assertContainsTaggedFloat(t, &acc, "time_system", 8.2, 0)
	assertContainsTaggedFloat(t, &acc, "time_idle", 80.1, 0)
	assertContainsTaggedFloat(t, &acc, "time_active", 19.9, 0.0005)
	assertContainsTaggedFloat(t, &acc, "time_nice", 1.3, 0)
	assertContainsTaggedFloat(t, &acc, "time_iowait", 0.8389, 0)
	assertContainsTaggedFloat(t, &acc, "time_irq", 0.6, 0)
	assertContainsTaggedFloat(t, &acc, "time_softirq", 0.11, 0)
	assertContainsTaggedFloat(t, &acc, "time_steal", 0.0511, 0)
	assertContainsTaggedFloat(t, &acc, "time_guest", 3.1, 0)
	assertContainsTaggedFloat(t, &acc, "time_guest_nice", 0.324, 0)

	mps2 := psutil.MockPS{}
	mps2.On("CPUTimes").Return([]cpu.TimesStat{cts2}, nil)
	cs.ps = &mps2

	// Should have added cpu percentages too
	err = cs.Gather(&acc)
	require.NoError(t, err)

	assertContainsTaggedFloat(t, &acc, "time_user", 24.9, 0)
	assertContainsTaggedFloat(t, &acc, "time_system", 10.9, 0)
	assertContainsTaggedFloat(t, &acc, "time_idle", 157.9798, 0)
	assertContainsTaggedFloat(t, &acc, "time_active", 42.0202, 0.0005)
	assertContainsTaggedFloat(t, &acc, "time_nice", 3.5, 0)
	assertContainsTaggedFloat(t, &acc, "time_iowait", 0.929, 0)
	assertContainsTaggedFloat(t, &acc, "time_irq", 1.2, 0)
	assertContainsTaggedFloat(t, &acc, "time_softirq", 0.31, 0)
	assertContainsTaggedFloat(t, &acc, "time_steal", 0.2812, 0)
	assertContainsTaggedFloat(t, &acc, "time_guest", 11.4, 0)
	assertContainsTaggedFloat(t, &acc, "time_guest_nice", 2.524, 0)

	assertContainsTaggedFloat(t, &acc, "usage_user", 7.8, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_system", 2.7, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_idle", 77.8798, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_active", 22.1202, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_nice", 0, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_iowait", 0.0901, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_irq", 0.6, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_softirq", 0.2, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_steal", 0.2301, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_guest", 8.3, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_guest_nice", 2.2, 0.0005)
}

// Asserts that a given accumulator contains a measurement of type float64 with
// specific tags within a certain distance of a given expected value. Asserts a failure
// if the measurement is of the wrong type, or if no matching measurements are found
//
// Parameters:
//
//	t *testing.T            : Testing object to use
//	acc testutil.Accumulator: Accumulator to examine
//	field string            : Name of field to examine
//	expectedValue float64   : Value to search for within the measurement
//	delta float64           : Maximum acceptable distance of an accumulated value
//	                          from the expectedValue parameter. Useful when
//	                          floating-point arithmetic imprecision makes looking
//	                          for an exact match impractical
func assertContainsTaggedFloat(
	t *testing.T,
	acc *testutil.Accumulator,
	field string,
	expectedValue, delta float64,
) {
	var actualValue float64
	measurement := "cpu" // always cpu
	for _, pt := range acc.Metrics {
		if pt.Measurement == measurement {
			for fieldname, value := range pt.Fields {
				if fieldname == field {
					if value, ok := value.(float64); ok {
						actualValue = value
						if (value >= expectedValue-delta) && (value <= expectedValue+delta) {
							// Found the point, return without failing
							return
						}
					} else {
						require.Failf(t, "Wrong type", "Measurement %q does not have type float64", measurement)
					}
				}
			}
		}
	}
	require.Failf(t, "Measurement not found",
		"Could not find measurement %q with requested tags within %f of %f, Actual: %f", measurement, delta, expectedValue, actualValue)
}

// TestCPUCountChange tests that no errors are encountered if the number of
// CPUs increases as reported with LXC.
func TestCPUCountIncrease(t *testing.T) {
	var mps psutil.MockPS
	var mps2 psutil.MockPS
	var acc testutil.Accumulator
	var err error

	cs := newCPUStats(&mps)

	mps.On("CPUTimes").Return(
		[]cpu.TimesStat{
			{
				CPU: "cpu0",
			},
		}, nil)

	err = cs.Gather(&acc)
	require.NoError(t, err)

	mps2.On("CPUTimes").Return(
		[]cpu.TimesStat{
			{
				CPU: "cpu0",
			},
			{
				CPU: "cpu1",
			},
		}, nil)
	cs.ps = &mps2

	err = cs.Gather(&acc)
	require.NoError(t, err)
}

// TestCPUTimesDecrease tests that telegraf continue to works after
// CPU times decrease, which seems to occur when Linux system is suspended.
func TestCPUTimesDecrease(t *testing.T) {
	var mps psutil.MockPS
	defer mps.AssertExpectations(t)
	var acc testutil.Accumulator

	cts := cpu.TimesStat{
		CPU:    "cpu0",
		User:   18,
		Idle:   80,
		Iowait: 2,
	}

	cts2 := cpu.TimesStat{
		CPU:    "cpu0",
		User:   38, // increased by 20
		Idle:   40, // decreased by 40
		Iowait: 1,  // decreased by 1
	}

	cts3 := cpu.TimesStat{
		CPU:    "cpu0",
		User:   56,  // increased by 18
		Idle:   120, // increased by 80
		Iowait: 3,   // increased by 2
	}

	mps.On("CPUTimes").Return([]cpu.TimesStat{cts}, nil)

	cs := newCPUStats(&mps)

	err := cs.Gather(&acc)
	require.NoError(t, err)

	// Computed values are checked with delta > 0 because of floating point arithmetic
	// imprecision
	assertContainsTaggedFloat(t, &acc, "time_user", 18, 0)
	assertContainsTaggedFloat(t, &acc, "time_idle", 80, 0)
	assertContainsTaggedFloat(t, &acc, "time_iowait", 2, 0)

	mps2 := psutil.MockPS{}
	mps2.On("CPUTimes").Return([]cpu.TimesStat{cts2}, nil)
	cs.ps = &mps2

	// CPU times decreased. An error should be raised
	err = cs.Gather(&acc)
	require.Error(t, err)

	mps3 := psutil.MockPS{}
	mps3.On("CPUTimes").Return([]cpu.TimesStat{cts3}, nil)
	cs.ps = &mps3

	err = cs.Gather(&acc)
	require.NoError(t, err)

	assertContainsTaggedFloat(t, &acc, "time_user", 56, 0)
	assertContainsTaggedFloat(t, &acc, "time_idle", 120, 0)
	assertContainsTaggedFloat(t, &acc, "time_iowait", 3, 0)

	assertContainsTaggedFloat(t, &acc, "usage_user", 18, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_idle", 80, 0.0005)
	assertContainsTaggedFloat(t, &acc, "usage_iowait", 2, 0.0005)
}
