package graphitebridge

import (
	"bufio"
	"bytes"
	"io"
	"net"
	"regexp"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/prometheus/common/model"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCountersAsDelta(t *testing.T) {
	b, err := NewBridge(&Config{
		URL:             "localhost:12345",
		CountersAsDelta: true,
	})
	require.NoError(t, err)
	ty := dto.MetricType(0)
	mf := &dto.MetricFamily{
		Type:   &ty,
		Metric: []*dto.Metric{},
	}
	m := model.Metric{}

	var want float64
	var got float64
	want = float64(1)
	got = b.replaceCounterWithDelta(mf, m, model.SampleValue(1))
	require.Equal(t, want, got)

	got = b.replaceCounterWithDelta(mf, m, model.SampleValue(2))
	require.Equal(t, want, got)
}

func TestCountersAsDeltaDisabled(t *testing.T) {
	b, _ := NewBridge(&Config{
		URL:             "localhost:12345",
		CountersAsDelta: false,
	})
	ty := dto.MetricType(0)
	mf := &dto.MetricFamily{
		Type:   &ty,
		Metric: []*dto.Metric{},
	}
	m := model.Metric{}

	var want float64
	var got float64
	want = float64(1)
	got = b.replaceCounterWithDelta(mf, m, model.SampleValue(1))
	require.Equal(t, want, got)

	want = float64(2)
	got = b.replaceCounterWithDelta(mf, m, model.SampleValue(2))
	require.Equal(t, want, got)
}

func TestSanitize(t *testing.T) {
	testCases := []struct {
		in, out string
	}{
		{in: "hello", out: "hello"},
		{in: "hE/l1o", out: "hE_l1o"},
		{in: "he,*ll(.o", out: "he_ll_o"},
		{in: "hello_there%^&", out: "hello_there_"},
	}

	var buf bytes.Buffer
	w := bufio.NewWriter(&buf)

	for i, tc := range testCases {
		if err := writeSanitized(w, tc.in); err != nil {
			t.Fatalf("write failed: %v", err)
		}
		if err := w.Flush(); err != nil {
			t.Fatalf("flush failed: %v", err)
		}

		if want, got := tc.out, buf.String(); want != got {
			t.Fatalf("test case index %d: got sanitized string %s, want %s", i, got, want)
		}

		buf.Reset()
	}
}

func TestSanitizePrefix(t *testing.T) {
	testCases := []struct {
		in, out string
	}{
		{in: "service.prod.", out: "service.prod."},
		{in: "service.prod", out: "service.prod"},
	}

	var buf bytes.Buffer
	w := bufio.NewWriter(&buf)

	for i, tc := range testCases {
		if err := writePrefix(w, tc.in); err != nil {
			t.Fatalf("write failed: %v", err)
		}
		if err := w.Flush(); err != nil {
			t.Fatalf("flush failed: %v", err)
		}

		if want, got := tc.out, buf.String(); want != got {
			t.Fatalf("test case index %d: got sanitized string %s, want %s", i, got, want)
		}

		buf.Reset()
	}
}

func TestWriteSummary(t *testing.T) {
	sumVec := prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Name:        "name",
			Help:        "docstring",
			Namespace:   "grafana",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
			Objectives:  map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
		},
		[]string{"labelname"},
	)

	reg := prometheus.NewRegistry()
	reg.MustRegister(sumVec)

	b, err := NewBridge(&Config{
		URL:             "localhost:8080",
		Gatherer:        reg,
		CountersAsDelta: true,
	})
	if err != nil {
		t.Fatalf("cannot create bridge. err: %v", err)
	}

	sumVec.WithLabelValues("val1").Observe(float64(10))
	sumVec.WithLabelValues("val1").Observe(float64(20))
	sumVec.WithLabelValues("val1").Observe(float64(30))
	sumVec.WithLabelValues("val2").Observe(float64(20))
	sumVec.WithLabelValues("val2").Observe(float64(30))
	sumVec.WithLabelValues("val2").Observe(float64(40))

	mfs, err := reg.Gather()
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	now := model.Time(1477043083)
	var buf bytes.Buffer
	err = b.writeMetrics(&buf, mfs, "prefix.", now)
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	want := `prefix.name.constname.constvalue.labelname.val1.quantile.0_5 20 1477043
prefix.name.constname.constvalue.labelname.val1.quantile.0_9 30 1477043
prefix.name.constname.constvalue.labelname.val1.quantile.0_99 30 1477043
prefix.name_sum.constname.constvalue.labelname.val1 60 1477043
prefix.name_count.constname.constvalue.labelname.val1.count 3 1477043
prefix.name.constname.constvalue.labelname.val2.quantile.0_5 30 1477043
prefix.name.constname.constvalue.labelname.val2.quantile.0_9 40 1477043
prefix.name.constname.constvalue.labelname.val2.quantile.0_99 40 1477043
prefix.name_sum.constname.constvalue.labelname.val2 90 1477043
prefix.name_count.constname.constvalue.labelname.val2.count 3 1477043
`

	if got := buf.String(); want != got {
		t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
	}
}

func TestWriteHistogram(t *testing.T) {
	histVec := prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:        "name",
			Help:        "docstring",
			Namespace:   "grafana",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
			Buckets:     []float64{0.01, 0.02, 0.05, 0.1},
		},
		[]string{"labelname"},
	)

	reg := prometheus.NewRegistry()
	reg.MustRegister(histVec)

	b, err := NewBridge(&Config{
		URL:             "localhost:8080",
		Gatherer:        reg,
		CountersAsDelta: true,
	})
	if err != nil {
		t.Fatalf("error creating bridge: %v", err)
	}

	histVec.WithLabelValues("val1").Observe(float64(10))
	histVec.WithLabelValues("val1").Observe(float64(20))
	histVec.WithLabelValues("val1").Observe(float64(30))
	histVec.WithLabelValues("val2").Observe(float64(20))
	histVec.WithLabelValues("val2").Observe(float64(30))
	histVec.WithLabelValues("val2").Observe(float64(40))

	mfs, err := reg.Gather()
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	now := model.Time(1477043083)
	var buf bytes.Buffer
	err = b.writeMetrics(&buf, mfs, "prefix.", now)
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	want := `prefix.name_bucket.constname.constvalue.labelname.val1.le.0_01 0 1477043
prefix.name_bucket.constname.constvalue.labelname.val1.le.0_02 0 1477043
prefix.name_bucket.constname.constvalue.labelname.val1.le.0_05 0 1477043
prefix.name_bucket.constname.constvalue.labelname.val1.le.0_1 0 1477043
prefix.name_sum.constname.constvalue.labelname.val1.sum 60 1477043
prefix.name_count.constname.constvalue.labelname.val1.count 3 1477043
prefix.name_bucket.constname.constvalue.labelname.val1.le._Inf 3 1477043
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_01 0 1477043
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_02 0 1477043
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_05 0 1477043
prefix.name_bucket.constname.constvalue.labelname.val2.le.0_1 0 1477043
prefix.name_sum.constname.constvalue.labelname.val2.sum 90 1477043
prefix.name_count.constname.constvalue.labelname.val2.count 3 1477043
prefix.name_bucket.constname.constvalue.labelname.val2.le._Inf 3 1477043
`
	if got := buf.String(); want != got {
		t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
	}
}

func TestCounterVec(t *testing.T) {
	cntVec := prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name:        "page_response",
			Namespace:   "grafana",
			Help:        "docstring",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
		},
		[]string{"labelname"},
	)

	apicntVec := prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name:        "api_response",
			Namespace:   "grafana",
			Help:        "docstring",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
		},
		[]string{"labelname"},
	)

	reg := prometheus.NewRegistry()
	reg.MustRegister(cntVec)
	reg.MustRegister(apicntVec)

	cntVec.WithLabelValues("val1").Inc()
	cntVec.WithLabelValues("val2").Inc()
	apicntVec.WithLabelValues("val1").Inc()
	apicntVec.WithLabelValues("val2").Inc()

	b, err := NewBridge(&Config{
		URL:             "localhost:8080",
		Gatherer:        reg,
		CountersAsDelta: true,
	})
	if err != nil {
		t.Fatalf("error creating bridge: %v", err)
	}

	// first collect
	mfs, err := reg.Gather()
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	var buf bytes.Buffer
	err = b.writeMetrics(&buf, mfs, "prefix.", model.Time(1477043083))
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	want := `prefix.api.response.constname.constvalue.labelname.val1.count 1 1477043
prefix.api.response.constname.constvalue.labelname.val2.count 1 1477043
prefix.page.response.constname.constvalue.labelname.val1.count 1 1477043
prefix.page.response.constname.constvalue.labelname.val2.count 1 1477043
`
	if got := buf.String(); want != got {
		t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
	}

	// next collect
	cntVec.WithLabelValues("val1").Inc()
	cntVec.WithLabelValues("val2").Inc()
	apicntVec.WithLabelValues("val1").Inc()
	apicntVec.WithLabelValues("val2").Inc()

	mfs, err = reg.Gather()
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	buf = bytes.Buffer{}
	err = b.writeMetrics(&buf, mfs, "prefix.", model.Time(1477053083))
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	want2 := `prefix.api.response.constname.constvalue.labelname.val1.count 1 1477053
prefix.api.response.constname.constvalue.labelname.val2.count 1 1477053
prefix.page.response.constname.constvalue.labelname.val1.count 1 1477053
prefix.page.response.constname.constvalue.labelname.val2.count 1 1477053
`
	if got := buf.String(); want2 != got {
		t.Fatalf("wanted \n%s\n, got \n%s\n", want2, got)
	}
}

func TestCounter(t *testing.T) {
	cntVec := prometheus.NewCounter(
		prometheus.CounterOpts{
			Name:        "page_response",
			Help:        "docstring",
			Namespace:   "grafana",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
		})

	reg := prometheus.NewRegistry()
	reg.MustRegister(cntVec)

	cntVec.Inc()

	b, err := NewBridge(&Config{
		URL:             "localhost:8080",
		Gatherer:        reg,
		CountersAsDelta: true,
	})
	if err != nil {
		t.Fatalf("error creating bridge: %v", err)
	}

	// first collect
	mfs, err := reg.Gather()
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	var buf bytes.Buffer
	err = b.writeMetrics(&buf, mfs, "prefix.", model.Time(1477043083))
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	want := "prefix.page.response.constname.constvalue.count 1 1477043\n"
	if got := buf.String(); want != got {
		t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
	}

	// next collect
	cntVec.Inc()

	mfs, err = reg.Gather()
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	buf = bytes.Buffer{}
	err = b.writeMetrics(&buf, mfs, "prefix.", model.Time(1477053083))
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	want2 := "prefix.page.response.constname.constvalue.count 1 1477053\n"
	if got := buf.String(); want2 != got {
		t.Fatalf("wanted \n%s\n, got \n%s\n", want2, got)
	}
}

func TestTrimGrafanaNamespace(t *testing.T) {
	cntVec := prometheus.NewCounter(
		prometheus.CounterOpts{
			Name:        "http_request_total",
			Help:        "docstring",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
		})

	reg := prometheus.NewRegistry()
	reg.MustRegister(cntVec)

	cntVec.Inc()

	b, err := NewBridge(&Config{
		URL:             "localhost:8080",
		Gatherer:        reg,
		CountersAsDelta: true,
	})
	if err != nil {
		t.Fatalf("error creating bridge: %v", err)
	}

	// first collect
	mfs, err := reg.Gather()
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	var buf bytes.Buffer
	err = b.writeMetrics(&buf, mfs, "prefix.", model.Time(1477043083))
	if err != nil {
		t.Fatalf("error: %v", err)
	}

	want := "prefix.http_request_total.constname.constvalue.count 1 1477043\n"
	if got := buf.String(); want != got {
		t.Fatalf("wanted \n%s\n, got \n%s\n", want, got)
	}
}

func TestSkipNanValues(t *testing.T) {
	cntVec := prometheus.NewSummary(
		prometheus.SummaryOpts{
			Name:        "http_request_total",
			Help:        "docstring",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
		})

	reg := prometheus.NewRegistry()
	reg.MustRegister(cntVec)

	b, err := NewBridge(&Config{
		URL:             "localhost:8080",
		Gatherer:        reg,
		CountersAsDelta: true,
	})
	require.NoError(t, err)

	// first collect
	mfs, err := reg.Gather()
	require.NoError(t, err)

	var buf bytes.Buffer
	err = b.writeMetrics(&buf, mfs, "prefix.", model.Time(1477043083))
	require.NoError(t, err)

	want := `prefix.http_request_total_sum.constname.constvalue 0 1477043
prefix.http_request_total_count.constname.constvalue.count 0 1477043
`

	assert.Equal(t, want, buf.String())
}

func TestPush(t *testing.T) {
	reg := prometheus.NewRegistry()
	cntVec := prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name:        "name",
			Help:        "docstring",
			Namespace:   "grafana",
			ConstLabels: prometheus.Labels{"constname": "constvalue"},
		},
		[]string{"labelname"},
	)
	cntVec.WithLabelValues("val1").Inc()
	cntVec.WithLabelValues("val2").Inc()
	reg.MustRegister(cntVec)

	host := "localhost"
	port := ":56789"
	b, err := NewBridge(&Config{
		URL:      host + port,
		Gatherer: reg,
		Prefix:   "prefix.",
	})
	require.NoError(t, err)

	nmg, err := newMockGraphite(port)
	require.NoError(t, err)
	t.Cleanup(func() {
		err := nmg.Close()
		require.NoError(t, err)
	})

	err = b.Push()
	require.NoError(t, err)

	wants := []string{
		"prefix.name.constname.constvalue.labelname.val1.count 1",
		"prefix.name.constname.constvalue.labelname.val2.count 1",
	}

	select {
	case got := <-nmg.readc:
		for _, want := range wants {
			matched, err := regexp.MatchString(want, got)
			if err != nil {
				t.Fatalf("error pushing: %v", err)
			}
			if !matched {
				t.Fatalf("missing metric:\nno match for %s received by server:\n%s", want, got)
			}
		}
		return
	case err := <-nmg.errc:
		t.Fatalf("error reading push: %v", err)
	case <-time.After(50 * time.Millisecond):
		t.Fatalf("no result from graphite server")
	}
}

func newMockGraphite(port string) (*mockGraphite, error) {
	readc := make(chan string)
	errc := make(chan error)
	ln, err := net.Listen("tcp", port)
	if err != nil {
		return nil, err
	}

	go func() {
		conn, err := ln.Accept()
		if err != nil {
			errc <- err
			return
		}

		var b bytes.Buffer
		if _, err := io.Copy(&b, conn); err != nil {
			errc <- err
			return
		}

		readc <- b.String()
	}()

	return &mockGraphite{
		readc:    readc,
		errc:     errc,
		Listener: ln,
	}, nil
}

type mockGraphite struct {
	readc chan string
	errc  chan error

	net.Listener
}
