package driver

import (
	"net/url"
	"os"
	"path/filepath"
	"reflect"
	"testing"
)

// settingsDirAndFile returns a directory in which settings should be stored
// and the name of the settings file. The caller must delete the directory when
// done.
func settingsDirAndFile(t *testing.T) (string, string) {
	tmpDir, err := os.MkdirTemp("", "pprof_settings_test")
	if err != nil {
		t.Fatalf("error creating temporary directory: %v", err)
	}
	return tmpDir, filepath.Join(tmpDir, "settings.json")
}

func TestSettings(t *testing.T) {
	tmpDir, fname := settingsDirAndFile(t)
	defer os.RemoveAll(tmpDir)
	s, err := readSettings(fname)
	if err != nil {
		t.Fatalf("error reading empty settings: %v", err)
	}
	if len(s.Configs) != 0 {
		t.Fatalf("expected empty settings; got %v", s)
	}
	s.Configs = append(s.Configs, namedConfig{
		Name: "Foo",
		config: config{
			Focus: "focus",
			// Ensure that transient fields are not saved/restored.
			Output:     "output",
			SourcePath: "source",
			TrimPath:   "trim",
			DivideBy:   -2,
		},
	})
	if err := writeSettings(fname, s); err != nil {
		t.Fatal(err)
	}
	s2, err := readSettings(fname)
	if err != nil {
		t.Fatal(err)
	}

	// Change the transient fields to their expected values.
	s.Configs[0].resetTransient()
	if !reflect.DeepEqual(s, s2) {
		t.Fatalf("ReadSettings = %v; expected %v", s2, s)
	}
}

func TestParseConfig(t *testing.T) {
	// Use all the fields to check they are saved/restored from URL.
	cfg := config{
		Output:              "",
		DropNegative:        true,
		CallTree:            true,
		RelativePercentages: true,
		Unit:                "auto",
		CompactLabels:       true,
		SourcePath:          "",
		TrimPath:            "",
		NodeCount:           10,
		NodeFraction:        0.1,
		EdgeFraction:        0.2,
		Trim:                true,
		Focus:               "focus",
		Ignore:              "ignore",
		PruneFrom:           "prune_from",
		Hide:                "hide",
		Show:                "show",
		ShowFrom:            "show_from",
		TagFocus:            "tagfocus",
		TagIgnore:           "tagignore",
		TagShow:             "tagshow",
		TagHide:             "taghide",
		DivideBy:            1,
		Mean:                true,
		Normalize:           true,
		Sort:                "cum",
		Granularity:         "functions",
		NoInlines:           true,
	}
	url, changed := cfg.makeURL(url.URL{})
	if !changed {
		t.Error("applyConfig returned changed=false after applying non-empty config")
	}
	cfg2 := defaultConfig()
	if err := cfg2.applyURL(url.Query()); err != nil {
		t.Fatalf("fromURL failed: %v", err)
	}
	if !reflect.DeepEqual(cfg, cfg2) {
		t.Fatalf("parsed config = %+v; expected match with %+v", cfg2, cfg)
	}
	if url2, changed := cfg.makeURL(url); changed {
		t.Errorf("ApplyConfig returned changed=true after applying same config (%q instead of expected %q", url2.String(), url.String())
	}
}

// TestDefaultConfig verifies that default config values are omitted from URL.
func TestDefaultConfig(t *testing.T) {
	cfg := defaultConfig()
	url, changed := cfg.makeURL(url.URL{})
	if changed {
		t.Error("applyConfig returned changed=true after applying default config")
	}
	if url.String() != "" {
		t.Errorf("applyConfig returned %q; expecting %q", url.String(), "")
	}
}

func TestConfigMenu(t *testing.T) {
	// Save some test settings.
	tmpDir, fname := settingsDirAndFile(t)
	defer os.RemoveAll(tmpDir)
	a, b := defaultConfig(), defaultConfig()
	a.Focus, b.Focus = "foo", "bar"
	s := &settings{
		Configs: []namedConfig{
			{Name: "A", config: a},
			{Name: "B", config: b},
		},
	}
	if err := writeSettings(fname, s); err != nil {
		t.Fatal("error writing settings", err)
	}

	pageURL, _ := url.Parse("/top?f=foo")
	menu := configMenu(fname, *pageURL)
	want := []configMenuEntry{
		{Name: "Default", URL: "?", Current: false, UserConfig: false},
		{Name: "A", URL: "?f=foo", Current: true, UserConfig: true},
		{Name: "B", URL: "?f=bar", Current: false, UserConfig: true},
	}
	if !reflect.DeepEqual(menu, want) {
		t.Errorf("ConfigMenu returned %v; want %v", menu, want)
	}
}

func TestEditConfig(t *testing.T) {
	tmpDir, fname := settingsDirAndFile(t)
	defer os.RemoveAll(tmpDir)

	type testConfig struct {
		name  string
		focus string
		hide  string
	}
	type testCase struct {
		remove  bool
		request string
		expect  []testConfig
	}
	for _, c := range []testCase{
		// Create setting c1
		{false, "/?config=c1&f=foo", []testConfig{
			{"c1", "foo", ""},
		}},
		// Create setting c2
		{false, "/?config=c2&h=bar", []testConfig{
			{"c1", "foo", ""},
			{"c2", "", "bar"},
		}},
		// Overwrite c1
		{false, "/?config=c1&f=baz", []testConfig{
			{"c1", "baz", ""},
			{"c2", "", "bar"},
		}},
		// Delete c2
		{true, "c2", []testConfig{
			{"c1", "baz", ""},
		}},
	} {
		if c.remove {
			if err := removeConfig(fname, c.request); err != nil {
				t.Errorf("error removing config %s: %v", c.request, err)
				continue
			}
		} else {
			req, err := url.Parse(c.request)
			if err != nil {
				t.Errorf("error parsing request %q: %v", c.request, err)
				continue
			}
			if err := setConfig(fname, *req); err != nil {
				t.Errorf("error saving request %q: %v", c.request, err)
				continue
			}
		}

		// Check resulting settings.
		s, err := readSettings(fname)
		if err != nil {
			t.Errorf("error reading settings after applying %q: %v", c.request, err)
			continue
		}
		// Convert to a list that can be compared to c.expect
		got := make([]testConfig, len(s.Configs))
		for i, c := range s.Configs {
			got[i] = testConfig{c.Name, c.Focus, c.Hide}
		}
		if !reflect.DeepEqual(got, c.expect) {
			t.Errorf("Settings after applying %q = %v; want %v", c.request, got, c.expect)
		}
	}
}

func TestAssign(t *testing.T) {
	baseConfig := currentConfig()
	defer setCurrentConfig(baseConfig)

	// Test assigning to a simple field.
	if err := configure("nodecount", "20"); err != nil {
		t.Errorf("error setting nodecount: %v", err)
	}
	if n := currentConfig().NodeCount; n != 20 {
		t.Errorf("incorrect nodecount; expecting 20, got %d", n)
	}

	// Test assignment to a group field.
	if err := configure("granularity", "files"); err != nil {
		t.Errorf("error setting granularity: %v", err)
	}
	if g := currentConfig().Granularity; g != "files" {
		t.Errorf("incorrect granularity; expecting %v, got %v", "files", g)
	}

	// Test assignment to one choice of a group field.
	if err := configure("lines", "t"); err != nil {
		t.Errorf("error setting lines: %v", err)
	}
	if g := currentConfig().Granularity; g != "lines" {
		t.Errorf("incorrect granularity; expecting %v, got %v", "lines", g)
	}

	// Test assignment to invalid choice,
	if err := configure("granularity", "cheese"); err == nil {
		t.Errorf("allowed assignment of invalid granularity")
	}
}
