// Copyright 2019 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !noperf
// +build !noperf

package collector

import (
	"io"
	"log/slog"
	"os"
	"runtime"
	"strconv"
	"strings"
	"testing"

	"github.com/prometheus/client_golang/prometheus"
)

func canTestPerf(t *testing.T) {
	paranoidBytes, err := os.ReadFile("/proc/sys/kernel/perf_event_paranoid")
	if err != nil {
		t.Skip("Procfs not mounted, skipping perf tests")
	}
	paranoidStr := strings.Replace(string(paranoidBytes), "\n", "", -1)
	paranoid, err := strconv.Atoi(paranoidStr)
	if err != nil {
		t.Fatalf("Expected perf_event_paranoid to be an int, got: %s", paranoidStr)
	}
	if paranoid >= 1 {
		t.Skip("Skipping perf tests, set perf_event_paranoid to 0")
	}
}

func TestPerfCollector(t *testing.T) {
	canTestPerf(t)
	collector, err := NewPerfCollector(slog.New(slog.NewTextHandler(io.Discard, nil)))
	if err != nil {
		t.Fatal(err)
	}

	// Setup background goroutine to capture metrics.
	metrics := make(chan prometheus.Metric)
	defer close(metrics)
	go func() {
		i := 0
		for range metrics {
			i++
		}
	}()
	if err := collector.Update(metrics); err != nil {
		t.Fatal(err)
	}
}

func TestPerfCollectorStride(t *testing.T) {
	canTestPerf(t)

	tests := []struct {
		name   string
		flag   string
		exCPUs []int
	}{
		{
			name:   "valid single CPU",
			flag:   "1",
			exCPUs: []int{1},
		},
		{
			name:   "valid range CPUs",
			flag:   "1-5",
			exCPUs: []int{1, 2, 3, 4, 5},
		},
		{
			name:   "valid stride",
			flag:   "1-8:2",
			exCPUs: []int{1, 3, 5, 7},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ncpu := runtime.NumCPU()
			for _, cpu := range test.exCPUs {
				if cpu > ncpu {
					t.Skipf("Skipping test because runtime.NumCPU < %d", cpu)
				}
			}
			perfCPUsFlag = &test.flag
			collector, err := NewPerfCollector(slog.New(slog.NewTextHandler(io.Discard, nil)))
			if err != nil {
				t.Fatal(err)
			}

			c := collector.(*perfCollector)
			for _, cpu := range test.exCPUs {
				if _, ok := c.perfHwProfilers[cpu]; !ok {
					t.Fatalf("Expected CPU %v in hardware profilers", cpu)
				}
				if _, ok := c.perfSwProfilers[cpu]; !ok {
					t.Fatalf("Expected CPU %v in software profilers", cpu)
				}
				if _, ok := c.perfCacheProfilers[cpu]; !ok {
					t.Fatalf("Expected CPU %v in cache profilers", cpu)
				}
			}
		})
	}
}

func TestPerfCPUFlagToCPUs(t *testing.T) {
	tests := []struct {
		name   string
		flag   string
		exCpus []int
		errStr string
	}{
		{
			name:   "valid single CPU",
			flag:   "1",
			exCpus: []int{1},
		},
		{
			name:   "valid range CPUs",
			flag:   "1-5",
			exCpus: []int{1, 2, 3, 4, 5},
		},
		{
			name:   "valid double digit",
			flag:   "10",
			exCpus: []int{10},
		},
		{
			name:   "valid double digit range",
			flag:   "10-12",
			exCpus: []int{10, 11, 12},
		},
		{
			name:   "valid double digit stride",
			flag:   "10-20:5",
			exCpus: []int{10, 15, 20},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			cpus, err := perfCPUFlagToCPUs(test.flag)
			if test.errStr != "" {
				if err != nil {
					t.Fatal("expected error to not be nil")
				}
				if test.errStr != err.Error() {
					t.Fatalf(
						"expected error %q, got %q",
						test.errStr,
						err.Error(),
					)
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			if len(cpus) != len(test.exCpus) {
				t.Fatalf(
					"expected CPUs %v, got %v",
					test.exCpus,
					cpus,
				)
			}
			for i := range cpus {
				if test.exCpus[i] != cpus[i] {
					t.Fatalf(
						"expected CPUs %v, got %v",
						test.exCpus[i],
						cpus[i],
					)
				}
			}
		})
	}
}

func TestPerfTracepointFlagToTracepoints(t *testing.T) {
	tests := []struct {
		name          string
		flag          []string
		exTracepoints []*perfTracepoint
		errStr        string
	}{
		{
			name: "valid single tracepoint",
			flag: []string{"sched:sched_kthread_stop"},
			exTracepoints: []*perfTracepoint{
				{
					subsystem: "sched",
					event:     "sched_kthread_stop",
				},
			},
		},
		{
			name: "valid multiple tracepoints",
			flag: []string{"sched:sched_kthread_stop", "sched:sched_process_fork"},
			exTracepoints: []*perfTracepoint{
				{
					subsystem: "sched",
					event:     "sched_kthread_stop",
				},
				{
					subsystem: "sched",
					event:     "sched_process_fork",
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			tracepoints, err := perfTracepointFlagToTracepoints(test.flag)
			if test.errStr != "" {
				if err != nil {
					t.Fatal("expected error to not be nil")
				}
				if test.errStr != err.Error() {
					t.Fatalf(
						"expected error %q, got %q",
						test.errStr,
						err.Error(),
					)
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			for i := range tracepoints {
				if test.exTracepoints[i].event != tracepoints[i].event &&
					test.exTracepoints[i].subsystem != tracepoints[i].subsystem {
					t.Fatalf(
						"expected tracepoint %v, got %v",
						test.exTracepoints[i],
						tracepoints[i],
					)
				}
			}
		})
	}
}
