package suite_test

import (
	"bytes"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/ginkgo/internal/suite"
	. "github.com/onsi/gomega"

	"math/rand"
	"time"

	"github.com/onsi/ginkgo/config"
	"github.com/onsi/ginkgo/internal/codelocation"
	Failer "github.com/onsi/ginkgo/internal/failer"
	Writer "github.com/onsi/ginkgo/internal/writer"
	"github.com/onsi/ginkgo/reporters"
	"github.com/onsi/ginkgo/types"
)

var _ = Describe("Suite", func() {
	var (
		specSuite *Suite
		fakeT     *fakeTestingT
		fakeR     *reporters.FakeReporter
		writer    *Writer.FakeGinkgoWriter
		failer    *Failer.Failer
	)

	BeforeEach(func() {
		writer = Writer.NewFake()
		fakeT = &fakeTestingT{}
		fakeR = reporters.NewFakeReporter()
		failer = Failer.New()
		specSuite = New(failer)
	})

	Describe("running a suite", func() {
		var (
			runOrder             []string
			randomizeAllSpecs    bool
			randomSeed           int64
			focusString          string
			parallelNode         int
			parallelTotal        int
			runResult            bool
			hasProgrammaticFocus bool
		)

		var f = func(runText string) func() {
			return func() {
				runOrder = append(runOrder, runText)
			}
		}

		BeforeEach(func() {
			randomizeAllSpecs = false
			randomSeed = 11
			parallelNode = 1
			parallelTotal = 1
			focusString = ""

			runOrder = make([]string, 0)
			specSuite.SetBeforeSuiteNode(f("BeforeSuite"), codelocation.New(0), 0)
			specSuite.PushBeforeEachNode(f("top BE"), codelocation.New(0), 0)
			specSuite.PushJustBeforeEachNode(f("top JBE"), codelocation.New(0), 0)
			specSuite.PushAfterEachNode(f("top AE"), codelocation.New(0), 0)

			specSuite.PushContainerNode("container", func() {
				specSuite.PushBeforeEachNode(f("BE"), codelocation.New(0), 0)
				specSuite.PushJustBeforeEachNode(f("JBE"), codelocation.New(0), 0)
				specSuite.PushAfterEachNode(f("AE"), codelocation.New(0), 0)
				specSuite.PushItNode("it", f("IT"), types.FlagTypeNone, codelocation.New(0), 0)

				specSuite.PushContainerNode("inner container", func() {
					specSuite.PushItNode("inner it", f("inner IT"), types.FlagTypeNone, codelocation.New(0), 0)
				}, types.FlagTypeNone, codelocation.New(0))
			}, types.FlagTypeNone, codelocation.New(0))

			specSuite.PushContainerNode("container 2", func() {
				specSuite.PushBeforeEachNode(f("BE 2"), codelocation.New(0), 0)
				specSuite.PushItNode("it 2", f("IT 2"), types.FlagTypeNone, codelocation.New(0), 0)
			}, types.FlagTypeNone, codelocation.New(0))

			specSuite.PushItNode("top level it", f("top IT"), types.FlagTypeNone, codelocation.New(0), 0)

			specSuite.SetAfterSuiteNode(f("AfterSuite"), codelocation.New(0), 0)
		})

		JustBeforeEach(func() {
			runResult, hasProgrammaticFocus = specSuite.Run(fakeT, "suite description", []reporters.Reporter{fakeR}, writer, config.GinkgoConfigType{
				RandomSeed:        randomSeed,
				RandomizeAllSpecs: randomizeAllSpecs,
				FocusString:       focusString,
				ParallelNode:      parallelNode,
				ParallelTotal:     parallelTotal,
			})
		})

		It("provides the config and suite description to the reporter", func() {
			Ω(fakeR.Config.RandomSeed).Should(Equal(int64(randomSeed)))
			Ω(fakeR.Config.RandomizeAllSpecs).Should(Equal(randomizeAllSpecs))
			Ω(fakeR.BeginSummary.SuiteDescription).Should(Equal("suite description"))
		})

		It("reports that the BeforeSuite node ran", func() {
			Ω(fakeR.BeforeSuiteSummary).ShouldNot(BeNil())
		})

		It("reports that the AfterSuite node ran", func() {
			Ω(fakeR.AfterSuiteSummary).ShouldNot(BeNil())
		})

		It("provides information about the current test", func() {
			description := CurrentGinkgoTestDescription()
			Ω(description.ComponentTexts).Should(Equal([]string{"Suite", "running a suite", "provides information about the current test"}))
			Ω(description.FullTestText).Should(Equal("Suite running a suite provides information about the current test"))
			Ω(description.TestText).Should(Equal("provides information about the current test"))
			Ω(description.IsMeasurement).Should(BeFalse())
			Ω(description.FileName).Should(ContainSubstring("suite_test.go"))
			Ω(description.LineNumber).Should(BeNumerically(">", 50))
			Ω(description.LineNumber).Should(BeNumerically("<", 150))
		})

		Measure("should run measurements", func(b Benchmarker) {
			r := rand.New(rand.NewSource(time.Now().UnixNano()))

			runtime := b.Time("sleeping", func() {
				sleepTime := time.Duration(r.Float64() * 0.01 * float64(time.Second))
				time.Sleep(sleepTime)
			})
			Ω(runtime.Seconds()).Should(BeNumerically("<=", 0.015))
			Ω(runtime.Seconds()).Should(BeNumerically(">=", 0))

			randomValue := r.Float64() * 10.0
			b.RecordValue("random value", randomValue)
			Ω(randomValue).Should(BeNumerically("<=", 10.0))
			Ω(randomValue).Should(BeNumerically(">=", 0.0))
		}, 10)

		It("creates a node hierarchy, converts it to a spec collection, and runs it", func() {
			Ω(runOrder).Should(Equal([]string{
				"BeforeSuite",
				"top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE",
				"top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE",
				"top BE", "BE 2", "top JBE", "IT 2", "top AE",
				"top BE", "top JBE", "top IT", "top AE",
				"AfterSuite",
			}))
		})

		Context("when told to randomize all specs", func() {
			BeforeEach(func() {
				randomizeAllSpecs = true
			})

			It("does", func() {
				Ω(runOrder).Should(Equal([]string{
					"BeforeSuite",
					"top BE", "top JBE", "top IT", "top AE",
					"top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE",
					"top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE",
					"top BE", "BE 2", "top JBE", "IT 2", "top AE",
					"AfterSuite",
				}))
			})
		})

		Describe("with ginkgo.parallel.total > 1", func() {
			BeforeEach(func() {
				parallelTotal = 2
				randomizeAllSpecs = true
			})

			Context("for one worker", func() {
				BeforeEach(func() {
					parallelNode = 1
				})

				It("should run a subset of tests", func() {
					Ω(runOrder).Should(Equal([]string{
						"BeforeSuite",
						"top BE", "top JBE", "top IT", "top AE",
						"top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE",
						"AfterSuite",
					}))
				})
			})

			Context("for another worker", func() {
				BeforeEach(func() {
					parallelNode = 2
				})

				It("should run a (different) subset of tests", func() {
					Ω(runOrder).Should(Equal([]string{
						"BeforeSuite",
						"top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE",
						"top BE", "BE 2", "top JBE", "IT 2", "top AE",
						"AfterSuite",
					}))
				})
			})
		})

		Context("when provided with a filter", func() {
			BeforeEach(func() {
				focusString = `inner|\d`
			})

			It("converts the filter to a regular expression and uses it to filter the running specs", func() {
				Ω(runOrder).Should(Equal([]string{
					"BeforeSuite",
					"top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE",
					"top BE", "BE 2", "top JBE", "IT 2", "top AE",
					"AfterSuite",
				}))
			})

			It("should not report a programmatic focus", func() {
				Ω(hasProgrammaticFocus).Should(BeFalse())
			})
		})

		Context("with a programatically focused spec", func() {
			BeforeEach(func() {
				specSuite.PushItNode("focused it", f("focused it"), types.FlagTypeFocused, codelocation.New(0), 0)

				specSuite.PushContainerNode("focused container", func() {
					specSuite.PushItNode("inner focused it", f("inner focused it"), types.FlagTypeFocused, codelocation.New(0), 0)
					specSuite.PushItNode("inner unfocused it", f("inner unfocused it"), types.FlagTypeNone, codelocation.New(0), 0)
				}, types.FlagTypeFocused, codelocation.New(0))

			})

			It("should only run the focused test, applying backpropagation to favor most deeply focused leaf nodes", func() {
				Ω(runOrder).Should(Equal([]string{
					"BeforeSuite",
					"top BE", "top JBE", "focused it", "top AE",
					"top BE", "top JBE", "inner focused it", "top AE",
					"AfterSuite",
				}))
			})

			It("should report a programmatic focus", func() {
				Ω(hasProgrammaticFocus).Should(BeTrue())
			})
		})

		Context("when the specs pass", func() {
			It("doesn't report a failure", func() {
				Ω(fakeT.didFail).Should(BeFalse())
			})

			It("should return true", func() {
				Ω(runResult).Should(BeTrue())
			})
		})

		Context("when a spec fails", func() {
			var location types.CodeLocation
			BeforeEach(func() {
				specSuite.PushItNode("top level it", func() {
					location = codelocation.New(0)
					failer.Fail("oops!", location)
				}, types.FlagTypeNone, codelocation.New(0), 0)
			})

			It("should return false", func() {
				Ω(runResult).Should(BeFalse())
			})

			It("reports a failure", func() {
				Ω(fakeT.didFail).Should(BeTrue())
			})

			It("generates the correct failure data", func() {
				Ω(fakeR.SpecSummaries[0].Failure.Message).Should(Equal("oops!"))
				Ω(fakeR.SpecSummaries[0].Failure.Location).Should(Equal(location))
			})
		})

		Context("when runnable nodes are nested within other runnable nodes", func() {
			Context("when an It is nested", func() {
				BeforeEach(func() {
					specSuite.PushItNode("top level it", func() {
						specSuite.PushItNode("nested it", f("oops"), types.FlagTypeNone, codelocation.New(0), 0)
					}, types.FlagTypeNone, codelocation.New(0), 0)
				})

				It("should fail", func() {
					Ω(fakeT.didFail).Should(BeTrue())
				})
			})

			Context("when a Measure is nested", func() {
				BeforeEach(func() {
					specSuite.PushItNode("top level it", func() {
						specSuite.PushMeasureNode("nested measure", func(Benchmarker) {}, types.FlagTypeNone, codelocation.New(0), 10)
					}, types.FlagTypeNone, codelocation.New(0), 0)
				})

				It("should fail", func() {
					Ω(fakeT.didFail).Should(BeTrue())
				})
			})

			Context("when a BeforeEach is nested", func() {
				BeforeEach(func() {
					specSuite.PushItNode("top level it", func() {
						specSuite.PushBeforeEachNode(f("nested bef"), codelocation.New(0), 0)
					}, types.FlagTypeNone, codelocation.New(0), 0)
				})

				It("should fail", func() {
					Ω(fakeT.didFail).Should(BeTrue())
				})
			})

			Context("when a JustBeforeEach is nested", func() {
				BeforeEach(func() {
					specSuite.PushItNode("top level it", func() {
						specSuite.PushJustBeforeEachNode(f("nested jbef"), codelocation.New(0), 0)
					}, types.FlagTypeNone, codelocation.New(0), 0)
				})

				It("should fail", func() {
					Ω(fakeT.didFail).Should(BeTrue())
				})
			})

			Context("when a AfterEach is nested", func() {
				BeforeEach(func() {
					specSuite.PushItNode("top level it", func() {
						specSuite.PushAfterEachNode(f("nested aft"), codelocation.New(0), 0)
					}, types.FlagTypeNone, codelocation.New(0), 0)
				})

				It("should fail", func() {
					Ω(fakeT.didFail).Should(BeTrue())
				})
			})
		})
	})

	Describe("BeforeSuite", func() {
		Context("when setting BeforeSuite more than once", func() {
			It("should panic", func() {
				specSuite.SetBeforeSuiteNode(func() {}, codelocation.New(0), 0)

				Ω(func() {
					specSuite.SetBeforeSuiteNode(func() {}, codelocation.New(0), 0)
				}).Should(Panic())

			})
		})
	})

	Describe("AfterSuite", func() {
		Context("when setting AfterSuite more than once", func() {
			It("should panic", func() {
				specSuite.SetAfterSuiteNode(func() {}, codelocation.New(0), 0)

				Ω(func() {
					specSuite.SetAfterSuiteNode(func() {}, codelocation.New(0), 0)
				}).Should(Panic())
			})
		})
	})

	Describe("By", func() {
		It("writes to the GinkgoWriter", func() {
			originalGinkgoWriter := GinkgoWriter
			buffer := &bytes.Buffer{}

			GinkgoWriter = buffer
			By("Saying Hello GinkgoWriter")
			GinkgoWriter = originalGinkgoWriter

			Ω(buffer.String()).Should(ContainSubstring("STEP"))
			Ω(buffer.String()).Should(ContainSubstring(": Saying Hello GinkgoWriter\n"))
		})

		It("calls the passed-in callback if present", func() {
			a := 0
			By("calling the callback", func() {
				a = 1
			})
			Ω(a).Should(Equal(1))
		})

		It("panics if there is more than one callback", func() {
			Ω(func() {
				By("registering more than one callback", func() {}, func() {})
			}).Should(Panic())
		})
	})
})
