// Package tt supports table-driven tests with little boilerplate.
//
// See the test case for this package for example usage.
package tt

import (
	"bytes"
	"fmt"
	"reflect"
)

// Table represents a test table.
type Table []*Case

// Case represents a test case. It is created by the C function, and offers
// setters that augment and return itself; those calls can be chained like
// C(...).Rets(...).
type Case struct {
	args         []interface{}
	retsMatchers [][]interface{}
}

// Args returns a new Case with the given arguments.
func Args(args ...interface{}) *Case {
	return &Case{args: args}
}

// Rets modifies the test case so that it requires the return values to match
// the given values. It returns the receiver.
func (c *Case) Rets(matchers ...interface{}) *Case {
	c.retsMatchers = append(c.retsMatchers, matchers)
	return c
}

func match(matchers, actual []interface{}) bool {
	for i, matcher := range matchers {
		// TODO: Support custom matching strategy
		if !reflect.DeepEqual(matcher, actual[i]) {
			return false
		}
	}
	return true
}

// FnToTest describes a function to test.
type FnToTest struct {
	name    string
	body    interface{}
	argsFmt string
	retsFmt string
}

// Fn makes a new FnToTest with the given function name and body.
func Fn(name string, body interface{}) *FnToTest {
	return &FnToTest{name: name, body: body}
}

// ArgsFmt sets the string for formatting arguments in test error messages, and
// return fn itself.
func (fn *FnToTest) ArgsFmt(s string) *FnToTest {
	fn.argsFmt = s
	return fn
}

// RetsFmt sets the string for formatting return values in test error messages,
// and return fn itself.
func (fn *FnToTest) RetsFmt(s string) *FnToTest {
	fn.retsFmt = s
	return fn
}

// T is the interface for accessing testing.T.
type T interface {
	Errorf(format string, args ...interface{})
}

// Test tests a function against test cases.
func Test(t T, fn *FnToTest, tests Table) {
	for _, test := range tests {
		rets := call(fn.body, test.args)
		for _, retsMatcher := range test.retsMatchers {
			if !match(retsMatcher, rets) {
				var argsString, retsString, wantRetsString string
				if fn.argsFmt == "" {
					argsString = sprintArgs(test.args...)
				} else {
					argsString = fmt.Sprintf(fn.argsFmt, test.args...)
				}
				if fn.retsFmt == "" {
					retsString = sprintRets(rets...)
					wantRetsString = sprintRets(retsMatcher...)
				} else {
					retsString = fmt.Sprintf(fn.retsFmt, rets...)
					wantRetsString = fmt.Sprintf(fn.retsFmt, retsMatcher...)
				}
				t.Errorf("%s(%s) -> %s, want %s", fn.name, argsString, retsString, wantRetsString)
			}
		}
	}
}

func sprintArgs(args ...interface{}) string {
	return sprintCommaDelimited(args...)
}

func sprintRets(rets ...interface{}) string {
	if len(rets) == 1 {
		return fmt.Sprint(rets[0])
	}
	return "(" + sprintCommaDelimited(rets...) + ")"
}

func sprintCommaDelimited(args ...interface{}) string {
	var b bytes.Buffer
	for i, arg := range args {
		if i > 0 {
			b.WriteString(", ")
		}
		fmt.Fprint(&b, arg)
	}
	return b.String()
}

func call(fn interface{}, args []interface{}) []interface{} {
	argsReflect := make([]reflect.Value, len(args))
	for i, arg := range args {
		argsReflect[i] = reflect.ValueOf(arg)
	}
	retsReflect := reflect.ValueOf(fn).Call(argsReflect)
	rets := make([]interface{}, len(retsReflect))
	for i, retReflect := range retsReflect {
		rets[i] = retReflect.Interface()
	}
	return rets
}
