// Copyright 2014 Roger Peppe.
// See LICENCE file for details.

package errgo_test

import (
	"fmt"
	"io/ioutil"
	"runtime"
	"strings"
	"testing"

	gc "launchpad.net/gocheck"

	"gopkg.in/errgo.v1"
)

var (
	_ errgo.Wrapper    = (*errgo.Err)(nil)
	_ errgo.Locationer = (*errgo.Err)(nil)
	_ errgo.Causer     = (*errgo.Err)(nil)
)

func Test(t *testing.T) {
	gc.TestingT(t)
}

type errorsSuite struct{}

var _ = gc.Suite(&errorsSuite{})

func (*errorsSuite) TestNew(c *gc.C) {
	err := errgo.New("foo") //err TestNew
	checkErr(c, err, nil, "foo", "[{$TestNew$: foo}]", err)
}

func (*errorsSuite) TestNewf(c *gc.C) {
	err := errgo.Newf("foo %d", 5) //err TestNewf
	checkErr(c, err, nil, "foo 5", "[{$TestNewf$: foo 5}]", err)
}

var someErr = errgo.New("some error") //err varSomeErr

func annotate1() error {
	err := errgo.Notef(someErr, "annotate1") //err annotate1
	return err
}

func annotate2() error {
	err := annotate1()
	err = errgo.Notef(err, "annotate2") //err annotate2
	return err
}

func (*errorsSuite) TestNoteUsage(c *gc.C) {
	err0 := annotate2()
	err, ok := err0.(errgo.Wrapper)
	c.Assert(ok, gc.Equals, true)
	underlying := err.Underlying()
	checkErr(
		c, err0, underlying,
		"annotate2: annotate1: some error",
		"[{$annotate2$: annotate2} {$annotate1$: annotate1} {$varSomeErr$: some error}]",
		err0)
}

func (*errorsSuite) TestMask(c *gc.C) {
	err0 := errgo.WithCausef(nil, someErr, "foo") //err TestMask#0
	err := errgo.Mask(err0)                       //err TestMask#1
	checkErr(c, err, err0, "foo", "[{$TestMask#1$: } {$TestMask#0$: foo}]", err)

	err = errgo.Mask(nil)
	c.Assert(err, gc.IsNil)
}

func (*errorsSuite) TestNotef(c *gc.C) {
	err0 := errgo.WithCausef(nil, someErr, "foo") //err TestNotef#0
	err := errgo.Notef(err0, "bar")               //err TestNotef#1
	checkErr(c, err, err0, "bar: foo", "[{$TestNotef#1$: bar} {$TestNotef#0$: foo}]", err)

	err = errgo.Notef(nil, "bar") //err TestNotef#2
	checkErr(c, err, nil, "bar", "[{$TestNotef#2$: bar}]", err)
}

func (*errorsSuite) TestNoteMask(c *gc.C) {
	err0 := errgo.WithCausef(nil, someErr, "foo") //err TestNoteMask#0
	err := errgo.NoteMask(err0, "bar")            //err TestNoteMask#1
	checkErr(c, err, err0, "bar: foo", "[{$TestNoteMask#1$: bar} {$TestNoteMask#0$: foo}]", err)

	err = errgo.NoteMask(err0, "bar", errgo.Is(someErr)) //err TestNoteMask#2
	checkErr(c, err, err0, "bar: foo", "[{$TestNoteMask#2$: bar} {$TestNoteMask#0$: foo}]", someErr)

	err = errgo.NoteMask(err0, "") //err TestNoteMask#3
	checkErr(c, err, err0, "foo", "[{$TestNoteMask#3$: } {$TestNoteMask#0$: foo}]", err)
}

func (*errorsSuite) TestMaskFunc(c *gc.C) {
	err0 := errgo.New("zero")
	err1 := errgo.New("one")

	allowVals := func(vals ...error) (r []func(error) bool) {
		for _, val := range vals {
			r = append(r, errgo.Is(val))
		}
		return
	}
	tests := []struct {
		err    error
		allow0 []func(error) bool
		allow1 []func(error) bool
		cause  error
	}{{
		err:    err0,
		allow0: allowVals(err0),
		cause:  err0,
	}, {
		err:    err1,
		allow0: allowVals(err0),
		cause:  nil,
	}, {
		err:    err0,
		allow1: allowVals(err0),
		cause:  err0,
	}, {
		err:    err0,
		allow0: allowVals(err1),
		allow1: allowVals(err0),
		cause:  err0,
	}, {
		err:    err0,
		allow0: allowVals(err0, err1),
		cause:  err0,
	}, {
		err:    err1,
		allow0: allowVals(err0, err1),
		cause:  err1,
	}, {
		err:    err0,
		allow1: allowVals(err0, err1),
		cause:  err0,
	}, {
		err:    err1,
		allow1: allowVals(err0, err1),
		cause:  err1,
	}}
	for i, test := range tests {
		c.Logf("test %d", i)
		wrap := errgo.MaskFunc(test.allow0...)
		err := wrap(test.err, test.allow1...)
		cause := errgo.Cause(err)
		wantCause := test.cause
		if wantCause == nil {
			wantCause = err
		}
		c.Check(cause, gc.Equals, wantCause)
	}
}

type embed struct {
	*errgo.Err
}

func (*errorsSuite) TestCause(c *gc.C) {
	c.Assert(errgo.Cause(someErr), gc.Equals, someErr)

	causeErr := errgo.New("cause error")
	underlyingErr := errgo.New("underlying error")                 //err TestCause#1
	err := errgo.WithCausef(underlyingErr, causeErr, "foo %d", 99) //err TestCause#2
	c.Assert(errgo.Cause(err), gc.Equals, causeErr)

	checkErr(c, err, underlyingErr, "foo 99: underlying error", "[{$TestCause#2$: foo 99} {$TestCause#1$: underlying error}]", causeErr)

	err = &embed{err.(*errgo.Err)}
	c.Assert(errgo.Cause(err), gc.Equals, causeErr)
}

func (*errorsSuite) TestWithCausefNoMessage(c *gc.C) {
	cause := errgo.New("cause")
	err := errgo.WithCausef(nil, cause, "")
	c.Assert(err, gc.ErrorMatches, "cause")
	c.Assert(errgo.Cause(err), gc.Equals, cause)
}

func (*errorsSuite) TestDetails(c *gc.C) {
	c.Assert(errgo.Details(nil), gc.Equals, "[]")

	otherErr := fmt.Errorf("other")
	checkErr(c, otherErr, nil, "other", "[{other}]", otherErr)

	err0 := &embed{errgo.New("foo").(*errgo.Err)} //err TestStack#0
	checkErr(c, err0, nil, "foo", "[{$TestStack#0$: foo}]", err0)

	err1 := &embed{errgo.Notef(err0, "bar").(*errgo.Err)} //err TestStack#1
	checkErr(c, err1, err0, "bar: foo", "[{$TestStack#1$: bar} {$TestStack#0$: foo}]", err1)

	err2 := errgo.Mask(err1) //err TestStack#2
	checkErr(c, err2, err1, "bar: foo", "[{$TestStack#2$: } {$TestStack#1$: bar} {$TestStack#0$: foo}]", err2)
}

func (*errorsSuite) TestMatch(c *gc.C) {
	type errTest func(error) bool
	allow := func(ss ...string) []func(error) bool {
		fns := make([]func(error) bool, len(ss))
		for i, s := range ss {
			s := s
			fns[i] = func(err error) bool {
				return err != nil && err.Error() == s
			}
		}
		return fns
	}
	tests := []struct {
		err error
		fns []func(error) bool
		ok  bool
	}{{
		err: errgo.New("foo"),
		fns: allow("foo"),
		ok:  true,
	}, {
		err: errgo.New("foo"),
		fns: allow("bar"),
		ok:  false,
	}, {
		err: errgo.New("foo"),
		fns: allow("bar", "foo"),
		ok:  true,
	}, {
		err: errgo.New("foo"),
		fns: nil,
		ok:  false,
	}, {
		err: nil,
		fns: nil,
		ok:  false,
	}}

	for i, test := range tests {
		c.Logf("test %d", i)
		c.Assert(errgo.Match(test.err, test.fns...), gc.Equals, test.ok)
	}
}

func checkErr(c *gc.C, err, underlying error, msg string, details string, cause error) {
	c.Assert(err, gc.NotNil)
	c.Assert(err.Error(), gc.Equals, msg)
	if err, ok := err.(errgo.Wrapper); ok {
		c.Assert(err.Underlying(), gc.Equals, underlying)
	} else {
		c.Assert(underlying, gc.IsNil)
	}
	c.Assert(errgo.Cause(err), gc.Equals, cause)
	wantDetails := replaceLocations(details)
	c.Assert(errgo.Details(err), gc.Equals, wantDetails)
}

func replaceLocations(s string) string {
	t := ""
	for {
		i := strings.Index(s, "$")
		if i == -1 {
			break
		}
		t += s[0:i]
		s = s[i+1:]
		i = strings.Index(s, "$")
		if i == -1 {
			panic("no second $")
		}
		file, line := location(s[0:i])
		t += fmt.Sprintf("%s:%d", file, line)
		s = s[i+1:]
	}
	t += s
	return t
}

func location(tag string) (string, int) {
	line, ok := tagToLine[tag]
	if !ok {
		panic(fmt.Errorf("tag %q not found", tag))
	}
	return filename, line
}

var tagToLine = make(map[string]int)
var filename string

func init() {
	data, err := ioutil.ReadFile("errors_test.go")
	if err != nil {
		panic(err)
	}
	lines := strings.Split(string(data), "\n")
	for i, line := range lines {
		if j := strings.Index(line, "//err "); j >= 0 {
			tagToLine[line[j+len("//err "):]] = i + 1
		}
	}
	_, filename, _, _ = runtime.Caller(0)
}
