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

import (
	"fmt"
	"math/rand"
	"strings"
	"testing"

	"github.com/google/pprof/internal/plugin"
	"github.com/google/pprof/internal/proftest"
	"github.com/google/pprof/internal/report"
	"github.com/google/pprof/internal/transport"
	"github.com/google/pprof/profile"
)

func TestShell(t *testing.T) {
	p := &profile.Profile{}
	generateReportWrapper = checkValue
	defer func() { generateReportWrapper = generateReport }()

	// Use test commands and variables to exercise interactive processing
	var savedCommands commands
	savedCommands, pprofCommands = pprofCommands, testCommands
	defer func() { pprofCommands = savedCommands }()

	savedVariables := pprofVariables
	defer func() { pprofVariables = savedVariables }()

	// Random interleave of independent scripts
	pprofVariables = testVariables(savedVariables)

	// pass in HTTPTransport when setting defaults, because otherwise default
	// transport will try to add flags to the default flag set.
	o := setDefaults(&plugin.Options{HTTPTransport: transport.New(nil)})
	o.UI = newUI(t, interleave(script, 0))
	if err := interactive(p, o); err != nil {
		t.Error("first attempt:", err)
	}
	// Random interleave of independent scripts
	pprofVariables = testVariables(savedVariables)
	o.UI = newUI(t, interleave(script, 1))
	if err := interactive(p, o); err != nil {
		t.Error("second attempt:", err)
	}

	// Random interleave of independent scripts with shortcuts
	pprofVariables = testVariables(savedVariables)
	var scScript []string
	pprofShortcuts, scScript = makeShortcuts(interleave(script, 2), 1)
	o.UI = newUI(t, scScript)
	if err := interactive(p, o); err != nil {
		t.Error("first shortcut attempt:", err)
	}

	// Random interleave of independent scripts with shortcuts
	pprofVariables = testVariables(savedVariables)
	pprofShortcuts, scScript = makeShortcuts(interleave(script, 1), 2)
	o.UI = newUI(t, scScript)
	if err := interactive(p, o); err != nil {
		t.Error("second shortcut attempt:", err)
	}

	// Group with invalid value
	pprofVariables = testVariables(savedVariables)
	ui := &proftest.TestUI{
		T:       t,
		Input:   []string{"cumulative=this"},
		AllowRx: `Unrecognized value for cumulative: "this". Use one of cum, flat`,
	}
	o.UI = ui
	if err := interactive(p, o); err != nil {
		t.Error("invalid group value:", err)
	}
	// Confirm error message written out once.
	if ui.NumAllowRxMatches != 1 {
		t.Errorf("want error message to be printed 1 time, got %v", ui.NumAllowRxMatches)
	}
	// Verify propagation of IO errors
	pprofVariables = testVariables(savedVariables)
	o.UI = newUI(t, []string{"**error**"})
	if err := interactive(p, o); err == nil {
		t.Error("expected IO error, got nil")
	}

}

var testCommands = commands{
	"check": &command{report.Raw, nil, nil, true, "", ""},
}

func testVariables(base variables) variables {
	v := base.makeCopy()

	v["b"] = &variable{boolKind, "f", "", ""}
	v["bb"] = &variable{boolKind, "f", "", ""}
	v["i"] = &variable{intKind, "0", "", ""}
	v["ii"] = &variable{intKind, "0", "", ""}
	v["f"] = &variable{floatKind, "0", "", ""}
	v["ff"] = &variable{floatKind, "0", "", ""}
	v["s"] = &variable{stringKind, "", "", ""}
	v["ss"] = &variable{stringKind, "", "", ""}

	v["ta"] = &variable{boolKind, "f", "radio", ""}
	v["tb"] = &variable{boolKind, "f", "radio", ""}
	v["tc"] = &variable{boolKind, "t", "radio", ""}

	return v
}

// script contains sequences of commands to be executed for testing. Commands
// are split by semicolon and interleaved randomly, so they must be
// independent from each other.
var script = []string{
	"bb=true;bb=false;check bb=false;bb=yes;check bb=true",
	"b=1;check b=true;b=n;check b=false",
	"i=-1;i=-2;check i=-2;i=999999;check i=999999",
	"check ii=0;ii=-1;check ii=-1;ii=100;check ii=100",
	"f=-1;f=-2.5;check f=-2.5;f=0.0001;check f=0.0001",
	"check ff=0;ff=-1.01;check ff=-1.01;ff=100;check ff=100",
	"s=one;s=two;check s=two",
	"ss=tree;check ss=tree;ss=;check ss;ss=forest;check ss=forest",
	"ta=true;check ta=true;check tb=false;check tc=false;tb=1;check tb=true;check ta=false;check tc=false;tc=yes;check tb=false;check ta=false;check tc=true",
}

func makeShortcuts(input []string, seed int) (shortcuts, []string) {
	rand.Seed(int64(seed))

	s := shortcuts{}
	var output, chunk []string
	for _, l := range input {
		chunk = append(chunk, l)
		switch rand.Intn(3) {
		case 0:
			// Create a macro for commands in 'chunk'.
			macro := fmt.Sprintf("alias%d", len(s))
			s[macro] = chunk
			output = append(output, macro)
			chunk = nil
		case 1:
			// Append commands in 'chunk' by themselves.
			output = append(output, chunk...)
			chunk = nil
		case 2:
			// Accumulate commands into 'chunk'
		}
	}
	output = append(output, chunk...)
	return s, output
}

func newUI(t *testing.T, input []string) plugin.UI {
	return &proftest.TestUI{
		T:     t,
		Input: input,
	}
}

func checkValue(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
	if len(cmd) != 2 {
		return fmt.Errorf("expected len(cmd)==2, got %v", cmd)
	}

	input := cmd[1]
	args := strings.SplitN(input, "=", 2)
	if len(args) == 0 {
		return fmt.Errorf("unexpected empty input")
	}
	name, value := args[0], ""
	if len(args) == 2 {
		value = args[1]
	}

	gotv := vars[name]
	if gotv == nil {
		return fmt.Errorf("Could not find variable named %s", name)
	}

	if got := gotv.stringValue(); got != value {
		return fmt.Errorf("Variable %s, want %s, got %s", name, value, got)
	}
	return nil
}

func interleave(input []string, seed int) []string {
	var inputs [][]string
	for _, s := range input {
		inputs = append(inputs, strings.Split(s, ";"))
	}
	rand.Seed(int64(seed))
	var output []string
	for len(inputs) > 0 {
		next := rand.Intn(len(inputs))
		output = append(output, inputs[next][0])
		if tail := inputs[next][1:]; len(tail) > 0 {
			inputs[next] = tail
		} else {
			inputs = append(inputs[:next], inputs[next+1:]...)
		}
	}
	return output
}

func TestInteractiveCommands(t *testing.T) {
	type interactiveTestcase struct {
		input string
		want  map[string]string
	}

	testcases := []interactiveTestcase{
		{
			"top 10 --cum focus1 -ignore focus2",
			map[string]string{
				"functions": "true",
				"nodecount": "10",
				"cum":       "true",
				"focus":     "focus1|focus2",
				"ignore":    "ignore",
			},
		},
		{
			"top10 --cum focus1 -ignore focus2",
			map[string]string{
				"functions": "true",
				"nodecount": "10",
				"cum":       "true",
				"focus":     "focus1|focus2",
				"ignore":    "ignore",
			},
		},
		{
			"dot",
			map[string]string{
				"functions": "true",
				"nodecount": "80",
				"cum":       "false",
			},
		},
		{
			"tags   -ignore1 -ignore2 focus1 >out",
			map[string]string{
				"functions": "true",
				"nodecount": "80",
				"cum":       "false",
				"output":    "out",
				"tagfocus":  "focus1",
				"tagignore": "ignore1|ignore2",
			},
		},
		{
			"weblist  find -test",
			map[string]string{
				"functions": "false",
				"addresses": "true",
				"noinlines": "true",
				"nodecount": "0",
				"cum":       "false",
				"flat":      "true",
				"ignore":    "test",
			},
		},
		{
			"callgrind   fun -ignore  >out",
			map[string]string{
				"functions": "false",
				"addresses": "true",
				"nodecount": "0",
				"cum":       "false",
				"flat":      "true",
				"output":    "out",
			},
		},
		{
			"999",
			nil, // Error
		},
	}

	for _, tc := range testcases {
		cmd, vars, err := parseCommandLine(strings.Fields(tc.input))
		if tc.want == nil && err != nil {
			// Error expected
			continue
		}
		if err != nil {
			t.Errorf("failed on %q: %v", tc.input, err)
			continue
		}

		// Get report output format
		c := pprofCommands[cmd[0]]
		if c == nil {
			t.Errorf("unexpected nil command")
		}
		vars = applyCommandOverrides(cmd[0], c.format, vars)

		for n, want := range tc.want {
			if got := vars[n].stringValue(); got != want {
				t.Errorf("failed on %q, cmd=%q, %s got %s, want %s", tc.input, cmd, n, got, want)
			}
		}
	}
}
