/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package statsd_test

import (
	"bytes"
	"fmt"
	"strings"

	kitstatsd "github.com/go-kit/kit/metrics/statsd"
	"github.com/hyperledger/fabric/common/metrics"
	"github.com/hyperledger/fabric/common/metrics/statsd"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Provider", func() {
	var (
		s        *kitstatsd.Statsd
		provider *statsd.Provider
	)

	BeforeEach(func() {
		s = kitstatsd.New("", nil)
		provider = &statsd.Provider{Statsd: s}
	})

	It("implements metrics.Provider", func() {
		var p metrics.Provider = &statsd.Provider{}
		Expect(p).NotTo(BeNil())
	})

	Describe("NewCounter", func() {
		var counterOpts metrics.CounterOpts

		BeforeEach(func() {
			counterOpts = metrics.CounterOpts{
				Namespace:    "namespace",
				Subsystem:    "subsystem",
				Name:         "name",
				StatsdFormat: "%{#namespace}.%{#subsystem}.%{#name}.%{alpha}.%{beta}",
				LabelNames:   []string{"alpha", "beta"},
			}
		})

		It("creates counters that include label values in bucket names", func() {
			counter := provider.NewCounter(counterOpts)
			for _, alpha := range []string{"x", "y", "z"} {
				counter.With("alpha", alpha, "beta", "b").Add(1)
				buf := &bytes.Buffer{}
				s.WriteTo(buf)
				Expect(buf.String()).To(Equal(fmt.Sprintf("namespace.subsystem.name.%s.b:%f|c\n", alpha, float64(1))))
			}
		})

		Context("when a statsd format is not provided", func() {
			BeforeEach(func() {
				counterOpts.LabelNames = nil
				counterOpts.StatsdFormat = ""
			})

			It("uses the default format", func() {
				counter := provider.NewCounter(counterOpts)
				counter.Add(1)

				buf := &bytes.Buffer{}
				s.WriteTo(buf)
				Expect(buf.String()).To(Equal("namespace.subsystem.name:1.000000|c\n"))
			})
		})

		Context("when label values are not specified in counter options", func() {
			BeforeEach(func() {
				counterOpts.LabelNames = nil
				counterOpts.StatsdFormat = "%{#namespace}.%{#subsystem}.%{#name}"
			})

			It("creates counters that do not require calls to With", func() {
				counter := provider.NewCounter(counterOpts)
				for i := 0; i < 10; i++ {
					counter.Add(float64(i))

					buf := &bytes.Buffer{}
					s.WriteTo(buf)
					Expect(buf.String()).To(Equal(fmt.Sprintf("namespace.subsystem.name:%f|c\n", float64(i))))
				}
			})
		})

		Context("when labels are specified and with is not called", func() {
			It("panics with an appropriate message when Add is called", func() {
				counter := provider.NewCounter(counterOpts)
				panicMessage := func() (panicMessage interface{}) {
					defer func() { panicMessage = recover() }()
					counter.Add(1)
					return
				}()
				Expect(panicMessage).To(Equal("label values must be provided by calling With"))
			})
		})
	})

	Describe("NewGauge", func() {
		var gaugeOpts metrics.GaugeOpts

		BeforeEach(func() {
			gaugeOpts = metrics.GaugeOpts{
				Namespace:    "namespace",
				Subsystem:    "subsystem",
				Name:         "name",
				StatsdFormat: "%{#namespace}.%{#subsystem}.%{#name}.%{alpha}.%{beta}",
				LabelNames:   []string{"alpha", "beta"},
			}
		})

		It("creates gauges that support Set with label values in bucket names", func() {
			gauge := provider.NewGauge(gaugeOpts)
			for i := 1; i <= 5; i++ {
				for _, alpha := range []string{"x", "y", "z"} {
					gauge.With("alpha", alpha, "beta", "b").Set(float64(i))
					buf := &bytes.Buffer{}
					s.WriteTo(buf)
					Expect(buf.String()).To(Equal(fmt.Sprintf("namespace.subsystem.name.%s.b:%f|g\n", alpha, float64(i))))
				}
			}
		})

		It("creates gauges that support Add with label values in bucket names", func() {
			gauge := provider.NewGauge(gaugeOpts)

			for _, alpha := range []string{"x", "y", "z"} {
				gauge.With("alpha", alpha, "beta", "b").Set(float64(1.0))
			}
			for _, alpha := range []string{"x", "y", "z"} {
				for i := 0; i < 5; i++ {
					gauge.With("alpha", alpha, "beta", "b").Add(float64(1.0))
				}
			}
			buf := &bytes.Buffer{}
			s.WriteTo(buf)
			Expect(strings.SplitN(buf.String(), "\n", -1)).To(ConsistOf(
				Equal("namespace.subsystem.name.x.b:6.000000|g"),
				Equal("namespace.subsystem.name.y.b:6.000000|g"),
				Equal("namespace.subsystem.name.z.b:6.000000|g"),
				Equal(""),
			))
		})

		Context("when a statsd format is not provided", func() {
			BeforeEach(func() {
				gaugeOpts.LabelNames = nil
				gaugeOpts.StatsdFormat = ""
			})

			It("uses the default format", func() {
				gauge := provider.NewGauge(gaugeOpts)
				gauge.Set(1)

				buf := &bytes.Buffer{}
				s.WriteTo(buf)
				Expect(buf.String()).To(Equal("namespace.subsystem.name:1.000000|g\n"))
			})
		})

		Context("when label values are not specified in gauge options", func() {
			BeforeEach(func() {
				gaugeOpts.LabelNames = nil
				gaugeOpts.StatsdFormat = "%{#namespace}.%{#subsystem}.%{#name}"
			})

			It("creates gauges that do not require calls to With", func() {
				gauge := provider.NewGauge(gaugeOpts)
				for i := 0; i < 10; i++ {
					gauge.Add(float64(i))

					buf := &bytes.Buffer{}
					s.WriteTo(buf)
					Expect(buf.String()).To(Equal(fmt.Sprintf("namespace.subsystem.name:%f|g\n", float64(i))))
				}
			})
		})

		Context("when labels are specified and with is not called", func() {
			It("panics with an appropriate message when Add is called", func() {
				gauge := provider.NewGauge(gaugeOpts)
				panicMessage := func() (panicMessage interface{}) {
					defer func() { panicMessage = recover() }()
					gauge.Add(float64(1))
					return
				}()
				Expect(panicMessage).To(Equal("label values must be provided by calling With"))
			})

			It("panics with an appropriate message when Set is called", func() {
				gauge := provider.NewGauge(gaugeOpts)
				panicMessage := func() (panicMessage interface{}) {
					defer func() { panicMessage = recover() }()
					gauge.Set(float64(1))
					return
				}()
				Expect(panicMessage).To(Equal("label values must be provided by calling With"))
			})
		})
	})

	Describe("NewHistogram", func() {
		var histogramOpts metrics.HistogramOpts

		BeforeEach(func() {
			histogramOpts = metrics.HistogramOpts{
				Namespace:    "namespace",
				Subsystem:    "subsystem",
				Name:         "name",
				StatsdFormat: "%{#namespace}.%{#subsystem}.%{#name}.%{alpha}.%{beta}",
				LabelNames:   []string{"alpha", "beta"},
			}
		})

		It("creates histograms that support Observe with label values in bucket names", func() {
			histogram := provider.NewHistogram(histogramOpts)
			for i := 1; i <= 5; i++ {
				for _, alpha := range []string{"x", "y", "z"} {
					histogram.With("alpha", alpha, "beta", "b").Observe(float64(i))
					buf := &bytes.Buffer{}
					s.WriteTo(buf)
					Expect(buf.String()).To(Equal(fmt.Sprintf("namespace.subsystem.name.%s.b:%f|ms\n", alpha, float64(i))))
				}
			}
		})

		Context("when a statsd format is not provided", func() {
			BeforeEach(func() {
				histogramOpts.LabelNames = nil
				histogramOpts.StatsdFormat = ""
			})

			It("uses the default format", func() {
				histogram := provider.NewHistogram(histogramOpts)
				histogram.Observe(1)

				buf := &bytes.Buffer{}
				s.WriteTo(buf)
				Expect(buf.String()).To(Equal("namespace.subsystem.name:1.000000|ms\n"))
			})
		})

		Context("when label values are not specified in counter options", func() {
			BeforeEach(func() {
				histogramOpts.LabelNames = nil
				histogramOpts.StatsdFormat = "%{#namespace}.%{#subsystem}.%{#name}"
			})

			It("creates histograms that do not require calls to With", func() {
				histogram := provider.NewHistogram(histogramOpts)
				for i := 0; i < 10; i++ {
					histogram.Observe(float64(i))

					buf := &bytes.Buffer{}
					s.WriteTo(buf)
					Expect(buf.String()).To(Equal(fmt.Sprintf("namespace.subsystem.name:%f|ms\n", float64(i))))
				}
			})
		})

		Context("when labels are specified and with is not called", func() {
			It("panics with an appropriate message when Observe is called", func() {
				histogram := provider.NewHistogram(histogramOpts)
				panicMessage := func() (panicMessage interface{}) {
					defer func() { panicMessage = recover() }()
					histogram.Observe(float64(1))
					return
				}()
				Expect(panicMessage).To(Equal("label values must be provided by calling With"))
			})
		})
	})
})
