// 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 graph

import (
	"bytes"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
	"testing"

	"github.com/google/pprof/internal/proftest"
)

var updateFlag = flag.Bool("update", false, "Update the golden files")

func TestComposeWithStandardGraph(t *testing.T) {
	g := baseGraph()
	a, c := baseAttrsAndConfig()

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose1.dot")
}

func TestComposeWithNodeAttributesAndZeroFlat(t *testing.T) {
	g := baseGraph()
	a, c := baseAttrsAndConfig()

	// Set NodeAttributes for Node 1.
	a.Nodes[g.Nodes[0]] = &DotNodeAttributes{
		Shape:       "folder",
		Bold:        true,
		Peripheries: 2,
		URL:         "www.google.com",
		Formatter: func(ni *NodeInfo) string {
			return strings.ToUpper(ni.Name)
		},
	}

	// Set Flat value to zero on Node 2.
	g.Nodes[1].Flat = 0

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose2.dot")
}

func TestComposeWithTagsAndResidualEdge(t *testing.T) {
	g := baseGraph()
	a, c := baseAttrsAndConfig()

	// Add tags to Node 1.
	g.Nodes[0].LabelTags["a"] = &Tag{
		Name: "tag1",
		Cum:  10,
		Flat: 10,
	}
	g.Nodes[0].NumericTags[""] = TagMap{
		"b": &Tag{
			Name: "tag2",
			Cum:  20,
			Flat: 20,
			Unit: "ms",
		},
	}

	// Set edge to be Residual.
	g.Nodes[0].Out[g.Nodes[1]].Residual = true

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose3.dot")
}

func TestComposeWithNestedTags(t *testing.T) {
	g := baseGraph()
	a, c := baseAttrsAndConfig()

	// Add tags to Node 1.
	g.Nodes[0].LabelTags["tag1"] = &Tag{
		Name: "tag1",
		Cum:  10,
		Flat: 10,
	}
	g.Nodes[0].NumericTags["tag1"] = TagMap{
		"tag2": &Tag{
			Name: "tag2",
			Cum:  20,
			Flat: 20,
			Unit: "ms",
		},
	}

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose5.dot")
}

func TestComposeWithEmptyGraph(t *testing.T) {
	g := &Graph{}
	a, c := baseAttrsAndConfig()

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose4.dot")
}

func TestComposeWithStandardGraphAndURL(t *testing.T) {
	g := baseGraph()
	a, c := baseAttrsAndConfig()
	c.LegendURL = "http://example.com"

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose6.dot")
}

func TestComposeWithNamesThatNeedEscaping(t *testing.T) {
	g := baseGraph()
	a, c := baseAttrsAndConfig()
	g.Nodes[0].Info = NodeInfo{Name: `var"src"`}
	g.Nodes[1].Info = NodeInfo{Name: `var"#dest#"`}

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose7.dot")
}

func TestComposeWithCommentsWithNewlines(t *testing.T) {
	g := baseGraph()
	a, c := baseAttrsAndConfig()
	// comments that could be added with the -add_comment command line tool
	// the first label is used as the dot "node name"; the others are escaped as labels
	c.Labels = []string{"comment line 1\ncomment line 2 \"unterminated double quote", `second comment "double quote"`}

	var buf bytes.Buffer
	ComposeDot(&buf, g, a, c)

	compareGraphs(t, buf.Bytes(), "compose9.dot")
}

func baseGraph() *Graph {
	src := &Node{
		Info:        NodeInfo{Name: "src"},
		Flat:        10,
		Cum:         25,
		In:          make(EdgeMap),
		Out:         make(EdgeMap),
		LabelTags:   make(TagMap),
		NumericTags: make(map[string]TagMap),
	}
	dest := &Node{
		Info:        NodeInfo{Name: "dest"},
		Flat:        15,
		Cum:         25,
		In:          make(EdgeMap),
		Out:         make(EdgeMap),
		LabelTags:   make(TagMap),
		NumericTags: make(map[string]TagMap),
	}
	edge := &Edge{
		Src:    src,
		Dest:   dest,
		Weight: 10,
	}
	src.Out[dest] = edge
	src.In[src] = edge
	return &Graph{
		Nodes: Nodes{
			src,
			dest,
		},
	}
}

func baseAttrsAndConfig() (*DotAttributes, *DotConfig) {
	a := &DotAttributes{
		Nodes: make(map[*Node]*DotNodeAttributes),
	}
	c := &DotConfig{
		Title:  "testtitle",
		Labels: []string{"label1", "label2", `label3: "foo"`},
		Total:  100,
		FormatValue: func(v int64) string {
			return strconv.FormatInt(v, 10)
		},
	}
	return a, c
}

func compareGraphs(t *testing.T, got []byte, wantFile string) {
	wantFile = filepath.Join("testdata", wantFile)
	want, err := os.ReadFile(wantFile)
	if err != nil {
		t.Fatalf("error reading test file %s: %v", wantFile, err)
	}

	if string(got) != string(want) {
		d, err := proftest.Diff(got, want)
		if err != nil {
			t.Fatalf("error finding diff: %v", err)
		}
		t.Errorf("Compose incorrectly wrote %s", string(d))
		if *updateFlag {
			err := os.WriteFile(wantFile, got, 0644)
			if err != nil {
				t.Errorf("failed to update the golden file %q: %v", wantFile, err)
			}
		}
	}
}

func TestNodeletCountCapping(t *testing.T) {
	labelTags := make(TagMap)
	for i := 0; i < 10; i++ {
		name := fmt.Sprintf("tag-%d", i)
		labelTags[name] = &Tag{
			Name: name,
			Flat: 10,
			Cum:  10,
		}
	}
	numTags := make(TagMap)
	for i := 0; i < 10; i++ {
		name := fmt.Sprintf("num-tag-%d", i)
		numTags[name] = &Tag{
			Name:  name,
			Unit:  "mb",
			Value: 16,
			Flat:  10,
			Cum:   10,
		}
	}
	node1 := &Node{
		Info:        NodeInfo{Name: "node1-with-tags"},
		Flat:        10,
		Cum:         10,
		NumericTags: map[string]TagMap{"": numTags},
		LabelTags:   labelTags,
	}
	node2 := &Node{
		Info: NodeInfo{Name: "node2"},
		Flat: 15,
		Cum:  15,
	}
	node3 := &Node{
		Info: NodeInfo{Name: "node3"},
		Flat: 15,
		Cum:  15,
	}
	g := &Graph{
		Nodes: Nodes{
			node1,
			node2,
			node3,
		},
	}
	for n := 1; n <= 3; n++ {
		input := maxNodelets + n
		if got, want := len(g.SelectTopNodes(input, true)), n; got != want {
			t.Errorf("SelectTopNodes(%d): got %d nodes, want %d", input, got, want)
		}
	}
}

func TestMultilinePrintableName(t *testing.T) {
	ni := &NodeInfo{
		Name:    "test1.test2::test3",
		File:    "src/file.cc",
		Address: 123,
		Lineno:  999,
	}

	want := fmt.Sprintf(`%016x\ntest1\ntest2\ntest3\nfile.cc:999\n`, 123)
	if got := multilinePrintableName(ni); got != want {
		t.Errorf("multilinePrintableName(%#v) == %q, want %q", ni, got, want)
	}
}

func TestTagCollapse(t *testing.T) {

	makeTag := func(name, unit string, value, flat, cum int64) *Tag {
		return &Tag{name, unit, value, flat, 0, cum, 0}
	}

	tagSource := []*Tag{
		makeTag("12mb", "mb", 12, 100, 100),
		makeTag("1kb", "kb", 1, 1, 1),
		makeTag("1mb", "mb", 1, 1000, 1000),
		makeTag("2048mb", "mb", 2048, 1000, 1000),
		makeTag("1b", "b", 1, 100, 100),
		makeTag("2b", "b", 2, 100, 100),
		makeTag("7b", "b", 7, 100, 100),
	}

	tagWant := [][]*Tag{
		{
			makeTag("1B..2GB", "", 0, 2401, 2401),
		},
		{
			makeTag("2GB", "", 0, 1000, 1000),
			makeTag("1B..12MB", "", 0, 1401, 1401),
		},
		{
			makeTag("2GB", "", 0, 1000, 1000),
			makeTag("12MB", "", 0, 100, 100),
			makeTag("1B..1MB", "", 0, 1301, 1301),
		},
		{
			makeTag("2GB", "", 0, 1000, 1000),
			makeTag("1MB", "", 0, 1000, 1000),
			makeTag("2B..1kB", "", 0, 201, 201),
			makeTag("1B", "", 0, 100, 100),
			makeTag("12MB", "", 0, 100, 100),
		},
	}

	for _, tc := range tagWant {
		var got, want []*Tag
		b := builder{nil, &DotAttributes{}, &DotConfig{}}
		got = b.collapsedTags(tagSource, len(tc), true)
		want = SortTags(tc, true)

		if !reflect.DeepEqual(got, want) {
			t.Errorf("collapse to %d, got:\n%v\nwant:\n%v", len(tc), tagString(got), tagString(want))
		}
	}
}

func TestEscapeForDot(t *testing.T) {
	for _, tc := range []struct {
		desc  string
		input []string
		want  []string
	}{
		{
			desc:  "with multiple doubles quotes",
			input: []string{`label: "foo" and "bar"`},
			want:  []string{`label: \"foo\" and \"bar\"`},
		},
		{
			desc:  "with graphviz center line character",
			input: []string{"label: foo \n bar"},
			want:  []string{`label: foo \l bar`},
		},
		{
			desc:  "with two backslashes",
			input: []string{`label: \\`},
			want:  []string{`label: \\\\`},
		},
		{
			desc:  "with two double quotes together",
			input: []string{`label: ""`},
			want:  []string{`label: \"\"`},
		},
		{
			desc:  "with multiple labels",
			input: []string{`label1: "foo"`, `label2: "bar"`},
			want:  []string{`label1: \"foo\"`, `label2: \"bar\"`},
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			if got := escapeAllForDot(tc.input); !reflect.DeepEqual(got, tc.want) {
				t.Errorf("escapeAllForDot(%s) = %s, want %s", tc.input, got, tc.want)
			}
		})
	}
}

func tagString(t []*Tag) string {
	var ret []string
	for _, s := range t {
		ret = append(ret, fmt.Sprintln(s))
	}
	return strings.Join(ret, ":")
}
