package integration

import (
	"net/http/httptest"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	vegeta "github.com/tsenart/vegeta/v12/lib"
)

const routerLatencyThreshold = 20 * time.Millisecond

var _ = Describe("Performance", func() {

	Context("with two healthy backends", func() {
		var (
			backend1 *httptest.Server
			backend2 *httptest.Server
		)

		BeforeEach(func() {
			backend1 = startSimpleBackend("backend 1", backends["backend-1"])
			backend2 = startSimpleBackend("backend 2", backends["backend-2"])
			addRoute("/one", NewBackendRoute("backend-1"))
			addRoute("/two", NewBackendRoute("backend-2"))
			reloadRoutes(apiPort)
		})
		AfterEach(func() {
			backend1.Close()
			backend2.Close()
		})

		It("Router should not cause errors or much latency", func() {
			assertPerformantRouter(backend1, backend2, 100)
		})

		Describe("when the routes are being reloaded repeatedly", func() {
			It("Router should not cause errors or much latency", func() {
				stopCh := make(chan struct{})
				defer close(stopCh)
				go func() {
					ticker := time.NewTicker(100 * time.Millisecond)
					defer ticker.Stop()
					select {
					case <-stopCh:
						return
					case <-ticker.C:
						reloadRoutes(apiPort)
					}
				}()

				assertPerformantRouter(backend1, backend2, 100)
			})
		})

		Describe("with one slow backend hit separately", func() {
			It("Router should not cause errors or much latency", func() {
				slowBackend := startTarpitBackend(backends["slow-1"], time.Second)
				defer slowBackend.Close()
				addRoute("/slow", NewBackendRoute("slow-1"))
				reloadRoutes(apiPort)

				_, gen := generateLoad([]string{routerURL(routerPort, "/slow")}, 50)
				defer gen.Stop()

				assertPerformantRouter(backend1, backend2, 50)
			})
		})

		Describe("with one downed backend hit separately", func() {
			It("Router should not cause errors or much latency", func() {
				addRoute("/down", NewBackendRoute("down"))
				reloadRoutes(apiPort)

				_, gen := generateLoad([]string{routerURL(routerPort, "/down")}, 50)
				defer gen.Stop()

				assertPerformantRouter(backend1, backend2, 50)
			})
		})

		Describe("with high request throughput", func() {
			It("Router should not cause errors or much latency", func() {
				assertPerformantRouter(backend1, backend2, 500)
			})
		})
	})

	Describe("many concurrent slow connections", func() {
		var backend1 *httptest.Server
		var backend2 *httptest.Server

		BeforeEach(func() {
			backend1 = startTarpitBackend(backends["slow-1"], time.Second)
			backend2 = startTarpitBackend(backends["slow-2"], time.Second)
			addRoute("/one", NewBackendRoute("slow-1"))
			addRoute("/two", NewBackendRoute("slow-2"))
			reloadRoutes(apiPort)
		})
		AfterEach(func() {
			backend1.Close()
			backend2.Close()
		})

		It("Router should not cause errors or much latency", func() {
			assertPerformantRouter(backend1, backend2, 500)
		})
	})
})

func assertPerformantRouter(backend1, backend2 *httptest.Server, rps int) {
	directResultsCh, _ := generateLoad([]string{backend1.URL + "/one", backend2.URL + "/two"}, rps)
	routerResultsCh, _ := generateLoad([]string{routerURL(routerPort, "/one"), routerURL(routerPort, "/two")}, rps)

	directResults := <-directResultsCh
	routerResults := <-routerResultsCh

	Expect(routerResults.Requests).To(Equal(directResults.Requests))
	Expect(routerResults.Success).To(BeNumerically("~", 1.0))
	Expect(directResults.Success).To(BeNumerically("~", 1.0))

	Expect(routerResults.Latencies.Mean).To(BeNumerically("~", directResults.Latencies.Mean, routerLatencyThreshold))
	Expect(routerResults.Latencies.P95).To(BeNumerically("~", directResults.Latencies.P95, routerLatencyThreshold))
	Expect(routerResults.Latencies.P99).To(BeNumerically("~", directResults.Latencies.P99, routerLatencyThreshold*2))
	Expect(routerResults.Latencies.Max).To(BeNumerically("~", directResults.Latencies.Max, routerLatencyThreshold*2))
}

func generateLoad(targetURLs []string, rps int) (chan *vegeta.Metrics, *vegeta.Attacker) {
	targets := make([]vegeta.Target, 0, len(targetURLs))
	for _, url := range targetURLs {
		targets = append(targets, vegeta.Target{
			Method: "GET",
			URL:    url,
		})
	}
	targeter := vegeta.NewStaticTargeter(targets...)
	metrics := make(chan *vegeta.Metrics, 1)
	veg := vegeta.NewAttacker()
	go vegetaAttack(veg, targeter, rps, metrics)
	return metrics, veg
}

func vegetaAttack(veg *vegeta.Attacker, targets vegeta.Targeter, rps int, metrics chan *vegeta.Metrics) {
	pace := vegeta.Pacer(vegeta.ConstantPacer{Freq: rps, Per: time.Second})

	var m vegeta.Metrics
	for res := range veg.Attack(targets, pace, 10*time.Second, "load") {
		m.Add(res)
	}
	m.Close()

	metrics <- &m
}
