package test

// These tests check for meta level items, like trailing whitespace, correct file naming etc.

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"unicode"
)

func TestFileNameHyphen(t *testing.T) {
	walker := hasHyphenWalker{}
	err := filepath.Walk("..", walker.walk)

	if err != nil {
		t.Fatal(err)
	}

	if len(walker.Errors) > 0 {
		for _, err = range walker.Errors {
			t.Error(err)
		}
	}
}

type hasHyphenWalker struct {
	Errors []error
}

func (w *hasHyphenWalker) walk(path string, info os.FileInfo, _ error) error {
	// only for regular files, not starting with a . and those that are go files.
	if !info.Mode().IsRegular() {
		return nil
	}
	if strings.HasPrefix(path, "../.") {
		return nil
	}
	if strings.Contains(path, "/vendor") {
		return nil
	}
	if filepath.Ext(path) != ".go" {
		return nil
	}

	if strings.Index(path, "-") > 0 {
		absPath, _ := filepath.Abs(path)
		w.Errors = append(w.Errors, fmt.Errorf("file %q has a hyphen, please use underscores in file names", absPath))
	}

	return nil
}

// Test if error messages start with an upper case.
func TestLowercaseLog(t *testing.T) {
	walker := hasLowercaseWalker{}
	err := filepath.Walk("..", walker.walk)

	if err != nil {
		t.Fatal(err)
	}

	if len(walker.Errors) > 0 {
		for _, err = range walker.Errors {
			t.Error(err)
		}
	}
}

type hasLowercaseWalker struct {
	Errors []error
}

func (w *hasLowercaseWalker) walk(path string, info os.FileInfo, _ error) error {
	// only for regular files, not starting with a . and those that are go files.
	if !info.Mode().IsRegular() {
		return nil
	}
	if strings.HasPrefix(path, "../.") {
		return nil
	}
	if strings.Contains(path, "/vendor") {
		return nil
	}
	if !strings.HasSuffix(path, "_test.go") {
		return nil
	}

	fs := token.NewFileSet()
	f, err := parser.ParseFile(fs, path, nil, parser.AllErrors)
	if err != nil {
		return err
	}
	l := &logfmt{}
	ast.Walk(l, f)
	if l.err != nil {
		w.Errors = append(w.Errors, l.err)
	}
	return nil
}

type logfmt struct {
	err error
}

func (l logfmt) Visit(n ast.Node) ast.Visitor {
	if n == nil {
		return nil
	}
	ce, ok := n.(*ast.CallExpr)
	if !ok {
		return l
	}
	se, ok := ce.Fun.(*ast.SelectorExpr)
	if !ok {
		return l
	}
	id, ok := se.X.(*ast.Ident)
	if !ok {
		return l
	}
	if id.Name != "t" { // t *testing.T
		return l
	}

	switch se.Sel.Name {
	case "Errorf":
	case "Logf":
	case "Log":
	case "Fatalf":
	case "Fatal":
	default:
		return l
	}
	// Check first arg, that should have basic lit with capital
	if len(ce.Args) < 1 {
		return l
	}
	bl, ok := ce.Args[0].(*ast.BasicLit)
	if !ok {
		return l
	}
	if bl.Kind != token.STRING {
		return l
	}
	if strings.HasPrefix(bl.Value, "\"%s") || strings.HasPrefix(bl.Value, "\"%d") {
		return l
	}
	if strings.HasPrefix(bl.Value, "\"%v") || strings.HasPrefix(bl.Value, "\"%+v") {
		return l
	}
	for i, u := range bl.Value {
		// disregard "
		if i == 1 && !unicode.IsUpper(u) {
			return nil
		}
		if i == 1 {
			break
		}
	}
	return l
}

func TestImportTesting(t *testing.T) {
	walker := hasLowercaseWalker{}
	err := filepath.Walk("..", walker.walk)

	if err != nil {
		t.Fatal(err)
	}

	if len(walker.Errors) > 0 {
		for _, err = range walker.Errors {
			t.Error(err)
		}
	}
}

func TestImportOrdering(t *testing.T) {
	walker := testImportOrderingWalker{}
	err := filepath.Walk("..", walker.walk)

	if err != nil {
		t.Fatal(err)
	}

	if len(walker.Errors) > 0 {
		for _, err = range walker.Errors {
			t.Error(err)
		}
	}
}

type testImportOrderingWalker struct {
	Errors []error
}

func (w *testImportOrderingWalker) walk(path string, info os.FileInfo, _ error) error {
	if !info.Mode().IsRegular() {
		return nil
	}
	if strings.HasPrefix(path, "../.") {
		return nil
	}
	if strings.Contains(path, "/vendor") {
		return nil
	}
	if filepath.Ext(path) != ".go" {
		return nil
	}
	// pb files are autogenerated by protoc
	if strings.HasPrefix(path, "../pb/") {
		return nil
	}

	fs := token.NewFileSet()
	f, err := parser.ParseFile(fs, path, nil, parser.AllErrors)
	if err != nil {
		return err
	}
	if len(f.Imports) == 0 {
		return nil
	}

	// 3 blocks total, if
	// 3 blocks: std + coredns + 3rd party
	// 2 blocks: std + coredns, std + 3rd party, coredns + 3rd party
	// 1 block: std, coredns, 3rd party
	// first entry in a block specifies the type (std, coredns, 3rd party)
	// we want: std, coredns, 3rd party
	// more than 3 blocks as an error
	blocks := [3][]*ast.ImportSpec{}
	prevpos := 0
	bl := 0
	for _, im := range f.Imports {
		line := fs.Position(im.Path.Pos()).Line
		if line-prevpos > 1 && prevpos > 0 {
			bl++
		}
		if bl > 2 {
			absPath, _ := filepath.Abs(path)
			w.Errors = append(w.Errors, fmt.Errorf("more than %d import blocks in %q", bl, absPath))
		}
		blocks[bl] = append(blocks[bl], im)
		prevpos = line
	}
	// if it:
	// contains strings github.com/coredns -> coredns
	// contains dots -> 3rd
	// no dots -> std
	ip := [3]string{} // type per block, just string, either std, coredns, 3rd
	for i := 0; i <= bl; i++ {
		ip[i] = importtype(blocks[i][0].Path.Value)
	}

	// Ok, now that we have the type, let's see if all members adhere to it.
	// After that we check if they are in the right order.
	for i := 0; i <= bl; i++ {
		for _, p := range blocks[i] {
			t := importtype(p.Path.Value)
			if t != ip[i] {
				absPath, _ := filepath.Abs(path)
				w.Errors = append(w.Errors, fmt.Errorf("import path for %s is not of the same type %q in %q", p.Path.Value, ip[i], absPath))
			}
		}
	}

	// check order
	switch bl {
	case 0:
		// we don't care
	case 1:
		if ip[0] == "std" && ip[1] == "coredns" {
			break // OK
		}
		if ip[0] == "std" && ip[1] == "3rd" {
			break // OK
		}
		if ip[0] == "coredns" && ip[1] == "3rd" {
			break // OK
		}
		absPath, _ := filepath.Abs(path)
		w.Errors = append(w.Errors, fmt.Errorf("import path in %q are not in the right order (std -> coredns -> 3rd)", absPath))
	case 2:
		if ip[0] == "std" && ip[1] == "coredns" && ip[2] == "3rd" {
			break // OK
		}
		absPath, _ := filepath.Abs(path)
		w.Errors = append(w.Errors, fmt.Errorf("import path in %q are not in the right order (std -> coredns -> 3rd)", absPath))
	}

	return nil
}

func importtype(s string) string {
	if strings.Contains(s, "github.com/coredns") {
		return "coredns"
	}
	if strings.Contains(s, ".") {
		return "3rd"
	}
	return "std"
}

// TestMetricNaming tests the imports path used for metrics. It depends on faillint to be installed: go install github.com/fatih/faillint
func TestPrometheusImports(t *testing.T) {
	if _, err := exec.LookPath("faillint"); err != nil {
		fmt.Fprintf(os.Stderr, "Not executing TestPrometheusImports: faillint not found\n")
		return
	}

	// make this multiline?
	p := `github.com/prometheus/client_golang/prometheus.{NewCounter,NewCounterVec,NewCounterVec,NewGauge,NewGaugeVec,NewGaugeFunc,NewHistorgram,NewHistogramVec,NewSummary,NewSummaryVec}=github.com/prometheus/client_golang/prometheus/promauto.{NewCounter,NewCounterVec,NewCounterVec,NewGauge,NewGaugeVec,NewGaugeFunc,NewHistorgram,NewHistogramVec,NewSummary,NewSummaryVec}`

	cmd := exec.Command("faillint", "-paths", p, "./...")
	cmd.Dir = ".."
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("Failed: %s\n%s", err, out)
	}
}
