// Copyright 2013 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_test

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/doc"
	"go/format"
	"go/parser"
	"go/token"
	"internal/diff"
	"internal/txtar"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
)

func TestExamples(t *testing.T) {
	dir := filepath.Join("testdata", "examples")
	filenames, err := filepath.Glob(filepath.Join(dir, "*.go"))
	if err != nil {
		t.Fatal(err)
	}
	for _, filename := range filenames {
		t.Run(strings.TrimSuffix(filepath.Base(filename), ".go"), func(t *testing.T) {
			fset := token.NewFileSet()
			astFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) // requires object resolution
			if err != nil {
				t.Fatal(err)
			}
			goldenFilename := strings.TrimSuffix(filename, ".go") + ".golden"
			archive, err := txtar.ParseFile(goldenFilename)
			if err != nil {
				t.Fatal(err)
			}
			golden := map[string]string{}
			for _, f := range archive.Files {
				golden[f.Name] = strings.TrimSpace(string(f.Data))
			}

			// Collect the results of doc.Examples in a map keyed by example name.
			examples := map[string]*doc.Example{}
			for _, e := range doc.Examples(astFile) {
				examples[e.Name] = e
				// Treat missing sections in the golden as empty.
				for _, kind := range []string{"Play", "Output"} {
					key := e.Name + "." + kind
					if _, ok := golden[key]; !ok {
						golden[key] = ""
					}
				}
			}

			// Each section in the golden file corresponds to an example we expect
			// to see.
			for sectionName, want := range golden {
				name, kind, found := strings.Cut(sectionName, ".")
				if !found {
					t.Fatalf("bad section name %q, want EXAMPLE_NAME.KIND", sectionName)
				}
				ex := examples[name]
				if ex == nil {
					t.Fatalf("no example named %q", name)
				}

				var got string
				switch kind {
				case "Play":
					got = strings.TrimSpace(formatFile(t, fset, ex.Play))

				case "Output":
					got = strings.TrimSpace(ex.Output)
				default:
					t.Fatalf("bad section kind %q", kind)
				}

				if got != want {
					t.Errorf("%s mismatch:\n%s", sectionName,
						diff.Diff("want", []byte(want), "got", []byte(got)))
				}
			}
		})
	}
}

func formatFile(t *testing.T, fset *token.FileSet, n *ast.File) string {
	t.Helper()
	if n == nil {
		return "<nil>"
	}
	var buf bytes.Buffer
	if err := format.Node(&buf, fset, n); err != nil {
		t.Fatal(err)
	}
	return buf.String()
}

// This example illustrates how to use NewFromFiles
// to compute package documentation with examples.
func ExampleNewFromFiles() {
	// src and test are two source files that make up
	// a package whose documentation will be computed.
	const src = `
// This is the package comment.
package p

import "fmt"

// This comment is associated with the Greet function.
func Greet(who string) {
	fmt.Printf("Hello, %s!\n", who)
}
`
	const test = `
package p_test

// This comment is associated with the ExampleGreet_world example.
func ExampleGreet_world() {
	Greet("world")
}
`

	// Create the AST by parsing src and test.
	fset := token.NewFileSet()
	files := []*ast.File{
		mustParse(fset, "src.go", src),
		mustParse(fset, "src_test.go", test),
	}

	// Compute package documentation with examples.
	p, err := doc.NewFromFiles(fset, files, "example.com/p")
	if err != nil {
		panic(err)
	}

	fmt.Printf("package %s - %s", p.Name, p.Doc)
	fmt.Printf("func %s - %s", p.Funcs[0].Name, p.Funcs[0].Doc)
	fmt.Printf(" ⤷ example with suffix %q - %s", p.Funcs[0].Examples[0].Suffix, p.Funcs[0].Examples[0].Doc)

	// Output:
	// package p - This is the package comment.
	// func Greet - This comment is associated with the Greet function.
	//  ⤷ example with suffix "world" - This comment is associated with the ExampleGreet_world example.
}

func TestClassifyExamples(t *testing.T) {
	const src = `
package p

const Const1 = 0
var   Var1   = 0

type (
	Type1     int
	Type1_Foo int
	Type1_foo int
	type2     int

	Embed struct { Type1 }
	Uembed struct { type2 }
)

func Func1()     {}
func Func1_Foo() {}
func Func1_foo() {}
func func2()     {}

func (Type1) Func1() {}
func (Type1) Func1_Foo() {}
func (Type1) Func1_foo() {}
func (Type1) func2() {}

func (type2) Func1() {}

type (
	Conflict          int
	Conflict_Conflict int
	Conflict_conflict int
)

func (Conflict) Conflict() {}

func GFunc[T any]() {}

type GType[T any] int

func (GType[T]) M() {}
`
	const test = `
package p_test

func ExampleConst1() {} // invalid - no support for consts and vars
func ExampleVar1()   {} // invalid - no support for consts and vars

func Example()               {}
func Example_()              {} // invalid - suffix must start with a lower-case letter
func Example_suffix()        {}
func Example_suffix_xX_X_x() {}
func Example_世界()           {} // invalid - suffix must start with a lower-case letter
func Example_123()           {} // invalid - suffix must start with a lower-case letter
func Example_BadSuffix()     {} // invalid - suffix must start with a lower-case letter

func ExampleType1()               {}
func ExampleType1_()              {} // invalid - suffix must start with a lower-case letter
func ExampleType1_suffix()        {}
func ExampleType1_BadSuffix()     {} // invalid - suffix must start with a lower-case letter
func ExampleType1_Foo()           {}
func ExampleType1_Foo_suffix()    {}
func ExampleType1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
func ExampleType1_foo()           {}
func ExampleType1_foo_suffix()    {}
func ExampleType1_foo_Suffix()    {} // matches Type1, instead of Type1_foo
func Exampletype2()               {} // invalid - cannot match unexported

func ExampleFunc1()               {}
func ExampleFunc1_()              {} // invalid - suffix must start with a lower-case letter
func ExampleFunc1_suffix()        {}
func ExampleFunc1_BadSuffix()     {} // invalid - suffix must start with a lower-case letter
func ExampleFunc1_Foo()           {}
func ExampleFunc1_Foo_suffix()    {}
func ExampleFunc1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
func ExampleFunc1_foo()           {}
func ExampleFunc1_foo_suffix()    {}
func ExampleFunc1_foo_Suffix()    {} // matches Func1, instead of Func1_foo
func Examplefunc1()               {} // invalid - cannot match unexported
func ExampleFunc1_params(a int)   {} // invalid - has parameter
func ExampleFunc1_results() int   {} // invalid - has results

func ExampleType1_Func1()               {}
func ExampleType1_Func1_()              {} // invalid - suffix must start with a lower-case letter
func ExampleType1_Func1_suffix()        {}
func ExampleType1_Func1_BadSuffix()     {} // invalid - suffix must start with a lower-case letter
func ExampleType1_Func1_Foo()           {}
func ExampleType1_Func1_Foo_suffix()    {}
func ExampleType1_Func1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
func ExampleType1_Func1_foo()           {}
func ExampleType1_Func1_foo_suffix()    {}
func ExampleType1_Func1_foo_Suffix()    {} // matches Type1.Func1, instead of Type1.Func1_foo
func ExampleType1_func2()               {} // matches Type1, instead of Type1.func2

func ExampleEmbed_Func1()         {} // invalid - no support for forwarded methods from embedding exported type
func ExampleUembed_Func1()        {} // methods from embedding unexported types are OK
func ExampleUembed_Func1_suffix() {}

func ExampleConflict_Conflict()        {} // ambiguous with either Conflict or Conflict_Conflict type
func ExampleConflict_conflict()        {} // ambiguous with either Conflict or Conflict_conflict type
func ExampleConflict_Conflict_suffix() {} // ambiguous with either Conflict or Conflict_Conflict type
func ExampleConflict_conflict_suffix() {} // ambiguous with either Conflict or Conflict_conflict type

func ExampleGFunc() {}
func ExampleGFunc_suffix() {}

func ExampleGType_M() {}
func ExampleGType_M_suffix() {}
`

	// Parse literal source code as a *doc.Package.
	fset := token.NewFileSet()
	files := []*ast.File{
		mustParse(fset, "src.go", src),
		mustParse(fset, "src_test.go", test),
	}
	p, err := doc.NewFromFiles(fset, files, "example.com/p")
	if err != nil {
		t.Fatalf("doc.NewFromFiles: %v", err)
	}

	// Collect the association of examples to top-level identifiers.
	got := map[string][]string{}
	got[""] = exampleNames(p.Examples)
	for _, f := range p.Funcs {
		got[f.Name] = exampleNames(f.Examples)
	}
	for _, t := range p.Types {
		got[t.Name] = exampleNames(t.Examples)
		for _, f := range t.Funcs {
			got[f.Name] = exampleNames(f.Examples)
		}
		for _, m := range t.Methods {
			got[t.Name+"."+m.Name] = exampleNames(m.Examples)
		}
	}

	want := map[string][]string{
		"": {"", "suffix", "suffix_xX_X_x"}, // Package-level examples.

		"Type1":     {"", "foo_Suffix", "func2", "suffix"},
		"Type1_Foo": {"", "suffix"},
		"Type1_foo": {"", "suffix"},

		"Func1":     {"", "foo_Suffix", "suffix"},
		"Func1_Foo": {"", "suffix"},
		"Func1_foo": {"", "suffix"},

		"Type1.Func1":     {"", "foo_Suffix", "suffix"},
		"Type1.Func1_Foo": {"", "suffix"},
		"Type1.Func1_foo": {"", "suffix"},

		"Uembed.Func1": {"", "suffix"},

		// These are implementation dependent due to the ambiguous parsing.
		"Conflict_Conflict": {"", "suffix"},
		"Conflict_conflict": {"", "suffix"},

		"GFunc":   {"", "suffix"},
		"GType.M": {"", "suffix"},
	}

	for id := range got {
		if !reflect.DeepEqual(got[id], want[id]) {
			t.Errorf("classification mismatch for %q:\ngot  %q\nwant %q", id, got[id], want[id])
		}
		delete(want, id)
	}
	if len(want) > 0 {
		t.Errorf("did not find:\n%q", want)
	}
}

func exampleNames(exs []*doc.Example) (out []string) {
	for _, ex := range exs {
		out = append(out, ex.Suffix)
	}
	return out
}

func mustParse(fset *token.FileSet, filename, src string) *ast.File {
	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.SkipObjectResolution)
	if err != nil {
		panic(err)
	}
	return f
}
