//go:build linux && amd64

package intel_powerstat

import (
	"errors"
	"fmt"
	"strconv"
	"testing"

	ptel "github.com/intel/powertelemetry"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf/testutil"
)

type parsePackageMetricTestCase struct {
	name    string
	metrics []packageMetricType
	parsed  []packageMetricType
	err     error
}

type parseCPUMetricTestCase struct {
	name    string
	metrics []cpuMetricType
	parsed  []cpuMetricType
	err     error
}

func TestParsePackageMetrics(t *testing.T) {
	testCases := []parsePackageMetricTestCase{
		{
			name:    "NilSlice",
			metrics: nil,
			parsed: []packageMetricType{
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},
		},
		{
			name:    "EmptySlice",
			metrics: make([]packageMetricType, 0),
			parsed:  make([]packageMetricType, 0),
		},
		{
			name: "HasDuplicates",
			metrics: []packageMetricType{
				packageCurrentPowerConsumption,
				packageThermalDesignPower,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower, // duplicate
			},
			err: errors.New("package metrics contains duplicates"),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := &PowerStat{
				PackageMetrics: tc.metrics,
			}

			err := p.parsePackageMetrics()

			if tc.err != nil {
				require.ErrorContains(t, err, tc.err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.parsed, p.PackageMetrics)
			}
		})
	}
}

func TestParseCPUMetrics(t *testing.T) {
	testCases := []parseCPUMetricTestCase{
		{
			name:    "NilSlice",
			metrics: nil,
			parsed:  nil,
		},
		{
			name:    "EmptySlice",
			metrics: make([]cpuMetricType, 0),
			parsed:  make([]cpuMetricType, 0),
		},
		{
			name: "HasDuplicates",
			metrics: []cpuMetricType{
				cpuC0StateResidency,
				cpuC1StateResidency,
				cpuTemperature,
				cpuC0StateResidency, // duplicate
			},
			err: errors.New("cpu metrics contains duplicates"),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := &PowerStat{
				CPUMetrics: tc.metrics,
			}

			err := p.parseCPUMetrics()

			if tc.err != nil {
				require.ErrorContains(t, err, tc.err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.parsed, p.CPUMetrics)
			}
		})
	}
}

func TestParseCPUTimeRelatedMsrMetrics(t *testing.T) {
	testCases := []parseCPUMetricTestCase{
		{
			name:    "EmptySlice",
			metrics: make([]cpuMetricType, 0),
			parsed:  make([]cpuMetricType, 0),
		},
		{
			name: "NotFound",
			metrics: []cpuMetricType{
				// Metric not relying on MSR.
				cpuFrequency,

				// Metric relying on single MSR read.
				cpuTemperature,

				// Metrics relying on perf events.
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
				cpuC0SubstateC0WaitPercent,
			},
			parsed: make([]cpuMetricType, 0),
		},
		{
			name: "Found",
			metrics: []cpuMetricType{
				// Metric not relying on MSR.
				cpuFrequency,

				// Metric relying on single MSR read.
				cpuTemperature,

				// Metrics relying on time-related MSR offset reads.
				cpuC0StateResidency,
				cpuC1StateResidency,
				cpuC3StateResidency,
				cpuC6StateResidency,
				cpuC7StateResidency,
				cpuBusyCycles,
				cpuBusyFrequency,

				// Metrics relying on perf events.
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
			},
			parsed: []cpuMetricType{
				cpuC0StateResidency,
				cpuC1StateResidency,
				cpuC3StateResidency,
				cpuC6StateResidency,
				cpuC7StateResidency,
				cpuBusyCycles,
				cpuBusyFrequency,
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := &PowerStat{
				CPUMetrics: tc.metrics,
			}

			p.parseCPUTimeRelatedMsrMetrics()
			require.Equal(t, tc.parsed, p.parsedCPUTimedMsrMetrics)
		})
	}
}

func TestParseCPUPerfMetrics(t *testing.T) {
	testCases := []parseCPUMetricTestCase{
		{
			name:    "EmptySlice",
			metrics: make([]cpuMetricType, 0),
			parsed:  make([]cpuMetricType, 0),
		},
		{
			name: "NotFound",
			metrics: []cpuMetricType{
				// Metric not relying on MSR.
				cpuFrequency,

				// Metric relying on single MSR read.
				cpuTemperature,

				// Metrics relying on time-related MSR offset reads.
				cpuC3StateResidency,
				cpuC6StateResidency,
				cpuBusyFrequency,
			},
			parsed: make([]cpuMetricType, 0),
		},
		{
			name: "Found",
			metrics: []cpuMetricType{
				// Metric not relying on MSR.
				cpuFrequency,

				// Metric relying on single MSR read.
				cpuTemperature,

				// Metrics relying on perf events.
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
				cpuC0SubstateC0WaitPercent,

				// Metrics relying on time-related MSR offset reads.
				cpuC3StateResidency,
				cpuC6StateResidency,
				cpuBusyFrequency,
			},
			parsed: []cpuMetricType{
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
				cpuC0SubstateC0WaitPercent,
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := &PowerStat{
				CPUMetrics: tc.metrics,
			}

			p.parseCPUPerfMetrics()
			require.Equal(t, tc.parsed, p.parsedCPUPerfMetrics)
		})
	}
}

func TestParsePackageRaplMetrics(t *testing.T) {
	testCases := []parsePackageMetricTestCase{
		{
			name:    "EmptySlice",
			metrics: make([]packageMetricType, 0),
			parsed:  make([]packageMetricType, 0),
		},
		{
			name: "NotFound",
			metrics: []packageMetricType{
				// Metrics not relying on rapl.
				packageTurboLimit,
				packageCPUBaseFrequency,
				packageUncoreFrequency,
			},
			parsed: make([]packageMetricType, 0),
		},
		{
			name: "Found",
			metrics: []packageMetricType{
				// Metrics not relying on rapl.
				packageTurboLimit,
				packageCPUBaseFrequency,
				packageUncoreFrequency,

				// Metrics relying on rapl.
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},
			parsed: []packageMetricType{
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := &PowerStat{
				PackageMetrics: tc.metrics,
			}

			p.parsePackageRaplMetrics()
			require.Equal(t, tc.parsed, p.parsedPackageRaplMetrics)
		})
	}
}

func TestParsePackageMsrMetrics(t *testing.T) {
	testCases := []parsePackageMetricTestCase{
		{
			name:    "EmptySlice",
			metrics: make([]packageMetricType, 0),
			parsed:  make([]packageMetricType, 0),
		},
		{
			name: "NotFound",
			metrics: []packageMetricType{
				// Metrics not relying on msr.
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},
			parsed: make([]packageMetricType, 0),
		},
		{
			name: "Found",
			metrics: []packageMetricType{
				// Metrics not relying on msr.
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,

				// Metrics relying uniquely on msr.
				packageTurboLimit,
				packageCPUBaseFrequency,
			},
			parsed: []packageMetricType{
				packageTurboLimit,
				packageCPUBaseFrequency,
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := &PowerStat{
				PackageMetrics: tc.metrics,
			}

			p.parsePackageMsrMetrics()
			require.Equal(t, tc.parsed, p.parsedPackageMsrMetrics)
		})
	}
}

func TestParseCoreRange(t *testing.T) {
	testCases := []struct {
		name      string
		coreRange string
		cores     []int
		err       error
	}{
		{
			name:      "InvalidFormat",
			coreRange: "1,3",
			cores:     nil,
			err:       errors.New("invalid core range format"),
		},
		{
			name:      "LowerBoundNonNumeric",
			coreRange: "a-10",
			cores:     nil,
			err:       errors.New("failed to parse low bounds' core range"),
		},
		{
			name:      "MissingLowerBound",
			coreRange: "-10",
			cores:     nil,
			err:       errors.New("failed to parse low bounds' core range"),
		},
		{
			name:      "HigherBoundNonNumeric",
			coreRange: "0-a",
			cores:     nil,
			err:       errors.New("failed to parse high bounds' core range"),
		},
		{
			name:      "MissingHigherBound",
			coreRange: "0-",
			cores:     nil,
			err:       errors.New("failed to parse high bounds' core range"),
		},
		{
			name:      "InvalidBounds",
			coreRange: "10-1",
			cores:     nil,
			err:       errors.New("high bound of core range cannot be less than low bound"),
		},
		{
			name:      "SingleCore",
			coreRange: "1-1",
			cores:     []int{1},
		},
		{
			name:      "CoreRange",
			coreRange: "5-10",
			cores:     []int{5, 6, 7, 8, 9, 10},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			coresOut, err := parseCoreRange(tc.coreRange)

			require.Equal(t, tc.cores, coresOut)
			if tc.err != nil {
				require.ErrorContains(t, err, tc.err.Error())
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestParseGroupCores(t *testing.T) {
	testCases := []struct {
		name      string
		coreGroup string
		cores     []int
		err       error
	}{
		{
			name:      "FailedToParseCoreRange",
			coreGroup: "1-a,7,9,11",
			cores:     nil,
			err:       errors.New("failed to parse core range \"1-a\""),
		},
		{
			name:      "FailedToParseSingleCore",
			coreGroup: "1-5,7,b,11",
			cores:     nil,
			err:       errors.New("failed to parse single core"),
		},
		{
			name:      "Ok",
			coreGroup: "1-5,7,9,11",
			cores:     []int{1, 2, 3, 4, 5, 7, 9, 11},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			coresOut, err := parseGroupCores(tc.coreGroup)

			require.Equal(t, tc.cores, coresOut)
			if tc.err != nil {
				require.ErrorContains(t, err, tc.err.Error())
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestHasDuplicate(t *testing.T) {
	t.Run("Int", func(t *testing.T) {
		t.Run("False", func(t *testing.T) {
			nums := []int{0, 1, 2, 3}
			require.False(t, hasDuplicate(nums))
		})

		t.Run("True", func(t *testing.T) {
			nums := []uint32{0, 1, 2, 3, 4, 5, 6, 1}
			require.True(t, hasDuplicate(nums))
		})
	})

	t.Run("String", func(t *testing.T) {
		t.Run("False", func(t *testing.T) {
			strs := []string{"1", "2", "3", "4"}
			require.False(t, hasDuplicate(strs))
		})

		t.Run("True", func(t *testing.T) {
			strs := []string{"1", "2", "3", "1"}
			require.True(t, hasDuplicate(strs))
		})
	})
}

func TestParseCores(t *testing.T) {
	testCases := []struct {
		name       string
		coreGroups []string
		cores      []int
		err        error
	}{
		{
			name:       "InvalidCoreGroup",
			coreGroups: []string{"1-4,11", "10-b"},
			cores:      nil,
			err:        errors.New("failed to parse core group"),
		},
		{
			name:       "FoundDuplicates",
			coreGroups: []string{"1-4,11", "10-12"},
			cores:      nil,
			err:        errors.New("core values cannot be duplicated"),
		},
		{
			name:       "CoresIsNil",
			coreGroups: nil,
			cores:      make([]int, 0),
		},
		{
			name:       "CoresIsEmpty",
			coreGroups: make([]string, 0),
			cores:      make([]int, 0),
		},
		{
			name:       "Ok",
			coreGroups: []string{"1-4,6", "8", "10-12", "15,20"},
			cores:      []int{1, 2, 3, 4, 6, 8, 10, 11, 12, 15, 20},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			coresOut, err := parseCores(tc.coreGroups)

			require.Equal(t, tc.cores, coresOut)
			if tc.err != nil {
				require.ErrorContains(t, err, tc.err.Error())
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestParseConfig(t *testing.T) {
	t.Run("BothCPUOptionsProvided", func(t *testing.T) {
		p := &PowerStat{
			IncludedCPUs: []string{"0-10,20-22"},
			ExcludedCPUs: []string{"0"},
		}

		require.ErrorContains(t, p.parseConfig(), "both 'included_cpus' and 'excluded_cpus' configured; provide only one or none of the two")
	})

	t.Run("FailedToParseIncludedCPUs", func(t *testing.T) {
		p := &PowerStat{
			// has duplicates
			IncludedCPUs: []string{"1-4,11", "10-12"},
		}

		require.ErrorContains(t, p.parseConfig(), "failed to parse included CPUs")
	})

	t.Run("FailedToParseExcludedCPUs", func(t *testing.T) {
		p := &PowerStat{
			// has non-numeric CPU ID
			ExcludedCPUs: []string{"1-4,b"},
		}

		require.ErrorContains(t, p.parseConfig(), "failed to parse excluded CPUs")
	})

	t.Run("FailedToParseCPUMetrics", func(t *testing.T) {
		p := &PowerStat{
			// has duplicates
			CPUMetrics: []cpuMetricType{
				cpuFrequency,
				cpuTemperature,
				cpuFrequency, // duplicate
			},
		}

		require.ErrorContains(t, p.parseConfig(), "failed to parse cpu metrics")
	})

	t.Run("EventDefinitionsNotProvidedForPerf", func(t *testing.T) {
		p := &PowerStat{
			// has duplicates
			CPUMetrics: []cpuMetricType{
				cpuC0SubstateC01Percent,
			},
		}

		require.ErrorContains(t, p.parseConfig(), "'event_definitions' contains an empty path")
	})

	t.Run("EventDefinitionsDoesNotExist", func(t *testing.T) {
		p := &PowerStat{
			// has duplicates
			CPUMetrics: []cpuMetricType{
				cpuC0SubstateC02Percent,
			},
			EventDefinitions: "./testdata/doesNotExist.json",
		}

		require.ErrorContains(t, p.parseConfig(), "'event_definitions' file \"./testdata/doesNotExist.json\" does not exist")
	})

	t.Run("NoMetricsProvided", func(t *testing.T) {
		p := &PowerStat{
			// Disable default package metrics.
			PackageMetrics: make([]packageMetricType, 0),
		}

		require.ErrorContains(t, p.parseConfig(), "no metrics were found in the configuration file")
	})

	t.Run("DisablePackageMetrics", func(t *testing.T) {
		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				cpuBusyFrequency,
			},
			// Disable default package metrics.
			PackageMetrics: make([]packageMetricType, 0),
		}

		require.NoError(t, p.parseConfig())
		require.Empty(t, p.PackageMetrics)
		require.Len(t, p.CPUMetrics, 1)
	})

	t.Run("DefaultPackageMetrics", func(t *testing.T) {
		p := &PowerStat{
			PackageMetrics: nil, // default package metrics
		}

		require.NoError(t, p.parseConfig())
		require.Equal(t,
			[]packageMetricType{
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},
			p.PackageMetrics)
	})

	t.Run("IncludedCPUs", func(t *testing.T) {
		p := &PowerStat{
			IncludedCPUs: []string{"0-5"},
		}

		require.NoError(t, p.parseConfig())
		require.Equal(t, []int{0, 1, 2, 3, 4, 5}, p.parsedIncludedCores)
		require.Nil(t, p.parsedExcludedCores)
	})

	t.Run("ExcludedCPUs", func(t *testing.T) {
		p := &PowerStat{
			ExcludedCPUs: []string{"2-6", "8", "10"},
		}

		require.NoError(t, p.parseConfig())
		require.Equal(t, []int{2, 3, 4, 5, 6, 8, 10}, p.parsedExcludedCores)
		require.Nil(t, p.parsedIncludedCores)
	})

	t.Run("MetricsWithIncludedCPUs", func(t *testing.T) {
		p := &PowerStat{
			IncludedCPUs: []string{"0-3,6"},
			CPUMetrics: []cpuMetricType{
				cpuC0StateResidency,
				cpuC1StateResidency,
				cpuC3StateResidency,
			},
			PackageMetrics: []packageMetricType{
				packageUncoreFrequency,
				packageTurboLimit,
			},
		}

		require.NoError(t, p.parseConfig())
		require.Equal(t, []int{0, 1, 2, 3, 6}, p.parsedIncludedCores)

		// Check flags
		require.True(t, p.needsMsrCPU)
		require.True(t, p.needsTimeRelatedMsr)
		require.False(t, p.needsCoreFreq)
		require.False(t, p.needsPerf)
	})
}

type mockOptGenerator struct {
	mock.Mock
}

func (m *mockOptGenerator) generate(cfg optConfig) []ptel.Option {
	args := m.Called(cfg)
	return args.Get(0).([]ptel.Option)
}

func TestSampleConfig(t *testing.T) {
	p := &PowerStat{}
	require.NotEmpty(t, p.SampleConfig())
}

func TestInit(t *testing.T) {
	t.Run("FailedToDisableUnsupportedMetrics", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata/cpu_model_missing")

		p := &PowerStat{}

		require.ErrorContains(t, p.Init(), "error occurred while parsing CPU model")
	})

	t.Run("FailedToParseConfigWithDuplicates", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata")

		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			// has duplicates
			IncludedCPUs: []string{"1-4,11", "10-12"},
			Log:          logger,
		}

		require.ErrorContains(t, p.Init(), "failed to parse included CPUs")
		require.Empty(t, logger.Warnings())
	})

	t.Run("FailedToParseConfigWithNegativeTimeout", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata")

		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			// negative value
			MsrReadTimeout: -2,
			Log:            logger,
		}

		require.ErrorContains(t, p.Init(), "msr_read_timeout should be positive number or equal to 0 (to disable timeouts)")
		require.Empty(t, logger.Warnings())
	})
}

func TestStart(t *testing.T) {
	t.Run("FailedToStartCPUDoesNotExist", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata")

		acc := &testutil.Accumulator{}
		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			// has CPU ID out of bounds
			parsedIncludedCores: []int{0, 9},
			Log:                 logger,

			option: &optGenerator{},
		}

		require.ErrorContains(t, p.Start(acc), "failed to initialize metric fetcher interface")
		require.Empty(t, logger.Warnings())
	})

	t.Run("FailedToStartNotSupportedCPUVendor", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata/vendor_not_supported")

		acc := &testutil.Accumulator{}
		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			Log:    logger,
			option: &optGenerator{},
		}

		require.ErrorContains(t, p.Start(acc), "host processor is not supported")
	})

	t.Run("WithWarning", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata")

		acc := &testutil.Accumulator{}
		logger := &testutil.CaptureLogger{}

		mOptGenerator := &mockOptGenerator{}
		mOptGenerator.On("generate", mock.AnythingOfType("optConfig")).Return(
			[]ptel.Option{
				ptel.WithRapl("/dummy/path"),
			},
		)

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				packageCurrentPowerConsumption, // needs rapl
			},
			Log: logger,

			option: mOptGenerator,
		}

		require.NoError(t, p.Start(acc))
		require.Len(t, logger.Warnings(), 1)
		require.Contains(t, logger.Warnings()[0], "Plugin started with errors")
	})
}

func TestGather(t *testing.T) {
	t.Run("WithoutMetrics", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		p := &PowerStat{
			PackageMetrics: make([]packageMetricType, 0),
		}

		require.NoError(t, p.Gather(acc))

		require.Empty(t, acc.Errors)
		require.Empty(t, acc.GetTelegrafMetrics())
	})

	t.Run("WithDefaultPackageMetrics", func(t *testing.T) {
		packageID := 0

		packagePower := 10.0
		dramPower := 5.0
		thermalDesignPower := 20.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available package IDs.
		mFetcher.On("GetPackageIDs").Return([]int{packageID}).Once()

		// mock getting current package power consumption metric.
		mFetcher.On("GetCurrentPackagePowerConsumptionWatts", packageID).Return(packagePower, nil).Once()

		// mock getting current dram power consumption metric.
		mFetcher.On("GetCurrentDramPowerConsumptionWatts", packageID).Return(dramPower, nil).Once()

		// mock getting package thermal design power metric.
		mFetcher.On("GetPackageThermalDesignPowerWatts", packageID).Return(thermalDesignPower, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		require.NoError(t, p.Gather(acc))

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 3)
		require.True(t, acc.HasField("powerstat_package", "current_power_consumption_watts"))
		require.True(t, acc.HasField("powerstat_package", "current_dram_power_consumption_watts"))
		require.True(t, acc.HasField("powerstat_package", "thermal_design_power_watts"))
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithPackageMetrics", func(t *testing.T) {
		packageIDs := []int{0, 1, 2, 3}

		baseFreq := uint64(200)
		packagePower := 30.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available package IDs.
		mFetcher.On("GetPackageIDs").Return(packageIDs).Once()

		// mock getting CPU base frequency metric.
		mFetcher.On("GetCPUBaseFrequency", mock.AnythingOfType("int")).Return(baseFreq, nil).Times(len(packageIDs))

		// mock getting current package power consumption metric.
		mFetcher.On("GetCurrentPackagePowerConsumptionWatts", mock.AnythingOfType("int")).Return(packagePower, nil).Times(len(packageIDs))

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				packageCurrentPowerConsumption,
				packageCPUBaseFrequency,
			},
			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		require.NoError(t, p.Gather(acc))

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 8)
		require.True(t, acc.HasField("powerstat_package", "cpu_base_frequency_mhz"))
		require.True(t, acc.HasField("powerstat_package", "current_power_consumption_watts"))
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithCPUMetrics", func(t *testing.T) {
		cpuIDs := []int{0, 1, 2, 3}

		cpuFreq := 123.5
		cpuTemp := uint64(20)
		cpuBusyFreq := 456.7

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available CPU IDs with access to msr registers and coreFreq.
		mFetcher.On("GetMsrCPUIDs").Return(cpuIDs).Once()

		// mock getting core ID for CPU IDs.
		mFetcher.On("GetCPUCoreID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDs))

		// mock getting package ID for CPU IDs.
		mFetcher.On("GetCPUPackageID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDs))

		// mock updating msr time-related metrics for CPU IDs.
		mFetcher.On("UpdatePerCPUMetrics", mock.AnythingOfType("int")).Return(nil).Times(len(cpuIDs))

		// mock getting CPU frequency for CPU IDs.
		mFetcher.On("GetCPUFrequency", mock.AnythingOfType("int")).Return(cpuFreq, nil).Times(len(cpuIDs))

		// mock getting CPU temperature metric for CPU IDs.
		mFetcher.On("GetCPUTemperature", mock.AnythingOfType("int")).Return(cpuTemp, nil).Times(len(cpuIDs))

		// mock getting CPU busy frequency metric for CPU IDs.
		mFetcher.On("GetCPUBusyFrequencyMhz", mock.AnythingOfType("int")).Return(cpuBusyFreq, nil).Times(len(cpuIDs))

		p := &PowerStat{
			// Disables package metrics
			PackageMetrics: make([]packageMetricType, 0),
			CPUMetrics: []cpuMetricType{
				cpuFrequency,
				cpuTemperature,
				cpuBusyFrequency,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		require.NoError(t, p.Gather(acc))

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 12)
		require.True(t, acc.HasField("powerstat_core", "cpu_frequency_mhz"))
		require.True(t, acc.HasField("powerstat_core", "cpu_temperature_celsius"))
		require.True(t, acc.HasField("powerstat_core", "cpu_busy_frequency_mhz"))
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithPerfMetrics", func(t *testing.T) {
		cpuIDs := []int{0, 1, 2}

		c01 := 0.1
		c02 := 1.2
		c0Wait := 2.3

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock reading perf events.
		mFetcher.On("ReadPerfEvents").Return(nil).Once()

		// mock getting available CPU IDs with access to msr registers and coreFreq.
		mFetcher.On("GetPerfCPUIDs").Return(cpuIDs).Once()

		// mock getting core ID for CPU IDs.
		mFetcher.On("GetCPUCoreID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDs))

		// mock getting package ID for CPU IDs.
		mFetcher.On("GetCPUPackageID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDs))

		// mock getting CPU C01 metric.
		mFetcher.On("GetCPUC0SubstateC01Percent", mock.AnythingOfType("int")).Return(c01, nil).Times(len(cpuIDs))

		// mock getting CPU C02 metric.
		mFetcher.On("GetCPUC0SubstateC02Percent", mock.AnythingOfType("int")).Return(c02, nil).Times(len(cpuIDs))

		// mock getting CPU C0Wait metric.
		mFetcher.On("GetCPUC0SubstateC0WaitPercent", mock.AnythingOfType("int")).Return(c0Wait, nil).Times(len(cpuIDs))

		p := &PowerStat{
			// Disables package metrics
			PackageMetrics: make([]packageMetricType, 0),
			CPUMetrics: []cpuMetricType{
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
				cpuC0SubstateC0WaitPercent,
			},
			EventDefinitions: "./testdata/sapphirerapids_core.json",

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		require.NoError(t, p.Gather(acc))

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 9)
		require.True(t, acc.HasField("powerstat_core", "cpu_c0_substate_c01_percent"))
		require.True(t, acc.HasField("powerstat_core", "cpu_c0_substate_c02_percent"))
		require.True(t, acc.HasField("powerstat_core", "cpu_c0_substate_c0_wait_percent"))
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithPerfAndMsrCPUMetrics", func(t *testing.T) {
		cpuIDsMsr := []int{0, 1, 2, 3}
		cpuIDsPerf := []int{0, 1}

		c1 := 0.5
		c6 := 1.5
		c01 := 0.1
		c02 := 1.2

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available CPU IDs with access to msr registers.
		mFetcher.On("GetMsrCPUIDs").Return(cpuIDsMsr).Once()

		// mock getting core ID for CPU IDs.
		mFetcher.On("GetCPUCoreID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDsMsr) + len(cpuIDsPerf))

		// mock getting package ID for CPU IDs.
		mFetcher.On("GetCPUPackageID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDsMsr) + len(cpuIDsPerf))

		// mock updating msr time-related metrics for CPU IDs.
		mFetcher.On("UpdatePerCPUMetrics", mock.AnythingOfType("int")).Return(nil).Times(len(cpuIDsMsr))

		// mock getting CPU C1 state residency metric for CPU IDs.
		mFetcher.On("GetCPUC1StateResidency", mock.AnythingOfType("int")).Return(c1, nil).Times(len(cpuIDsMsr))

		// mock getting CPU C6 state residency metric for CPU IDs.
		mFetcher.On("GetCPUC6StateResidency", mock.AnythingOfType("int")).Return(c6, nil).Times(len(cpuIDsMsr))

		// mock reading perf events.
		mFetcher.On("ReadPerfEvents").Return(nil).Once()

		// mock getting available CPU IDs with access to msr registers and coreFreq.
		mFetcher.On("GetPerfCPUIDs").Return(cpuIDsPerf).Once()

		// mock getting CPU C01 metric.
		mFetcher.On("GetCPUC0SubstateC01Percent", mock.AnythingOfType("int")).Return(c01, nil).Times(len(cpuIDsPerf))

		// mock getting CPU C02 metric.
		mFetcher.On("GetCPUC0SubstateC02Percent", mock.AnythingOfType("int")).Return(c02, nil).Times(len(cpuIDsPerf))

		p := &PowerStat{
			// Disables package metrics
			PackageMetrics: make([]packageMetricType, 0),
			CPUMetrics: []cpuMetricType{
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
				cpuC1StateResidency,
				cpuC6StateResidency,
			},
			EventDefinitions: "./testdata/sapphirerapids_core.json",

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		require.NoError(t, p.Gather(acc))

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 12)
		require.True(t, acc.HasField("powerstat_core", "cpu_c0_substate_c01_percent"))
		require.True(t, acc.HasField("powerstat_core", "cpu_c0_substate_c02_percent"))
		require.True(t, acc.HasField("powerstat_core", "cpu_c1_state_residency_percent"))
		require.True(t, acc.HasField("powerstat_core", "cpu_c6_state_residency_percent"))
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithCPUAndPackageMetrics", func(t *testing.T) {
		cpuIDs := []int{10, 12}
		packageIDs := []int{0, 1, 2, 3}
		dieIDs := []int{0, 1}

		c7 := 0.12

		initMin := 200.0
		initMax := 1200.0
		currMin := 300.0
		currMax := 1300.0
		curr := 800.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available CPU IDs with access to msr registers and coreFreq.
		mFetcher.On("GetMsrCPUIDs").Return(cpuIDs).Once()

		// mock getting core ID for CPU IDs.
		mFetcher.On("GetCPUCoreID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDs))

		// mock getting package ID for CPU IDs.
		mFetcher.On("GetCPUPackageID", mock.AnythingOfType("int")).Return(1, nil).Times(len(cpuIDs))

		// mock updating msr time-related metrics for CPU IDs.
		mFetcher.On("UpdatePerCPUMetrics", mock.AnythingOfType("int")).Return(nil).Times(len(cpuIDs))

		// mock getting C7 state residency metric for CPU IDs.
		mFetcher.On("GetCPUC7StateResidency", mock.AnythingOfType("int")).Return(c7, nil).Times(len(cpuIDs))

		// mock getting available package IDs.
		mFetcher.On("GetPackageIDs").Return(packageIDs).Once()

		// mock getting die IDs for package ID.
		mFetcher.On("GetPackageDieIDs", mock.AnythingOfType("int")).Return(dieIDs, nil).Times(len(packageIDs))

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(initMin, nil).
			Times(len(packageIDs) * len(dieIDs))

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(initMax, nil).
			Times(len(packageIDs) * len(dieIDs))

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(currMin, nil).
			Times(len(packageIDs) * len(dieIDs))

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(currMax, nil).
			Times(len(packageIDs) * len(dieIDs))

		// mock getting current uncore frequency value.
		mFetcher.On("GetCurrentUncoreFrequency", mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(curr, nil).Times(len(packageIDs) * len(dieIDs))

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				packageUncoreFrequency,
			},
			CPUMetrics: []cpuMetricType{
				cpuC7StateResidency,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		require.NoError(t, p.Gather(acc))

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 18)
		require.True(t, acc.HasField("powerstat_core", "cpu_c7_state_residency_percent"))
		require.True(t, acc.HasField("powerstat_package", "uncore_frequency_limit_mhz_min"))
		require.True(t, acc.HasField("powerstat_package", "uncore_frequency_limit_mhz_max"))
		require.True(t, acc.HasField("powerstat_package", "uncore_frequency_mhz_cur"))
		require.True(t, acc.HasTag("powerstat_package", "type"))
		mFetcher.AssertExpectations(t)
	})
}

func TestStop(t *testing.T) {
	t.Run("NoErrorWithoutPerf", func(t *testing.T) {
		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			Log: logger,

			needsPerf: false,
		}

		p.Stop()

		require.Empty(t, logger.Errors())
	})

	t.Run("FailedToDeactivatePerfEvents", func(t *testing.T) {
		logger := &testutil.CaptureLogger{}

		mFetcher := &fetcherMock{}

		// mock deactivating perf events.
		mFetcher.On("DeactivatePerfEvents").Return(errors.New("mock error")).Once()

		p := &PowerStat{
			Log: logger,

			fetcher: mFetcher,

			needsPerf: true,
		}

		p.Stop()

		require.Len(t, logger.Errors(), 1)
		require.Contains(t, logger.Errors()[0], "Failed to deactivate perf events")
	})

	t.Run("NoErrorWithPerf", func(t *testing.T) {
		logger := &testutil.CaptureLogger{}

		mFetcher := &fetcherMock{}

		// mock deactivating perf events.
		mFetcher.On("DeactivatePerfEvents").Return(nil).Once()

		p := &PowerStat{
			Log: logger,

			fetcher: mFetcher,

			needsPerf: true,
		}

		p.Stop()

		require.Empty(t, logger.Errors())
	})
}

func TestDisableUnsupportedMetrics(t *testing.T) {
	t.Run("ModelMissing", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata/cpu_model_missing")

		p := &PowerStat{}

		err := p.disableUnsupportedMetrics()

		require.Error(t, err, "error occurred while parsing CPU model")
	})

	t.Run("MsrFlagNotFound", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata/msr_flag_not_found")

		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// Metrics relying on msr flag
				cpuC0StateResidency,
				cpuC1StateResidency,
				cpuC6StateResidency,
				cpuBusyFrequency,
				cpuBusyCycles,
				cpuTemperature,
			},
			PackageMetrics: []packageMetricType{
				// Metrics relying on msr flag
				packageCPUBaseFrequency,
				packageTurboLimit,

				// Metrics not relying on msr flag
				packageCurrentPowerConsumption,
				packageThermalDesignPower,
			},

			Log: logger,
		}

		err := p.disableUnsupportedMetrics()

		require.NoError(t, err)
		require.Empty(t, p.CPUMetrics)
		require.Len(t, p.PackageMetrics, 2)
		require.Contains(t, p.PackageMetrics, packageCurrentPowerConsumption)
		require.Contains(t, p.PackageMetrics, packageThermalDesignPower)
		require.Len(t, logger.Warnings(), 8)
	})

	t.Run("AperfMperfFlagNotFound", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata/aperfmperf_flag_not_found")

		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// Metrics relying on aperfmperf flag
				cpuC0StateResidency,
				cpuC1StateResidency,
				cpuBusyFrequency,
				cpuBusyCycles,

				// Metrics not relying on aperfmperf flag
				cpuTemperature,
			},

			Log: logger,
		}

		err := p.disableUnsupportedMetrics()

		require.NoError(t, err)
		require.Len(t, p.CPUMetrics, 1)
		require.Contains(t, p.CPUMetrics, cpuTemperature)
		require.Len(t, logger.Warnings(), 4)
	})

	t.Run("DtsFlagNotFound", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata/dts_flag_not_found")

		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// Metrics relying on dts flag
				cpuTemperature,

				// Metrics not relying on dts flag
				cpuBusyFrequency,
			},
			PackageMetrics: make([]packageMetricType, 0),

			Log: logger,
		}

		err := p.disableUnsupportedMetrics()

		require.NoError(t, err)
		require.Len(t, p.CPUMetrics, 1)
		require.Contains(t, p.CPUMetrics, cpuBusyFrequency)
		require.Len(t, logger.Warnings(), 1)
	})

	t.Run("ModelNotSupported", func(t *testing.T) {
		t.Setenv("HOST_PROC", "./testdata/model_not_supported")

		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// Metrics not supported by CPU
				cpuTemperature,
				cpuC1StateResidency,
				cpuC3StateResidency,
				cpuC6StateResidency,
				cpuC7StateResidency,
			},
			PackageMetrics: []packageMetricType{
				// Metrics not supported by CPU
				packageCPUBaseFrequency,

				packageUncoreFrequency,
			},

			Log: logger,
		}

		err := p.disableUnsupportedMetrics()

		require.NoError(t, err)
		require.Empty(t, p.CPUMetrics)
		require.Contains(t, p.PackageMetrics, packageUncoreFrequency)
		require.Len(t, logger.Warnings(), 6)
	})
}

func TestDisableCPUMetric(t *testing.T) {
	t.Run("NoMetricsRemoved", func(t *testing.T) {
		expStartLen := 1
		expEndLen := 1
		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{cpuC1StateResidency},
			Log:        logger,
		}

		require.Len(t, p.CPUMetrics, expStartLen)
		p.disableCPUMetric(cpuC3StateResidency)
		require.Len(t, p.CPUMetrics, expEndLen)

		require.Empty(t, logger.Warnings())
	})
	t.Run("TwoMetricsRemoved", func(t *testing.T) {
		expStartLen := 3
		expEndLen := 1
		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{cpuC1StateResidency, cpuC3StateResidency, cpuC6StateResidency},
			Log:        logger,
		}

		require.Len(t, p.CPUMetrics, expStartLen)
		p.disableCPUMetric(cpuC3StateResidency)
		p.disableCPUMetric(cpuC1StateResidency)
		require.Len(t, p.CPUMetrics, expEndLen)

		require.Len(t, logger.Warnings(), 2)
		require.Contains(t, logger.Warnings()[0], "\"cpu_c3_state_residency\" is not supported by CPU, metric will not be gathered")
		require.Contains(t, logger.Warnings()[1], "\"cpu_c1_state_residency\" is not supported by CPU, metric will not be gathered")
	})
}

func TestDisablePackageMetric(t *testing.T) {
	t.Run("NoMetricsRemoved", func(t *testing.T) {
		expStartLen := 1
		expEndLen := 1
		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			PackageMetrics: []packageMetricType{packageCurrentPowerConsumption},
			Log:            logger,
		}

		require.Len(t, p.PackageMetrics, expStartLen)
		p.disablePackageMetric(packageCPUBaseFrequency)
		require.Len(t, p.PackageMetrics, expEndLen)

		require.Empty(t, logger.Warnings())
	})
	t.Run("TwoMetricsRemoved", func(t *testing.T) {
		expStartLen := 3
		expEndLen := 1
		logger := &testutil.CaptureLogger{}

		p := &PowerStat{
			PackageMetrics: []packageMetricType{packageCurrentPowerConsumption, packageTurboLimit, packageCPUBaseFrequency},
			Log:            logger,
		}

		require.Len(t, p.PackageMetrics, expStartLen)
		p.disablePackageMetric(packageCPUBaseFrequency)
		p.disablePackageMetric(packageTurboLimit)
		require.Len(t, p.PackageMetrics, expEndLen)

		require.Len(t, logger.Warnings(), 2)
		require.Contains(t, logger.Warnings()[0], "\"cpu_base_frequency\" is not supported by CPU, metric will not be gathered")
		require.Contains(t, logger.Warnings()[1], "\"max_turbo_frequency\" is not supported by CPU, metric will not be gathered")
	})
}

type fetcherMock struct {
	mock.Mock
}

func (m *fetcherMock) GetMsrCPUIDs() []int {
	args := m.Called()
	if args.Get(0) == nil {
		return nil
	}
	return args.Get(0).([]int)
}

func (m *fetcherMock) GetPerfCPUIDs() []int {
	args := m.Called()
	if args.Get(0) == nil {
		return nil
	}
	return args.Get(0).([]int)
}

func (m *fetcherMock) GetPackageIDs() []int {
	args := m.Called()
	if args.Get(0) == nil {
		return nil
	}
	return args.Get(0).([]int)
}

func (m *fetcherMock) GetCPUPackageID(cpuID int) (int, error) {
	args := m.Called(cpuID)
	return args.Int(0), args.Error(1)
}

func (m *fetcherMock) GetCPUCoreID(cpuID int) (int, error) {
	args := m.Called(cpuID)
	return args.Int(0), args.Error(1)
}

func (m *fetcherMock) GetPackageDieIDs(packageID int) ([]int, error) {
	args := m.Called(packageID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]int), args.Error(1)
}

func (m *fetcherMock) GetCPUFrequency(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) UpdatePerCPUMetrics(cpuID int) error {
	args := m.Called(cpuID)
	return args.Error(0)
}

func (m *fetcherMock) GetCPUTemperature(cpuID int) (uint64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(uint64), args.Error(1)
}

func (m *fetcherMock) GetCPUC0StateResidency(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUC1StateResidency(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUC3StateResidency(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUC6StateResidency(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUC7StateResidency(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUBusyFrequencyMhz(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) ReadPerfEvents() error {
	args := m.Called()
	return args.Error(0)
}

func (m *fetcherMock) DeactivatePerfEvents() error {
	args := m.Called()
	return args.Error(0)
}

func (m *fetcherMock) GetCPUC0SubstateC01Percent(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUC0SubstateC02Percent(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUC0SubstateC0WaitPercent(cpuID int) (float64, error) {
	args := m.Called(cpuID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCPUBaseFrequency(packageID int) (uint64, error) {
	args := m.Called(packageID)
	return args.Get(0).(uint64), args.Error(1)
}

func (m *fetcherMock) GetInitialUncoreFrequencyMin(packageID, dieID int) (float64, error) {
	args := m.Called(packageID, dieID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCustomizedUncoreFrequencyMin(packageID, dieID int) (float64, error) {
	args := m.Called(packageID, dieID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetInitialUncoreFrequencyMax(packageID, dieID int) (float64, error) {
	args := m.Called(packageID, dieID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCustomizedUncoreFrequencyMax(packageID, dieID int) (float64, error) {
	args := m.Called(packageID, dieID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCurrentUncoreFrequency(packageID, dieID int) (float64, error) {
	args := m.Called(packageID, dieID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCurrentPackagePowerConsumptionWatts(packageID int) (float64, error) {
	args := m.Called(packageID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetCurrentDramPowerConsumptionWatts(packageID int) (float64, error) {
	args := m.Called(packageID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetPackageThermalDesignPowerWatts(packageID int) (float64, error) {
	args := m.Called(packageID)
	return args.Get(0).(float64), args.Error(1)
}

func (m *fetcherMock) GetMaxTurboFreqList(packageID int) ([]ptel.MaxTurboFreq, error) {
	args := m.Called(packageID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]ptel.MaxTurboFreq), args.Error(1)
}

func TestAddCPUMetrics(t *testing.T) {
	// Disable package metrics when parseConfig method is called.
	packageMetrics := make([]packageMetricType, 0)

	t.Run("NoAvailableCPUs", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available CPU IDs with access to msr registers.
		mFetcher.On("GetMsrCPUIDs").Return(nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		p.addCPUMetrics(acc)

		require.Empty(t, acc.Errors)
		require.Empty(t, acc.GetTelegrafMetrics())
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithDataCPUIDErrors", func(t *testing.T) {
		t.Run("SingleCPUID", func(t *testing.T) {
			cpuID := 0

			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock getting available CPU IDs with access to msr registers and coreFreq.
			mFetcher.On("GetMsrCPUIDs").Return([]int{cpuID}).Once()

			// mock getting core ID for CPU ID.
			mFetcher.On("GetCPUCoreID", cpuID).Return(0, errors.New("mock error")).Once()

			p := &PowerStat{
				fetcher: mFetcher,
			}

			p.addCPUMetrics(acc)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get coreFreq and/or msr metrics for CPU ID %v", cpuID))
			require.Empty(t, acc.GetTelegrafMetrics())
			mFetcher.AssertExpectations(t)
		})

		t.Run("MultipleCPUIDs", func(t *testing.T) {
			cpuID := 1
			coreID := 2
			packageID := 3
			cpuFreq := 500.0

			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock getting available CPU IDs with access to msr registers and coreFreq.
			mFetcher.On("GetMsrCPUIDs").Return([]int{0, cpuID}).Once()

			// mock getting core ID for CPU ID 0.
			mFetcher.On("GetCPUCoreID", 0).Return(0, errors.New("mock error")).Once()

			// mock getting core ID for CPU ID 1.
			mFetcher.On("GetCPUCoreID", cpuID).Return(coreID, nil).Once()

			// mock getting package ID for CPU ID 1.
			mFetcher.On("GetCPUPackageID", cpuID).Return(packageID, nil).Once()

			// mock getting CPU frequency for CPU ID 1.
			mFetcher.On("GetCPUFrequency", cpuID).Return(cpuFreq, nil).Once()

			p := &PowerStat{
				CPUMetrics: []cpuMetricType{
					// Metric which relies on coreFreq.
					cpuFrequency,

					// Metrics which do not rely on coreFreq nor msr.
					cpuC0SubstateC01Percent,
					cpuC0SubstateC02Percent,
				},
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addCPUMetrics(acc)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), "failed to get coreFreq and/or msr metrics for CPU ID 0")
			require.Len(t, acc.GetTelegrafMetrics(), 1)
			acc.AssertContainsTaggedFields(
				t,
				// measurement
				"powerstat_core",
				// fields
				map[string]interface{}{
					"cpu_frequency_mhz": cpuFreq,
				},
				// tags
				map[string]string{
					"cpu_id":     strconv.Itoa(cpuID),
					"core_id":    strconv.Itoa(coreID),
					"package_id": strconv.Itoa(packageID),
				},
			)
			mFetcher.AssertExpectations(t)
		})
	})

	t.Run("WithCoreFreqMetrics", func(t *testing.T) {
		cpuFreq := 500.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available CPU IDs with access to coreFreq.
		mFetcher.On("GetMsrCPUIDs").Return([]int{0, 1}).Once()

		// mock getting corresponding core ID to CPU IDs 0 and 1.
		mFetcher.On("GetCPUCoreID", mock.AnythingOfType("int")).Return(0, nil).Twice()

		// mock getting corresponding package ID to CPU IDs 0 and 1.
		mFetcher.On("GetCPUPackageID", mock.AnythingOfType("int")).Return(1, nil).Twice()

		// mock getting CPU frequency for CPU ID 0.
		mFetcher.On("GetCPUFrequency", 0).Return(cpuFreq, nil).Once()

		// mock getting CPU frequency for CPU ID 1.
		mFetcher.On("GetCPUFrequency", 1).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// Metric which relies on coreFreq.
				cpuFrequency,

				// Metrics which do not rely on coreFreq nor msr
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
			},
			PackageMetrics:   packageMetrics,
			EventDefinitions: "./testdata/sapphirerapids_core.json",

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addCPUMetrics(acc)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID 1", cpuFrequency))
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_frequency_mhz": cpuFreq,
			},
			// tags
			map[string]string{
				"cpu_id":     "0",
				"core_id":    "0",
				"package_id": "1",
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithMsrMetrics", func(t *testing.T) {
		t.Run("SingleRead", func(t *testing.T) {
			cpuTemp := uint64(18)

			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock getting available CPU IDs with access to msr registers.
			mFetcher.On("GetMsrCPUIDs").Return([]int{0, 1}).Once()

			// mock getting corresponding core ID to CPU IDs 0 and 1.
			mFetcher.On("GetCPUCoreID", mock.AnythingOfType("int")).Return(0, nil).Twice()

			// mock getting corresponding package ID to CPU IDs 0 and 1.
			mFetcher.On("GetCPUPackageID", mock.AnythingOfType("int")).Return(1, nil).Twice()

			// mock getting CPU temperature metric for CPU ID 0.
			mFetcher.On("GetCPUTemperature", 0).Return(cpuTemp, nil).Once()

			// mock getting CPU temperature metric for CPU ID 1.
			mFetcher.On("GetCPUTemperature", 1).Return(uint64(0), errors.New("mock error")).Once()

			p := &PowerStat{
				CPUMetrics: []cpuMetricType{
					// Metrics which rely on single-read msr registers.
					cpuTemperature,

					// Metrics which do not rely on coreFreq nor msr
					cpuC0SubstateC01Percent,
					cpuC0SubstateC02Percent,
				},
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addCPUMetrics(acc)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID 1", cpuTemperature))
			require.Len(t, acc.GetTelegrafMetrics(), 1)
			acc.AssertContainsTaggedFields(
				t,
				// measurement
				"powerstat_core",
				// fields
				map[string]interface{}{
					"cpu_temperature_celsius": cpuTemp,
				},
				// tags
				map[string]string{
					"cpu_id":     "0",
					"core_id":    "0",
					"package_id": "1",
				},
			)
			mFetcher.AssertExpectations(t)
		})

		t.Run("TimeRelated", func(t *testing.T) {
			cpuBusyFreq := 750.0

			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock getting available CPU IDs with access to msr registers.
			mFetcher.On("GetMsrCPUIDs").Return([]int{0, 1}).Once()

			// mock getting corresponding core ID to CPU IDs 0 and 1.
			mFetcher.On("GetCPUCoreID", mock.AnythingOfType("int")).Return(0, nil).Twice()

			// mock getting corresponding package ID to CPU IDs 0 and 1.
			mFetcher.On("GetCPUPackageID", mock.AnythingOfType("int")).Return(1, nil).Twice()

			// mock updating msr time-related metrics for CPU ID 0.
			mFetcher.On("UpdatePerCPUMetrics", 0).Return(errors.New("mock error")).Once()

			// mock updating msr time-related metrics for CPU ID 1.
			mFetcher.On("UpdatePerCPUMetrics", 1).Return(nil).Once()

			// mock getting CPU busy frequency metric for CPU ID 1.
			mFetcher.On("GetCPUBusyFrequencyMhz", 1).Return(cpuBusyFreq, nil).Once()

			p := &PowerStat{
				CPUMetrics: []cpuMetricType{
					// Metrics which rely on time-related msr reads.
					cpuBusyFrequency,

					// Metrics which do not rely on coreFreq nor msr
					cpuC0SubstateC01Percent,
					cpuC0SubstateC02Percent,
					cpuC0SubstateC0WaitPercent,
				},
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addCPUMetrics(acc)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), "failed to update MSR time-related metrics for CPU ID 0")
			require.Len(t, acc.GetTelegrafMetrics(), 1)
			acc.AssertContainsTaggedFields(
				t,
				// measurement
				"powerstat_core",
				// fields
				map[string]interface{}{
					"cpu_busy_frequency_mhz": cpuBusyFreq,
				},
				// tags
				map[string]string{
					"cpu_id":     "1",
					"core_id":    "0",
					"package_id": "1",
				},
			)
			mFetcher.AssertExpectations(t)
		})
	})
}

func TestAddPerCPUMsrMetrics(t *testing.T) {
	// Disable package metrics when parseConfig method is called.
	packageMetrics := make([]packageMetricType, 0)

	t.Run("WithoutMsrMetrics", func(t *testing.T) {
		cpuID := 0
		coreID := 1
		packageID := 0

		acc := &testutil.Accumulator{}

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// metrics which do not rely on msr
				cpuFrequency,
				cpuC0SubstateC01Percent,
			},
		}

		p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.Errors)
		require.Empty(t, acc.GetTelegrafMetrics())
	})

	t.Run("WithSingleMsrReadMetrics", func(t *testing.T) {
		cpuID := 0
		coreID := 1
		packageID := 0
		cpuMetrics := []cpuMetricType{
			// metric that relies on a single msr read.
			cpuTemperature,

			// metrics that do not rely on msr.
			cpuFrequency,
			cpuC0SubstateC01Percent,
		}

		t.Run("WithError", func(t *testing.T) {
			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock getting CPU temperature metric.
			mFetcher.On("GetCPUTemperature", cpuID).Return(uint64(0), errors.New("mock error")).Once()

			p := &PowerStat{
				CPUMetrics:       cpuMetrics,
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,

				logOnce: map[string]struct{}{},
			}

			require.NoError(t, p.parseConfig())

			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuTemperature, cpuID))
			require.Empty(t, p.logOnce)
			require.Empty(t, acc.GetTelegrafMetrics())
			mFetcher.AssertExpectations(t)
		})

		t.Run("WithModuleNotInitializedError", func(t *testing.T) {
			acc := &testutil.Accumulator{}

			mErr := &ptel.ModuleNotInitializedError{Name: "msr"}
			mFetcher := &fetcherMock{}

			// mock getting CPU temperature metric.
			mFetcher.On("GetCPUTemperature", cpuID).Return(uint64(0), mErr).Twice()

			p := &PowerStat{
				CPUMetrics:       cpuMetrics,
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,

				logOnce: map[string]struct{}{},
			}

			require.NoError(t, p.parseConfig())

			// First call adds the error to the accumulator and logOnce map.
			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			// Second call detects previous error in logOnce map and skips adding it to the accumulator.
			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q: %v", cpuTemperature, mErr))
			require.Empty(t, acc.GetTelegrafMetrics())

			require.Len(t, p.logOnce, 1)
			require.Contains(t, p.logOnce, "msr_cpu_temperature")

			mFetcher.AssertExpectations(t)
		})

		t.Run("WithoutErrors", func(t *testing.T) {
			cpuTemp := uint64(20)

			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock getting CPU temperature metric.
			mFetcher.On("GetCPUTemperature", cpuID).Return(cpuTemp, nil).Once()

			p := &PowerStat{
				CPUMetrics:       cpuMetrics,
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			require.Empty(t, acc.Errors)
			require.Len(t, acc.GetTelegrafMetrics(), 1)
			acc.AssertContainsTaggedFields(
				t,
				// measurement
				"powerstat_core",
				// fields
				map[string]interface{}{
					"cpu_temperature_celsius": cpuTemp,
				},
				// tags
				map[string]string{
					"cpu_id":     strconv.Itoa(cpuID),
					"core_id":    strconv.Itoa(coreID),
					"package_id": strconv.Itoa(packageID),
				},
			)
			mFetcher.AssertExpectations(t)
		})
	})

	t.Run("WithTimeRelatedMsrMetrics", func(t *testing.T) {
		cpuID := 0
		coreID := 1
		packageID := 0

		c1State := 5.15
		c6State := 8.10

		cpuMetrics := []cpuMetricType{
			// metrics that rely on a time-related msr.
			cpuC1StateResidency,
			cpuC6StateResidency,

			// metrics which do not rely on msr.
			cpuFrequency,
			cpuC0SubstateC01Percent,
		}

		t.Run("FailedToUpdate", func(t *testing.T) {
			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock updating msr time-related metrics.
			mFetcher.On("UpdatePerCPUMetrics", cpuID).Return(errors.New("mock error")).Once()

			p := &PowerStat{
				CPUMetrics:       cpuMetrics,
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to update MSR time-related metrics for CPU ID %v", cpuID))
			require.Empty(t, acc.GetTelegrafMetrics())
			mFetcher.AssertExpectations(t)
		})

		t.Run("FailedToUpdateModuleNotInitializedError", func(t *testing.T) {
			acc := &testutil.Accumulator{}

			mErr := &ptel.ModuleNotInitializedError{Name: "msr"}
			mFetcher := &fetcherMock{}

			// mock updating msr time-related metrics.
			mFetcher.On("UpdatePerCPUMetrics", cpuID).Return(mErr).Twice()

			p := &PowerStat{
				CPUMetrics:       cpuMetrics,
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,

				logOnce: map[string]struct{}{},
			}

			require.NoError(t, p.parseConfig())

			// First call adds the error to the accumulator and key to logOnce map.
			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			// Second call detects previous error in logOnce map and skips adding it to the accumulator.
			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to update MSR time-related metrics: %v", mErr))
			require.Empty(t, acc.GetTelegrafMetrics())

			require.Len(t, p.logOnce, 1)
			require.Contains(t, p.logOnce, "msr_time_related")

			mFetcher.AssertExpectations(t)
		})

		t.Run("WithoutErrors", func(t *testing.T) {
			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock updating msr time-related metrics.
			mFetcher.On("UpdatePerCPUMetrics", cpuID).Return(nil).Once()

			// mock getting C1 state residency.
			mFetcher.On("GetCPUC1StateResidency", cpuID).Return(c1State, nil).Once()

			// mock getting C6 state residency.
			mFetcher.On("GetCPUC6StateResidency", cpuID).Return(c6State, nil).Once()

			p := &PowerStat{
				CPUMetrics:       cpuMetrics,
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addPerCPUMsrMetrics(acc, cpuID, coreID, packageID)

			require.Empty(t, acc.Errors)
			require.Len(t, acc.GetTelegrafMetrics(), 2)
			acc.AssertContainsTaggedFields(
				t,
				// measurement
				"powerstat_core",
				// fields
				map[string]interface{}{
					"cpu_c1_state_residency_percent": c1State,
				},
				// flags
				map[string]string{
					"cpu_id":     strconv.Itoa(cpuID),
					"core_id":    strconv.Itoa(coreID),
					"package_id": strconv.Itoa(packageID),
				},
			)
			acc.AssertContainsTaggedFields(
				t,
				// measurement
				"powerstat_core",
				// fields
				map[string]interface{}{
					"cpu_c6_state_residency_percent": c6State,
				},
				// flags
				map[string]string{
					"cpu_id":     strconv.Itoa(cpuID),
					"core_id":    strconv.Itoa(coreID),
					"package_id": strconv.Itoa(packageID),
				},
			)
			mFetcher.AssertExpectations(t)
		})
	})
}

func TestAddCPUTimeRelatedMsrMetrics(t *testing.T) {
	cpuID := 0
	coreID := 1
	packageID := 0

	c0State := 3.0
	c1State := 2.0
	c3State := 1.5
	c6State := 1.0
	busyCycles := 0.5

	acc := &testutil.Accumulator{}

	mFetcher := &fetcherMock{}

	// mock getting CPU C0 state residency value.
	mFetcher.On("GetCPUC0StateResidency", cpuID).Return(c0State, nil).Once()

	// mock getting CPU C1 state residency value.
	mFetcher.On("GetCPUC1StateResidency", cpuID).Return(c1State, nil).Once()

	// mock getting CPU C3 state residency value.
	mFetcher.On("GetCPUC3StateResidency", cpuID).Return(c3State, nil).Once()

	// mock getting CPU C6 state residency value.
	mFetcher.On("GetCPUC6StateResidency", cpuID).Return(c6State, nil).Once()

	// mock getting CPU C0 state residency value, triggered when calling add CPU busy cycle metric.
	mFetcher.On("GetCPUC0StateResidency", cpuID).Return(busyCycles, nil).Once()

	p := &PowerStat{
		CPUMetrics: []cpuMetricType{
			// Metrics which are not time-related MSR.
			cpuFrequency,
			cpuTemperature,
			cpuC0SubstateC01Percent,

			// Time-related MSR metrics.
			cpuC0StateResidency,
			cpuC1StateResidency,
			cpuC3StateResidency,
			cpuC6StateResidency,
			cpuBusyCycles,
		},
		PackageMetrics:   make([]packageMetricType, 0),
		EventDefinitions: "./testdata/sapphirerapids_core.json",

		fetcher: mFetcher,
	}

	require.NoError(t, p.parseConfig())
	require.Empty(t, acc.GetTelegrafMetrics())

	p.addCPUTimeRelatedMsrMetrics(acc, cpuID, coreID, packageID)

	require.Empty(t, acc.Errors)
	require.Len(t, acc.GetTelegrafMetrics(), 5)
	acc.AssertContainsTaggedFields(
		t,
		// measurement
		"powerstat_core",
		// fields
		map[string]interface{}{
			"cpu_c0_state_residency_percent": c0State,
		},
		// tags
		map[string]string{
			"cpu_id":     strconv.Itoa(cpuID),
			"core_id":    strconv.Itoa(coreID),
			"package_id": strconv.Itoa(packageID),
		},
	)
	acc.AssertContainsTaggedFields(
		t,
		// measurement
		"powerstat_core",
		// fields
		map[string]interface{}{
			"cpu_c1_state_residency_percent": c1State,
		},
		// tags
		map[string]string{
			"cpu_id":     strconv.Itoa(cpuID),
			"core_id":    strconv.Itoa(coreID),
			"package_id": strconv.Itoa(packageID),
		},
	)
	acc.AssertContainsTaggedFields(
		t,
		// measurement
		"powerstat_core",
		// fields
		map[string]interface{}{
			"cpu_c6_state_residency_percent": c6State,
		},
		// tags
		map[string]string{
			"cpu_id":     strconv.Itoa(cpuID),
			"core_id":    strconv.Itoa(coreID),
			"package_id": strconv.Itoa(packageID),
		},
	)
	acc.AssertContainsTaggedFields(
		t,
		// measurement
		"powerstat_core",
		// fields
		map[string]interface{}{
			"cpu_c3_state_residency_percent": c3State,
		},
		// tags
		map[string]string{
			"cpu_id":     strconv.Itoa(cpuID),
			"core_id":    strconv.Itoa(coreID),
			"package_id": strconv.Itoa(packageID),
		},
	)
	acc.AssertContainsTaggedFields(
		t,
		// measurement
		"powerstat_core",
		// fields
		map[string]interface{}{
			"cpu_busy_cycles_percent": busyCycles,
		},
		// tags
		map[string]string{
			"cpu_id":     strconv.Itoa(cpuID),
			"core_id":    strconv.Itoa(coreID),
			"package_id": strconv.Itoa(packageID),
		},
	)
	mFetcher.AssertExpectations(t)
}

func TestAddCPUPerfMetrics(t *testing.T) {
	// Disable package metrics when parseConfig method is called.
	packageMetrics := make([]packageMetricType, 0)

	t.Run("FailedToReadPerfEvents", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock reading perf events.
		mFetcher.On("ReadPerfEvents").Return(errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		p.addCPUPerfMetrics(acc)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), "failed to read perf events")
		mFetcher.AssertExpectations(t)
	})

	t.Run("FailedToReadPerfEventsModuleNotInitializedError", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mErr := &ptel.ModuleNotInitializedError{Name: "perf"}
		mFetcher := &fetcherMock{}

		// mock reading perf events.
		mFetcher.On("ReadPerfEvents").Return(mErr).Twice()

		p := &PowerStat{
			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		// First call adds the error to the accumulator and key to logOnce map.
		p.addCPUPerfMetrics(acc)

		// Second call detects previous error in logOnce map and skips adding it to the accumulator.
		p.addCPUPerfMetrics(acc)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to read perf events: %v", mErr))
		require.Empty(t, acc.GetTelegrafMetrics())

		require.Len(t, p.logOnce, 1)
		require.Contains(t, p.logOnce, "perf_read")

		mFetcher.AssertExpectations(t)
	})

	t.Run("NoAvailableCPUs", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock reading perf events.
		mFetcher.On("ReadPerfEvents").Return(nil).Once()

		// mock getting available CPU IDs for perf events.
		mFetcher.On("GetPerfCPUIDs").Return(nil).Once()

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// Metrics which do not rely on perf.
				cpuFrequency,
				cpuTemperature,
				cpuBusyCycles,

				// Metrics which rely on perf.
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
				cpuC0SubstateC0WaitPercent,
			},
			PackageMetrics:   make([]packageMetricType, 0),
			EventDefinitions: "./testdata/sapphirerapids_core.json",

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addCPUPerfMetrics(acc)

		require.Empty(t, acc.Errors)
		require.Empty(t, acc.GetTelegrafMetrics())
		mFetcher.AssertExpectations(t)
	})

	t.Run("FailedToGetDataCPUID", func(t *testing.T) {
		t.Run("SingleCPUID", func(t *testing.T) {
			cpuID := 0

			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock reading perf events.
			mFetcher.On("ReadPerfEvents").Return(nil).Once()

			// mock getting available CPU IDs for perf events.
			mFetcher.On("GetPerfCPUIDs").Return([]int{cpuID}).Once()

			// mock getting corresponding core ID to CPU ID 0.
			mFetcher.On("GetCPUCoreID", cpuID).Return(1, nil).Once()

			// mock getting corresponding package ID to CPU ID 0.
			mFetcher.On("GetCPUPackageID", cpuID).Return(0, errors.New("mock error")).Once()

			p := &PowerStat{
				CPUMetrics: []cpuMetricType{
					// Metrics which do not rely on perf.
					cpuFrequency,
					cpuTemperature,
					cpuBusyCycles,

					// Metrics which rely on perf.
					cpuC0SubstateC01Percent,
					cpuC0SubstateC02Percent,
					cpuC0SubstateC0WaitPercent,
				},
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addCPUPerfMetrics(acc)

			require.Empty(t, acc.GetTelegrafMetrics())
			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get perf metrics for CPU ID %v", cpuID))
			mFetcher.AssertExpectations(t)
		})

		t.Run("MultipleCPUIDs", func(t *testing.T) {
			cpuID := 0
			coreID := 2
			packageID := 3

			c01Percent := 0.2

			acc := &testutil.Accumulator{}

			mFetcher := &fetcherMock{}

			// mock reading perf events.
			mFetcher.On("ReadPerfEvents").Return(nil).Once()

			// mock getting available CPU IDs for perf events.
			mFetcher.On("GetPerfCPUIDs").Return([]int{cpuID, 1}).Once()

			// mock getting corresponding core ID to CPU ID 0.
			mFetcher.On("GetCPUCoreID", cpuID).Return(coreID, nil).Once()

			// mock getting corresponding package ID to CPU ID 0.
			mFetcher.On("GetCPUPackageID", cpuID).Return(packageID, nil).Once()

			// mock getting CPU C01 metric.
			mFetcher.On("GetCPUC0SubstateC01Percent", cpuID).Return(c01Percent, nil).Once()

			// mock getting corresponding core ID to CPU ID 1.
			mFetcher.On("GetCPUCoreID", 1).Return(5, nil).Once()

			// mock getting corresponding package ID to CPU ID 1.
			mFetcher.On("GetCPUPackageID", 1).Return(0, errors.New("mock error")).Once()

			p := &PowerStat{
				CPUMetrics: []cpuMetricType{
					// Metrics which do not rely on perf.
					cpuFrequency,
					cpuTemperature,
					cpuC6StateResidency,

					// Metrics which rely on perf.
					cpuC0SubstateC01Percent,
				},
				PackageMetrics:   packageMetrics,
				EventDefinitions: "./testdata/sapphirerapids_core.json",

				fetcher: mFetcher,
			}

			require.NoError(t, p.parseConfig())

			p.addCPUPerfMetrics(acc)

			require.Len(t, acc.Errors, 1)
			require.ErrorContains(t, acc.FirstError(), "failed to get perf metrics for CPU ID 1")
			require.Len(t, acc.GetTelegrafMetrics(), 1)
			acc.AssertContainsTaggedFields(
				t,
				// measurement
				"powerstat_core",
				// fields
				map[string]interface{}{
					"cpu_c0_substate_c01_percent": c01Percent,
				},
				// tags
				map[string]string{
					"cpu_id":     strconv.Itoa(cpuID),
					"core_id":    strconv.Itoa(coreID),
					"package_id": strconv.Itoa(packageID),
				},
			)
			mFetcher.AssertExpectations(t)
		})
	})

	t.Run("WithError", func(t *testing.T) {
		cpuID := 0
		coreID := 1
		packageID := 0

		c01Percent := 0.5
		c0Wait := 2.5

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock reading perf events.
		mFetcher.On("ReadPerfEvents").Return(nil).Once()

		// mock getting available CPU IDs for perf events.
		mFetcher.On("GetPerfCPUIDs").Return([]int{cpuID}).Once()

		// mock getting corresponding core ID to CPU ID 0.
		mFetcher.On("GetCPUCoreID", cpuID).Return(coreID, nil).Once()

		// mock getting corresponding package ID to CPU ID 0.
		mFetcher.On("GetCPUPackageID", cpuID).Return(packageID, nil).Once()

		// mock getting CPU C01 metric.
		mFetcher.On("GetCPUC0SubstateC01Percent", cpuID).Return(c01Percent, nil).Once()

		// mock getting CPU C02 metric.
		mFetcher.On("GetCPUC0SubstateC02Percent", cpuID).Return(0.0, errors.New("mock error")).Once()

		// mock getting CPU C0Wait metric.
		mFetcher.On("GetCPUC0SubstateC0WaitPercent", cpuID).Return(c0Wait, nil).Once()

		p := &PowerStat{
			CPUMetrics: []cpuMetricType{
				// Metrics which do not rely on perf.
				cpuFrequency,
				cpuTemperature,
				cpuC6StateResidency,

				// Metrics which rely on perf.
				cpuC0SubstateC01Percent,
				cpuC0SubstateC02Percent,
				cpuC0SubstateC0WaitPercent,
			},
			PackageMetrics:   packageMetrics,
			EventDefinitions: "./testdata/sapphirerapids_core.json",

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addCPUPerfMetrics(acc)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC0SubstateC02Percent, cpuID))
		require.Len(t, acc.GetTelegrafMetrics(), 2)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c0_substate_c01_percent": c01Percent,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c0_substate_c0_wait_percent": c0Wait,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
	})
}

func TestAddPerCPUPerfMetrics(t *testing.T) {
	cpuID := 0
	coreID := 1
	packageID := 0

	c01Percent := 1.09
	c02Percent := 2.12

	acc := &testutil.Accumulator{}

	mFetcher := &fetcherMock{}

	// mock getting CPU C01 metric.
	mFetcher.On("GetCPUC0SubstateC01Percent", cpuID).Return(c01Percent, nil).Once()

	// mock getting CPU C02 metric.
	mFetcher.On("GetCPUC0SubstateC02Percent", cpuID).Return(c02Percent, nil).Once()

	p := &PowerStat{
		CPUMetrics: []cpuMetricType{
			// Metrics which do not rely on perf.
			cpuFrequency,
			cpuTemperature,
			cpuC6StateResidency,

			// Metrics which rely on perf.
			cpuC0SubstateC01Percent,
			cpuC0SubstateC02Percent,
		},
		PackageMetrics:   make([]packageMetricType, 0),
		EventDefinitions: "./testdata/sapphirerapids_core.json",

		fetcher: mFetcher,
	}

	require.NoError(t, p.parseConfig())
	require.Empty(t, acc.GetTelegrafMetrics())

	p.addPerCPUPerfMetrics(acc, cpuID, coreID, packageID)

	require.Len(t, acc.GetTelegrafMetrics(), 2)
	acc.AssertContainsTaggedFields(
		t,
		// measurement
		"powerstat_core",
		// fields
		map[string]interface{}{
			"cpu_c0_substate_c01_percent": c01Percent,
		},
		// tags
		map[string]string{
			"cpu_id":     strconv.Itoa(cpuID),
			"core_id":    strconv.Itoa(coreID),
			"package_id": strconv.Itoa(packageID),
		},
	)
	acc.AssertContainsTaggedFields(
		t,
		// measurement
		"powerstat_core",
		// fields
		map[string]interface{}{
			"cpu_c0_substate_c02_percent": c02Percent,
		},
		// tags
		map[string]string{
			"cpu_id":     strconv.Itoa(cpuID),
			"core_id":    strconv.Itoa(coreID),
			"package_id": strconv.Itoa(packageID),
		},
	)
	mFetcher.AssertExpectations(t)
}

func TestGetDataCPUID(t *testing.T) {
	t.Run("FailedToGetCoreID", func(t *testing.T) {
		cpuID := 1

		mFetcher := &fetcherMock{}

		// mock getting core ID corresponding to the CPU ID.
		mFetcher.On("GetCPUCoreID", cpuID).Return(0, errors.New("mock error")).Once()

		coreID, packageID, err := getDataCPUID(mFetcher, cpuID)

		require.Equal(t, 0, coreID)
		require.Equal(t, 0, packageID)
		require.ErrorContains(t, err, fmt.Sprintf("failed to get core ID from CPU ID %v", cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("FailedToGetPackageID", func(t *testing.T) {
		cpuID := 1

		mFetcher := &fetcherMock{}

		// mock getting core ID corresponding to the CPU ID.
		mFetcher.On("GetCPUCoreID", cpuID).Return(1, nil).Once()

		// mock getting package ID corresponding to the CPU ID.
		mFetcher.On("GetCPUPackageID", cpuID).Return(0, errors.New("mock error")).Once()

		coreID, packageID, err := getDataCPUID(mFetcher, cpuID)

		require.Equal(t, 0, coreID)
		require.Equal(t, 0, packageID)
		require.ErrorContains(t, err, fmt.Sprintf("failed to get package ID from CPU ID %v", cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		cpuID := 1

		mFetcher := &fetcherMock{}

		// mock getting core ID corresponding to the CPU ID.
		mFetcher.On("GetCPUCoreID", cpuID).Return(1, nil).Once()

		// mock getting package ID corresponding to the CPU ID.
		mFetcher.On("GetCPUPackageID", cpuID).Return(2, nil).Once()

		coreID, packageID, err := getDataCPUID(mFetcher, cpuID)

		require.Equal(t, 1, coreID)
		require.Equal(t, 2, packageID)
		require.NoError(t, err)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddPackageMetrics(t *testing.T) {
	t.Run("NoPackageIDs", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available package IDs.
		mFetcher.On("GetPackageIDs").Return(nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		p.addPackageMetrics(acc)

		require.Empty(t, acc.Errors)
		require.Empty(t, acc.GetTelegrafMetrics())
	})

	t.Run("WithRaplMetrics", func(t *testing.T) {
		tdp := 80.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available package IDs.
		mFetcher.On("GetPackageIDs").Return([]int{0, 1}).Once()

		// mock getting package thermal design power metric for CPU ID 0.
		mFetcher.On("GetPackageThermalDesignPowerWatts", 0).Return(tdp, nil).Once()

		// mock getting package thermal design power metric for CPU ID 1.
		mFetcher.On("GetPackageThermalDesignPowerWatts", 1).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on rapl
				packageThermalDesignPower,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addPackageMetrics(acc)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID 1", packageThermalDesignPower))
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"thermal_design_power_watts": tdp,
			},
			// tags
			map[string]string{
				"package_id": "0",
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithMsrMetrics", func(t *testing.T) {
		baseFreq := uint64(400)

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available package IDs.
		mFetcher.On("GetPackageIDs").Return([]int{0, 1}).Once()

		// mock getting CPU base frequency metric, for package ID 0.
		mFetcher.On("GetCPUBaseFrequency", 0).Return(uint64(0), errors.New("mock error")).Once()

		// mock getting CPU base frequency metric, for package ID 1.
		mFetcher.On("GetCPUBaseFrequency", 1).Return(baseFreq, nil).Once()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on msr
				packageCPUBaseFrequency,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addPackageMetrics(acc)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID 0", packageCPUBaseFrequency))
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"cpu_base_frequency_mhz": baseFreq,
			},
			// tags
			map[string]string{
				"package_id": "1",
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithUncoreFreqMetric", func(t *testing.T) {
		dieID := 0

		initMin := 500.0
		initMax := 2500.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting available package IDs.
		mFetcher.On("GetPackageIDs").Return([]int{0, 1}).Once()

		// mock getting die IDs for package ID.
		mFetcher.On("GetPackageDieIDs", mock.AnythingOfType("int")).Return([]int{dieID}, nil).Twice()

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", mock.AnythingOfType("int"), dieID).Return(initMin, nil).Twice()

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", mock.AnythingOfType("int"), dieID).Return(initMax, nil).Twice()

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", mock.AnythingOfType("int"), dieID).Return(600.0, nil).Twice()

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", mock.AnythingOfType("int"), dieID).Return(0.0, errors.New("mock error")).Twice()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				packageUncoreFrequency,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addPackageMetrics(acc)

		require.Len(t, acc.Errors, 2)
		require.ErrorContains(t, acc.Errors[0], fmt.Sprintf("failed to get current uncore frequency values for package ID 0 and die ID %v", dieID))
		require.ErrorContains(t, acc.Errors[1], fmt.Sprintf("failed to get current uncore frequency values for package ID 1 and die ID %v", dieID))
		require.Len(t, acc.GetTelegrafMetrics(), 2)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": initMin,
				"uncore_frequency_limit_mhz_max": initMax,
			},
			// tags
			map[string]string{
				"package_id": "0",
				"type":       "initial",
				"die":        strconv.Itoa(dieID),
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": initMin,
				"uncore_frequency_limit_mhz_max": initMax,
			},
			// tags
			map[string]string{
				"package_id": "1",
				"type":       "initial",
				"die":        strconv.Itoa(dieID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddPerPackageRaplMetrics(t *testing.T) {
	t.Run("WithoutRaplMetrics", func(t *testing.T) {
		packageID := 0

		acc := &testutil.Accumulator{}

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which do not rely on rapl
				packageCPUBaseFrequency,
				packageUncoreFrequency,
				packageTurboLimit,
			},
		}

		p.addPerPackageRaplMetrics(acc, packageID)

		require.Empty(t, acc.Errors)
		require.Empty(t, acc.GetTelegrafMetrics())
	})

	t.Run("WithModuleNotInitializedError", func(t *testing.T) {
		packageID := 0

		acc := &testutil.Accumulator{}

		raplNotInitErr := &ptel.ModuleNotInitializedError{Name: "rapl"}
		mError := fmt.Errorf("mock error: %w", raplNotInitErr)
		mFetcher := &fetcherMock{}

		// mock getting current dram power consumption metric.
		mFetcher.On("GetCurrentDramPowerConsumptionWatts", packageID).Return(0.0, mError).Twice()

		// mock getting package thermal design power metric.
		mFetcher.On("GetPackageThermalDesignPowerWatts", packageID).Return(0.0, mError).Twice()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on rapl
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,

				// metrics which do not rely on rapl
				packageCPUBaseFrequency,
				packageUncoreFrequency,
				packageTurboLimit,
			},

			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		require.NoError(t, p.parseConfig())

		// First call adds the error to the accumulator and logOnce map.
		p.addPerPackageRaplMetrics(acc, packageID)

		// Second call detects previous error in logOnce map and skips adding it to the accumulator.
		p.addPerPackageRaplMetrics(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 2)
		require.ErrorContains(t, acc.Errors[0], fmt.Sprintf("failed to get %q: %v", packageCurrentDramPowerConsumption, raplNotInitErr))
		require.ErrorContains(t, acc.Errors[1], fmt.Sprintf("failed to get %q: %v", packageThermalDesignPower, raplNotInitErr))

		require.Len(t, p.logOnce, 2)
		require.Contains(t, p.logOnce, "rapl_current_dram_power_consumption")
		require.Contains(t, p.logOnce, "rapl_thermal_design_power")
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithErrors", func(t *testing.T) {
		packageID := 0
		currPower := 30.0
		tdp := 80.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting current package power consumption metric.
		mFetcher.On("GetCurrentPackagePowerConsumptionWatts", packageID).Return(currPower, nil).Once()

		// mock getting current dram power consumption metric.
		mFetcher.On("GetCurrentDramPowerConsumptionWatts", packageID).Return(0.0, errors.New("mock error")).Once()

		// mock getting package thermal design power metric.
		mFetcher.On("GetPackageThermalDesignPowerWatts", packageID).Return(tdp, nil).Once()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on rapl
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,

				// metrics which do not rely on rapl
				packageCPUBaseFrequency,
				packageUncoreFrequency,
				packageTurboLimit,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addPerPackageRaplMetrics(acc, packageID)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID %v", packageCurrentDramPowerConsumption, packageID))
		require.Len(t, acc.GetTelegrafMetrics(), 2)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"current_power_consumption_watts": currPower,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"thermal_design_power_watts": tdp,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithoutErrors", func(t *testing.T) {
		packageID := 0
		currPower := 10.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting current dram power consumption metric.
		mFetcher.On("GetCurrentDramPowerConsumptionWatts", packageID).Return(currPower, nil).Once()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on rapl
				packageCurrentDramPowerConsumption,

				// metrics which do not rely on rapl
				packageCPUBaseFrequency,
				packageUncoreFrequency,
				packageTurboLimit,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addPerPackageRaplMetrics(acc, packageID)

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"current_dram_power_consumption_watts": currPower,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddPerPackageMsrMetrics(t *testing.T) {
	t.Run("WithoutMsrMetrics", func(t *testing.T) {
		packageID := 0

		acc := &testutil.Accumulator{}

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which do not rely on msr
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},
		}

		p.addPerPackageMsrMetrics(acc, packageID)

		require.Empty(t, acc.Errors)
		require.Empty(t, acc.GetTelegrafMetrics())
	})

	t.Run("WithModuleNotInitializedError", func(t *testing.T) {
		packageID := 0

		acc := &testutil.Accumulator{}

		msrNotInitErr := &ptel.ModuleNotInitializedError{Name: "msr"}
		mError := fmt.Errorf("mock error: %w", msrNotInitErr)
		mFetcher := &fetcherMock{}

		// mock getting CPU base frequency metric.
		mFetcher.On("GetCPUBaseFrequency", packageID).Return(uint64(400), mError).Twice()

		// mock getting max turbo frequency list.
		mFetcher.On("GetMaxTurboFreqList", packageID).Return(nil, mError).Twice()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on msr
				packageCPUBaseFrequency,
				packageTurboLimit,

				// metrics which do not rely on msr
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},

			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		require.NoError(t, p.parseConfig())

		// First call adds the error to the accumulator and logOnce map.
		p.addPerPackageMsrMetrics(acc, packageID)

		// Second call detects previous error in logOnce map and skips adding it to the accumulator.
		p.addPerPackageMsrMetrics(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 2)
		require.ErrorContains(t, acc.Errors[0], fmt.Sprintf("failed to get %q: %v", packageCPUBaseFrequency, msrNotInitErr))
		require.ErrorContains(t, acc.Errors[1], fmt.Sprintf("failed to get %q: %v", packageTurboLimit, msrNotInitErr))

		require.Len(t, p.logOnce, 2)
		require.Contains(t, p.logOnce, "msr_cpu_base_frequency")
		require.Contains(t, p.logOnce, "msr_max_turbo_frequency")
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithErrors", func(t *testing.T) {
		packageID := 0
		baseFreq := uint64(400)

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting CPU base frequency metric.
		mFetcher.On("GetCPUBaseFrequency", packageID).Return(baseFreq, nil).Once()

		// mock getting max turbo frequency list.
		mFetcher.On("GetMaxTurboFreqList", packageID).Return(nil, errors.New("mock error")).Once()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on msr
				packageCPUBaseFrequency,
				packageTurboLimit,

				// metrics which do not rely on msr
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addPerPackageMsrMetrics(acc, packageID)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID %v", packageTurboLimit, packageID))
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"cpu_base_frequency_mhz": baseFreq,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithoutErrors", func(t *testing.T) {
		packageID := 0
		baseFreq := uint64(400)
		maxTurboFreqList := []ptel.MaxTurboFreq{
			{
				Value:       1000,
				ActiveCores: 10,
			},
		}

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting CPU base frequency metric.
		mFetcher.On("GetCPUBaseFrequency", packageID).Return(baseFreq, nil).Once()

		// mock getting max turbo frequency list.
		mFetcher.On("GetMaxTurboFreqList", packageID).Return(maxTurboFreqList, nil).Once()

		p := &PowerStat{
			PackageMetrics: []packageMetricType{
				// metrics which rely on msr
				packageCPUBaseFrequency,
				packageTurboLimit,

				// metrics which do not rely on msr
				packageCurrentPowerConsumption,
				packageCurrentDramPowerConsumption,
				packageThermalDesignPower,
			},

			fetcher: mFetcher,
		}

		require.NoError(t, p.parseConfig())

		p.addPerPackageMsrMetrics(acc, packageID)

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 2)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"cpu_base_frequency_mhz": baseFreq,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"cpu_base_frequency_mhz": baseFreq,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"max_turbo_frequency_mhz": maxTurboFreqList[0].Value,
			},
			// tags
			map[string]string{
				"package_id":   strconv.Itoa(packageID),
				"active_cores": strconv.Itoa(int(maxTurboFreqList[0].ActiveCores)),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUFrequency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU frequency metric.
		mFetcher.On("GetCPUFrequency", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUFrequency(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuFrequency, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		cpuID := 0
		coreID := 1
		packageID := 0
		cpuFreq := 800.001
		cpuFreqExp := 800.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting CPU frequency metric.
		mFetcher.On("GetCPUFrequency", cpuID).Return(cpuFreq, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUFrequency(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_frequency_mhz"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_frequency_mhz": cpuFreqExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUTemperature(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU temperature metric.
		mFetcher.On("GetCPUTemperature", cpuID).Return(uint64(0), errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUTemperature(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuTemperature, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		cpuTemp := uint64(25)

		mFetcher := &fetcherMock{}

		// mock getting cpu temperature metric.
		mFetcher.On("GetCPUTemperature", cpuID).Return(cpuTemp, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUTemperature(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasUIntField("powerstat_core", "cpu_temperature_celsius"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_temperature_celsius": cpuTemp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC0StateResidency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C0 state residency metric.
		mFetcher.On("GetCPUC0StateResidency", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0StateResidency(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC0StateResidency, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		cpuID := 0
		coreID := 1
		packageID := 0
		c0State := 10.1199
		c0StateExp := 10.12

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting CPU C0 state residency metric.
		mFetcher.On("GetCPUC0StateResidency", cpuID).Return(c0State, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0StateResidency(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c0_state_residency_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c0_state_residency_percent": c0StateExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC1StateResidency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C1 state residency metric.
		mFetcher.On("GetCPUC1StateResidency", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC1StateResidency(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC1StateResidency, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		c1State := 10.1144
		c1StateExp := 10.11

		mFetcher := &fetcherMock{}

		// mock getting CPU C1 state residency metric.
		mFetcher.On("GetCPUC1StateResidency", cpuID).Return(c1State, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC1StateResidency(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c1_state_residency_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c1_state_residency_percent": c1StateExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC3StateResidency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C3 state residency metric.
		mFetcher.On("GetCPUC3StateResidency", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC3StateResidency(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC3StateResidency, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		c3State := 20.1178
		c3StateExp := 20.12

		mFetcher := &fetcherMock{}

		// mock getting CPU C3 state residency metric.
		mFetcher.On("GetCPUC3StateResidency", cpuID).Return(c3State, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC3StateResidency(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c3_state_residency_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c3_state_residency_percent": c3StateExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC6StateResidency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C6 state residency metric.
		mFetcher.On("GetCPUC6StateResidency", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC6StateResidency(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC6StateResidency, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		c6State := 9.115
		c6StateExp := 9.12

		mFetcher := &fetcherMock{}

		// mock getting CPU C6 state residency metric.
		mFetcher.On("GetCPUC6StateResidency", cpuID).Return(c6State, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC6StateResidency(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c6_state_residency_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c6_state_residency_percent": c6StateExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC7StateResidency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C7 state residency metric.
		mFetcher.On("GetCPUC7StateResidency", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC7StateResidency(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC7StateResidency, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		c7State := 9.1149
		c7StateExp := 9.11

		mFetcher := &fetcherMock{}

		// mock getting CPU C7 state residency metric.
		mFetcher.On("GetCPUC7StateResidency", cpuID).Return(c7State, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC7StateResidency(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c7_state_residency_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c7_state_residency_percent": c7StateExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUBusyFrequency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU busy frequency metric.
		mFetcher.On("GetCPUBusyFrequencyMhz", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUBusyFrequency(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuBusyFrequency, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		cpuBusyFreq := 800.119
		cpuBusyFreqExp := 800.12

		mFetcher := &fetcherMock{}

		// mock getting CPU busy frequency metric.
		mFetcher.On("GetCPUBusyFrequencyMhz", cpuID).Return(cpuBusyFreq, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUBusyFrequency(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_busy_frequency_mhz"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_busy_frequency_mhz": cpuBusyFreqExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUBusyCycles(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU busy cycles metric.
		mFetcher.On("GetCPUC0StateResidency", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUBusyCycles(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuBusyCycles, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		cpuBusyCycles := 10.1149
		cpuBusyCyclesExp := 10.11

		mFetcher := &fetcherMock{}

		// mock getting CPU C0 state residency metric.
		mFetcher.On("GetCPUC0StateResidency", cpuID).Return(cpuBusyCycles, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUBusyCycles(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_busy_cycles_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_busy_cycles_percent": cpuBusyCyclesExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC0SubstateC01Percent(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C01 metric.
		mFetcher.On("GetCPUC0SubstateC01Percent", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0SubstateC01Percent(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC0SubstateC01Percent, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		c01Percent := 5.9229
		c01PercentExp := 5.92

		mFetcher := &fetcherMock{}

		// mock getting CPU C01 metric.
		mFetcher.On("GetCPUC0SubstateC01Percent", cpuID).Return(c01Percent, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0SubstateC01Percent(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c0_substate_c01_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c0_substate_c01_percent": c01PercentExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC0SubstateC02Percent(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C02 metric.
		mFetcher.On("GetCPUC0SubstateC02Percent", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0SubstateC02Percent(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC0SubstateC02Percent, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		c02Percent := 0.001
		c02PercentExp := 0.0

		mFetcher := &fetcherMock{}

		// mock getting CPU C02 metric.
		mFetcher.On("GetCPUC0SubstateC02Percent", cpuID).Return(c02Percent, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0SubstateC02Percent(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c0_substate_c02_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c0_substate_c02_percent": c02PercentExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUC0SubstateC0WaitPercent(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU C0Wait metric.
		mFetcher.On("GetCPUC0SubstateC0WaitPercent", cpuID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0SubstateC0WaitPercent(acc, cpuID, coreID, packageID)

		require.Empty(t, acc.GetTelegrafMetrics(), 0)
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for CPU ID %v", cpuC0SubstateC0WaitPercent, cpuID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		cpuID := 0
		coreID := 1
		packageID := 0
		c0WaitPercent := 0.995
		c0WaitPercentExp := 1.0

		mFetcher := &fetcherMock{}

		// mock getting CPU C0Wait metric.
		mFetcher.On("GetCPUC0SubstateC0WaitPercent", cpuID).Return(c0WaitPercent, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUC0SubstateC0WaitPercent(acc, cpuID, coreID, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_core", "cpu_c0_substate_c0_wait_percent"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_core",
			// fields
			map[string]interface{}{
				"cpu_c0_substate_c0_wait_percent": c0WaitPercentExp,
			},
			// tags
			map[string]string{
				"cpu_id":     strconv.Itoa(cpuID),
				"core_id":    strconv.Itoa(coreID),
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCurrentPackagePowerConsumption(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting current package power consumption metric.
		mFetcher.On("GetCurrentPackagePowerConsumptionWatts", packageID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCurrentPackagePower(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID %v", packageCurrentPowerConsumption, packageID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0
		currPower := float64(30.1999)
		currPowerRounded := float64(30.2)

		mFetcher := &fetcherMock{}

		// mock getting current package power consumption metric.
		mFetcher.On("GetCurrentPackagePowerConsumptionWatts", packageID).Return(currPower, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCurrentPackagePower(acc, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_package", "current_power_consumption_watts"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"current_power_consumption_watts": currPowerRounded,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCurrentDramPowerConsumption(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting current dram power consumption metric.
		mFetcher.On("GetCurrentDramPowerConsumptionWatts", packageID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCurrentDramPower(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID %v", packageCurrentDramPowerConsumption, packageID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0
		currPower := float64(30.8235)
		currPowerRounded := float64(30.82)

		mFetcher := &fetcherMock{}

		// mock getting current dram power consumption metric.
		mFetcher.On("GetCurrentDramPowerConsumptionWatts", packageID).Return(currPower, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCurrentDramPower(acc, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_package", "current_dram_power_consumption_watts"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"current_dram_power_consumption_watts": currPowerRounded,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddThermalDesignPower(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting package thermal design power metric.
		mFetcher.On("GetPackageThermalDesignPowerWatts", packageID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addThermalDesignPower(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID %v", packageThermalDesignPower, packageID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Rounded", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0
		tdp := float64(80.1999)
		tdpRounded := float64(80.2)

		mFetcher := &fetcherMock{}

		// mock getting package thermal design power metric.
		mFetcher.On("GetPackageThermalDesignPowerWatts", packageID).Return(tdp, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addThermalDesignPower(acc, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasFloatField("powerstat_package", "thermal_design_power_watts"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"thermal_design_power_watts": tdpRounded,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddCPUBaseFrequency(t *testing.T) {
	t.Run("FailedToGetMetric", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0

		mFetcher := &fetcherMock{}

		// mock getting CPU base frequency metric.
		mFetcher.On("GetCPUBaseFrequency", packageID).Return(uint64(0), errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUBaseFrequency(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID %v", packageCPUBaseFrequency, packageID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		packageID := 0
		baseFreq := uint64(700)

		mFetcher := &fetcherMock{}

		// mock getting CPU base frequency metric.
		mFetcher.On("GetCPUBaseFrequency", packageID).Return(baseFreq, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addCPUBaseFrequency(acc, packageID)

		require.Len(t, acc.GetTelegrafMetrics(), 1)
		require.True(t, acc.HasUIntField("powerstat_package", "cpu_base_frequency_mhz"))
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"cpu_base_frequency_mhz": baseFreq,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddUncoreFrequency(t *testing.T) {
	packageID, dieID := 1, 0

	t.Run("FailedToGetDieIDs", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting die IDs for package ID.
		mFetcher.On("GetPackageDieIDs", packageID).Return(nil, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequency(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(
			t,
			acc.FirstError(),
			fmt.Sprintf("failed to get die IDs for package ID %v", packageID),
		)
	})

	t.Run("FailedToGetInitialLimits", func(t *testing.T) {
		currMin := 500.0
		currMax := 2500.0
		curr := 1000.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting die IDs for package ID.
		mFetcher.On("GetPackageDieIDs", packageID).Return([]int{dieID}, nil).Once()

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(800.0, nil).Once()

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(currMin, nil).Once()

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", packageID, dieID).Return(currMax, nil).Once()

		// mock getting current uncore frequency value.
		mFetcher.On("GetCurrentUncoreFrequency", packageID, dieID).Return(curr, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequency(acc, packageID)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(
			t,
			acc.FirstError(),
			fmt.Sprintf("failed to get initial uncore frequency limits for package ID %v and die ID %v", packageID, dieID),
		)
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": currMin,
				"uncore_frequency_limit_mhz_max": currMax,
				"uncore_frequency_mhz_cur":       uint64(curr),
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
				"type":       "current",
				"die":        strconv.Itoa(dieID),
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("FailedToGetCurrentValues", func(t *testing.T) {
		initMin := 300.0
		initMax := 1200.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting die IDs for package ID.
		mFetcher.On("GetPackageDieIDs", packageID).Return([]int{dieID}, nil).Once()

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(initMin, nil).Once()

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", packageID, dieID).Return(initMax, nil).Once()

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(500.0, nil).Once()

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", packageID, dieID).Return(1300.0, nil).Once()

		// mock getting current uncore frequency value.
		mFetcher.On("GetCurrentUncoreFrequency", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequency(acc, packageID)

		require.Len(t, acc.Errors, 1)
		require.ErrorContains(
			t,
			acc.FirstError(),
			fmt.Sprintf("failed to get current uncore frequency values for package ID %v and die ID %v", packageID, dieID),
		)
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": initMin,
				"uncore_frequency_limit_mhz_max": initMax,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
				"type":       "initial",
				"die":        strconv.Itoa(dieID),
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		initMin := 300.0
		initMax := 1200.0
		currMin := 500.0
		currMax := 2500.0
		curr := 1000.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting die IDs for package ID.
		mFetcher.On("GetPackageDieIDs", packageID).Return([]int{dieID}, nil).Once()

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(initMin, nil).Once()

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", packageID, dieID).Return(initMax, nil).Once()

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(currMin, nil).Once()

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", packageID, dieID).Return(currMax, nil).Once()

		// mock getting current uncore frequency value.
		mFetcher.On("GetCurrentUncoreFrequency", packageID, dieID).Return(curr, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequency(acc, packageID)

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 2)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": initMin,
				"uncore_frequency_limit_mhz_max": initMax,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
				"type":       "initial",
				"die":        strconv.Itoa(dieID),
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": currMin,
				"uncore_frequency_limit_mhz_max": currMax,
				"uncore_frequency_mhz_cur":       uint64(curr),
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
				"type":       "current",
				"die":        strconv.Itoa(dieID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddUncoreFrequencyInitialLimits(t *testing.T) {
	packageID, dieID := 0, 0

	t.Run("WithModuleNotInitializedError", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		uncoreFreqErr := &ptel.ModuleNotInitializedError{Name: "uncore_frequency"}
		mError := fmt.Errorf("mock error: %w", uncoreFreqErr)
		mFetcher := &fetcherMock{}

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(0.0, mError).Twice()

		p := &PowerStat{
			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		// First call adds the error to the accumulator and logOnce map.
		p.addUncoreFrequencyInitialLimits(acc, packageID, dieID)

		// Second call detects previous error in logOnce map and skips adding it to the accumulator.
		p.addUncoreFrequencyInitialLimits(acc, packageID, dieID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q initial limits", packageUncoreFrequency))
		require.ErrorContains(t, acc.FirstError(), uncoreFreqErr.Error())

		require.Len(t, p.logOnce, 1)
		require.Contains(t, p.logOnce, fmt.Sprintf("%s_%s_initial", "uncore_frequency", packageUncoreFrequency))
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithError", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequencyInitialLimits(acc, packageID, dieID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(
			t,
			acc.FirstError(),
			fmt.Sprintf("failed to get initial uncore frequency limits for package ID %v and die ID %v", packageID, dieID),
		)
		require.Empty(t, p.logOnce)
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		initMin := 300.0
		initMax := 1200.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(initMin, nil).Once()

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", packageID, dieID).Return(initMax, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequencyInitialLimits(acc, packageID, dieID)

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": initMin,
				"uncore_frequency_limit_mhz_max": initMax,
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
				"type":       "initial",
				"die":        strconv.Itoa(dieID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddUncoreFrequencyCurrentValues(t *testing.T) {
	packageID, dieID := 0, 0

	t.Run("WithModuleNotInitializedError", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		uncoreFreqErr := &ptel.ModuleNotInitializedError{Name: "uncore_frequency"}
		mError := fmt.Errorf("mock error: %w", uncoreFreqErr)
		mFetcher := &fetcherMock{}

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(0.0, mError).Twice()

		p := &PowerStat{
			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		// First call adds the error to the accumulator and logOnce map.
		p.addUncoreFrequencyCurrentValues(acc, packageID, dieID)

		// Second call detects previous error in logOnce map and skips adding it to the accumulator.
		p.addUncoreFrequencyCurrentValues(acc, packageID, dieID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q current value and limits", packageUncoreFrequency))
		require.ErrorContains(t, acc.FirstError(), uncoreFreqErr.Error())

		require.Len(t, p.logOnce, 1)
		require.Contains(t, p.logOnce, fmt.Sprintf("%s_%s_current", "uncore_frequency", packageUncoreFrequency))
		mFetcher.AssertExpectations(t)
	})

	t.Run("WithError", func(t *testing.T) {
		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequencyCurrentValues(acc, packageID, dieID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(
			t,
			acc.FirstError(),
			fmt.Sprintf("failed to get current uncore frequency values for package ID %v and die ID %v", packageID, dieID),
		)
		require.Empty(t, p.logOnce)
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		currMin := 500.0
		currMax := 2500.0
		curr := 1000.0

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(currMin, nil).Once()

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", packageID, dieID).Return(currMax, nil).Once()

		// mock getting current uncore frequency value.
		mFetcher.On("GetCurrentUncoreFrequency", packageID, dieID).Return(curr, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addUncoreFrequencyCurrentValues(acc, packageID, dieID)

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 1)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"uncore_frequency_limit_mhz_min": currMin,
				"uncore_frequency_limit_mhz_max": currMax,
				"uncore_frequency_mhz_cur":       uint64(curr),
			},
			// tags
			map[string]string{
				"package_id": strconv.Itoa(packageID),
				"type":       "current",
				"die":        strconv.Itoa(dieID),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}

func TestGetUncoreFreqInitialLimits(t *testing.T) {
	packageID, dieID := 0, 0

	t.Run("FailsToGetInitialMinLimit", func(t *testing.T) {
		mFetcher := &fetcherMock{}

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		initMin, initMax, err := getUncoreFreqInitialLimits(mFetcher, packageID, dieID)

		require.ErrorContains(t, err, "failed to get initial minimum uncore frequency limit")
		require.Zero(t, initMin)
		require.Zero(t, initMax)
		mFetcher.AssertExpectations(t)
	})

	t.Run("FailsToGetInitialMaxLimit", func(t *testing.T) {
		mFetcher := &fetcherMock{}

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(800.0, nil).Once()

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		initMin, initMax, err := getUncoreFreqInitialLimits(mFetcher, packageID, dieID)

		require.ErrorContains(t, err, "failed to get initial maximum uncore frequency limit")
		require.Zero(t, initMin)
		require.Zero(t, initMax)
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		initMinExp := 300.0
		initMaxExp := 1500.0

		mFetcher := &fetcherMock{}

		// mock getting initial minimum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMin", packageID, dieID).Return(initMinExp, nil).Once()

		// mock getting initial maximum uncore frequency limit.
		mFetcher.On("GetInitialUncoreFrequencyMax", packageID, dieID).Return(initMaxExp, nil).Once()

		initMin, initMax, err := getUncoreFreqInitialLimits(mFetcher, packageID, dieID)

		require.NoError(t, err)
		require.InDelta(t, initMinExp, initMin, testutil.DefaultDelta)
		require.InDelta(t, initMaxExp, initMax, testutil.DefaultDelta)
		mFetcher.AssertExpectations(t)
	})
}

func TestGetUncoreFreqCurrentValues(t *testing.T) {
	packageID, dieID := 0, 0

	t.Run("FailsToGetCurrentMinLimit", func(t *testing.T) {
		mFetcher := &fetcherMock{}

		// mock getting current minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		values, err := getUncoreFreqCurrentValues(mFetcher, packageID, dieID)

		require.ErrorContains(t, err, "failed to get current minimum uncore frequency limit")
		require.Equal(t, uncoreFreqValues{}, values)
		mFetcher.AssertExpectations(t)
	})

	t.Run("FailsToGetCurrentMaxLimit", func(t *testing.T) {
		mFetcher := &fetcherMock{}

		// mock getting current minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(1000.0, nil).Once()

		// mock getting current maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		values, err := getUncoreFreqCurrentValues(mFetcher, packageID, dieID)

		require.ErrorContains(t, err, "failed to get current maximum uncore frequency limit")
		require.Equal(t, uncoreFreqValues{}, values)
		mFetcher.AssertExpectations(t)
	})

	t.Run("FailsToGetCurrentValue", func(t *testing.T) {
		mFetcher := &fetcherMock{}

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(1000.0, nil).Once()

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", packageID, dieID).Return(2000.0, nil).Once()

		// mock getting current uncore frequency value.
		mFetcher.On("GetCurrentUncoreFrequency", packageID, dieID).Return(0.0, errors.New("mock error")).Once()

		values, err := getUncoreFreqCurrentValues(mFetcher, packageID, dieID)

		require.ErrorContains(t, err, "failed to get current uncore frequency")
		require.Equal(t, uncoreFreqValues{}, values)
		mFetcher.AssertExpectations(t)
	})

	t.Run("Ok", func(t *testing.T) {
		minUncore := 500.0
		maxUncore := 1500.0
		current := 750.0

		uncoreFreqValExp := uncoreFreqValues{
			currMin: minUncore,
			currMax: maxUncore,
			curr:    current,
		}

		mFetcher := &fetcherMock{}

		// mock getting custom minimum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMin", packageID, dieID).Return(minUncore, nil).Once()

		// mock getting custom maximum uncore frequency limit.
		mFetcher.On("GetCustomizedUncoreFrequencyMax", packageID, dieID).Return(maxUncore, nil).Once()

		// mock getting current uncore frequency value.
		mFetcher.On("GetCurrentUncoreFrequency", packageID, dieID).Return(current, nil).Once()

		values, err := getUncoreFreqCurrentValues(mFetcher, packageID, dieID)

		require.NoError(t, err)
		require.Equal(t, uncoreFreqValExp, values)
		mFetcher.AssertExpectations(t)
	})
}

func TestAddMaxTurboFreqLimits(t *testing.T) {
	t.Run("FailedToGetMetricModuleNotInitializedError", func(t *testing.T) {
		packageID := 1

		acc := &testutil.Accumulator{}

		mErr := &ptel.ModuleNotInitializedError{Name: "msr"}
		mFetcher := &fetcherMock{}

		// mock getting max turbo frequency list.
		mFetcher.On("GetMaxTurboFreqList", packageID).Return(nil, mErr).Twice()

		p := &PowerStat{
			fetcher: mFetcher,

			logOnce: map[string]struct{}{},
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		// First call adds the error to the accumulator and key to logOnce map.
		p.addMaxTurboFreqLimits(acc, packageID)

		// Second call detects previous error in logOnce map and skips adding it to the accumulator.
		p.addMaxTurboFreqLimits(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q: %v", packageTurboLimit, mErr))

		require.Len(t, p.logOnce, 1)
		require.Contains(t, p.logOnce, "msr_max_turbo_frequency")

		mFetcher.AssertExpectations(t)
	})

	t.Run("FailedToGetMetric", func(t *testing.T) {
		packageID := 1

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting max turbo frequency list.
		mFetcher.On("GetMaxTurboFreqList", packageID).Return(nil, errors.New("mock error")).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addMaxTurboFreqLimits(acc, packageID)

		require.Empty(t, acc.GetTelegrafMetrics())
		require.Len(t, acc.Errors, 1)
		require.ErrorContains(t, acc.FirstError(), fmt.Sprintf("failed to get %q for package ID %v", packageTurboLimit, packageID))
		mFetcher.AssertExpectations(t)
	})

	t.Run("CPUIsHybrid", func(t *testing.T) {
		packageID := 1

		maxTurboFreqList := []ptel.MaxTurboFreq{
			{
				Value:       1000,
				ActiveCores: 10,
				Secondary:   true,
			},
			{
				Value:       2000,
				ActiveCores: 20,
			},
		}

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting max turbo frequency list.
		mFetcher.On("GetMaxTurboFreqList", packageID).Return(maxTurboFreqList, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addMaxTurboFreqLimits(acc, packageID)

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 2)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"max_turbo_frequency_mhz": maxTurboFreqList[0].Value,
			},
			// tags
			map[string]string{
				"package_id":   strconv.Itoa(packageID),
				"active_cores": strconv.Itoa(int(maxTurboFreqList[0].ActiveCores)),
				"hybrid":       "secondary",
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"max_turbo_frequency_mhz": maxTurboFreqList[1].Value,
			},
			// tags
			map[string]string{
				"package_id":   strconv.Itoa(packageID),
				"active_cores": strconv.Itoa(int(maxTurboFreqList[1].ActiveCores)),
				"hybrid":       "primary",
			},
		)
		mFetcher.AssertExpectations(t)
	})

	t.Run("CPUIsNotHybrid", func(t *testing.T) {
		packageID := 1

		maxTurboFreqList := []ptel.MaxTurboFreq{
			{
				Value:       1000,
				ActiveCores: 10,
			},
			{
				Value:       2000,
				ActiveCores: 20,
			},
		}

		acc := &testutil.Accumulator{}

		mFetcher := &fetcherMock{}

		// mock getting max turbo frequency list.
		mFetcher.On("GetMaxTurboFreqList", packageID).Return(maxTurboFreqList, nil).Once()

		p := &PowerStat{
			fetcher: mFetcher,
		}

		require.Empty(t, acc.GetTelegrafMetrics())

		p.addMaxTurboFreqLimits(acc, packageID)

		require.Empty(t, acc.Errors)
		require.Len(t, acc.GetTelegrafMetrics(), 2)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"max_turbo_frequency_mhz": maxTurboFreqList[0].Value,
			},
			// tags
			map[string]string{
				"package_id":   strconv.Itoa(packageID),
				"active_cores": strconv.Itoa(int(maxTurboFreqList[0].ActiveCores)),
			},
		)
		acc.AssertContainsTaggedFields(
			t,
			// measurement
			"powerstat_package",
			// fields
			map[string]interface{}{
				"max_turbo_frequency_mhz": maxTurboFreqList[1].Value,
			},
			// tags
			map[string]string{
				"package_id":   strconv.Itoa(packageID),
				"active_cores": strconv.Itoa(int(maxTurboFreqList[1].ActiveCores)),
			},
		)
		mFetcher.AssertExpectations(t)
	})
}
