// Copyright 2012 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 doc

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/printer"
	"go/token"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
	"text/template"
)

var update = flag.Bool("update", false, "update golden (.out) files")
var files = flag.String("files", "", "consider only Go test files matching this regular expression")

const dataDir = "testdata"

var templateTxt = readTemplate("template.txt")

func readTemplate(filename string) *template.Template {
	t := template.New(filename)
	t.Funcs(template.FuncMap{
		"node":     nodeFmt,
		"synopsis": synopsisFmt,
		"indent":   indentFmt,
	})
	return template.Must(t.ParseFiles(filepath.Join(dataDir, filename)))
}

func nodeFmt(node any, fset *token.FileSet) string {
	var buf bytes.Buffer
	printer.Fprint(&buf, fset, node)
	return strings.ReplaceAll(strings.TrimSpace(buf.String()), "\n", "\n\t")
}

func synopsisFmt(s string) string {
	const n = 64
	if len(s) > n {
		// cut off excess text and go back to a word boundary
		s = s[0:n]
		if i := strings.LastIndexAny(s, "\t\n "); i >= 0 {
			s = s[0:i]
		}
		s = strings.TrimSpace(s) + " ..."
	}
	return "// " + strings.ReplaceAll(s, "\n", " ")
}

func indentFmt(indent, s string) string {
	end := ""
	if strings.HasSuffix(s, "\n") {
		end = "\n"
		s = s[:len(s)-1]
	}
	return indent + strings.ReplaceAll(s, "\n", "\n"+indent) + end
}

func isGoFile(fi fs.FileInfo) bool {
	name := fi.Name()
	return !fi.IsDir() &&
		len(name) > 0 && name[0] != '.' && // ignore .files
		filepath.Ext(name) == ".go"
}

type bundle struct {
	*Package
	FSet *token.FileSet
}

func test(t *testing.T, mode Mode) {
	// determine file filter
	filter := isGoFile
	if *files != "" {
		rx, err := regexp.Compile(*files)
		if err != nil {
			t.Fatal(err)
		}
		filter = func(fi fs.FileInfo) bool {
			return isGoFile(fi) && rx.MatchString(fi.Name())
		}
	}

	// get packages
	fset := token.NewFileSet()
	pkgs, err := parser.ParseDir(fset, dataDir, filter, parser.ParseComments)
	if err != nil {
		t.Fatal(err)
	}

	// test packages
	for _, pkg := range pkgs {
		t.Run(pkg.Name, func(t *testing.T) {
			importPath := dataDir + "/" + pkg.Name
			var files []*ast.File
			for _, f := range pkg.Files {
				files = append(files, f)
			}
			doc, err := NewFromFiles(fset, files, importPath, mode)
			if err != nil {
				t.Fatal(err)
			}

			// golden files always use / in filenames - canonicalize them
			for i, filename := range doc.Filenames {
				doc.Filenames[i] = filepath.ToSlash(filename)
			}

			// print documentation
			var buf bytes.Buffer
			if err := templateTxt.Execute(&buf, bundle{doc, fset}); err != nil {
				t.Fatal(err)
			}
			got := buf.Bytes()

			// update golden file if necessary
			golden := filepath.Join(dataDir, fmt.Sprintf("%s.%d.golden", pkg.Name, mode))
			if *update {
				err := os.WriteFile(golden, got, 0644)
				if err != nil {
					t.Fatal(err)
				}
			}

			// get golden file
			want, err := os.ReadFile(golden)
			if err != nil {
				t.Fatal(err)
			}

			// compare
			if !bytes.Equal(got, want) {
				t.Errorf("package %s\n\tgot:\n%s\n\twant:\n%s", pkg.Name, got, want)
			}
		})
	}
}

func Test(t *testing.T) {
	t.Run("default", func(t *testing.T) { test(t, 0) })
	t.Run("AllDecls", func(t *testing.T) { test(t, AllDecls) })
	t.Run("AllMethods", func(t *testing.T) { test(t, AllMethods) })
}

func TestAnchorID(t *testing.T) {
	const in = "Important Things 2 Know & Stuff"
	const want = "hdr-Important_Things_2_Know___Stuff"
	got := anchorID(in)
	if got != want {
		t.Errorf("anchorID(%q) = %q; want %q", in, got, want)
	}
}

func TestFuncs(t *testing.T) {
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, "funcs.go", strings.NewReader(funcsTestFile), parser.ParseComments)
	if err != nil {
		t.Fatal(err)
	}
	doc, err := NewFromFiles(fset, []*ast.File{file}, "importPath", Mode(0))
	if err != nil {
		t.Fatal(err)
	}

	for _, f := range doc.Funcs {
		f.Decl = nil
	}
	for _, ty := range doc.Types {
		for _, f := range ty.Funcs {
			f.Decl = nil
		}
		for _, m := range ty.Methods {
			m.Decl = nil
		}
	}

	compareFuncs := func(t *testing.T, msg string, got, want *Func) {
		// ignore Decl and Examples
		got.Decl = nil
		got.Examples = nil
		if !(got.Doc == want.Doc &&
			got.Name == want.Name &&
			got.Recv == want.Recv &&
			got.Orig == want.Orig &&
			got.Level == want.Level) {
			t.Errorf("%s:\ngot  %+v\nwant %+v", msg, got, want)
		}
	}

	compareSlices(t, "Funcs", doc.Funcs, funcsPackage.Funcs, compareFuncs)
	compareSlices(t, "Types", doc.Types, funcsPackage.Types, func(t *testing.T, msg string, got, want *Type) {
		if got.Name != want.Name {
			t.Errorf("%s.Name: got %q, want %q", msg, got.Name, want.Name)
		} else {
			compareSlices(t, got.Name+".Funcs", got.Funcs, want.Funcs, compareFuncs)
			compareSlices(t, got.Name+".Methods", got.Methods, want.Methods, compareFuncs)
		}
	})
}

func compareSlices[E any](t *testing.T, name string, got, want []E, compareElem func(*testing.T, string, E, E)) {
	if len(got) != len(want) {
		t.Errorf("%s: got %d, want %d", name, len(got), len(want))
	}
	for i := 0; i < len(got) && i < len(want); i++ {
		compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i])
	}
}

const funcsTestFile = `
package funcs

func F() {}

type S1 struct {
	S2  // embedded, exported
	s3  // embedded, unexported
}

func NewS1()  S1 {return S1{} }
func NewS1p() *S1 { return &S1{} }

func (S1) M1() {}
func (r S1) M2() {}
func(S1) m3() {}		// unexported not shown
func (*S1) P1() {}		// pointer receiver

type S2 int
func (S2) M3() {}		// shown on S2

type s3 int
func (s3) M4() {}		// shown on S1

type G1[T any] struct {
	*s3
}

func NewG1[T any]() G1[T] { return G1[T]{} }

func (G1[T]) MG1() {}
func (*G1[U]) MG2() {}

type G2[T, U any] struct {}

func NewG2[T, U any]() G2[T, U] { return G2[T, U]{} }

func (G2[T, U]) MG3() {}
func (*G2[A, B]) MG4() {}


`

var funcsPackage = &Package{
	Funcs: []*Func{{Name: "F"}},
	Types: []*Type{
		{
			Name:  "G1",
			Funcs: []*Func{{Name: "NewG1"}},
			Methods: []*Func{
				{Name: "M4", Recv: "G1", // TODO: synthesize a param for G1?
					Orig: "s3", Level: 1},
				{Name: "MG1", Recv: "G1[T]", Orig: "G1[T]", Level: 0},
				{Name: "MG2", Recv: "*G1[U]", Orig: "*G1[U]", Level: 0},
			},
		},
		{
			Name:  "G2",
			Funcs: []*Func{{Name: "NewG2"}},
			Methods: []*Func{
				{Name: "MG3", Recv: "G2[T, U]", Orig: "G2[T, U]", Level: 0},
				{Name: "MG4", Recv: "*G2[A, B]", Orig: "*G2[A, B]", Level: 0},
			},
		},
		{
			Name:  "S1",
			Funcs: []*Func{{Name: "NewS1"}, {Name: "NewS1p"}},
			Methods: []*Func{
				{Name: "M1", Recv: "S1", Orig: "S1", Level: 0},
				{Name: "M2", Recv: "S1", Orig: "S1", Level: 0},
				{Name: "M4", Recv: "S1", Orig: "s3", Level: 1},
				{Name: "P1", Recv: "*S1", Orig: "*S1", Level: 0},
			},
		},
		{
			Name: "S2",
			Methods: []*Func{
				{Name: "M3", Recv: "S2", Orig: "S2", Level: 0},
			},
		},
	},
}
