// Licensed under the MIT license, see LICENSE file for details.

package quicktest_test

import (
	"bytes"
	"errors"
	"fmt"
	"strings"
	"testing"

	qt "github.com/frankban/quicktest"
)

var _ testing.TB = (*qt.C)(nil)

var cTests = []struct {
	about           string
	checker         qt.Checker
	got             interface{}
	args            []interface{}
	format          func(interface{}) string
	expectedFailure string
}{{
	about:   "success",
	checker: qt.Equals,
	got:     42,
	args:    []interface{}{42},
}, {
	about:   "failure",
	checker: qt.Equals,
	got:     "42",
	args:    []interface{}{"47"},
	expectedFailure: `
error:
  values are not equal
got:
  "42"
want:
  "47"
`,
}, {
	about:   "failure with % signs",
	checker: qt.Equals,
	got:     "42%x",
	args:    []interface{}{"47%y"},
	expectedFailure: `
error:
  values are not equal
got:
  "42%x"
want:
  "47%y"
`,
}, {
	about:   "failure with comment",
	checker: qt.Equals,
	got:     true,
	args:    []interface{}{false, qt.Commentf("apparently %v != %v", true, false)},
	expectedFailure: `
error:
  values are not equal
comment:
  apparently true != false
got:
  bool(true)
want:
  bool(false)
`,
}, {
	about:   "another failure with comment",
	checker: qt.IsNil,
	got:     42,
	args:    []interface{}{qt.Commentf("bad wolf: %d", 42)},
	expectedFailure: `
error:
  got non-nil value
comment:
  bad wolf: 42
got:
  int(42)
`,
}, {
	about:   "failure with constant comment",
	checker: qt.IsNil,
	got:     "something",
	args:    []interface{}{qt.Commentf("these are the voyages")},
	expectedFailure: `
error:
  got non-nil value
comment:
  these are the voyages
got:
  "something"
`,
}, {
	about:   "failure with empty comment",
	checker: qt.IsNil,
	got:     47,
	args:    []interface{}{qt.Commentf("")},
	expectedFailure: `
error:
  got non-nil value
got:
  int(47)
`,
}, {
	about:   "failure with multiple comments",
	checker: qt.IsNil,
	got:     42,
	args: []interface{}{
		qt.Commentf("bad wolf: %d", 42),
		qt.Commentf("second comment"),
	},
	expectedFailure: `
error:
  got non-nil value
comment:
  bad wolf: 42
comment:
  second comment
got:
  int(42)
`,
}, {
	about: "nil checker",
	expectedFailure: `
error:
  bad check: nil checker provided
`,
}, {
	about:   "not enough arguments",
	checker: qt.Equals,
	got:     42,
	args:    []interface{}{},
	expectedFailure: `
error:
  bad check: not enough arguments provided to checker: got 0, want 1
want args:
  want
`,
}, {
	about:   "not enough arguments with comment",
	checker: qt.DeepEquals,
	got:     42,
	args:    []interface{}{qt.Commentf("test %d", 0)},
	expectedFailure: `
error:
  bad check: not enough arguments provided to checker: got 0, want 1
comment:
  test 0
want args:
  want
`,
}, {
	about:   "too many arguments",
	checker: qt.Matches,
	got:     42,
	args:    []interface{}{42, 47},
	expectedFailure: `
error:
  bad check: too many arguments provided to checker: got 2, want 1
got args:
  []interface {}{
      int(42),
      int(47),
  }
want args:
  regexp
`,
}, {
	about:   "really too many arguments",
	checker: qt.DeepEquals,
	got:     42,
	args:    []interface{}{42, 47, nil, "stop"},
	expectedFailure: `
error:
  bad check: too many arguments provided to checker: got 4, want 1
got args:
  []interface {}{
      int(42),
      int(47),
      nil,
      "stop",
  }
want args:
  want
`,
}, {
	about:   "too many arguments with comment",
	checker: qt.IsNil,
	got:     42,
	args:    []interface{}{nil, qt.Commentf("these are the voyages")},
	expectedFailure: `
error:
  bad check: too many arguments provided to checker: got 1, want 0
comment:
  these are the voyages
got args:
  []interface {}{
      nil,
  }
`,
}, {
	about: "many arguments and notes",
	checker: &testingChecker{
		argNames: []string{"arg1", "arg2", "arg3"},
		addNotes: func(note func(key string, value interface{})) {
			note("note1", "these")
			note("note2", qt.Unquoted("are"))
			note("note3", "the")
			note("note4", "voyages")
			note("note5", true)
		},
		err: errors.New("bad wolf"),
	},
	got:  42,
	args: []interface{}{"val2", "val3"},
	expectedFailure: `
error:
  bad wolf
note1:
  "these"
note2:
  are
note3:
  "the"
note4:
  "voyages"
note5:
  bool(true)
arg1:
  int(42)
arg2:
  "val2"
arg3:
  "val3"
`,
}, {
	about: "many arguments and notes with the same value",
	checker: &testingChecker{
		argNames: []string{"arg1", "arg2", "arg3", "arg4"},
		addNotes: func(note func(key string, value interface{})) {
			note("note1", "value1")
			note("note2", []int{42})
			note("note3", "value1")
			note("note4", nil)
		},
		err: errors.New("bad wolf"),
	},
	got:  "value1",
	args: []interface{}{"value1", []int{42}, nil},
	expectedFailure: `
error:
  bad wolf
note1:
  "value1"
note2:
  []int{42}
note3:
  <same as "note1">
note4:
  nil
arg1:
  <same as "note1">
arg2:
  <same as "note1">
arg3:
  <same as "note2">
arg4:
  <same as "note4">
`,
}, {
	about: "many arguments and notes with custom format function",
	checker: &testingChecker{
		argNames: []string{"arg1", "arg2", "arg3"},
		addNotes: func(note func(key string, value interface{})) {
			note("note1", "these")
			note("note2", qt.Unquoted("are"))
			note("note3", "the")
			note("note4", "voyages")
			note("note5", true)
		},
		err: errors.New("bad wolf"),
	},
	got:  42,
	args: []interface{}{"val2", "val3"},
	format: func(v interface{}) string {
		return fmt.Sprintf("bad wolf %v", v)
	},
	expectedFailure: `
error:
  bad wolf
note1:
  bad wolf these
note2:
  are
note3:
  bad wolf the
note4:
  bad wolf voyages
note5:
  bad wolf true
arg1:
  bad wolf 42
arg2:
  bad wolf val2
arg3:
  bad wolf val3
`,
}, {
	about: "bad check with notes",
	checker: &testingChecker{
		argNames: []string{"got", "want"},
		addNotes: func(note func(key string, value interface{})) {
			note("note", 42)
		},
		err: qt.BadCheckf("bad wolf"),
	},
	got:  42,
	args: []interface{}{"want"},
	expectedFailure: `
error:
  bad check: bad wolf
note:
  int(42)
`,
}, {
	about: "silent failure with notes",
	checker: &testingChecker{
		argNames: []string{"got", "want"},
		addNotes: func(note func(key string, value interface{})) {
			note("note1", "first note")
			note("note2", qt.Unquoted("second note"))
		},
		err: qt.ErrSilent,
	},
	got:  42,
	args: []interface{}{"want"},
	expectedFailure: `
note1:
  "first note"
note2:
  second note
`,
}}

func TestCAssertCheck(t *testing.T) {
	for _, test := range cTests {
		t.Run("Assert: "+test.about, func(t *testing.T) {
			if test.format != nil {
				t.Skip("changing format not supported when using qt.Assert directly")
			}
			tt := &testingT{}
			ok := qt.Assert(tt, test.got, test.checker, test.args...)
			checkResult(t, ok, tt.fatalString(), test.expectedFailure)
			if tt.errorString() != "" {
				t.Fatalf("no error messages expected, but got %q", tt.errorString())
			}
		})
		t.Run("Check: "+test.about, func(t *testing.T) {
			if test.format != nil {
				t.Skip("changing format not supported when using qt.Check directly")
			}
			tt := &testingT{}
			ok := qt.Check(tt, test.got, test.checker, test.args...)
			checkResult(t, ok, tt.errorString(), test.expectedFailure)
			if tt.fatalString() != "" {
				t.Fatalf("no fatal messages expected, but got %q", tt.fatalString())
			}
		})
		t.Run("c.Assert: "+test.about, func(t *testing.T) {
			tt := &testingT{}
			c := qt.New(tt)
			if test.format != nil {
				c.SetFormat(test.format)
			}
			ok := c.Assert(test.got, test.checker, test.args...)
			checkResult(t, ok, tt.fatalString(), test.expectedFailure)
			if tt.errorString() != "" {
				t.Fatalf("no error messages expected, but got %q", tt.errorString())
			}
		})
		t.Run("c.Check: "+test.about, func(t *testing.T) {
			tt := &testingT{}
			c := qt.New(tt)
			if test.format != nil {
				c.SetFormat(test.format)
			}
			ok := c.Check(test.got, test.checker, test.args...)
			checkResult(t, ok, tt.errorString(), test.expectedFailure)
			if tt.fatalString() != "" {
				t.Fatalf("no fatal messages expected, but got %q", tt.fatalString())
			}
		})
	}
}

func TestCRunSuccess(t *testing.T) {
	tt := &testingT{}
	c := qt.New(tt)
	var run bool
	subTestName := "my test"
	ok := c.Run(subTestName, func(innerC *qt.C) {
		run = true
		if innerC == c {
			t.Fatal("subtest C: same instance provided")
		}
		if innerC.TB != tt.subTestT {
			t.Fatalf("subtest testing object: got %p, want %p", innerC.TB, tt.subTestT)
		}
		if tt.subTestName != subTestName {
			t.Fatalf("subtest name: got %q, want %q", tt.subTestName, subTestName)
		}
	})
	assertBool(t, run, true)
	assertBool(t, ok, false)

	// Simulate a test success.
	tt.subTestResult = true
	ok = c.Run(subTestName, func(innerC *qt.C) {})
	assertBool(t, ok, true)
}

func TestCRunOnBenchmark(t *testing.T) {
	called := false
	testing.Benchmark(func(b *testing.B) {
		c := qt.New(b)
		c.Run("c", func(c *qt.C) {
			b1, ok := c.TB.(*testing.B)
			if !ok {
				t.Errorf("c.TB is type %T not *testing.B", c.TB)
				return
			}
			if b1 == b {
				t.Errorf("c.TB hasn't been given a new B value")
				return
			}
			called = true
		})
	})
	if !called {
		t.Fatalf("sub-benchmark was never called")
	}
}

// wrongRun1 has Run method with wrong arg count.
type wrongRun1 struct {
	testing.TB
}

func (wrongRun1) Run() {}

// wrongRun2 has no Run method.
type wrongRun2 struct {
	testing.TB
}

// wrongRun3 has Run method that takes a type not
// assignable to testing.TB.
type wrongRun3 struct {
	testing.TB
}

func (wrongRun3) Run(string, func(string)) bool { return false }

// wrongRun4 has Run method that doesn't return bool.
type wrongRun4 struct {
	testing.TB
}

func (wrongRun4) Run(string, func(*testing.T)) {}

var CRunPanicTests = []struct {
	tb          testing.TB
	expectPanic string
}{{
	tb:          wrongRun1{},
	expectPanic: "wrong argument count for Run method",
}, {
	tb:          wrongRun2{},
	expectPanic: "no Run method",
}, {
	tb:          wrongRun3{},
	expectPanic: "bad first argument type for Run method",
}, {
	tb:          wrongRun4{},
	expectPanic: "wrong argument count for Run method",
}}

func TestCRunPanic(t *testing.T) {
	for _, test := range CRunPanicTests {
		t.Run(fmt.Sprintf("%T", test.tb), func(t *testing.T) {
			c := qt.New(test.tb)
			defer func() {
				got := recover()
				want := fmt.Sprintf(
					"cannot execute Run with underlying concrete type %T (%s)",
					test.tb, test.expectPanic,
				)
				if got != want {
					t.Fatalf("unexpected panic recover message; got %q want %q", got, want)
				}
			}()
			c.Run("panic", func(innerC *qt.C) {})
		})
	}
}

func TestCRunFormat(t *testing.T) {
	tt, innerTT := &testingT{}, &testingT{}
	c := qt.New(tt)
	c.SetFormat(func(v interface{}) string {
		return fmt.Sprintf("myfmt(%v)", v)
	})
	c.Run("my test", func(innerC *qt.C) {
		innerC.TB = innerTT
		innerC.Check(42, qt.Equals, nil)
	})
	assertPrefix(t, innerTT.errorString(), `
error:
  values are not equal
got:
  myfmt(42)
want:
  myfmt(<nil>)
`)
}

func TestHelper(t *testing.T) {
	tt := &testingT{}
	qt.Assert(tt, true, qt.IsFalse)
	if tt.helperCalls != 3 {
		t.Fatalf("want 3 calls (Assert, c.Assert, check), got %d", tt.helperCalls)
	}
}

func TestCHelper(t *testing.T) {
	tt := &testingT{}
	c := qt.New(tt)
	c.Assert(true, qt.IsFalse)
	if tt.helperCalls != 2 {
		t.Fatalf("want 2 calls (c.Assert, check), got %d", tt.helperCalls)
	}
}

func TestCParallel(t *testing.T) {
	tt := &testingT{}
	c := qt.New(tt)
	c.Parallel()
	if !tt.parallel {
		t.Fatalf("parallel not called")
	}
}

func TestCParallelPanic(t *testing.T) {
	c := qt.New(&testing.B{})
	defer func() {
		r := recover()
		if r != "cannot execute Parallel with underlying concrete type *testing.B" {
			t.Fatalf("unexpected panic recover: %v", r)
		}
	}()
	c.Parallel()
}

func TestCDefer(t *testing.T) {
	c := qt.New(t)
	var defers []int
	c.Run("subtest", func(c *qt.C) {
		c.Defer(func() { defers = append(defers, 1) })
		c.Defer(func() { defers = append(defers, 2) })
		// Calling Done twice should not do anything more.
		c.Done()
	})
	c.Assert(defers, qt.DeepEquals, []int{2, 1})
}

func TestCDeferCalledEvenAfterGoexit(t *testing.T) {
	// The testing package uses runtime.Goexit on
	// assertion failure, so check that defers are still
	// called in that case.
	c := qt.New(t)
	defers := 0
	c.Run("subtest", func(c *qt.C) {
		c.Defer(func() {
			defers++
		})
		c.Defer(func() {
			c.SkipNow()
		})
	})
	c.Assert(defers, qt.Equals, 1)
}

func TestCRunDefer(t *testing.T) {
	c := qt.New(t)
	defers := 0
	c.Run("subtest", func(c *qt.C) {
		c.Run("x", func(c *qt.C) {
			c.Defer(func() { defers++ })
		})
	})
	c.Assert(defers, qt.Equals, 1)
}

type customT struct {
	*testing.T
	data int
}

func (t *customT) Run(name string, f func(*customT)) bool {
	return t.T.Run(name, func(t1 *testing.T) {
		f(&customT{t1, t.data})
	})
}

func TestCRunCustomType(t *testing.T) {
	ct := &customT{t, 99}
	c := qt.New(ct)
	called := 0
	c.Run("test", func(c *qt.C) {
		called++
		ct1, ok := c.TB.(*customT)
		if !ok {
			t.Error("TB isn't expected type")
		}
		if ct1.data != ct.data {
			t.Errorf("data not copied correctly; got %v want %v", ct1.data, ct.data)
		}
		if ct1 == ct {
			t.Errorf("old instance passed, not new")
		}
	})
	if called != 1 {
		t.Fatalf("subtest was called %d times, not once", called)
	}
}

func checkResult(t *testing.T, ok bool, got, want string) {
	t.Helper()
	if want != "" {
		assertPrefix(t, got, want+"stack:\n")
		assertBool(t, ok, false)
		return
	}
	if got != "" {
		t.Fatalf("output:\ngot  %q\nwant empty", got)
	}
	assertBool(t, ok, true)
}

// testingT can be passed to qt.New for testing purposes.
type testingT struct {
	testing.TB

	errorBuf bytes.Buffer
	fatalBuf bytes.Buffer

	subTestResult bool
	subTestName   string
	subTestT      *testing.T

	helperCalls int
	parallel    bool
}

// Error overrides testing.TB.Error so that messages are collected.
func (t *testingT) Error(a ...interface{}) {
	fmt.Fprint(&t.errorBuf, a...)
}

// Fatal overrides testing.TB.Fatal so that messages are collected and the
// goroutine is not killed.
func (t *testingT) Fatal(a ...interface{}) {
	fmt.Fprint(&t.fatalBuf, a...)
}

// Parallel overrides testing.TB.Parallel in order to record the call.
func (t *testingT) Parallel() {
	t.parallel = true
}

// Helper overrides testing.TB.Helper in order to count calls.
func (t *testingT) Helper() {
	t.helperCalls += 1
}

// Fatal overrides *testing.T.Fatal so that messages are collected and the
// goroutine is not killed.
func (t *testingT) Run(name string, f func(t *testing.T)) bool {
	t.subTestName, t.subTestT = name, &testing.T{}
	f(t.subTestT)
	return t.subTestResult
}

// errorString returns the error message.
func (t *testingT) errorString() string {
	return t.errorBuf.String()
}

// fatalString returns the fatal error message.
func (t *testingT) fatalString() string {
	return t.fatalBuf.String()
}

// assertPrefix fails if the got value does not have the given prefix.
func assertPrefix(t testing.TB, got, prefix string) {
	t.Helper()
	if prefix == "" {
		t.Fatal("prefix: empty value provided")
	}
	if !strings.HasPrefix(got, prefix) {
		t.Fatalf(`prefix:
got  %q
want %q
-------------------- got --------------------
%s
-------------------- want -------------------
%s
---------------------------------------------`, got, prefix, got, prefix)
	}
}

// assertBool fails if the given boolean values don't match.
func assertBool(t testing.TB, got, want bool) {
	t.Helper()
	if got != want {
		t.Fatalf("bool:\ngot  %v\nwant %v", got, want)
	}
}

// testingChecker is a quicktest.Checker used in tests. It receives the
// provided argNames, adds notes via the provided addNotes function, and when
// the check is run the provided error is returned.
type testingChecker struct {
	argNames []string
	addNotes func(note func(key string, value interface{}))
	err      error
}

// Check implements quicktest.Checker by returning the stored error.
func (c *testingChecker) Check(got interface{}, args []interface{}, note func(key string, value interface{})) error {
	if c.addNotes != nil {
		c.addNotes(note)
	}
	return c.err
}

// Info implements quicktest.Checker by returning the stored args.
func (c *testingChecker) ArgNames() []string {
	return c.argNames
}
