// 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.

// run runs the docs tests found in this directory.
package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"time"
)

const usage = `go run run.go [tests]

run.go runs the docs tests in this directory.
If no tests are provided, it runs all tests.
Tests may be specified without their .go suffix.
`

func main() {
	start := time.Now()

	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, usage)
		flag.PrintDefaults()
		os.Exit(2)
	}

	flag.Parse()
	if flag.NArg() == 0 {
		// run all tests
		fixcgo()
	} else {
		// run specified tests
		onlyTest(flag.Args()...)
	}

	tmpdir, err := ioutil.TempDir("", "go-progs")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	// ratec limits the number of tests running concurrently.
	// None of the tests are intensive, so don't bother
	// trying to manually adjust for slow builders.
	ratec := make(chan bool, runtime.NumCPU())
	errc := make(chan error, len(tests))

	for _, tt := range tests {
		tt := tt
		ratec <- true
		go func() {
			errc <- test(tmpdir, tt.file, tt.want)
			<-ratec
		}()
	}

	var rc int
	for range tests {
		if err := <-errc; err != nil {
			fmt.Fprintln(os.Stderr, err)
			rc = 1
		}
	}
	os.Remove(tmpdir)
	if rc == 0 {
		fmt.Printf("ok\t%s\t%s\n", filepath.Base(os.Args[0]), time.Since(start).Round(time.Millisecond))
	}
	os.Exit(rc)
}

// test builds the test in the given file.
// If want is non-empty, test also runs the test
// and checks that the output matches the regexp want.
func test(tmpdir, file, want string) error {
	// Build the program.
	prog := filepath.Join(tmpdir, file+".exe")
	cmd := exec.Command("go", "build", "-o", prog, file+".go")
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("go build %s.go failed: %v\nOutput:\n%s", file, err, out)
	}
	defer os.Remove(prog)

	// Only run the test if we have output to check.
	if want == "" {
		return nil
	}

	cmd = exec.Command(prog)
	out, err = cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("%s failed: %v\nOutput:\n%s", file, err, out)
	}

	// Canonicalize output.
	out = bytes.TrimRight(out, "\n")
	out = bytes.Replace(out, []byte{'\n'}, []byte{' '}, -1)

	// Check the result.
	match, err := regexp.Match(want, out)
	if err != nil {
		return fmt.Errorf("failed to parse regexp %q: %v", want, err)
	}
	if !match {
		return fmt.Errorf("%s.go:\n%q\ndoes not match %s", file, out, want)
	}

	return nil
}

type testcase struct {
	file string
	want string
}

var tests = []testcase{
	// defer_panic_recover
	{"defer", `^0 3210 2$`},
	{"defer2", `^Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.$`},

	// effective_go
	{"eff_bytesize", `^1.00YB 9.09TB$`},
	{"eff_qr", ""},
	{"eff_sequence", `^\[-1 2 6 16 44\]$`},
	{"eff_unused2", ""},

	// error_handling
	{"error", ""},
	{"error2", ""},
	{"error3", ""},
	{"error4", ""},

	// law_of_reflection
	{"interface", ""},
	{"interface2", `^type: float64$`},

	// c_go_cgo
	{"cgo1", ""},
	{"cgo2", ""},
	{"cgo3", ""},
	{"cgo4", ""},

	// timeout
	{"timeout1", ""},
	{"timeout2", ""},

	// gobs
	{"gobs1", ""},
	{"gobs2", ""},

	// json
	{"json1", `^$`},
	{"json2", `the reciprocal of i is`},
	{"json3", `Age is int 6`},
	{"json4", `^$`},
	{"json5", ""},

	// image_package
	{"image_package1", `^X is 2 Y is 1$`},
	{"image_package2", `^3 4 false$`},
	{"image_package3", `^3 4 true$`},
	{"image_package4", `^image.Point{X:2, Y:1}$`},
	{"image_package5", `^{255 0 0 255}$`},
	{"image_package6", `^8 4 true$`},

	// other
	{"go1", `^Christmas is a holiday: true .*go1.go already exists$`},
	{"slices", ""},
}

func onlyTest(files ...string) {
	var new []testcase
NextFile:
	for _, file := range files {
		file = strings.TrimSuffix(file, ".go")
		for _, tt := range tests {
			if tt.file == file {
				new = append(new, tt)
				continue NextFile
			}
		}
		fmt.Fprintf(os.Stderr, "test %s.go not found\n", file)
		os.Exit(1)
	}
	tests = new
}

func skipTest(file string) {
	for i, tt := range tests {
		if tt.file == file {
			copy(tests[i:], tests[i+1:])
			tests = tests[:len(tests)-1]
			return
		}
	}
	panic("delete(" + file + "): not found")
}

func fixcgo() {
	if os.Getenv("CGO_ENABLED") != "1" {
		skipTest("cgo1")
		skipTest("cgo2")
		skipTest("cgo3")
		skipTest("cgo4")
		return
	}

	switch runtime.GOOS {
	case "freebsd":
		// cgo1 and cgo2 don't run on freebsd, srandom has a different signature
		skipTest("cgo1")
		skipTest("cgo2")
	case "netbsd":
		// cgo1 and cgo2 don't run on netbsd, srandom has a different signature
		skipTest("cgo1")
		skipTest("cgo2")
	}
}
