/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package prometheus

import (
	"io"
	"net/http"
	"sync"
	"testing"
	"time"
)

import (
	prom "github.com/prometheus/client_golang/prometheus"

	"github.com/stretchr/testify/assert"
)

import (
	"dubbo.apache.org/dubbo-go/v3/common"
	"dubbo.apache.org/dubbo-go/v3/common/constant"
	"dubbo.apache.org/dubbo-go/v3/metrics"
)

var (
	tags     = map[string]string{"app": "dubbo", "version": "1.0.0"}
	metricId = &metrics.MetricId{Name: "dubbo_request", Desc: "request", Tags: tags}
	url      = common.NewURLWithOptions(
		common.WithProtocol(constant.ProtocolPrometheus),
		common.WithParamsValue(constant.PrometheusExporterEnabledKey, "true"),
		common.WithParamsValue(constant.PrometheusExporterMetricsPortKey, "9999"),
		common.WithParamsValue(constant.PrometheusExporterMetricsPathKey, "/prometheus"),
		common.WithParamsValue(constant.ApplicationKey, "dubbo"),
		common.WithParamsValue(constant.AppVersionKey, "1.0.0"),
		common.WithParamsValue(constant.PrometheusPushgatewayEnabledKey, "true"),
		common.WithParamsValue(constant.PrometheusPushgatewayBaseUrlKey, "localhost:9091"),
		common.WithParamsValue(constant.PrometheusPushgatewayUsernameKey, ""),
		common.WithParamsValue(constant.PrometheusPushgatewayPasswordKey, ""),
		common.WithParamsValue(constant.PrometheusPushgatewayPushIntervalKey, "2"),
		common.WithParamsValue(constant.PrometheusPushgatewayJobKey, "dubbo-push"),
	)
)

func TestPromMetricRegistryCounter(t *testing.T) {
	p := NewPromMetricRegistry(prom.NewRegistry(), url)
	p.Counter(metricId).Inc()
	text, err := p.Scrape()
	assert.Nil(t, err)
	assert.Contains(t, text, "# HELP dubbo_request request\n# TYPE dubbo_request counter")
	assert.Contains(t, text, `dubbo_request{app="dubbo",version="1.0.0"} 1`)
}

func TestPromMetricRegistryGauge(t *testing.T) {
	p := NewPromMetricRegistry(prom.NewRegistry(), url)
	p.Gauge(metricId).Set(100)
	text, err := p.Scrape()
	assert.Nil(t, err)
	assert.Contains(t, text, "# HELP dubbo_request request\n# TYPE dubbo_request gauge")
	assert.Contains(t, text, `dubbo_request{app="dubbo",version="1.0.0"} 100`)

}

func TestPromMetricRegistryHistogram(t *testing.T) {
	p := NewPromMetricRegistry(prom.NewRegistry(), url)
	p.Histogram(metricId).Observe(100)
	text, err := p.Scrape()
	assert.Nil(t, err)
	assert.Contains(t, text, "# HELP dubbo_request request\n# TYPE dubbo_request histogram")
	assert.Contains(t, text, `dubbo_request_bucket{app="dubbo",version="1.0.0",le="+Inf"} 1`)
	assert.Contains(t, text, `dubbo_request_sum{app="dubbo",version="1.0.0"} 100`)
	assert.Contains(t, text, `dubbo_request_count{app="dubbo",version="1.0.0"} 1`)
}

func TestPromMetricRegistrySummary(t *testing.T) {
	p := NewPromMetricRegistry(prom.NewRegistry(), url)
	p.Summary(metricId).Observe(100)
	text, err := p.Scrape()
	assert.Nil(t, err)
	assert.Contains(t, text, "# HELP dubbo_request request\n# TYPE dubbo_request summary")
	assert.Contains(t, text, "dubbo_request_sum{app=\"dubbo\",version=\"1.0.0\"} 100")
	assert.Contains(t, text, "dubbo_request_count{app=\"dubbo\",version=\"1.0.0\"} 1")
}

func TestPromMetricRegistryRt(t *testing.T) {
	p := NewPromMetricRegistry(prom.NewRegistry(), url)
	for i := 0; i < 10; i++ {
		p.Rt(metricId, &metrics.RtOpts{}).Observe(10 * float64(i))
	}
	text, err := p.Scrape()
	assert.Nil(t, err)
	assert.Contains(t, text, "# HELP dubbo_request_avg Average request\n# TYPE dubbo_request_avg gauge\ndubbo_request_avg{app=\"dubbo\",version=\"1.0.0\"} 45")
	assert.Contains(t, text, "# HELP dubbo_request_last Last request\n# TYPE dubbo_request_last gauge\ndubbo_request_last{app=\"dubbo\",version=\"1.0.0\"} 90")
	assert.Contains(t, text, "# HELP dubbo_request_max Max request\n# TYPE dubbo_request_max gauge\ndubbo_request_max{app=\"dubbo\",version=\"1.0.0\"} 90")
	assert.Contains(t, text, "# HELP dubbo_request_min Min request\n# TYPE dubbo_request_min gauge\ndubbo_request_min{app=\"dubbo\",version=\"1.0.0\"} 0")
	assert.Contains(t, text, "# HELP dubbo_request_sum Sum request\n# TYPE dubbo_request_sum gauge\ndubbo_request_sum{app=\"dubbo\",version=\"1.0.0\"} 450")

	p = NewPromMetricRegistry(prom.NewRegistry(), url)
	for i := 0; i < 10; i++ {
		p.Rt(metricId, &metrics.RtOpts{Aggregate: true, BucketNum: 10, TimeWindowSeconds: 60}).Observe(10 * float64(i))
	}
	text, err = p.Scrape()
	assert.Nil(t, err)
	assert.Contains(t, text, "# HELP dubbo_request_avg_milliseconds_aggregate The average request\n# TYPE dubbo_request_avg_milliseconds_aggregate gauge\ndubbo_request_avg_milliseconds_aggregate{app=\"dubbo\",version=\"1.0.0\"} 45")
	assert.Contains(t, text, "# HELP dubbo_request_max_milliseconds_aggregate The maximum request\n# TYPE dubbo_request_max_milliseconds_aggregate gauge\ndubbo_request_max_milliseconds_aggregate{app=\"dubbo\",version=\"1.0.0\"} 90")
	assert.Contains(t, text, "# HELP dubbo_request_min_milliseconds_aggregate The minimum request\n# TYPE dubbo_request_min_milliseconds_aggregate gauge\ndubbo_request_min_milliseconds_aggregate{app=\"dubbo\",version=\"1.0.0\"} 0")
}

func TestPromMetricRegistryCounterConcurrent(t *testing.T) {
	p := NewPromMetricRegistry(prom.NewRegistry(), url)
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			p.Counter(metricId).Inc()
			wg.Done()
		}()
	}
	wg.Wait()
	text, err := p.Scrape()
	assert.Nil(t, err)
	assert.Contains(t, text, "# HELP dubbo_request request\n# TYPE dubbo_request counter")
	assert.Contains(t, text, `dubbo_request{app="dubbo",version="1.0.0"} 10`)
}

func TestPromMetricRegistryExport(t *testing.T) {
	p := NewPromMetricRegistry(prom.NewRegistry(), url)
	go func() {
		for {
			p.Rt(metricId, &metrics.RtOpts{}).Observe(10 * float64(1))
			time.Sleep(1 * time.Second)
		}
	}()
	p.Export()
	// test push
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		err := http.ListenAndServe(url.GetParam(constant.PrometheusPushgatewayBaseUrlKey, ""),
			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				bodyBytes, err := io.ReadAll(r.Body)
				assert.Nil(t, err)
				text := string(bodyBytes)
				assert.Contains(t, text, "dubbo_request_avg")
				wg.Done()
			}))
		assert.Nil(t, err)
	}()
	timeout := url.GetParamByIntValue(constant.PrometheusPushgatewayPushIntervalKey, constant.PrometheusDefaultPushInterval)
	if waitTimeout(&wg, time.Duration(timeout+1)*time.Second) {
		assert.Fail(t, "wait pushgateway data timeout")
	}
	// test pull
	resp, err := http.Get("http://localhost:" +
		url.GetParam(constant.PrometheusExporterMetricsPortKey, constant.PrometheusDefaultMetricsPort) +
		url.GetParam(constant.PrometheusExporterMetricsPathKey, constant.PrometheusDefaultMetricsPath),
	)
	assert.Nil(t, err)
	defer resp.Body.Close()
	bodyBytes, err := io.ReadAll(resp.Body)
	assert.Nil(t, err)
	text := string(bodyBytes)
	assert.Contains(t, text, "dubbo_request_avg")
}

func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
	c := make(chan struct{})
	go func() {
		defer close(c)
		wg.Wait()
	}()
	select {
	case <-c:
		return false // completed normally
	case <-time.After(timeout):
		return true // timed out
	}
}
