package driver

import (
	"fmt"
	"strings"
	"testing"

	"m7s.live/v5/plugin/debug/pkg/internal/proftest"
	"m7s.live/v5/plugin/debug/pkg/profile"
)

const mainBinary = "/bin/main"

var cpuF = []*profile.Function{
	{ID: 1, Name: "main", SystemName: "main", Filename: "main.c"},
	{ID: 2, Name: "foo", SystemName: "foo", Filename: "foo.c"},
	{ID: 3, Name: "foo_caller", SystemName: "foo_caller", Filename: "foo.c"},
	{ID: 4, Name: "bar", SystemName: "bar", Filename: "bar.c"},
}

var cpuM = []*profile.Mapping{
	{
		ID:              1,
		Start:           0x10000,
		Limit:           0x40000,
		File:            mainBinary,
		HasFunctions:    true,
		HasFilenames:    true,
		HasLineNumbers:  true,
		HasInlineFrames: true,
	},
	{
		ID:              2,
		Start:           0x1000,
		Limit:           0x4000,
		File:            "/lib/lib.so",
		HasFunctions:    true,
		HasFilenames:    true,
		HasLineNumbers:  true,
		HasInlineFrames: true,
	},
}

var cpuL = []*profile.Location{
	{
		ID:      1000,
		Mapping: cpuM[1],
		Address: 0x1000,
		Line: []profile.Line{
			{Function: cpuF[0], Line: 1},
		},
	},
	{
		ID:      2000,
		Mapping: cpuM[0],
		Address: 0x2000,
		Line: []profile.Line{
			{Function: cpuF[1], Line: 2},
			{Function: cpuF[2], Line: 1},
		},
	},
	{
		ID:      3000,
		Mapping: cpuM[0],
		Address: 0x3000,
		Line: []profile.Line{
			{Function: cpuF[1], Line: 2},
			{Function: cpuF[2], Line: 1},
		},
	},
	{
		ID:      3001,
		Mapping: cpuM[0],
		Address: 0x3001,
		Line: []profile.Line{
			{Function: cpuF[2], Line: 2},
		},
	},
	{
		ID:      3002,
		Mapping: cpuM[0],
		Address: 0x3002,
		Line: []profile.Line{
			{Function: cpuF[2], Line: 3},
		},
	},
	{
		ID:      3003,
		Mapping: cpuM[0],
		Address: 0x3003,
		Line: []profile.Line{
			{Function: cpuF[3], Line: 1},
		},
	},
}

var testProfile1 = &profile.Profile{
	TimeNanos:     10000,
	PeriodType:    &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
	Period:        1,
	DurationNanos: 10e9,
	SampleType: []*profile.ValueType{
		{Type: "samples", Unit: "count"},
		{Type: "cpu", Unit: "milliseconds"},
	},
	Sample: []*profile.Sample{
		{
			Location: []*profile.Location{cpuL[0]},
			Value:    []int64{1000, 1000},
			Label: map[string][]string{
				"key1": {"tag1"},
				"key2": {"tag1"},
			},
		},
		{
			Location: []*profile.Location{cpuL[1], cpuL[0]},
			Value:    []int64{100, 100},
			Label: map[string][]string{
				"key1": {"tag2"},
				"key3": {"tag2"},
			},
		},
		{
			Location: []*profile.Location{cpuL[2], cpuL[0]},
			Value:    []int64{10, 10},
			Label: map[string][]string{
				"key1": {"tag3"},
				"key2": {"tag2"},
			},
			NumLabel: map[string][]int64{
				"allocations": {1024},
			},
			NumUnit: map[string][]string{
				"allocations": {""},
			},
		},
		{
			Location: []*profile.Location{cpuL[3], cpuL[0]},
			Value:    []int64{10000, 10000},
			Label: map[string][]string{
				"key1": {"tag4"},
				"key2": {"tag1"},
			},
			NumLabel: map[string][]int64{
				"allocations": {1024, 2048},
			},
			NumUnit: map[string][]string{
				"allocations": {"bytes", "b"},
			},
		},
		{
			Location: []*profile.Location{cpuL[4], cpuL[0]},
			Value:    []int64{1, 1},
			Label: map[string][]string{
				"key1": {"tag4"},
				"key2": {"tag1", "tag5"},
			},
			NumLabel: map[string][]int64{
				"allocations": {1024, 1},
			},
			NumUnit: map[string][]string{
				"allocations": {"byte", "kilobyte"},
			},
		},
		{
			Location: []*profile.Location{cpuL[5], cpuL[0]},
			Value:    []int64{200, 200},
			NumLabel: map[string][]int64{
				"allocations": {1024},
			},
		},
	},
	Location: cpuL,
	Function: cpuF,
	Mapping:  cpuM,
}

func TestAddLabelNodesMatchBooleans(t *testing.T) {
	type addLabelNodesTestcase struct {
		name             string
		tagroot, tagleaf []string
		outputUnit       string
		rootm, leafm     bool
		// wantSampleFuncs contains expected stack functions and sample value after
		// adding nodes, in the same order as in the profile. The format is as
		// returned by stackCollapse function, which is "callee caller: <num>".
		wantSampleFuncs []string
	}
	for _, tc := range []addLabelNodesTestcase{
		{
			name: "Without tagroot or tagleaf, add no extra nodes, and should not match",
			wantSampleFuncs: []string{
				"main(main.c) 1000",
				"main(main.c);foo(foo.c);foo_caller(foo.c) 100",
				"main(main.c);foo(foo.c);foo_caller(foo.c) 10",
				"main(main.c);foo_caller(foo.c) 10000",
				"main(main.c);foo_caller(foo.c) 1",
				"main(main.c);bar(bar.c) 200",
			},
		},
		{
			name:    "Keys that aren't found add empty nodes, and should not match",
			tagroot: []string{"key404"},
			tagleaf: []string{"key404"},
			wantSampleFuncs: []string{
				"(key404);main(main.c);(key404) 1000",
				"(key404);main(main.c);foo(foo.c);foo_caller(foo.c);(key404) 100",
				"(key404);main(main.c);foo(foo.c);foo_caller(foo.c);(key404) 10",
				"(key404);main(main.c);foo_caller(foo.c);(key404) 10000",
				"(key404);main(main.c);foo_caller(foo.c);(key404) 1",
				"(key404);main(main.c);bar(bar.c);(key404) 200",
			},
		},
		{
			name:    "tagroot adds nodes for key1 and reports a match",
			tagroot: []string{"key1"},
			rootm:   true,
			wantSampleFuncs: []string{
				"tag1(key1);main(main.c) 1000",
				"tag2(key1);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
				"tag3(key1);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
				"tag4(key1);main(main.c);foo_caller(foo.c) 10000",
				"tag4(key1);main(main.c);foo_caller(foo.c) 1",
				"(key1);main(main.c);bar(bar.c) 200",
			},
		},
		{
			name:    "tagroot adds nodes for key2 and reports a match",
			tagroot: []string{"key2"},
			rootm:   true,
			wantSampleFuncs: []string{
				"tag1(key2);main(main.c) 1000",
				"(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
				"tag2(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
				"tag1(key2);main(main.c);foo_caller(foo.c) 10000",
				"tag1,tag5(key2);main(main.c);foo_caller(foo.c) 1",
				"(key2);main(main.c);bar(bar.c) 200",
			},
		},
		{
			name:    "tagleaf adds nodes for key1 and reports a match",
			tagleaf: []string{"key1"},
			leafm:   true,
			wantSampleFuncs: []string{
				"main(main.c);tag1(key1) 1000",
				"main(main.c);foo(foo.c);foo_caller(foo.c);tag2(key1) 100",
				"main(main.c);foo(foo.c);foo_caller(foo.c);tag3(key1) 10",
				"main(main.c);foo_caller(foo.c);tag4(key1) 10000",
				"main(main.c);foo_caller(foo.c);tag4(key1) 1",
				"main(main.c);bar(bar.c);(key1) 200",
			},
		},
		{
			name:    "tagleaf adds nodes for key3 and reports a match",
			tagleaf: []string{"key3"},
			leafm:   true,
			wantSampleFuncs: []string{
				"main(main.c);(key3) 1000",
				"main(main.c);foo(foo.c);foo_caller(foo.c);tag2(key3) 100",
				"main(main.c);foo(foo.c);foo_caller(foo.c);(key3) 10",
				"main(main.c);foo_caller(foo.c);(key3) 10000",
				"main(main.c);foo_caller(foo.c);(key3) 1",
				"main(main.c);bar(bar.c);(key3) 200",
			},
		},
		{
			name:    "tagroot adds nodes for key1,key2 in order and reports a match",
			tagroot: []string{"key1", "key2"},
			rootm:   true,
			wantSampleFuncs: []string{
				"tag1(key1);tag1(key2);main(main.c) 1000",
				"tag2(key1);(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
				"tag3(key1);tag2(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
				"tag4(key1);tag1(key2);main(main.c);foo_caller(foo.c) 10000",
				"tag4(key1);tag1,tag5(key2);main(main.c);foo_caller(foo.c) 1",
				"(key1);(key2);main(main.c);bar(bar.c) 200",
			},
		},
		{
			name:    "tagleaf adds nodes for key1,key2 in order and reports a match",
			tagleaf: []string{"key1", "key2"},
			leafm:   true,
			wantSampleFuncs: []string{
				"main(main.c);tag1(key1);tag1(key2) 1000",
				"main(main.c);foo(foo.c);foo_caller(foo.c);tag2(key1);(key2) 100",
				"main(main.c);foo(foo.c);foo_caller(foo.c);tag3(key1);tag2(key2) 10",
				"main(main.c);foo_caller(foo.c);tag4(key1);tag1(key2) 10000",
				"main(main.c);foo_caller(foo.c);tag4(key1);tag1,tag5(key2) 1",
				"main(main.c);bar(bar.c);(key1);(key2) 200",
			},
		},
		{
			name:    "Numeric units are added with units with tagleaf",
			tagleaf: []string{"allocations"},
			leafm:   true,
			wantSampleFuncs: []string{
				"main(main.c);(allocations) 1000",
				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
				"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
				"main(main.c);foo_caller(foo.c);1024B,2048B(allocations) 10000",
				"main(main.c);foo_caller(foo.c);1024B,1024B(allocations) 1",
				"main(main.c);bar(bar.c);1024(allocations) 200",
			},
		},
		{
			name:    "Numeric units are added with units with tagroot",
			tagroot: []string{"allocations"},
			rootm:   true,
			wantSampleFuncs: []string{
				"(allocations);main(main.c) 1000",
				"(allocations);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
				"1024(allocations);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
				"1024B,2048B(allocations);main(main.c);foo_caller(foo.c) 10000",
				"1024B,1024B(allocations);main(main.c);foo_caller(foo.c) 1",
				"1024(allocations);main(main.c);bar(bar.c) 200",
			},
		},
		{
			name:       "Numeric labels are formatted according to outputUnit",
			outputUnit: "kB",
			tagleaf:    []string{"allocations"},
			leafm:      true,
			wantSampleFuncs: []string{
				"main(main.c);(allocations) 1000",
				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
				"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
				"main(main.c);foo_caller(foo.c);1kB,2kB(allocations) 10000",
				"main(main.c);foo_caller(foo.c);1kB,1kB(allocations) 1",
				"main(main.c);bar(bar.c);1024(allocations) 200",
			},
		},
		{
			name:    "Numeric units with no units are handled properly by tagleaf",
			tagleaf: []string{"allocations"},
			leafm:   true,
			wantSampleFuncs: []string{
				"main(main.c);(allocations) 1000",
				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
				"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
				"main(main.c);foo_caller(foo.c);1024B,2048B(allocations) 10000",
				"main(main.c);foo_caller(foo.c);1024B,1024B(allocations) 1",
				"main(main.c);bar(bar.c);1024(allocations) 200",
			},
		},
	} {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			p := testProfile1.Copy()
			rootm, leafm := addLabelNodes(p, tc.tagroot, tc.tagleaf, tc.outputUnit)
			if rootm != tc.rootm {
				t.Errorf("Got rootm=%v, want=%v", rootm, tc.rootm)
			}
			if leafm != tc.leafm {
				t.Errorf("Got leafm=%v, want=%v", leafm, tc.leafm)
			}
			if got, want := strings.Join(stackCollapse(p), "\n")+"\n", strings.Join(tc.wantSampleFuncs, "\n")+"\n"; got != want {
				diff, err := proftest.Diff([]byte(want), []byte(got))
				if err != nil {
					t.Fatalf("Failed to get diff: %v", err)
				}
				t.Errorf("Profile samples got diff(want->got):\n%s", diff)
			}
		})
	}
}

// stackCollapse returns a slice of strings where each string represents one
// profile sample in Brendan Gregg's "Folded Stacks" format:
// "<root_fn>(filename);<fun2>(filename);<leaf_fn>(filename) <value>". This
// allows the expected values for test cases to be specified in human-readable
// strings.
func stackCollapse(p *profile.Profile) []string {
	var ret []string
	for _, s := range p.Sample {
		var funcs []string
		for i := range s.Location {
			loc := s.Location[len(s.Location)-1-i]
			for _, line := range loc.Line {
				funcs = append(funcs, fmt.Sprintf("%s(%s)", line.Function.Name, line.Function.Filename))
			}
		}
		ret = append(ret, fmt.Sprintf("%s %d", strings.Join(funcs, ";"), s.Value[0]))
	}
	return ret
}
