//  Copyright 2017 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"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"testing"

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

func TestWebList(t *testing.T) {
	if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
		t.Skip("weblist only tested on x86-64 linux")
	}

	cpu := readProfile(filepath.Join("testdata", "sample.cpu"), t)
	rpt := New(cpu, &Options{
		OutputFormat: WebList,
		Symbol:       regexp.MustCompile("busyLoop"),
		SampleValue:  func(v []int64) int64 { return v[1] },
		SampleUnit:   cpu.SampleType[1].Unit,
	})
	var buf bytes.Buffer
	if err := Generate(&buf, rpt, &binutils.Binutils{}); err != nil {
		t.Fatalf("could not generate weblist: %v", err)
	}
	output := buf.String()

	for _, expect := range []string{"func busyLoop", "call.*mapassign"} {
		if match, _ := regexp.MatchString(expect, output); !match {
			t.Errorf("weblist output does not contain '%s':\n%s", expect, output)
		}
	}
}

func TestSourceSyntheticAddress(t *testing.T) {
	testSourceMapping(t, true)
}

func TestSourceMissingMapping(t *testing.T) {
	testSourceMapping(t, false)
}

// testSourceMapping checks that source info is found even when no applicable
// Mapping/objectFile exists. The locations used in the test are either zero
// (if zeroAddress is true), or non-zero (otherwise).
func testSourceMapping(t *testing.T, zeroAddress bool) {
	nextAddr := uint64(0)

	makeLoc := func(name, fname string, line int64) *profile.Location {
		if !zeroAddress {
			nextAddr++
		}
		return &profile.Location{
			Address: nextAddr,
			Line: []profile.Line{
				{
					Function: &profile.Function{Name: name, Filename: fname},
					Line:     line,
				},
			},
		}
	}

	// Create profile that will need synthetic addresses since it has no mappings.
	foo100 := makeLoc("foo", "foo.go", 100)
	bar50 := makeLoc("bar", "bar.go", 50)
	prof := &profile.Profile{
		Sample: []*profile.Sample{
			{
				Value:    []int64{9},
				Location: []*profile.Location{foo100, bar50},
			},
			{
				Value:    []int64{17},
				Location: []*profile.Location{bar50},
			},
		},
	}
	rpt := &Report{
		prof: prof,
		options: &Options{
			Symbol:      regexp.MustCompile("foo|bar"),
			SampleValue: func(s []int64) int64 { return s[0] },
		},
		formatValue: func(v int64) string { return fmt.Sprint(v) },
	}

	var out bytes.Buffer
	err := PrintWebList(&out, rpt, nil, -1)
	if err != nil {
		t.Fatalf("PrintWebList returned unexpected error: %v", err)
	}
	got := out.String()
	expect := regexp.MustCompile(
		`(?s)` + // Allow "." to match newline
			`bar\.go.* 50\b.* 17 +26 .*` +
			`foo\.go.* 100\b.* 9 +9 `)
	if !expect.MatchString(got) {
		t.Errorf("expected regular expression %v does not match  output:\n%s\n", expect, got)
	}
}

func TestOpenSourceFile(t *testing.T) {
	tempdir, err := os.MkdirTemp("", "")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	const lsep = string(filepath.ListSeparator)
	for _, tc := range []struct {
		desc       string
		searchPath string
		trimPath   string
		fs         []string
		path       string
		wantPath   string // If empty, error is wanted.
	}{
		{
			desc:     "exact absolute path is found",
			fs:       []string{"foo/bar.cc"},
			path:     "$dir/foo/bar.cc",
			wantPath: "$dir/foo/bar.cc",
		},
		{
			desc:       "exact relative path is found",
			searchPath: "$dir",
			fs:         []string{"foo/bar.cc"},
			path:       "foo/bar.cc",
			wantPath:   "$dir/foo/bar.cc",
		},
		{
			desc:       "multiple search path",
			searchPath: "some/path" + lsep + "$dir",
			fs:         []string{"foo/bar.cc"},
			path:       "foo/bar.cc",
			wantPath:   "$dir/foo/bar.cc",
		},
		{
			desc:       "relative path is found in parent dir",
			searchPath: "$dir/foo/bar",
			fs:         []string{"bar.cc", "foo/bar/baz.cc"},
			path:       "bar.cc",
			wantPath:   "$dir/bar.cc",
		},
		{
			desc:       "trims configured prefix",
			searchPath: "$dir",
			trimPath:   "some-path" + lsep + "/some/remote/path",
			fs:         []string{"my-project/foo/bar.cc"},
			path:       "/some/remote/path/my-project/foo/bar.cc",
			wantPath:   "$dir/my-project/foo/bar.cc",
		},
		{
			desc:       "trims heuristically",
			searchPath: "$dir/my-project",
			fs:         []string{"my-project/foo/bar.cc"},
			path:       "/some/remote/path/my-project/foo/bar.cc",
			wantPath:   "$dir/my-project/foo/bar.cc",
		},
		{
			desc: "error when not found",
			path: "foo.cc",
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			defer func() {
				if err := os.RemoveAll(tempdir); err != nil {
					t.Fatalf("failed to remove dir %q: %v", tempdir, err)
				}
			}()
			for _, f := range tc.fs {
				path := filepath.Join(tempdir, filepath.FromSlash(f))
				dir := filepath.Dir(path)
				if err := os.MkdirAll(dir, 0755); err != nil {
					t.Fatalf("failed to create dir %q: %v", dir, err)
				}
				if err := os.WriteFile(path, nil, 0644); err != nil {
					t.Fatalf("failed to create file %q: %v", path, err)
				}
			}
			tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1))
			tc.path = filepath.FromSlash(strings.Replace(tc.path, "$dir", tempdir, 1))
			tc.wantPath = filepath.FromSlash(strings.Replace(tc.wantPath, "$dir", tempdir, 1))
			if file, err := openSourceFile(tc.path, tc.searchPath, tc.trimPath); err != nil && tc.wantPath != "" {
				t.Errorf("openSourceFile(%q, %q, %q) = err %v, want path %q", tc.path, tc.searchPath, tc.trimPath, err, tc.wantPath)
			} else if err == nil {
				defer file.Close()
				gotPath := file.Name()
				if tc.wantPath == "" {
					t.Errorf("openSourceFile(%q, %q, %q) = %q, want error", tc.path, tc.searchPath, tc.trimPath, gotPath)
				} else if gotPath != tc.wantPath {
					t.Errorf("openSourceFile(%q, %q, %q) = %q, want path %q", tc.path, tc.searchPath, tc.trimPath, gotPath, tc.wantPath)
				}
			}
		})
	}
}

func TestIndentation(t *testing.T) {
	for _, c := range []struct {
		str        string
		wantIndent int
	}{
		{"", 0},
		{"foobar", 0},
		{"  foo", 2},
		{"\tfoo", 8},
		{"\t foo", 9},
		{"  \tfoo", 8},
		{"       \tfoo", 8},
		{"        \tfoo", 16},
	} {
		if n := indentation(c.str); n != c.wantIndent {
			t.Errorf("indentation(%v): got %d, want %d", c.str, n, c.wantIndent)
		}
	}
}

func TestRightPad(t *testing.T) {
	for _, c := range []struct {
		pad    int
		in     string
		expect string
	}{
		{0, "", ""},
		{4, "", "    "},
		{4, "x", "x   "},
		{4, "abcd", "abcd"},   // No padding because of overflow
		{4, "abcde", "abcde"}, // No padding because of overflow
		{10, "\tx", "        x "},
		{10, "w\txy\tz", "w       xy      z"},
		{20, "w\txy\tz", "w       xy      z   "},
	} {
		out := rightPad(c.in, c.pad)
		if out != c.expect {
			t.Errorf("rightPad(%q, %d): got %q, want %q", c.in, c.pad, out, c.expect)
		}
	}
}

func readProfile(fname string, t *testing.T) *profile.Profile {
	file, err := os.Open(fname)
	if err != nil {
		t.Fatalf("%s: could not open profile: %v", fname, err)
	}
	defer file.Close()
	p, err := profile.Parse(file)
	if err != nil {
		t.Fatalf("%s: could not parse profile: %v", fname, err)
	}

	// Fix file names so they do not include absolute path names.
	fix := func(s string) string {
		const testdir = "/internal/report/"
		pos := strings.Index(s, testdir)
		if pos == -1 {
			return s
		}
		return s[pos+len(testdir):]
	}
	for _, m := range p.Mapping {
		m.File = fix(m.File)
	}
	for _, f := range p.Function {
		f.Filename = fix(f.Filename)
	}

	return p
}
