// Copyright 2018 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 gopathwalk

import (
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"sync"
	"testing"
)

func TestShouldTraverse(t *testing.T) {
	switch runtime.GOOS {
	case "windows", "plan9":
		t.Skipf("skipping symlink-requiring test on %s", runtime.GOOS)
	}

	dir, err := ioutil.TempDir("", "goimports-")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	// Note: mapToDir prepends "src" to each element, since
	// mapToDir was made for creating GOPATHs.
	if err := mapToDir(dir, map[string]string{
		"foo/foo2/file.txt":        "",
		"foo/foo2/link-to-src":     "LINK:../../",
		"foo/foo2/link-to-src-foo": "LINK:../../foo",
		"foo/foo2/link-to-dot":     "LINK:.",
		"bar/bar2/file.txt":        "",
		"bar/bar2/link-to-src-foo": "LINK:../../foo",

		"a/b/c": "LINK:../../a/d",
		"a/d/e": "LINK:../../a/b",
	}); err != nil {
		t.Fatal(err)
	}
	tests := []struct {
		dir  string
		file string
		want bool
	}{
		{
			dir:  "src/foo/foo2",
			file: "link-to-src-foo",
			want: false, // loop
		},
		{
			dir:  "src/foo/foo2",
			file: "link-to-src",
			want: false, // loop
		},
		{
			dir:  "src/foo/foo2",
			file: "link-to-dot",
			want: false, // loop
		},
		{
			dir:  "src/bar/bar2",
			file: "link-to-src-foo",
			want: true, // not a loop
		},
		{
			dir:  "src/a/b/c",
			file: "e",
			want: false, // loop: "e" is the same as "b".
		},
	}
	for i, tt := range tests {
		fi, err := os.Stat(filepath.Join(dir, tt.dir, tt.file))
		if err != nil {
			t.Errorf("%d. Stat = %v", i, err)
			continue
		}
		var w walker
		got := w.shouldTraverse(filepath.Join(dir, tt.dir), fi)
		if got != tt.want {
			t.Errorf("%d. shouldTraverse(%q, %q) = %v; want %v", i, tt.dir, tt.file, got, tt.want)
		}
	}
}

// TestSkip tests that various goimports rules are followed in non-modules mode.
func TestSkip(t *testing.T) {
	dir, err := ioutil.TempDir("", "goimports-")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	if err := mapToDir(dir, map[string]string{
		"ignoreme/f.go":     "package ignoreme",     // ignored by .goimportsignore
		"node_modules/f.go": "package nodemodules;", // ignored by hardcoded node_modules filter
		"v/f.go":            "package v;",           // ignored by hardcoded vgo cache rule
		"mod/f.go":          "package mod;",         // ignored by hardcoded vgo cache rule
		"shouldfind/f.go":   "package shouldfind;",  // not ignored

		".goimportsignore": "ignoreme\n",
	}); err != nil {
		t.Fatal(err)
	}

	var found []string
	var mu sync.Mutex
	walkDir(Root{filepath.Join(dir, "src"), RootGOPATH},
		func(root Root, dir string) {
			mu.Lock()
			defer mu.Unlock()
			found = append(found, dir[len(root.Path)+1:])
		}, func(root Root, dir string) bool {
			return false
		}, Options{ModulesEnabled: false, Logf: log.Printf})
	if want := []string{"shouldfind"}; !reflect.DeepEqual(found, want) {
		t.Errorf("expected to find only %v, got %v", want, found)
	}
}

// TestSkipFunction tests that scan successfully skips directories from user callback.
func TestSkipFunction(t *testing.T) {
	dir, err := ioutil.TempDir("", "goimports-")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	if err := mapToDir(dir, map[string]string{
		"ignoreme/f.go":           "package ignoreme",    // ignored by skip
		"ignoreme/subignore/f.go": "package subignore",   // also ignored by skip
		"shouldfind/f.go":         "package shouldfind;", // not ignored
	}); err != nil {
		t.Fatal(err)
	}

	var found []string
	var mu sync.Mutex
	walkDir(Root{filepath.Join(dir, "src"), RootGOPATH},
		func(root Root, dir string) {
			mu.Lock()
			defer mu.Unlock()
			found = append(found, dir[len(root.Path)+1:])
		}, func(root Root, dir string) bool {
			return strings.HasSuffix(dir, "ignoreme")
		},
		Options{ModulesEnabled: false})
	if want := []string{"shouldfind"}; !reflect.DeepEqual(found, want) {
		t.Errorf("expected to find only %v, got %v", want, found)
	}
}

func mapToDir(destDir string, files map[string]string) error {
	for path, contents := range files {
		file := filepath.Join(destDir, "src", path)
		if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
			return err
		}
		var err error
		if strings.HasPrefix(contents, "LINK:") {
			err = os.Symlink(strings.TrimPrefix(contents, "LINK:"), file)
		} else {
			err = ioutil.WriteFile(file, []byte(contents), 0644)
		}
		if err != nil {
			return err
		}
	}
	return nil
}
