/* 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 (
	"os"
	"path/filepath"
	"testing"

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

func TestOtherFileInfo(t *testing.T) {
	dir := "."
	for _, tc := range []struct {
		desc, name, source string
		wantTags           *buildTags
	}{
		{
			"empty file",
			"foo.c",
			"",
			nil,
		},
		{
			"tags file",
			"foo.c",
			`// +build foo bar
// +build baz,!ignore

`,
			&buildTags{
				expr:    mustParseBuildTag(t, "(foo || bar) && (baz && !ignore)"),
				rawTags: []string{"foo", "bar", "baz", "ignore"},
			},
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			if err := os.WriteFile(tc.name, []byte(tc.source), 0o600); err != nil {
				t.Fatal(err)
			}
			defer os.Remove(tc.name)

			got := otherFileInfo(filepath.Join(dir, tc.name))

			// Only check that we can extract tags. Everything else is covered
			// by other tests.
			if diff := cmp.Diff(tc.wantTags, got.tags, fileInfoCmpOption); diff != "" {
				t.Errorf("(-want, +got): %s", diff)
			}
		})
	}
}

var fi fileInfo

func BenchmarkFileNameInfo(b *testing.B) {
	b.ReportAllocs()
	for range b.N {
		fi = fileNameInfo("path/to/foo_linux_amd64.go")
		fi = fileNameInfo("path/to/foo_linux.go")
		fi = fileNameInfo("path/to/foo_linux_amd64_test.go")
		fi = fileNameInfo("foo_amd64.s")
	}
}

func TestFileNameInfo(t *testing.T) {
	for _, tc := range []struct {
		desc, name string
		want       fileInfo
	}{
		{
			"simple go file",
			"simple.go",
			fileInfo{
				ext: goExt,
			},
		},
		{
			"simple go test",
			"foo_test.go",
			fileInfo{
				ext:    goExt,
				isTest: true,
			},
		},
		{
			"test source",
			"test.go",
			fileInfo{
				ext:    goExt,
				isTest: false,
			},
		},
		{
			"_test source",
			"_test.go",
			fileInfo{
				ext: unknownExt,
			},
		},
		{
			"source with goos",
			"foo_linux.go",
			fileInfo{
				ext:  goExt,
				goos: "linux",
			},
		},
		{
			"source with goarch",
			"foo_amd64.go",
			fileInfo{
				ext:    goExt,
				goarch: "amd64",
			},
		},
		{
			"source with goos then goarch",
			"foo_linux_amd64.go",
			fileInfo{
				ext:    goExt,
				goos:   "linux",
				goarch: "amd64",
			},
		},
		{
			"source with goarch then goos",
			"foo_amd64_linux.go",
			fileInfo{
				ext:  goExt,
				goos: "linux",
			},
		},
		{
			"test with goos and goarch",
			"foo_linux_amd64_test.go",
			fileInfo{
				ext:    goExt,
				goos:   "linux",
				goarch: "amd64",
				isTest: true,
			},
		},
		{
			"test then goos",
			"foo_test_linux.go",
			fileInfo{
				ext:  goExt,
				goos: "linux",
			},
		},
		{
			"goos source",
			"linux.go",
			fileInfo{
				ext:  goExt,
				goos: "",
			},
		},
		{
			"goarch source",
			"amd64.go",
			fileInfo{
				ext:    goExt,
				goarch: "",
			},
		},
		{
			"goos test",
			"linux_test.go",
			fileInfo{
				ext:    goExt,
				goos:   "",
				isTest: true,
			},
		},
		{
			"c file",
			"foo_test.cxx",
			fileInfo{
				ext:    cExt,
				isTest: false,
			},
		},
		{
			"c os test file",
			"foo_linux_test.c",
			fileInfo{
				ext:    cExt,
				isTest: false,
				goos:   "linux",
			},
		},
		{
			"h file",
			"foo_linux.h",
			fileInfo{
				ext:  hExt,
				goos: "linux",
			},
		},
		{
			"go asm file",
			"foo_amd64.s",
			fileInfo{
				ext:    sExt,
				goarch: "amd64",
			},
		},
		{
			"c asm file",
			"foo.S",
			fileInfo{
				ext: csExt,
			},
		},
		{
			"unsupported file",
			"foo.m",
			fileInfo{
				ext: cExt,
			},
		},
		{
			"ignored test file",
			"foo_test.py",
			fileInfo{
				isTest: false,
			},
		},
		{
			"ignored xtest file",
			"foo_xtest.py",
			fileInfo{
				isTest: false,
			},
		},
		{
			"ignored file",
			"foo.txt",
			fileInfo{
				ext: unknownExt,
			},
		},
		{
			"hidden file",
			".foo.go",
			fileInfo{
				ext: unknownExt,
			},
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			tc.want.name = tc.name
			tc.want.path = filepath.Join("dir", tc.name)
			got := fileNameInfo(tc.want.path)
			if diff := cmp.Diff(tc.want, got, fileInfoCmpOption); diff != "" {
				t.Errorf("(-want, +got): %s", diff)
			}
		})
	}
}

func TestReadTags(t *testing.T) {
	for _, tc := range []struct {
		desc, source string
		want         *buildTags
	}{
		{
			"empty file",
			"",
			nil,
		},
		{
			"single comment without blank line",
			"// +build foo\npackage main",
			nil,
		},
		{
			"multiple comments without blank link",
			`// +build foo

// +build bar
package main

`,
			&buildTags{
				expr:    mustParseBuildTag(t, "foo"),
				rawTags: []string{"foo"},
			},
		},
		{
			"single comment",
			"// +build foo\n\n",
			&buildTags{
				expr:    mustParseBuildTag(t, "foo"),
				rawTags: []string{"foo"},
			},
		},
		{
			"multiple comments",
			`// +build foo
// +build bar

package main`,
			&buildTags{
				expr:    mustParseBuildTag(t, "foo && bar"),
				rawTags: []string{"foo", "bar"},
			},
		},
		{
			"multiple comments with blank",
			`// +build foo

// +build bar

package main`,
			&buildTags{
				expr:    mustParseBuildTag(t, "foo && bar"),
				rawTags: []string{"foo", "bar"},
			},
		},
		{
			"Basic go:build",
			`//go:build foo && bar

package main`,
			&buildTags{
				expr:    mustParseBuildTag(t, "foo && bar"),
				rawTags: []string{"foo", "bar"},
			},
		},
		{
			"Both go:build and +build",
			`//go:build foo && bar
// +build foo,bar

package main`,
			&buildTags{
				expr:    mustParseBuildTag(t, "foo && bar"),
				rawTags: []string{"foo", "bar"},
			},
		},
		{
			"comment with space",
			"  //   +build   foo   bar  \n\n",
			&buildTags{
				expr:    mustParseBuildTag(t, "foo || bar"),
				rawTags: []string{"foo", "bar"},
			},
		},
		{
			"slash star comment",
			"/* +build foo */\n\n",
			nil,
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			f, err := os.CreateTemp(".", "TestReadTags")
			if err != nil {
				t.Fatal(err)
			}
			path := f.Name()
			defer os.Remove(path)

			if _, err := f.WriteString(tc.source); err != nil {
				t.Fatal(err)
			}

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

func TestCheckConstraints(t *testing.T) {
	dir, err := os.MkdirTemp(os.Getenv("TEST_TEMPDIR"), "TestCheckConstraints")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)
	for _, tc := range []struct {
		desc                        string
		genericTags                 map[string]bool
		os, arch, filename, content string
		want                        bool
	}{
		{
			desc: "unconstrained",
			want: true,
		}, {
			desc:     "goos satisfied",
			filename: "foo_linux.go",
			os:       "linux",
			want:     true,
		}, {
			desc:     "goos unsatisfied",
			filename: "foo_linux.go",
			os:       "darwin",
			want:     false,
		}, {
			desc:     "goarch satisfied",
			filename: "foo_amd64.go",
			arch:     "amd64",
			want:     true,
		}, {
			desc:     "goarch unsatisfied",
			filename: "foo_amd64.go",
			arch:     "arm",
			want:     false,
		}, {
			desc:     "goos goarch satisfied",
			filename: "foo_linux_amd64.go",
			os:       "linux",
			arch:     "amd64",
			want:     true,
		}, {
			desc:     "goos goarch unsatisfied",
			filename: "foo_linux_amd64.go",
			os:       "darwin",
			arch:     "amd64",
			want:     false,
		}, {
			desc:     "unix filename on darwin",
			filename: "foo_unix.go",
			os:       "darwin",
			want:     true,
		}, {
			desc:     "unix filename on windows",
			filename: "foo_unix.go",
			os:       "windows",
			want:     true,
		}, {
			desc:     "non-unix tag on linux",
			filename: "foo_bar.go",
			os:       "darwin",
			content:  "//go:build !unix\n\npackage foo",
			want:     false,
		}, {
			desc:     "non-unix tag on windows",
			filename: "foo_bar.go",
			os:       "windows",
			content:  "//go:build !unix\n\npackage foo",
			want:     true,
		}, {
			desc:     "unix tag on windows",
			filename: "foo_bar.go",
			os:       "windows",
			content:  "//go:build unix\n\npackage foo",
			want:     false,
		}, {
			desc:     "unix tag on linux",
			filename: "foo_bar.go",
			os:       "linux",
			content:  "//go:build unix\n\npackage foo",
			want:     true,
		}, {
			desc:     "goos unsatisfied tags satisfied",
			filename: "foo_linux.go",
			content:  "// +build foo\n\npackage foo",
			want:     false,
		}, {
			desc:        "tags all satisfied",
			genericTags: map[string]bool{"a": true, "b": true},
			content:     "// +build a,b\n\npackage foo",
			want:        true,
		}, {
			desc:        "tags some satisfied",
			genericTags: map[string]bool{"a": true},
			content:     "// +build a,b\n\npackage foo",
			want:        false,
		}, {
			desc:    "tag unsatisfied negated",
			content: "// +build !a\n\npackage foo",
			want:    true,
		}, {
			desc:        "tag satisfied negated",
			genericTags: map[string]bool{"a": true},
			content:     "// +build !a\n\npackage foo",
			want:        true,
		}, {
			desc:    "tag double negative",
			content: "// +build !!a\n\npackage foo",
			want:    false,
		}, {
			desc:        "tag group and satisfied",
			genericTags: map[string]bool{"foo": true, "bar": true},
			content:     "// +build foo,bar\n\npackage foo",
			want:        true,
		}, {
			desc:        "tag group and unsatisfied",
			genericTags: map[string]bool{"foo": true},
			content:     "// +build foo,bar\n\npackage foo",
			want:        false,
		}, {
			desc:        "tag line or satisfied",
			genericTags: map[string]bool{"foo": true},
			content:     "// +build foo bar\n\npackage foo",
			want:        true,
		}, {
			desc:        "tag line or unsatisfied",
			genericTags: map[string]bool{"foo": true},
			content:     "// +build !foo bar\n\npackage foo",
			want:        true,
		}, {
			desc:        "tag lines and satisfied",
			genericTags: map[string]bool{"foo": true, "bar": true},
			content: `
// +build foo
// +build bar

package foo`,
			want: true,
		}, {
			desc:        "tag lines and unsatisfied",
			genericTags: map[string]bool{"foo": true},
			content: `
// +build foo
// +build bar

package foo`,
			want: false,
		}, {
			desc:        "cgo tags satisfied",
			os:          "linux",
			genericTags: map[string]bool{"foo": true},
			content: `
// +build foo

package foo

/*
#cgo linux CFLAGS: -Ilinux
*/
import "C"
`,
			want: true,
		}, {
			desc: "cgo tags unsatisfied",
			os:   "linux",
			content: `
package foo

/*
#cgo !linux CFLAGS: -Inotlinux
*/
import "C"
`,
			want: false,
		}, {
			desc:    "release tags",
			content: "// +build go1.7,go1.8,go1.9,go1.91,go2.0\n\npackage foo",
			want:    true,
		}, {
			desc:    "release tag negated",
			content: "// +build !go1.8\n\npackage foo",
			want:    true,
		}, {
			desc:    "cgo tag",
			content: "// +build cgo",
			want:    true,
		}, {
			desc:    "cgo tag negated",
			content: "// +build !cgo",
			want:    true,
		}, {
			desc:    "race msan tags",
			content: "// +build msan race",
			want:    true,
		}, {
			desc:    "race msan tags negated",
			content: "//+ build !msan,!race",
			want:    true,
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			c, _, _ := testConfig(t)
			gc := getGoConfig(c)
			gc.genericTags = tc.genericTags
			if gc.genericTags == nil {
				gc.genericTags = map[string]bool{"gc": true}
			}
			filename := tc.filename
			if filename == "" {
				filename = tc.desc + ".go"
			}
			content := []byte(tc.content)
			if len(content) == 0 {
				content = []byte(`package foo`)
			}

			path := filepath.Join(dir, filename)
			if err := os.WriteFile(path, content, 0o666); err != nil {
				t.Fatal(err)
			}

			fi := goFileInfo(path, "")
			var cgoTags *cgoTagsAndOpts
			if len(fi.copts) > 0 {
				cgoTags = fi.copts[0]
			}

			got := checkConstraints(c, tc.os, tc.arch, fi.goos, fi.goarch, fi.tags, cgoTags)
			if diff := cmp.Diff(tc.want, got); diff != "" {
				t.Errorf("(-want, +got): %s", diff)
			}
		})
	}
}

func TestIsOSArchSpecific(t *testing.T) {
	for _, tc := range []struct {
		desc              string
		filename, content string

		expectOSSpecific   bool
		expectArchSpecific bool
	}{
		{
			desc:               "normal",
			filename:           "foo.go",
			content:            "package foo",
			expectOSSpecific:   false,
			expectArchSpecific: false,
		},
		{
			desc:               "unix directive",
			filename:           "foo.go",
			content:            "//go:build unix\n\npackage foo",
			expectOSSpecific:   true,
			expectArchSpecific: false,
		},
		{
			desc:               "exclude-unix directive",
			filename:           "foo.go",
			content:            "//go:build !unix\n\npackage foo",
			expectOSSpecific:   true,
			expectArchSpecific: false,
		},
		{
			desc:               "arch directive",
			filename:           "foo.go",
			content:            "//go:build arm64\n\npackage foo",
			expectOSSpecific:   false,
			expectArchSpecific: true,
		},
		{
			desc:               "exclude-arch directive",
			filename:           "foo.go",
			content:            "//go:build !arm64\n\npackage foo",
			expectOSSpecific:   false,
			expectArchSpecific: true,
		},
		{
			desc:               "os directive",
			filename:           "foo.go",
			content:            "//go:build linux\n\npackage foo",
			expectOSSpecific:   true,
			expectArchSpecific: false,
		},
		{
			desc:               "exclude-os directive",
			filename:           "foo.go",
			content:            "//go:build !linux\n\npackage foo",
			expectOSSpecific:   true,
			expectArchSpecific: false,
		},
		{
			desc:               "os and arch directive",
			filename:           "foo.go",
			content:            "//go:build linux && amd64\n\npackage foo",
			expectOSSpecific:   true,
			expectArchSpecific: true,
		},
		{
			desc:               "unix and arch directive",
			filename:           "foo.go",
			content:            "//go:build unix && amd64\n\npackage foo",
			expectOSSpecific:   true,
			expectArchSpecific: true,
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			tmpDir, err := os.MkdirTemp(os.Getenv("TEST_TEMPDIR"), "TestIsOSSpecific_*")
			if err != nil {
				t.Fatal(err)
			}
			t.Cleanup(func() {
				os.RemoveAll(tmpDir)
			})

			path := filepath.Join(tmpDir, tc.filename)
			if err := os.WriteFile(path, []byte(tc.content), 0o666); err != nil {
				t.Fatal(err)
			}
			fi := goFileInfo(path, "")
			var cgoTags *cgoTagsAndOpts
			if len(fi.copts) > 0 {
				cgoTags = fi.copts[0]
			}

			gotOSSpecific, gotArchSpecific := isOSArchSpecific(fi, cgoTags)
			if diff := cmp.Diff(tc.expectOSSpecific, gotOSSpecific); diff != "" {
				t.Errorf("(-want, +got): %s", diff)
			}
			if diff := cmp.Diff(tc.expectArchSpecific, gotArchSpecific); diff != "" {
				t.Errorf("(-want, +got): %s", diff)
			}
		})
	}
}
