package test

import (
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

	"github.com/coredns/coredns/plugin"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/testutil/promlint"
	dto "github.com/prometheus/client_model/go"
)

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

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

	if len(walker.Metrics) > 0 {
		l := promlint.NewWithMetricFamilies(walker.Metrics)
		problems, err := l.Lint()
		if err != nil {
			t.Fatalf("Link found error: %s", err)
		}

		if len(problems) > 0 {
			t.Fatalf("A slice of Problems indicating any issues found in the metrics stream: %s", problems)
		}
	}
}

type validMetricWalker struct {
	Metrics []*dto.MetricFamily
}

func (w *validMetricWalker) 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
	}
	// Is it appropriate to compare the file name equals metrics.go directly？
	if strings.HasPrefix(path, "../.") {
		return nil
	}
	if strings.HasSuffix(path, "_test.go") {
		return nil
	}
	if !strings.HasSuffix(path, ".go") {
		return nil
	}

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

type metric struct {
	Metric *dto.MetricFamily
}

func (l *metric) 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 != "prometheus" { //prometheus
		return l
	}
	var metricsType dto.MetricType
	switch se.Sel.Name {
	case "NewCounterVec", "NewCounter":
		metricsType = dto.MetricType_COUNTER
	case "NewGaugeVec", "NewGauge":
		metricsType = dto.MetricType_GAUGE
	case "NewHistogramVec", "NewHistogram":
		metricsType = dto.MetricType_HISTOGRAM
	case "NewSummaryVec", "NewSummary":
		metricsType = dto.MetricType_SUMMARY
	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.CompositeLit)
	if !ok {
		return l
	}

	// parse Namespace Subsystem Name Help
	var subsystem, name, help string
	for _, elt := range bl.Elts {
		expr, ok := elt.(*ast.KeyValueExpr)
		if !ok {
			continue
		}
		object, ok := expr.Key.(*ast.Ident)
		if !ok {
			continue
		}
		value, ok := expr.Value.(*ast.BasicLit)
		if !ok {
			continue
		}

		// remove quotes
		stringLiteral, err := strconv.Unquote(value.Value)
		if err != nil {
			return l
		}

		switch object.Name {
		case "Subsystem":
			subsystem = stringLiteral
		case "Name":
			name = stringLiteral
		case "Help":
			help = stringLiteral
		}
	}

	// validate metrics field
	if len(name) == 0 || len(help) == 0 {
		return l
	}

	metricName := prometheus.BuildFQName(plugin.Namespace, subsystem, name)
	l.Metric = &dto.MetricFamily{
		Name: &metricName,
		Help: &help,
		Type: &metricsType,
	}
	return l
}
