package metrics

import (
	"context"
	"time"

	"github.com/golang/mock/gomock"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	. "github.com/pingcap/endless/pkg/matcher"
	mock "github.com/pingcap/endless/pkg/matcher/metrics/generated"
)

var _ = Describe("PromQLMatchers", func() {
	Context("PromQLEvaluatedToEmpty", func() {
		const EmitNonemptyScalarLabel = "/* testing purpose [NonEmptyScalar] */"
		const EmitEmptyVectorLabel = "/* testing purpose [EmptyVector] */"

		mockApi := mock.NewMockPrometheusAPI(GomockController)
		mockApi.EXPECT().
			Query(gomock.Any(), gomock.Eq(EmitNonemptyScalarLabel), gomock.Any()). // inject nonempty scalar emission
			Return(PrometheusNonemptyScalar, []string{}, nil).AnyTimes()
		mockApi.EXPECT().
			Query(gomock.Any(), gomock.Eq(EmitEmptyVectorLabel), gomock.Any()). // inject empty vector emission
			Return(PrometheusEmptyVector, []string{}, nil).AnyTimes()

		It("should passed with empty Vector", func() {
			success, err := PromQLEvaluatedToEmpty().Match(PromQLExpr{
				Description: "testing",
				Query:       EmitEmptyVectorLabel,
				Api:         mockApi,
			})

			Expect(err).ShouldNot(HaveOccurred())
			Expect(success).Should(BeTrue())
		})
		It("should failed with nonempty Scalar", func() {
			matcher := PromQLEvaluatedToEmpty()
			ql := PromQLExpr{
				Description: "testing",
				Query:       EmitNonemptyScalarLabel,
				Api:         mockApi,
			}
			success, err := matcher.Match(ql)

			Expect(err).ShouldNot(HaveOccurred())
			Expect(success).Should(BeFalse())
		})
		It("should contain first nonempty time in failure message", func() {
			matcher := PromQLEvaluatedToEmpty()
			ql := PromQLExpr{
				Description: "testing",
				Query:       EmitNonemptyScalarLabel,
				Api:         mockApi,
			}
			_, err := matcher.Match(ql)
			Expect(err).ShouldNot(HaveOccurred())
			Expect(matcher.FailureMessage(nil)).Should(ContainSubstring("firstNonemptyTime"))
			Expect(matcher.NegatedFailureMessage(nil)).Should(ContainSubstring("firstNonemptyTime"))
		})

		It("should works properly with nonempty Scalar and AtLeastFailedFor", func() {
			matcher := PromQLEvaluatedToEmpty()
			ql := PromQLExpr{
				Description: "testing",
				Query:       EmitNonemptyScalarLabel,
				Api:         mockApi,
			}
			ql.AtLeastFailedFor(300 * time.Millisecond)

			success, err := matcher.Match(ql)
			Expect(err).ShouldNot(HaveOccurred())
			Expect(success).Should(BeTrue()) // immediately True

			time.Sleep(100 * time.Millisecond)
			success, err = matcher.Match(ql)
			Expect(err).ShouldNot(HaveOccurred())
			Expect(success).Should(BeTrue()) // 100 ms later True

			time.Sleep(100 * time.Millisecond)
			success, err = matcher.Match(ql)
			Expect(err).ShouldNot(HaveOccurred())
			Expect(success).Should(BeTrue()) // 200 ms later True

			time.Sleep(200 * time.Millisecond)
			success, err = matcher.Match(ql)
			Expect(err).ShouldNot(HaveOccurred())
			Expect(success).Should(BeFalse()) // 400 ms later False
		})
	})
})

var _ = Describe("PromQLEvaluatedToEmpty", func() {
	var (
		mockApi *mock.MockPrometheusAPI
		matcher *promQLEvaluatedToEmpty
		expr1   PromQLExpr
	)

	mockQueryReturnNonEmpty := func() *gomock.Call {
		return mockApi.
			EXPECT().
			Query(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(PrometheusNonemptyScalar, []string{}, nil)
	}
	mockQueryReturnEmpty := func() *gomock.Call {
		return mockApi.
			EXPECT().
			Query(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(PrometheusEmptyVector, []string{}, nil)
	}

	BeforeEach(func() {
		mockApi = mock.NewMockPrometheusAPI(GomockController)
		expr1 = PromQLExpr{
			Description: "",
			Query:       "",
			Api:         mockApi,
		}
		expr1.AtLeastFailedFor(500 * time.Millisecond)
		matcher = PromQLEvaluatedToEmpty()
	})

	Context("query return nonempty for a time which is less than AtLeastFailedFor", func() {
		It("should match", func() {
			mockQueryReturnNonEmpty()
			Try(matcher.Match(expr1))

			time.Sleep(200 * time.Millisecond)
			mockQueryReturnEmpty()
			Try(matcher.Match(expr1))

			time.Sleep(400 * time.Millisecond)
			mockQueryReturnNonEmpty()
			success, _ := matcher.Match(expr1)
			Expect(success).Should(BeTrue())
		})
	})

	Context("query return nonempty-empty-nonempty", func() {
		It("should not match eventually", func() {
			mockQueryReturnNonEmpty()
			Try(matcher.Match(expr1))

			time.Sleep(200 * time.Millisecond)
			mockQueryReturnEmpty() // return empty once
			Try(matcher.Match(expr1))

			mockQueryReturnNonEmpty().AnyTimes()
			Try(matcher.Match(expr1))

			time.Sleep(400 * time.Millisecond)
			success, _ := matcher.Match(expr1)
			Expect(success).To(BeTrue())

			time.Sleep(200 * time.Millisecond)
			success, _ = matcher.Match(expr1)
			Expect(success).To(BeFalse())
			Expect(matcher.FailureMessage(expr1)).To(ContainSubstring("Diagnose Info"))
		})
	})

	Context("query return nonempty for a time which is long than AtLeastFailedFor", func() {
		It("should not match", func() {
			mockQueryReturnNonEmpty().AnyTimes()
			Try(matcher.Match(expr1))
			time.Sleep(600 * time.Millisecond)
			success, err := matcher.Match(expr1)
			Expect(err).Should(BeNil())
			Expect(success).Should(BeFalse())
		})
	})

	Context("query with done channel", func() {
		It("should still match", func() {
			ctx, cancel := context.WithCancel(context.TODO())
			cancel()

			mockQueryReturnNonEmpty().AnyTimes()
			expr := PromQLExpr{
				Description: "",
				Query:       "",
				Api:         mockApi,
				Ctx:         ctx,
			}
			match, err := matcher.Match(expr)
			Expect(match).To(BeTrue())
			Expect(err).To(Succeed())
			Expect(matcher.MatchMayChangeInTheFuture(expr)).To(BeFalse())
		})
	})
})
