// 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.17 && !windows
// +build go1.17,!windows

package main

import (
	"bufio"
	"context"
	"io/fs"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"testing"

	"golang.org/x/mod/modfile"
	"golang.org/x/vuln/internal/testenv"
	"golang.org/x/vuln/scan"
	"mvdan.cc/unparam/check"
)

// excluded contains the set of modules that x/vuln should not depend on.
var excluded = map[string]bool{
	"golang.org/x/exp": true,
}

var goHeader = regexp.MustCompile(`^// Copyright 20\d\d 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\.`)

func TestBashChecks(t *testing.T) {
	skipIfShort(t)
	bash, err := exec.LookPath("bash")
	if err != nil {
		t.Skipf("skipping: %v", err)
	}

	var cmd *exec.Cmd
	if os.Getenv("GO_BUILDER_NAME") != "" {
		cmd = exec.Command(bash, "./checks.bash", "trybots")
	} else {
		cmd = exec.Command(bash, "./checks.bash")
	}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		t.Fatal(err)
	}
}

func TestDependencies(t *testing.T) {
	dat, err := os.ReadFile("go.mod")
	if err != nil {
		t.Fatal(err)
	}
	f, err := modfile.Parse("go.mod", dat, nil)
	if err != nil {
		t.Fatalf("modfile.Parse: %v", err)
	}
	for _, r := range f.Require {
		// This is used by staticcheck.
		if strings.HasPrefix(r.Mod.Path, "golang.org/x/exp/typeparams") {
			continue
		}
		for ex := range excluded {
			if strings.HasPrefix(r.Mod.Path, ex) {
				t.Errorf("go.mod contains %q as a dependency, which should not happen", r.Mod.Path)
			}
		}
	}
}

func TestGovulncheck(t *testing.T) {
	skipIfShort(t)
	testenv.NeedsGoBuild(t)

	ctx := context.Background()

	cmd := scan.Command(ctx, "./...")
	err := cmd.Start()
	if err == nil {
		err = cmd.Wait()
	}
	switch err := err.(type) {
	case nil:
	case interface{ ExitCode() int }:
		if err.ExitCode() != 0 {
			t.Error("govulncheck found problems")
		}
	default:
		t.Error(err)
	}
}

func TestStaticCheck(t *testing.T) {
	skipIfShort(t)
	rungo(t, "run", "honnef.co/go/tools/cmd/staticcheck@v0.4.3", "./...")
}

func TestUnparam(t *testing.T) {
	testenv.NeedsGoBuild(t)
	warns, err := check.UnusedParams(false, false, false, "./...")
	if err != nil {
		t.Fatalf("check.UnusedParams: %v", err)
	}
	for _, warn := range warns {
		t.Errorf(warn)
	}
}

func TestVet(t *testing.T) {
	rungo(t, "vet", "-all", "./...")
}

func TestMisspell(t *testing.T) {
	skipIfShort(t)
	rungo(t, "run", "github.com/client9/misspell/cmd/misspell@v0.3.4", "-error", ".")
}

func TestHeaders(t *testing.T) {
	sfs := os.DirFS(".")
	fs.WalkDir(sfs, ".", func(path string, d fs.DirEntry, _ error) error {
		if d.IsDir() {
			if d.Name() == "testdata" {
				return fs.SkipDir
			}
			return nil
		}
		if !strings.HasSuffix(path, ".go") {
			return nil
		}
		f, err := sfs.Open(path)
		if err != nil {
			return err
		}
		defer f.Close()
		if !goHeader.MatchReader(bufio.NewReader(f)) {
			t.Errorf("%v: incorrect go header", path)
		}
		return nil
	})
}

func rungo(t *testing.T, args ...string) {
	t.Helper()
	testenv.NeedsGoBuild(t)

	cmd := exec.Command("go", args...)
	if output, err := cmd.CombinedOutput(); err != nil {
		t.Log("\n" + string(output))
		t.Error("command had non zero exit code")
	}
}

func skipIfShort(t *testing.T) {
	if testing.Short() {
		t.Skipf("skipping: short mode")
	}
}
