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

package vulncheck

import (
	"path"
	"testing"

	"github.com/google/go-cmp/cmp"
	"golang.org/x/tools/go/packages/packagestest"
	"golang.org/x/tools/go/ssa/ssautil"
	"golang.org/x/vuln/internal/osv"
)

func TestFixedVersion(t *testing.T) {
	for _, test := range []struct {
		name    string
		module  string
		version string
		in      []osv.Affected
		want    string
	}{
		{
			name: "empty",
			want: "",
		},
		{
			name:    "no semver",
			module:  "example.com/module",
			version: "v1.2.0",
			in: []osv.Affected{
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeType("unspecified"),
							Events: []osv.RangeEvent{
								{Introduced: "v1.0.0"}, {Fixed: "v1.2.3"},
							},
						}},
				},
			},
			want: "",
		},
		{
			name:    "one",
			module:  "example.com/module",
			version: "v1.0.1",
			in: []osv.Affected{
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								{Introduced: "v1.0.0"}, {Fixed: "v1.2.3"},
							},
						}},
				},
			},
			want: "v1.2.3",
		},
		{
			name:    "several",
			module:  "example.com/module",
			version: "v1.2.0",
			in: []osv.Affected{
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								{Introduced: "v1.0.0"}, {Fixed: "v1.2.3"},
								{Introduced: "v1.5.0"}, {Fixed: "v1.5.6"},
							},
						}},
				},
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								{Introduced: "v1.3.0"}, {Fixed: "v1.4.1"},
							},
						}},
				},
				{
					// This should be ignored.
					Module: osv.Module{
						Path: "example.com/anothermodule",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								{Introduced: "0"}, {Fixed: "v1.6.0"},
							},
						}},
				},
			},
			want: "v1.2.3",
		},
		{
			name:    "no v prefix",
			version: "1.18.1",
			module:  "example.com/module",
			in: []osv.Affected{
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								{Fixed: "1.17.2"},
							},
						}},
				},
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								{Introduced: "1.18.0"}, {Fixed: "1.18.4"},
							},
						}},
				},
			},
			want: "v1.18.4",
		},
		{
			name:   "overlapping",
			module: "example.com/module",
			in: []osv.Affected{
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								// v1.2.3 is nominally the earliest fix,
								// but it is contained in vulnerable range
								// for the next affected value.
								{Introduced: "v1.0.0"}, {Fixed: "v1.2.3"},
								{Introduced: "v1.5.0"},
							},
						}},
				},
				{
					Module: osv.Module{
						Path: "example.com/module",
					},
					Ranges: []osv.Range{
						{
							Type: osv.RangeTypeSemver,
							Events: []osv.RangeEvent{
								{Introduced: "v1.2.0"}, {Fixed: "v1.4.1"},
							},
						}},
				},
			},
			want: "v1.4.1",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			got := FixedVersion(test.module, test.version, test.in)
			if got != test.want {
				t.Errorf("got %q, want %q", got, test.want)
			}
		})
	}
}

func TestDbSymbolName(t *testing.T) {
	e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
		{
			Name: "golang.org/package",
			Files: map[string]interface{}{
				"x/x.go": `
			package x

			func Foo() {
				// needed for ssautil.Allfunctions
				x := a{}
				x.Do()
				x.NotDo()
				b := B[a]{}
				b.P()
				b.Q(x)
				Z[a]()
			}

			func bar() {}

			type a struct{}

			func (x a) Do()     {}
			func (x *a) NotDo() {
			}

			type B[T any] struct{}

			func (b *B[T]) P()   {}
			func (b B[T]) Q(t T) {}

			func Z[T any]() {}
			`},
		},
	})
	defer e.Cleanup()

	graph := NewPackageGraph("go1.18")
	err := graph.LoadPackagesAndMods(e.Config, nil, []string{path.Join(e.Temp(), "package/x")}, true)
	if err != nil {
		t.Fatal(err)
	}

	want := map[string]bool{
		"init":    true,
		"bar":     true,
		"B.P":     true,
		"B.Q":     true,
		"a.Do":    true,
		"a.NotDo": true,
		"Foo":     true,
		"Z":       true,
	}

	// test dbFuncName
	prog, _ := buildSSA(graph.TopPkgs(), graph.TopPkgs()[0].Fset)
	got := make(map[string]bool)
	for f := range ssautil.AllFunctions(prog) {
		got[dbFuncName(f)] = true
	}
	if diff := cmp.Diff(want, got); diff != "" {
		t.Errorf("(-want;got+): %s", diff)
	}
}
