// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build go1.18

package buildinfo

import (
	"fmt"
	"os/exec"
	"path/filepath"
	"sort"
	"testing"

	"github.com/google/go-cmp/cmp"
	"golang.org/x/tools/go/packages/packagestest"
	"golang.org/x/vuln/internal/test"
	"golang.org/x/vuln/internal/testenv"
)

// testAll executes testing function ft on all valid combinations
// of gooss and goarchs.
func testAll(t *testing.T, gooss, goarchs []string, ft func(*testing.T, string, string)) {
	// unsupported platforms for building Go binaries.
	var unsupported = map[string]bool{
		"darwin/386":  true,
		"darwin/arm":  true,
		"windows/arm": true, // go.dev/issue/71671
	}

	for _, g := range gooss {
		for _, a := range goarchs {
			goos := g
			goarch := a

			ga := goos + "/" + goarch
			if unsupported[ga] {
				continue
			}

			t.Run(ga, func(t *testing.T) {
				ft(t, goos, goarch)
			})
		}
	}
}

func TestExtractPackagesAndSymbols(t *testing.T) {
	testAll(t, []string{"linux", "darwin", "windows", "freebsd"}, []string{"amd64", "386", "arm", "arm64"},
		func(t *testing.T, goos, goarch string) {
			binary, done := test.GoBuild(t, "testdata/src", "", false, "GOOS", goos, "GOARCH", goarch)
			defer done()

			_, syms, _, err := ExtractPackagesAndSymbols(binary)
			if err != nil {
				t.Fatal(err)
			}

			got := sortedSymbols("main", syms)
			want := []Symbol{
				{"main", "f"},
				{"main", "g"},
				{"main", "main"},
			}

			if diff := cmp.Diff(want, got); diff != "" {
				t.Errorf("(-want,+got):%s", diff)
			}
		})
}

func TestAncientGoBinaries(t *testing.T) {
	_, _, bi, err := ExtractPackagesAndSymbols("testdata/bin/hello-world")
	if err != nil {
		t.Fatal(err)
	}
	if bi.GoVersion != "go1.6.4" {
		t.Errorf("want go1.6.4 Go binary version; got %s", bi.GoVersion)
	}
}

// sortedSymbols gets symbols for pkg and
// sorts them for testing purposes.
func sortedSymbols(pkg string, syms []Symbol) []Symbol {
	var s []Symbol
	for _, ps := range syms {
		if ps.Pkg == pkg {
			s = append(s, ps)
		}
	}
	sort.SliceStable(s, func(i, j int) bool { return s[i].Pkg+"."+s[i].Name < s[j].Pkg+"."+s[j].Name })
	return s
}

// Test58509 is supposed to test issue #58509 where a whole
// vulnerable function is deleted from the binary so we
// cannot detect its presence.
//
// Note: the issue is still not addressed and the test
// expectations are set to fail once it gets addressed.
func Test58509(t *testing.T) {
	testenv.NeedsGoBuild(t)

	vulnLib := `package bvuln

%s debug = true

func Vuln() {
	if debug {
		return
	}
	print("vuln")
}`

	for _, tc := range []struct {
		gl   string
		want bool
	}{
		{"const", false}, // TODO(https://go.dev/issue/58509): change expectations once issue is addressed
		{"var", true},
	} {
		tc := tc
		t.Run(tc.gl, func(t *testing.T) {
			e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
				{
					Name: "golang.org/entry",
					Files: map[string]interface{}{
						"main.go": `
			package main

			import (
				"golang.org/bmod/bvuln"
			)

			func main() {
				bvuln.Vuln()
			}
			`,
					}},
				{
					Name:  "golang.org/bmod@v0.5.0",
					Files: map[string]interface{}{"bvuln/bvuln.go": fmt.Sprintf(vulnLib, tc.gl)},
				},
			})
			defer e.Cleanup()

			cmd := exec.Command("go", "build", "-o", "entry")
			cmd.Dir = e.Config.Dir
			cmd.Env = e.Config.Env
			out, err := cmd.CombinedOutput()
			if err != nil || len(out) > 0 {
				t.Fatalf("failed to build the binary %v %v", err, string(out))
			}

			_, syms, _, err := ExtractPackagesAndSymbols(filepath.Join(e.Config.Dir, "entry"))
			if err != nil {
				t.Fatal(err)
			}

			// effectively, Vuln is not optimized away from the program
			got := len(sortedSymbols("golang.org/bmod/bvuln", syms)) != 0
			if got != tc.want {
				t.Errorf("want %t; got %t", tc.want, got)
			}
		})
	}
}
