/* Copyright 2017 The Bazel Authors. 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 golang

import (
	"go/build/constraint"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
)

var (
	fileInfoCmpOption = cmp.AllowUnexported(
		fileInfo{},
		fileEmbed{},
		buildTags{},
		cgoTagsAndOpts{},
	)
)

func TestGoFileInfo(t *testing.T) {
	for _, tc := range []struct {
		desc, name, source string
		want               fileInfo
	}{
		{
			"empty file",
			"foo.go",
			"package foo\n",
			fileInfo{
				packageName: "foo",
			},
		},
		{
			"xtest file",
			"foo_test.go",
			"package foo_test\n",
			fileInfo{
				packageName: "foo",
				isTest:      true,
			},
		},
		{
			"xtest suffix on non-test",
			"foo_xtest.go",
			"package foo_test\n",
			fileInfo{
				packageName: "foo_test",
				isTest:      false,
			},
		},
		{
			"single import",
			"foo.go",
			`package foo

import "github.com/foo/bar"
`,
			fileInfo{
				packageName: "foo",
				imports:     []string{"github.com/foo/bar"},
			},
		},
		{
			"multiple imports",
			"foo.go",
			`package foo

import (
	"github.com/foo/bar"
	x "github.com/local/project/y"
)
`,
			fileInfo{
				packageName: "foo",
				imports:     []string{"github.com/foo/bar", "github.com/local/project/y"},
			},
		},
		{
			"standard imports included",
			"foo.go",
			`package foo

import "fmt"
`,
			fileInfo{
				packageName: "foo",
				imports:     []string{"fmt"},
			},
		},
		{
			"cgo",
			"foo.go",
			`package foo

import "C"
`,
			fileInfo{
				packageName: "foo",
				isCgo:       true,
			},
		},
		{
			"build tags",
			"foo.go",
			`// +build linux darwin

// +build !ignore

package foo
`,
			fileInfo{
				packageName: "foo",
				tags: &buildTags{
					expr:    mustParseBuildTag(t, "(linux || darwin) && !ignore"),
					rawTags: []string{"linux", "darwin", "ignore"},
				},
			},
		},
		{
			"build tags without blank line",
			"route.go",
			`// Copyright 2017

// +build darwin dragonfly freebsd netbsd openbsd

// Package route provides basic functions for the manipulation of
// packet routing facilities on BSD variants.
package route
`,
			fileInfo{
				packageName: "route",
				tags: &buildTags{
					expr:    mustParseBuildTag(t, "darwin || dragonfly || freebsd || netbsd || openbsd"),
					rawTags: []string{"darwin", "dragonfly", "freebsd", "netbsd", "openbsd"},
				},
			},
		},
		{
			"embed",
			"embed.go",
			`package foo

import _ "embed"

//go:embed embed.go
var src string
`,
			fileInfo{
				packageName: "foo",
				imports:     []string{"embed"},
				embeds:      []fileEmbed{{path: "embed.go"}},
			},
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			dir, err := os.MkdirTemp(os.Getenv("TEST_TEMPDIR"), "TestGoFileInfo")
			if err != nil {
				t.Fatal(err)
			}
			defer os.RemoveAll(dir)
			path := filepath.Join(dir, tc.name)
			if err := os.WriteFile(path, []byte(tc.source), 0o600); err != nil {
				t.Fatal(err)
			}

			got := goFileInfo(path, "")
			// Clear fields we don't care about for testing.
			got = fileInfo{
				packageName: got.packageName,
				isTest:      got.isTest,
				imports:     got.imports,
				embeds:      got.embeds,
				isCgo:       got.isCgo,
				tags:        got.tags,
			}
			for i := range got.embeds {
				got.embeds[i] = fileEmbed{path: got.embeds[i].path}
			}

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

		})
	}
}

func TestGoFileInfoFailure(t *testing.T) {
	dir, err := os.MkdirTemp(os.Getenv("TEST_TEMPDIR"), "TestGoFileInfoFailure")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)
	name := "foo_linux_amd64.go"
	path := filepath.Join(dir, name)
	if err := os.WriteFile(path, []byte("pakcage foo"), 0o600); err != nil {
		t.Fatal(err)
	}

	got := goFileInfo(path, "")
	want := fileInfo{
		path:   path,
		name:   name,
		ext:    goExt,
		goos:   "linux",
		goarch: "amd64",
	}
	if diff := cmp.Diff(want, got, fileInfoCmpOption); diff != "" {
		t.Errorf("(-want, +got): %s", diff)
	}

}

func TestCgo(t *testing.T) {
	for _, tc := range []struct {
		desc, source string
		want         fileInfo
	}{
		{
			"not cgo",
			"package foo\n",
			fileInfo{isCgo: false},
		},
		{
			"empty cgo",
			`package foo

import "C"
`,
			fileInfo{isCgo: true},
		},
		{
			"simple flags",
			`package foo

/*
#cgo CFLAGS: -O0
	#cgo CPPFLAGS: -O1
#cgo   CXXFLAGS:   -O2
#cgo LDFLAGS: -O3 -O4
*/
import "C"
`,
			fileInfo{
				isCgo: true,
				cppopts: []*cgoTagsAndOpts{
					{opts: "-O1"},
				},
				copts: []*cgoTagsAndOpts{
					{opts: "-O0"},
				},
				cxxopts: []*cgoTagsAndOpts{
					{opts: "-O2"},
				},
				clinkopts: []*cgoTagsAndOpts{
					{opts: strings.Join([]string{"-O3", "-O4"}, optSeparator)},
				},
			},
		},
		{
			"cflags with conditions",
			`package foo

/*
#cgo foo bar,!baz CFLAGS: -O0
*/
import "C"
`,
			fileInfo{
				isCgo: true,
				copts: []*cgoTagsAndOpts{
					{
						buildTags: &buildTags{
							expr:    mustParseBuildTag(t, "foo || (bar && !baz)"),
							rawTags: []string{"foo", "bar", "baz"},
						},
						opts: "-O0",
					},
				},
			},
		},
		{
			"slashslash comments",
			`package foo

// #cgo CFLAGS: -O0
// #cgo CFLAGS: -O1
import "C"
`,
			fileInfo{
				isCgo: true,
				copts: []*cgoTagsAndOpts{
					{opts: "-O0"},
					{opts: "-O1"},
				},
			},
		},
		{
			"comment above single import group",
			`package foo

/*
#cgo CFLAGS: -O0
*/
import ("C")
`,
			fileInfo{
				isCgo: true,
				copts: []*cgoTagsAndOpts{
					{opts: "-O0"},
				},
			},
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			dir, err := os.MkdirTemp(os.Getenv("TEST_TEMPDIR"), "TestCgo")
			if err != nil {
				t.Fatal(err)
			}
			defer os.RemoveAll(dir)
			name := "TestCgo.go"
			path := filepath.Join(dir, name)
			if err := os.WriteFile(path, []byte(tc.source), 0o600); err != nil {
				t.Fatal(err)
			}

			got := goFileInfo(path, "")

			// Clear fields we don't care about for testing.
			got = fileInfo{
				isCgo:     got.isCgo,
				copts:     got.copts,
				cppopts:   got.cppopts,
				cxxopts:   got.cxxopts,
				clinkopts: got.clinkopts,
			}

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

		})
	}
}

// Copied from go/build build_test.go
var (
	expandSrcDirPath = filepath.Join(string(filepath.Separator)+"projects", "src", "add")
)

// Copied from go/build build_test.go
var expandSrcDirTests = []struct {
	input, expected string
}{
	{"-L ${SRCDIR}/libs -ladd", "-L /projects/src/add/libs -ladd"},
	{"${SRCDIR}/add_linux_386.a -pthread -lstdc++", "/projects/src/add/add_linux_386.a -pthread -lstdc++"},
	{"Nothing to expand here!", "Nothing to expand here!"},
	{"$", "$"},
	{"$$", "$$"},
	{"${", "${"},
	{"$}", "$}"},
	{"$FOO ${BAR}", "$FOO ${BAR}"},
	{"Find me the $SRCDIRECTORY.", "Find me the $SRCDIRECTORY."},
	{"$SRCDIR is missing braces", "$SRCDIR is missing braces"},
}

// Copied from go/build build_test.go
func TestExpandSrcDir(t *testing.T) {
	for _, test := range expandSrcDirTests {
		output, _ := expandSrcDir(test.input, expandSrcDirPath)
		if output != test.expected {
			t.Errorf("%q expands to %q with SRCDIR=%q when %q is expected", test.input, output, expandSrcDirPath, test.expected)
		} else {
			t.Logf("%q expands to %q with SRCDIR=%q", test.input, output, expandSrcDirPath)
		}
	}
}

var (
	goPackageCmpOption = cmp.AllowUnexported(
		goPackage{},
		goTarget{},
		protoTarget{},
		platformStringsBuilder{},
		platformStringInfo{},
	)
)

func TestExpandSrcDirRepoRelative(t *testing.T) {
	repo, err := os.MkdirTemp(os.Getenv("TEST_TEMPDIR"), "repo")
	if err != nil {
		t.Fatal(err)
	}
	sub := filepath.Join(repo, "sub")
	if err := os.Mkdir(sub, 0o755); err != nil {
		t.Fatal(err)
	}
	goFile := filepath.Join(sub, "sub.go")
	content := []byte(`package sub

/*
#cgo CFLAGS: -I${SRCDIR}/..
*/
import "C"
`)
	if err := os.WriteFile(goFile, content, 0o644); err != nil {
		t.Fatal(err)
	}
	c, _, _ := testConfig(
		t,
		"-repo_root="+repo,
		"-go_prefix=example.com/repo")
	fi := goFileInfo(filepath.Join(sub, "sub.go"), "sub")
	pkgs, _ := buildPackages(c, sub, "sub", false, nil, []fileInfo{fi})
	got, ok := pkgs["sub"]
	if !ok {
		t.Fatal("did not build package 'sub'")
	}
	want := &goPackage{
		name:    "sub",
		dir:     sub,
		rel:     "sub",
		library: goTarget{cgo: true},
	}
	want.library.sources.addGenericString("sub.go")
	want.library.copts.addGenericString("-Isub/..")
	if diff := cmp.Diff(want, got, goPackageCmpOption); diff != "" {
		t.Errorf("(-want, +got): %s", diff)
	}

}

// Copied from go/build build_test.go
func TestShellSafety(t *testing.T) {
	tests := []struct {
		input, srcdir, expected string
		result                  bool
	}{
		{"-I${SRCDIR}/../include", "/projects/src/issue 11868", "-I/projects/src/issue 11868/../include", true},
		{"-I${SRCDIR}", "wtf$@%", "-Iwtf$@%", true},
		{"-X${SRCDIR}/1,${SRCDIR}/2", "/projects/src/issue 11868", "-X/projects/src/issue 11868/1,/projects/src/issue 11868/2", true},
		{"-I/tmp -I/tmp", "/tmp2", "-I/tmp -I/tmp", false},
		{"-I/tmp", "/tmp/[0]", "-I/tmp", true},
		{"-I${SRCDIR}/dir", "/tmp/[0]", "-I/tmp/[0]/dir", false},
	}
	for _, test := range tests {
		output, ok := expandSrcDir(test.input, test.srcdir)
		if ok != test.result {
			t.Errorf("Expected %t while %q expands to %q with SRCDIR=%q; got %t", test.result, test.input, output, test.srcdir, ok)
		}
		if output != test.expected {
			t.Errorf("Expected %q while %q expands with SRCDIR=%q; got %q", test.expected, test.input, test.srcdir, output)
		}
	}
}

func mustParseBuildTag(t *testing.T, in string) constraint.Expr {
	x, err := constraint.Parse("//go:build " + in)
	if err != nil {
		t.Fatalf("%s: %s", in, err)
	}

	return x
}
