// Copyright 2015 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 main

import (
	"bytes"
	"flag"
	"regexp"
	"runtime"
	"strings"
	"testing"
)

func maybeSkip(t *testing.T) {
	if strings.HasPrefix(runtime.GOOS, "nacl") {
		t.Skip("nacl does not have a full file tree")
	}
	if runtime.GOOS == "darwin" && strings.HasPrefix(runtime.GOARCH, "arm") {
		t.Skip("darwin/arm does not have a full file tree")
	}
}

type test struct {
	name string
	args []string // Arguments to "[go] doc".
	yes  []string // Regular expressions that should match.
	no   []string // Regular expressions that should not match.
}

const p = "cmd/doc/testdata"

var tests = []test{
	// Sanity check.
	{
		"sanity check",
		[]string{p},
		[]string{`type ExportedType struct`},
		nil,
	},

	// Package dump includes import, package statement.
	{
		"package clause",
		[]string{p},
		[]string{`package pkg.*cmd/doc/testdata`},
		nil,
	},

	// Constants.
	// Package dump
	{
		"full package",
		[]string{p},
		[]string{
			`Package comment`,
			`const ExportedConstant = 1`,                                   // Simple constant.
			`const ConstOne = 1`,                                           // First entry in constant block.
			`const ConstFive ...`,                                          // From block starting with unexported constant.
			`var ExportedVariable = 1`,                                     // Simple variable.
			`var VarOne = 1`,                                               // First entry in variable block.
			`func ExportedFunc\(a int\) bool`,                              // Function.
			`func ReturnUnexported\(\) unexportedType`,                     // Function with unexported return type.
			`type ExportedType struct{ ... }`,                              // Exported type.
			`const ExportedTypedConstant ExportedType = iota`,              // Typed constant.
			`const ExportedTypedConstant_unexported unexportedType`,        // Typed constant, exported for unexported type.
			`const ConstLeft2 uint64 ...`,                                  // Typed constant using unexported iota.
			`const ConstGroup1 unexportedType = iota ...`,                  // Typed constant using unexported type.
			`const ConstGroup4 ExportedType = ExportedType{}`,              // Typed constant using exported type.
			`const MultiLineConst = ...`,                                   // Multi line constant.
			`var MultiLineVar = map\[struct{ ... }\]struct{ ... }{ ... }`,  // Multi line variable.
			`func MultiLineFunc\(x interface{ ... }\) \(r struct{ ... }\)`, // Multi line function.
			`var LongLine = newLongLine\(("someArgument[1-4]", ){4}...\)`,  // Long list of arguments.
			`type T1 = T2`, // Type alias
		},
		[]string{
			`const internalConstant = 2`,        // No internal constants.
			`var internalVariable = 2`,          // No internal variables.
			`func internalFunc(a int) bool`,     // No internal functions.
			`Comment about exported constant`,   // No comment for single constant.
			`Comment about exported variable`,   // No comment for single variable.
			`Comment about block of constants.`, // No comment for constant block.
			`Comment about block of variables.`, // No comment for variable block.
			`Comment before ConstOne`,           // No comment for first entry in constant block.
			`Comment before VarOne`,             // No comment for first entry in variable block.
			`ConstTwo = 2`,                      // No second entry in constant block.
			`VarTwo = 2`,                        // No second entry in variable block.
			`VarFive = 5`,                       // From block starting with unexported variable.
			`type unexportedType`,               // No unexported type.
			`unexportedTypedConstant`,           // No unexported typed constant.
			`\bField`,                           // No fields.
			`Method`,                            // No methods.
			`someArgument[5-8]`,                 // No truncated arguments.
			`type T1 T2`,                        // Type alias does not display as type declaration.
		},
	},
	// Package dump -u
	{
		"full package with u",
		[]string{`-u`, p},
		[]string{
			`const ExportedConstant = 1`,               // Simple constant.
			`const internalConstant = 2`,               // Internal constants.
			`func internalFunc\(a int\) bool`,          // Internal functions.
			`func ReturnUnexported\(\) unexportedType`, // Function with unexported return type.
		},
		[]string{
			`Comment about exported constant`,  // No comment for simple constant.
			`Comment about block of constants`, // No comment for constant block.
			`Comment about internal function`,  // No comment for internal function.
			`MultiLine(String|Method|Field)`,   // No data from multi line portions.
		},
	},

	// Single constant.
	{
		"single constant",
		[]string{p, `ExportedConstant`},
		[]string{
			`Comment about exported constant`, // Include comment.
			`const ExportedConstant = 1`,
		},
		nil,
	},
	// Single constant -u.
	{
		"single constant with -u",
		[]string{`-u`, p, `internalConstant`},
		[]string{
			`Comment about internal constant`, // Include comment.
			`const internalConstant = 2`,
		},
		nil,
	},
	// Block of constants.
	{
		"block of constants",
		[]string{p, `ConstTwo`},
		[]string{
			`Comment before ConstOne.\n.*ConstOne = 1`,    // First...
			`ConstTwo = 2.*Comment on line with ConstTwo`, // And second show up.
			`Comment about block of constants`,            // Comment does too.
		},
		[]string{
			`constThree`, // No unexported constant.
		},
	},
	// Block of constants -u.
	{
		"block of constants with -u",
		[]string{"-u", p, `constThree`},
		[]string{
			`constThree = 3.*Comment on line with constThree`,
		},
		nil,
	},
	// Block of constants with carryover type from unexported field.
	{
		"block of constants with carryover type",
		[]string{p, `ConstLeft2`},
		[]string{
			`ConstLeft2, constRight2 uint64`,
			`constLeft3, ConstRight3`,
			`ConstLeft4, ConstRight4`,
		},
		nil,
	},
	// Block of constants -u with carryover type from unexported field.
	{
		"block of constants with carryover type",
		[]string{"-u", p, `ConstLeft2`},
		[]string{
			`_, _ uint64 = 2 \* iota, 1 << iota`,
			`constLeft1, constRight1`,
			`ConstLeft2, constRight2`,
			`constLeft3, ConstRight3`,
			`ConstLeft4, ConstRight4`,
		},
		nil,
	},

	// Single variable.
	{
		"single variable",
		[]string{p, `ExportedVariable`},
		[]string{
			`ExportedVariable`, // Include comment.
			`var ExportedVariable = 1`,
		},
		nil,
	},
	// Single variable -u.
	{
		"single variable with -u",
		[]string{`-u`, p, `internalVariable`},
		[]string{
			`Comment about internal variable`, // Include comment.
			`var internalVariable = 2`,
		},
		nil,
	},
	// Block of variables.
	{
		"block of variables",
		[]string{p, `VarTwo`},
		[]string{
			`Comment before VarOne.\n.*VarOne = 1`,    // First...
			`VarTwo = 2.*Comment on line with VarTwo`, // And second show up.
			`Comment about block of variables`,        // Comment does too.
		},
		[]string{
			`varThree= 3`, // No unexported variable.
		},
	},
	// Block of variables -u.
	{
		"block of variables with -u",
		[]string{"-u", p, `varThree`},
		[]string{
			`varThree = 3.*Comment on line with varThree`,
		},
		nil,
	},

	// Function.
	{
		"function",
		[]string{p, `ExportedFunc`},
		[]string{
			`Comment about exported function`, // Include comment.
			`func ExportedFunc\(a int\) bool`,
		},
		nil,
	},
	// Function -u.
	{
		"function with -u",
		[]string{"-u", p, `internalFunc`},
		[]string{
			`Comment about internal function`, // Include comment.
			`func internalFunc\(a int\) bool`,
		},
		nil,
	},

	// Type.
	{
		"type",
		[]string{p, `ExportedType`},
		[]string{
			`Comment about exported type`, // Include comment.
			`type ExportedType struct`,    // Type definition.
			`Comment before exported field.*\n.*ExportedField +int` +
				`.*Comment on line with exported field.`,
			`ExportedEmbeddedType.*Comment on line with exported embedded field.`,
			`Has unexported fields`,
			`func \(ExportedType\) ExportedMethod\(a int\) bool`,
			`const ExportedTypedConstant ExportedType = iota`, // Must include associated constant.
			`func ExportedTypeConstructor\(\) \*ExportedType`, // Must include constructor.
			`io.Reader.*Comment on line with embedded Reader.`,
		},
		[]string{
			`unexportedField`,                // No unexported field.
			`int.*embedded`,                  // No unexported embedded field.
			`Comment about exported method.`, // No comment about exported method.
			`unexportedMethod`,               // No unexported method.
			`unexportedTypedConstant`,        // No unexported constant.
			`error`,                          // No embedded error.
		},
	},
	// Type T1 dump (alias).
	{
		"type T1",
		[]string{p + ".T1"},
		[]string{
			`type T1 = T2`,
		},
		[]string{
			`type T1 T2`,
			`type ExportedType`,
		},
	},
	// Type -u with unexported fields.
	{
		"type with unexported fields and -u",
		[]string{"-u", p, `ExportedType`},
		[]string{
			`Comment about exported type`, // Include comment.
			`type ExportedType struct`,    // Type definition.
			`Comment before exported field.*\n.*ExportedField +int`,
			`unexportedField.*int.*Comment on line with unexported field.`,
			`ExportedEmbeddedType.*Comment on line with exported embedded field.`,
			`\*ExportedEmbeddedType.*Comment on line with exported embedded \*field.`,
			`\*qualified.ExportedEmbeddedType.*Comment on line with exported embedded \*selector.field.`,
			`unexportedType.*Comment on line with unexported embedded field.`,
			`\*unexportedType.*Comment on line with unexported embedded \*field.`,
			`io.Reader.*Comment on line with embedded Reader.`,
			`error.*Comment on line with embedded error.`,
			`func \(ExportedType\) unexportedMethod\(a int\) bool`,
			`unexportedTypedConstant`,
		},
		[]string{
			`Has unexported fields`,
		},
	},
	// Unexported type with -u.
	{
		"unexported type with -u",
		[]string{"-u", p, `unexportedType`},
		[]string{
			`Comment about unexported type`, // Include comment.
			`type unexportedType int`,       // Type definition.
			`func \(unexportedType\) ExportedMethod\(\) bool`,
			`func \(unexportedType\) unexportedMethod\(\) bool`,
			`ExportedTypedConstant_unexported unexportedType = iota`,
			`const unexportedTypedConstant unexportedType = 1`,
		},
		nil,
	},

	// Interface.
	{
		"interface type",
		[]string{p, `ExportedInterface`},
		[]string{
			`Comment about exported interface`, // Include comment.
			`type ExportedInterface interface`, // Interface definition.
			`Comment before exported method.*\n.*ExportedMethod\(\)` +
				`.*Comment on line with exported method`,
			`io.Reader.*Comment on line with embedded Reader.`,
			`error.*Comment on line with embedded error.`,
			`Has unexported methods`,
		},
		[]string{
			`unexportedField`,               // No unexported field.
			`Comment about exported method`, // No comment about exported method.
			`unexportedMethod`,              // No unexported method.
			`unexportedTypedConstant`,       // No unexported constant.
		},
	},
	// Interface -u with unexported methods.
	{
		"interface type with unexported methods and -u",
		[]string{"-u", p, `ExportedInterface`},
		[]string{
			`Comment about exported interface`, // Include comment.
			`type ExportedInterface interface`, // Interface definition.
			`Comment before exported method.*\n.*ExportedMethod\(\)` +
				`.*Comment on line with exported method`,
			`unexportedMethod\(\).*Comment on line with unexported method.`,
			`io.Reader.*Comment on line with embedded Reader.`,
			`error.*Comment on line with embedded error.`,
		},
		[]string{
			`Has unexported methods`,
		},
	},

	// Interface method.
	{
		"interface method",
		[]string{p, `ExportedInterface.ExportedMethod`},
		[]string{
			`Comment before exported method.*\n.*ExportedMethod\(\)` +
				`.*Comment on line with exported method`,
		},
		[]string{
			`Comment about exported interface.`,
		},
	},

	// Method.
	{
		"method",
		[]string{p, `ExportedType.ExportedMethod`},
		[]string{
			`func \(ExportedType\) ExportedMethod\(a int\) bool`,
			`Comment about exported method.`,
		},
		nil,
	},
	// Method  with -u.
	{
		"method with -u",
		[]string{"-u", p, `ExportedType.unexportedMethod`},
		[]string{
			`func \(ExportedType\) unexportedMethod\(a int\) bool`,
			`Comment about unexported method.`,
		},
		nil,
	},

	// Field.
	{
		"field",
		[]string{p, `ExportedType.ExportedField`},
		[]string{
			`type ExportedType struct`,
			`ExportedField int`,
			`Comment before exported field.`,
			`Comment on line with exported field.`,
			`other fields elided`,
		},
		nil,
	},

	// Field with -u.
	{
		"method with -u",
		[]string{"-u", p, `ExportedType.unexportedField`},
		[]string{
			`unexportedField int`,
			`Comment on line with unexported field.`,
		},
		nil,
	},

	// Field of struct with only one field.
	{
		"single-field struct",
		[]string{p, `ExportedStructOneField.OnlyField`},
		[]string{`the only field`},
		[]string{`other fields elided`},
	},

	// Case matching off.
	{
		"case matching off",
		[]string{p, `casematch`},
		[]string{
			`CaseMatch`,
			`Casematch`,
		},
		nil,
	},

	// Case matching on.
	{
		"case matching on",
		[]string{"-c", p, `Casematch`},
		[]string{
			`Casematch`,
		},
		[]string{
			`CaseMatch`,
		},
	},

	// No dups with -u. Issue 21797.
	{
		"case matching on, no dups",
		[]string{"-u", p, `duplicate`},
		[]string{
			`Duplicate`,
			`duplicate`,
		},
		[]string{
			"\\)\n+const", // This will appear if the const decl appears twice.
		},
	},
}

func TestDoc(t *testing.T) {
	maybeSkip(t)
	for _, test := range tests {
		var b bytes.Buffer
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, test.args)
		if err != nil {
			t.Fatalf("%s: %s\n", test.name, err)
		}
		output := b.Bytes()
		failed := false
		for j, yes := range test.yes {
			re, err := regexp.Compile(yes)
			if err != nil {
				t.Fatalf("%s.%d: compiling %#q: %s", test.name, j, yes, err)
			}
			if !re.Match(output) {
				t.Errorf("%s.%d: no match for %s %#q", test.name, j, test.args, yes)
				failed = true
			}
		}
		for j, no := range test.no {
			re, err := regexp.Compile(no)
			if err != nil {
				t.Fatalf("%s.%d: compiling %#q: %s", test.name, j, no, err)
			}
			if re.Match(output) {
				t.Errorf("%s.%d: incorrect match for %s %#q", test.name, j, test.args, no)
				failed = true
			}
		}
		if failed {
			t.Logf("\n%s", output)
		}
	}
}

// Test the code to try multiple packages. Our test case is
//	go doc rand.Float64
// This needs to find math/rand.Float64; however crypto/rand, which doesn't
// have the symbol, usually appears first in the directory listing.
func TestMultiplePackages(t *testing.T) {
	if testing.Short() {
		t.Skip("scanning file system takes too long")
	}
	maybeSkip(t)
	var b bytes.Buffer // We don't care about the output.
	// Make sure crypto/rand does not have the symbol.
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"crypto/rand.float64"})
		if err == nil {
			t.Errorf("expected error from crypto/rand.float64")
		} else if !strings.Contains(err.Error(), "no symbol float64") {
			t.Errorf("unexpected error %q from crypto/rand.float64", err)
		}
	}
	// Make sure math/rand does have the symbol.
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"math/rand.float64"})
		if err != nil {
			t.Errorf("unexpected error %q from math/rand.float64", err)
		}
	}
	// Try the shorthand.
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"rand.float64"})
		if err != nil {
			t.Errorf("unexpected error %q from rand.float64", err)
		}
	}
	// Now try a missing symbol. We should see both packages in the error.
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"rand.doesnotexit"})
		if err == nil {
			t.Errorf("expected error from rand.doesnotexit")
		} else {
			errStr := err.Error()
			if !strings.Contains(errStr, "no symbol") {
				t.Errorf("error %q should contain 'no symbol", errStr)
			}
			if !strings.Contains(errStr, "crypto/rand") {
				t.Errorf("error %q should contain crypto/rand", errStr)
			}
			if !strings.Contains(errStr, "math/rand") {
				t.Errorf("error %q should contain math/rand", errStr)
			}
		}
	}
}

// Test the code to look up packages when given two args. First test case is
//	go doc binary BigEndian
// This needs to find encoding/binary.BigEndian, which means
// finding the package encoding/binary given only "binary".
// Second case is
//	go doc rand Float64
// which again needs to find math/rand and not give up after crypto/rand,
// which has no such function.
func TestTwoArgLookup(t *testing.T) {
	if testing.Short() {
		t.Skip("scanning file system takes too long")
	}
	maybeSkip(t)
	var b bytes.Buffer // We don't care about the output.
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"binary", "BigEndian"})
		if err != nil {
			t.Errorf("unexpected error %q from binary BigEndian", err)
		}
	}
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"rand", "Float64"})
		if err != nil {
			t.Errorf("unexpected error %q from rand Float64", err)
		}
	}
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"bytes", "Foo"})
		if err == nil {
			t.Errorf("expected error from bytes Foo")
		} else if !strings.Contains(err.Error(), "no symbol Foo") {
			t.Errorf("unexpected error %q from bytes Foo", err)
		}
	}
	{
		var flagSet flag.FlagSet
		err := do(&b, &flagSet, []string{"nosuchpackage", "Foo"})
		if err == nil {
			// actually present in the user's filesystem
		} else if !strings.Contains(err.Error(), "no such package") {
			t.Errorf("unexpected error %q from nosuchpackage Foo", err)
		}
	}
}

type trimTest struct {
	path   string
	prefix string
	result string
	ok     bool
}

var trimTests = []trimTest{
	{"", "", "", true},
	{"/usr/gopher", "/usr/gopher", "/usr/gopher", true},
	{"/usr/gopher/bar", "/usr/gopher", "bar", true},
	{"/usr/gopherflakes", "/usr/gopher", "/usr/gopherflakes", false},
	{"/usr/gopher/bar", "/usr/zot", "/usr/gopher/bar", false},
}

func TestTrim(t *testing.T) {
	for _, test := range trimTests {
		result, ok := trim(test.path, test.prefix)
		if ok != test.ok {
			t.Errorf("%s %s expected %t got %t", test.path, test.prefix, test.ok, ok)
			continue
		}
		if result != test.result {
			t.Errorf("%s %s expected %q got %q", test.path, test.prefix, test.result, result)
			continue
		}
	}
}
