//go:build linux && amd64

package intel_pmu

import (
	"errors"
	"fmt"
	"math"
	"testing"

	ia "github.com/intel/iaevents"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

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

func TestConfigParser_parseEntities(t *testing.T) {
	mSysInfo := &mockSysInfoProvider{}
	mConfigParser := &configParser{
		sys: mSysInfo,
		log: testutil.Logger{},
	}
	e := ia.CustomizableEvent{}

	t.Run("no entities", func(t *testing.T) {
		err := mConfigParser.parseEntities(nil, nil)
		require.Error(t, err)
		require.Contains(t, err.Error(), "neither core nor uncore entities configured")
	})

	// more specific parsing cases in TestConfigParser_parseIntRanges and TestConfigParser_parseEvents
	coreTests := []struct {
		name string

		coreEntity       *coreEventEntity
		parsedCoreEvents []*eventWithQuals
		parsedCores      []int
		coreAll          bool

		uncoreEntity       *uncoreEventEntity
		parsedUncoreEvents []*eventWithQuals
		parsedSockets      []int
		uncoreAll          bool

		failMsg string
	}{
		{"no events provided",
			&coreEventEntity{Events: nil, Cores: []string{"1"}}, nil, []int{1}, true,
			&uncoreEventEntity{Events: nil, Sockets: []string{"0"}}, nil, []int{0}, true,
			""},
		{"uncore entity is nil",
			&coreEventEntity{Events: []string{"EVENT"}, Cores: []string{"1,2"}}, []*eventWithQuals{{"EVENT", nil, e}}, []int{1, 2}, false,
			nil, nil, nil, false,
			"uncore entity is nil"},
		{"core entity is nil",
			nil, nil, nil, false,
			&uncoreEventEntity{Events: []string{"EVENT"}, Sockets: []string{"1,2"}}, []*eventWithQuals{{"EVENT", nil, e}}, []int{1, 2}, false,
			"core entity is nil"},
		{"error parsing sockets",
			&coreEventEntity{Events: nil, Cores: []string{"1,2"}}, nil, []int{1, 2}, true,
			&uncoreEventEntity{Events: []string{"E"}, Sockets: []string{"wrong sockets"}}, []*eventWithQuals{{"E", nil, e}}, nil, false,
			"error during sockets parsing"},
		{"error parsing cores",
			&coreEventEntity{Events: nil, Cores: []string{"wrong cpus"}}, nil, nil, true,
			&uncoreEventEntity{Events: nil, Sockets: []string{"0,1"}}, nil, []int{0, 1}, true,
			"error during cores parsing"},
		{"valid settings",
			&coreEventEntity{
				Events: []string{"E1", "E2:config=123"},
				Cores:  []string{"1-5"},
			}, []*eventWithQuals{{"E1", nil, e}, {"E2", []string{"config=123"}, e}}, []int{1, 2, 3, 4, 5}, false,
			&uncoreEventEntity{
				Events:  []string{"E1", "E2", "E3"},
				Sockets: []string{"0,2-6"},
			}, []*eventWithQuals{{"E1", nil, e}, {"E2", nil, e}, {"E3", nil, e}}, []int{0, 2, 3, 4, 5, 6}, false,
			""},
	}

	for _, test := range coreTests {
		t.Run(test.name, func(t *testing.T) {
			coreEntities := []*coreEventEntity{test.coreEntity}
			uncoreEntities := []*uncoreEventEntity{test.uncoreEntity}

			err := mConfigParser.parseEntities(coreEntities, uncoreEntities)

			if len(test.failMsg) > 0 {
				require.Error(t, err)
				require.Contains(t, err.Error(), test.failMsg)
				return
			}
			require.NoError(t, err)
			require.Equal(t, test.coreAll, test.coreEntity.allEvents)
			require.Equal(t, test.parsedCores, test.coreEntity.parsedCores)
			require.Equal(t, test.parsedCoreEvents, test.coreEntity.parsedEvents)

			require.Equal(t, test.uncoreAll, test.uncoreEntity.allEvents)
			require.Equal(t, test.parsedSockets, test.uncoreEntity.parsedSockets)
			require.Equal(t, test.parsedUncoreEvents, test.uncoreEntity.parsedEvents)
		})
	}
}

func TestConfigParser_parseCores(t *testing.T) {
	mSysInfo := &mockSysInfoProvider{}
	mConfigParser := &configParser{
		sys: mSysInfo,
		log: testutil.Logger{},
	}

	t.Run("no cores provided", func(t *testing.T) {
		t.Run("system info provider is nil", func(t *testing.T) {
			result, err := (&configParser{}).parseCores(nil)
			require.Error(t, err)
			require.Contains(t, err.Error(), "system info provider is nil")
			require.Nil(t, result)
		})
		t.Run("cannot gather all cpus info", func(t *testing.T) {
			mSysInfo.On("allCPUs").Return(nil, errors.New("all cpus error")).Once()
			result, err := mConfigParser.parseCores(nil)
			require.Error(t, err)
			require.Contains(t, err.Error(), "cannot obtain all cpus")
			require.Nil(t, result)
			mSysInfo.AssertExpectations(t)
		})
		t.Run("all cpus gathering succeeded", func(t *testing.T) {
			allCPUs := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}

			mSysInfo.On("allCPUs").Return(allCPUs, nil).Once()
			result, err := mConfigParser.parseCores(nil)
			require.NoError(t, err)
			require.Equal(t, allCPUs, result)
			mSysInfo.AssertExpectations(t)
		})
	})
}

func TestConfigParser_parseSockets(t *testing.T) {
	mSysInfo := &mockSysInfoProvider{}
	mConfigParser := &configParser{
		sys: mSysInfo,
		log: testutil.Logger{},
	}

	t.Run("no sockets provided", func(t *testing.T) {
		t.Run("system info provider is nil", func(t *testing.T) {
			result, err := (&configParser{}).parseSockets(nil)
			require.Error(t, err)
			require.Contains(t, err.Error(), "system info provider is nil")
			require.Nil(t, result)
		})
		t.Run("cannot gather all sockets info", func(t *testing.T) {
			mSysInfo.On("allSockets").Return(nil, errors.New("all sockets error")).Once()
			result, err := mConfigParser.parseSockets(nil)
			require.Error(t, err)
			require.Contains(t, err.Error(), "cannot obtain all sockets")
			require.Nil(t, result)
			mSysInfo.AssertExpectations(t)
		})
		t.Run("all cpus gathering succeeded", func(t *testing.T) {
			allSockets := []int{0, 1, 2, 3, 4}

			mSysInfo.On("allSockets").Return(allSockets, nil).Once()
			result, err := mConfigParser.parseSockets(nil)
			require.NoError(t, err)
			require.Equal(t, allSockets, result)
			mSysInfo.AssertExpectations(t)
		})
	})
}

func TestConfigParser_parseEvents(t *testing.T) {
	mConfigParser := &configParser{log: testutil.Logger{}}
	e := ia.CustomizableEvent{}

	tests := []struct {
		name   string
		input  []string
		result []*eventWithQuals
	}{
		{"no events", nil, nil},
		{"single string", []string{"mock string"}, []*eventWithQuals{{"mock string", nil, e}}},
		{"two events", []string{"EVENT.FIRST", "EVENT.SECOND"}, []*eventWithQuals{{"EVENT.FIRST", nil, e}, {"EVENT.SECOND", nil, e}}},
		{"event with configs", []string{"EVENT.SECOND:config1=0x404300k:config2=0x404300k"},
			[]*eventWithQuals{{"EVENT.SECOND", []string{"config1=0x404300k", "config2=0x404300k"}, e}}},
		{"two events with modifiers", []string{"EVENT.FIRST:config1=0x200300:config2=0x231100:u:H", "EVENT.SECOND:K:p"},
			[]*eventWithQuals{{"EVENT.FIRST", []string{"config1=0x200300", "config2=0x231100", "u", "H"}, e}, {"EVENT.SECOND", []string{"K", "p"}, e}}},
		{"duplicates", []string{"EVENT1", "EVENT1", "EVENT2"}, []*eventWithQuals{{"EVENT1", nil, e}, {"EVENT2", nil, e}}},
		{"duplicates with different configs", []string{"EVENT1:config1", "EVENT1:config2"},
			[]*eventWithQuals{{"EVENT1", []string{"config1"}, e}, {"EVENT1", []string{"config2"}, e}}},
		{"duplicates with the same modifiers", []string{"EVENT1:config1", "EVENT1:config1"},
			[]*eventWithQuals{{"EVENT1", []string{"config1"}, e}}},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			result := mConfigParser.parseEvents(test.input)
			require.Equal(t, test.result, result)
		})
	}
}

func TestConfigParser_parseIntRanges(t *testing.T) {
	mConfigParser := &configParser{log: testutil.Logger{}}
	tests := []struct {
		name    string
		input   []string
		result  []int
		failMsg string
	}{
		{"coma separated", []string{"0,1,2,3,4"}, []int{0, 1, 2, 3, 4}, ""},
		{"range", []string{"0-10"}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, ""},
		{"mixed", []string{"0-3", "4", "12-16"}, []int{0, 1, 2, 3, 4, 12, 13, 14, 15, 16}, ""},
		{"min and max values", []string{"-2147483648", "2147483647"}, []int{math.MinInt32, math.MaxInt32}, ""},
		{"should remove duplicates", []string{"1-5", "2-6"}, []int{1, 2, 3, 4, 5, 6}, ""},
		{"wrong format", []string{"1,2,3%$S,-100"}, nil, "wrong format for id"},
		{"start is greater than end", []string{"10-3"}, nil, "`10` is equal or greater than `3"},
		{"too big value", []string{"18446744073709551615"}, nil, "wrong format for id"},
		{"too much numbers", []string{fmt.Sprintf("0-%d", maxIDsSize)}, nil,
			fmt.Sprintf("requested number of IDs exceeds max size `%d`", maxIDsSize)},
		{"too much numbers mixed", []string{fmt.Sprintf("1-%d", maxIDsSize), "0"}, nil,
			fmt.Sprintf("requested number of IDs exceeds max size `%d`", maxIDsSize)},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			result, err := mConfigParser.parseIntRanges(test.input)
			require.Equal(t, test.result, result)
			if len(test.failMsg) > 0 {
				require.Error(t, err)
				require.Contains(t, err.Error(), test.failMsg)
				return
			}
			require.NoError(t, err)
		})
	}
}

// Mocking

// mockSysInfoProvider is an autogenerated mock type for the sysInfoProvider type
type mockSysInfoProvider struct {
	mock.Mock
}

// allCPUs provides a mock function with given fields:
func (_m *mockSysInfoProvider) allCPUs() ([]int, error) {
	ret := _m.Called()

	var r0 []int
	if rf, ok := ret.Get(0).(func() []int); ok {
		r0 = rf()
	} else {
		if ret.Get(0) != nil {
			r0 = ret.Get(0).([]int)
		}
	}

	var r1 error
	if rf, ok := ret.Get(1).(func() error); ok {
		r1 = rf()
	} else {
		r1 = ret.Error(1)
	}

	return r0, r1
}

// allSockets provides a mock function with given fields:
func (_m *mockSysInfoProvider) allSockets() ([]int, error) {
	ret := _m.Called()

	var r0 []int
	if rf, ok := ret.Get(0).(func() []int); ok {
		r0 = rf()
	} else {
		if ret.Get(0) != nil {
			r0 = ret.Get(0).([]int)
		}
	}

	var r1 error
	if rf, ok := ret.Get(1).(func() error); ok {
		r1 = rf()
	} else {
		r1 = ret.Error(1)
	}

	return r0, r1
}
