// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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.

package report

import (
	"bytes"
	"io/ioutil"
	"regexp"
	"runtime"
	"testing"

	"github.com/google/pprof/internal/binutils"
	"github.com/google/pprof/internal/graph"
	"github.com/google/pprof/internal/proftest"
	"github.com/google/pprof/profile"
)

type testcase struct {
	rpt  *Report
	want string
}

func TestSource(t *testing.T) {
	const path = "testdata/"

	sampleValue1 := func(v []int64) int64 {
		return v[1]
	}

	for _, tc := range []testcase{
		{
			rpt: New(
				testProfile.Copy(),
				&Options{
					OutputFormat: List,
					Symbol:       regexp.MustCompile(`.`),
					TrimPath:     "/some/path",

					SampleValue: sampleValue1,
					SampleUnit:  testProfile.SampleType[1].Unit,
				},
			),
			want: path + "source.rpt",
		},
		{
			rpt: New(
				testProfile.Copy(),
				&Options{
					OutputFormat: Dot,
					CallTree:     true,
					Symbol:       regexp.MustCompile(`.`),
					TrimPath:     "/some/path",

					SampleValue: sampleValue1,
					SampleUnit:  testProfile.SampleType[1].Unit,
				},
			),
			want: path + "source.dot",
		},
	} {
		var b bytes.Buffer
		if err := Generate(&b, tc.rpt, &binutils.Binutils{}); err != nil {
			t.Fatalf("%s: %v", tc.want, err)
		}

		gold, err := ioutil.ReadFile(tc.want)
		if err != nil {
			t.Fatalf("%s: %v", tc.want, err)
		}
		if runtime.GOOS == "windows" {
			if tc.rpt.options.OutputFormat == Dot {
				// The .dot test has the paths inside strings, so \ must be escaped.
				gold = bytes.Replace(gold, []byte("testdata/"), []byte(`testdata\\`), -1)
			} else {
				gold = bytes.Replace(gold, []byte("testdata/"), []byte(`testdata\`), -1)
			}
		}
		if string(b.String()) != string(gold) {
			d, err := proftest.Diff(gold, b.Bytes())
			if err != nil {
				t.Fatalf("%s: %v", "source", err)
			}
			t.Error("source" + "\n" + string(d) + "\n" + "gold:\n" + tc.want)
		}
	}
}

var testM = []*profile.Mapping{
	{
		ID:              1,
		HasFunctions:    true,
		HasFilenames:    true,
		HasLineNumbers:  true,
		HasInlineFrames: true,
	},
}

var testF = []*profile.Function{
	{
		ID:       1,
		Name:     "main",
		Filename: "testdata/source1",
	},
	{
		ID:       2,
		Name:     "foo",
		Filename: "testdata/source1",
	},
	{
		ID:       3,
		Name:     "bar",
		Filename: "testdata/source1",
	},
	{
		ID:       4,
		Name:     "tee",
		Filename: "/some/path/testdata/source2",
	},
}

var testL = []*profile.Location{
	{
		ID:      1,
		Mapping: testM[0],
		Line: []profile.Line{
			{
				Function: testF[0],
				Line:     2,
			},
		},
	},
	{
		ID:      2,
		Mapping: testM[0],
		Line: []profile.Line{
			{
				Function: testF[1],
				Line:     4,
			},
		},
	},
	{
		ID:      3,
		Mapping: testM[0],
		Line: []profile.Line{
			{
				Function: testF[2],
				Line:     10,
			},
		},
	},
	{
		ID:      4,
		Mapping: testM[0],
		Line: []profile.Line{
			{
				Function: testF[3],
				Line:     2,
			},
		},
	},
	{
		ID:      5,
		Mapping: testM[0],
		Line: []profile.Line{
			{
				Function: testF[3],
				Line:     8,
			},
		},
	},
}

var testProfile = &profile.Profile{
	PeriodType:    &profile.ValueType{Type: "cpu", Unit: "millisecond"},
	Period:        10,
	DurationNanos: 10e9,
	SampleType: []*profile.ValueType{
		{Type: "samples", Unit: "count"},
		{Type: "cpu", Unit: "cycles"},
	},
	Sample: []*profile.Sample{
		{
			Location: []*profile.Location{testL[0]},
			Value:    []int64{1, 1},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{1, 10},
		},
		{
			Location: []*profile.Location{testL[4], testL[2], testL[0]},
			Value:    []int64{1, 100},
		},
		{
			Location: []*profile.Location{testL[3], testL[0]},
			Value:    []int64{1, 1000},
		},
		{
			Location: []*profile.Location{testL[4], testL[3], testL[0]},
			Value:    []int64{1, 10000},
		},
	},
	Location: testL,
	Function: testF,
	Mapping:  testM,
}

func TestDisambiguation(t *testing.T) {
	parent1 := &graph.Node{Info: graph.NodeInfo{Name: "parent1"}}
	parent2 := &graph.Node{Info: graph.NodeInfo{Name: "parent2"}}
	child1 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent1}
	child2 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent2}
	child3 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent1}
	sibling := &graph.Node{Info: graph.NodeInfo{Name: "sibling"}, Function: parent1}

	n := []*graph.Node{parent1, parent2, child1, child2, child3, sibling}

	wanted := map[*graph.Node]string{
		parent1: "parent1",
		parent2: "parent2",
		child1:  "child [1/2]",
		child2:  "child [2/2]",
		child3:  "child [1/2]",
		sibling: "sibling",
	}

	g := &graph.Graph{Nodes: n}

	names := getDisambiguatedNames(g)

	for node, want := range wanted {
		if got := names[node]; got != want {
			t.Errorf("name %s, got %s, want %s", node.Info.Name, got, want)
		}
	}
}

func TestFunctionMap(t *testing.T) {

	fm := make(functionMap)
	nodes := []graph.NodeInfo{
		{Name: "fun1"},
		{Name: "fun2", File: "filename"},
		{Name: "fun1"},
		{Name: "fun2", File: "filename2"},
	}

	want := []struct {
		wantFunction profile.Function
		wantAdded    bool
	}{
		{profile.Function{ID: 1, Name: "fun1"}, true},
		{profile.Function{ID: 2, Name: "fun2", Filename: "filename"}, true},
		{profile.Function{ID: 1, Name: "fun1"}, false},
		{profile.Function{ID: 3, Name: "fun2", Filename: "filename2"}, true},
	}

	for i, tc := range nodes {
		gotFunc, gotAdded := fm.findOrAdd(tc)
		if got, want := gotFunc, want[i].wantFunction; *got != want {
			t.Errorf("%d: got %v, want %v", i, got, want)
		}
		if got, want := gotAdded, want[i].wantAdded; got != want {
			t.Errorf("%d: got %v, want %v", i, got, want)
		}
	}
}

func TestLegendActiveFilters(t *testing.T) {
	activeFilterInput := []string{
		"focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536|363738|acbdefghijklmnop",
		"show=short filter",
	}
	expectedLegendActiveFilter := []string{
		"Active filters:",
		"   focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536…",
		"   show=short filter",
	}
	legendActiveFilter := legendActiveFilters(activeFilterInput)
	if len(legendActiveFilter) != len(expectedLegendActiveFilter) {
		t.Errorf("wanted length %v got length %v", len(expectedLegendActiveFilter), len(legendActiveFilter))
	}
	for i := range legendActiveFilter {
		if legendActiveFilter[i] != expectedLegendActiveFilter[i] {
			t.Errorf("%d: want \"%v\", got \"%v\"", i, expectedLegendActiveFilter[i], legendActiveFilter[i])
		}
	}
}

func TestComputeTotal(t *testing.T) {
	p1 := testProfile.Copy()
	p1.Sample = []*profile.Sample{
		{
			Location: []*profile.Location{testL[0]},
			Value:    []int64{1, 1},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{1, 10},
		},
		{
			Location: []*profile.Location{testL[4], testL[2], testL[0]},
			Value:    []int64{1, 100},
		},
	}

	p2 := testProfile.Copy()
	p2.Sample = []*profile.Sample{
		{
			Location: []*profile.Location{testL[0]},
			Value:    []int64{1, 1},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{1, -10},
		},
		{
			Location: []*profile.Location{testL[4], testL[2], testL[0]},
			Value:    []int64{1, 100},
		},
	}

	p3 := testProfile.Copy()
	p3.Sample = []*profile.Sample{
		{
			Location: []*profile.Location{testL[0]},
			Value:    []int64{10000, 1},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{-10, 3},
			Label:    map[string][]string{"pprof::base": {"true"}},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{1000, -10},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{-9000, 3},
			Label:    map[string][]string{"pprof::base": {"true"}},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{-1, 3},
			Label:    map[string][]string{"pprof::base": {"true"}},
		},
		{
			Location: []*profile.Location{testL[4], testL[2], testL[0]},
			Value:    []int64{100, 100},
		},
		{
			Location: []*profile.Location{testL[2], testL[1], testL[0]},
			Value:    []int64{100, 3},
			Label:    map[string][]string{"pprof::base": {"true"}},
		},
	}

	testcases := []struct {
		desc           string
		prof           *profile.Profile
		value, meanDiv func(v []int64) int64
		wantTotal      int64
	}{
		{
			desc: "no diff base, all positive values, index 1",
			prof: p1,
			value: func(v []int64) int64 {
				return v[0]
			},
			wantTotal: 3,
		},
		{
			desc: "no diff base, all positive values, index 2",
			prof: p1,
			value: func(v []int64) int64 {
				return v[1]
			},
			wantTotal: 111,
		},
		{
			desc: "no diff base, some negative values",
			prof: p2,
			value: func(v []int64) int64 {
				return v[1]
			},
			wantTotal: 111,
		},
		{
			desc: "diff base, some negative values",
			prof: p3,
			value: func(v []int64) int64 {
				return v[0]
			},
			wantTotal: 9111,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.desc, func(t *testing.T) {
			if gotTotal := computeTotal(tc.prof, tc.value, tc.meanDiv); gotTotal != tc.wantTotal {
				t.Errorf("got total %d, want %v", gotTotal, tc.wantTotal)
			}
		})
	}
}
