package convey

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"path"
	"runtime"
	"strconv"
	"strings"
	"testing"

	"github.com/smartystreets/goconvey/convey/reporting"
)

func TestSingleScopeReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		So(1, ShouldEqual, 1)
	})

	expectEqual(t, "Begin|A|Success|Exit|End", myReporter.wholeStory())
}

func TestNestedScopeReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		Convey("B", func() {
			So(1, ShouldEqual, 1)
		})
	})

	expectEqual(t, "Begin|A|B|Success|Exit|Exit|End", myReporter.wholeStory())
}

func TestFailureReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		So(1, ShouldBeNil)
	})

	expectEqual(t, "Begin|A|Failure|Exit|End", myReporter.wholeStory())
}

func TestFirstFailureEndsScopeExecution(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		So(1, ShouldBeNil)
		So(nil, ShouldBeNil)
	})

	expectEqual(t, "Begin|A|Failure|Exit|End", myReporter.wholeStory())
}

func TestComparisonFailureDeserializedAndReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		So("hi", ShouldEqual, "bye")
	})

	expectEqual(t, "Begin|A|Failure(bye/hi)|Exit|End", myReporter.wholeStory())
}

func TestNestedFailureReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		Convey("B", func() {
			So(2, ShouldBeNil)
		})
	})

	expectEqual(t, "Begin|A|B|Failure|Exit|Exit|End", myReporter.wholeStory())
}

func TestSuccessAndFailureReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		So(nil, ShouldBeNil)
		So(1, ShouldBeNil)
	})

	expectEqual(t, "Begin|A|Success|Failure|Exit|End", myReporter.wholeStory())
}

func TestIncompleteActionReportedAsSkipped(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		Convey("B", nil)
	})

	expectEqual(t, "Begin|A|B|Skipped|Exit|Exit|End", myReporter.wholeStory())
}

func TestSkippedConveyReportedAsSkipped(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		SkipConvey("B", func() {
			So(1, ShouldEqual, 1)
		})
	})

	expectEqual(t, "Begin|A|B|Skipped|Exit|Exit|End", myReporter.wholeStory())
}

func TestMultipleSkipsAreReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		Convey("0", func() {
			So(nil, ShouldBeNil)
		})

		SkipConvey("1", func() {})
		SkipConvey("2", func() {})

		Convey("3", nil)
		Convey("4", nil)

		Convey("5", func() {
			So(nil, ShouldBeNil)
		})
	})

	expected := "Begin" +
		"|A|0|Success|Exit|Exit" +
		"|A|1|Skipped|Exit|Exit" +
		"|A|2|Skipped|Exit|Exit" +
		"|A|3|Skipped|Exit|Exit" +
		"|A|4|Skipped|Exit|Exit" +
		"|A|5|Success|Exit|Exit" +
		"|End"

	expectEqual(t, expected, myReporter.wholeStory())
}

func TestSkippedAssertionIsNotReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		SkipSo(1, ShouldEqual, 1)
	})

	expectEqual(t, "Begin|A|Skipped|Exit|End", myReporter.wholeStory())
}

func TestMultipleSkippedAssertionsAreNotReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		SkipSo(1, ShouldEqual, 1)
		So(1, ShouldEqual, 1)
		SkipSo(1, ShouldEqual, 1)
	})

	expectEqual(t, "Begin|A|Skipped|Success|Skipped|Exit|End", myReporter.wholeStory())
}

func TestErrorByManualPanicReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		panic("Gopher alert!")
	})

	expectEqual(t, "Begin|A|Error|Exit|End", myReporter.wholeStory())
}

func TestIterativeConveysReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		for x := 0; x < 3; x++ {
			Convey(strconv.Itoa(x), func() {
				So(x, ShouldEqual, x)
			})
		}
	})

	expectEqual(t, "Begin|A|0|Success|Exit|Exit|A|1|Success|Exit|Exit|A|2|Success|Exit|Exit|End", myReporter.wholeStory())
}

func TestNestedIterativeConveysReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func() {
		for x := 0; x < 3; x++ {
			Convey(strconv.Itoa(x), func() {
				for y := 0; y < 3; y++ {
					Convey("< "+strconv.Itoa(y), func() {
						So(x, ShouldBeLessThan, y)
					})
				}
			})
		}
	})

	expectEqual(t, ("Begin|" +
		"A|0|< 0|Failure|Exit|Exit|Exit|" +
		"A|0|< 1|Success|Exit|Exit|Exit|" +
		"A|0|< 2|Success|Exit|Exit|Exit|" +
		"A|1|< 0|Failure|Exit|Exit|Exit|" +
		"A|1|< 1|Failure|Exit|Exit|Exit|" +
		"A|1|< 2|Success|Exit|Exit|Exit|" +
		"A|2|< 0|Failure|Exit|Exit|Exit|" +
		"A|2|< 1|Failure|Exit|Exit|Exit|" +
		"A|2|< 2|Failure|Exit|Exit|Exit|" +
		"End"), myReporter.wholeStory())
}

func TestEmbeddedAssertionReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	Convey("A", test, func(c C) {
		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			c.So(r.FormValue("msg"), ShouldEqual, "ping")
		}))
		http.DefaultClient.Get(ts.URL + "?msg=ping")
	})

	expectEqual(t, "Begin|A|Success|Exit|End", myReporter.wholeStory())
}

func TestEmbeddedContextHelperReported(t *testing.T) {
	myReporter, test := setupFakeReporter()

	helper := func(c C) http.HandlerFunc {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			c.Convey("Embedded", func() {
				So(r.FormValue("msg"), ShouldEqual, "ping")
			})
		})
	}

	Convey("A", test, func(c C) {
		ts := httptest.NewServer(helper(c))
		http.DefaultClient.Get(ts.URL + "?msg=ping")
	})

	expectEqual(t, "Begin|A|Embedded|Success|Exit|Exit|End", myReporter.wholeStory())
}

func expectEqual(t *testing.T, expected any, actual any) {
	if expected != actual {
		_, file, line, _ := runtime.Caller(1)
		t.Errorf("Expected '%v' to be '%v' but it wasn't. See '%s' at line %d.",
			actual, expected, path.Base(file), line)
	}
}

func setupFakeReporter() (*fakeReporter, *fakeGoTest) {
	myReporter := new(fakeReporter)
	myReporter.calls = []string{}
	testReporter = myReporter
	return myReporter, new(fakeGoTest)
}

type fakeReporter struct {
	calls []string
}

func (self *fakeReporter) BeginStory(story *reporting.StoryReport) {
	self.calls = append(self.calls, "Begin")
}

func (self *fakeReporter) Enter(scope *reporting.ScopeReport) {
	self.calls = append(self.calls, scope.Title)
}

func (self *fakeReporter) Report(report *reporting.AssertionResult) {
	if report.Error != nil {
		self.calls = append(self.calls, "Error")
	} else if report.Failure != "" {
		message := "Failure"
		if report.Expected != "" || report.Actual != "" {
			message += fmt.Sprintf("(%s/%s)", report.Expected, report.Actual)
		}
		self.calls = append(self.calls, message)
	} else if report.Skipped {
		self.calls = append(self.calls, "Skipped")
	} else {
		self.calls = append(self.calls, "Success")
	}
}

func (self *fakeReporter) Exit() {
	self.calls = append(self.calls, "Exit")
}

func (self *fakeReporter) EndStory() {
	self.calls = append(self.calls, "End")
}

func (self *fakeReporter) Write(content []byte) (int, error) {
	return len(content), nil // no-op
}

func (self *fakeReporter) wholeStory() string {
	return strings.Join(self.calls, "|")
}

////////////////////////////////

type fakeGoTest struct{}

func (self *fakeGoTest) Fail()                                     {}
func (self *fakeGoTest) Fatalf(format string, args ...any) {}

var test t = new(fakeGoTest)
