package sqlmetrics

import (
	"bytes"
	"database/sql"
	"fmt"
	"html/template"
	"testing"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/testutil"
	"github.com/stretchr/testify/require"
)

type dbMock sql.DBStats

func (db dbMock) Stats() sql.DBStats {
	return sql.DBStats(db)
}

func TestNewDBStatsCollector(t *testing.T) {
	db := &dbMock{
		MaxOpenConnections: 100,
		OpenConnections:    10,
		InUse:              3,
		Idle:               7,
		WaitCount:          5,
		WaitDuration:       12,
		MaxIdleClosed:      7,
		MaxLifetimeClosed:  8,
	}

	dbName := "foo"
	defaultLabels := prometheus.Labels{dbNameLabel: dbName}

	tests := []struct {
		name           string
		opts           []DBStatsCollectorOption
		expectedLabels prometheus.Labels
	}{
		{
			name:           "default",
			expectedLabels: defaultLabels,
		},
		{
			name: "with custom labels",
			opts: []DBStatsCollectorOption{
				WithExtraLabels(map[string]string{"x": "y"}),
			},
			expectedLabels: prometheus.Labels{
				dbNameLabel: dbName,
				"x":         "y",
			},
		},
		{
			name: "does not override db_name label",
			opts: []DBStatsCollectorOption{
				WithExtraLabels(map[string]string{"db_name": "bar"}),
			},
			expectedLabels: defaultLabels,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			collector := NewDBStatsCollector(dbName, db, tt.opts...)

			validateMetric(t, collector, maxOpenConnectionsName, maxOpenConnectionsDesc, "gauge", float64(db.MaxOpenConnections), tt.expectedLabels)
			validateMetric(t, collector, openConnectionsName, openConnectionsDesc, "gauge", float64(db.OpenConnections), tt.expectedLabels)
			validateMetric(t, collector, inUseName, inUseDesc, "gauge", float64(db.InUse), tt.expectedLabels)
			validateMetric(t, collector, idleName, idleDesc, "gauge", float64(db.Idle), tt.expectedLabels)
			validateMetric(t, collector, waitCountName, waitCountDesc, "counter", float64(db.WaitCount), tt.expectedLabels)
			validateMetric(t, collector, waitDurationName, waitDurationDesc, "counter", db.WaitDuration.Seconds(), tt.expectedLabels)
			validateMetric(t, collector, maxIdleClosedName, maxIdleClosedDesc, "counter", float64(db.MaxIdleClosed), tt.expectedLabels)
			validateMetric(t, collector, maxLifetimeClosedName, maxLifetimeClosedDesc, "counter", float64(db.MaxLifetimeClosed), tt.expectedLabels)
		})
	}
}

type labelsIter struct {
	Dict    prometheus.Labels
	Counter int
}

func (l *labelsIter) HasMore() bool {
	l.Counter++
	return l.Counter < len(l.Dict)
}

func validateMetric(t *testing.T, collector prometheus.Collector, name string, desc string, valueType string, value float64, labels prometheus.Labels) {
	t.Helper()

	tmpl := template.New("")
	tmpl.Delims("[[", "]]")
	txt := `
# HELP [[.Name]] [[.Desc]]
# TYPE [[.Name]] [[.Type]]
[[.Name]]{[[range $k, $v := .Labels.Dict]][[$k]]="[[$v]]"[[if $.Labels.HasMore]],[[end]][[end]]} [[.Value]]
`
	_, err := tmpl.Parse(txt)
	require.NoError(t, err)

	var expected bytes.Buffer
	fullName := fmt.Sprintf("%s_%s_%s", namespace, subsystem, name)

	err = tmpl.Execute(&expected, struct {
		Name   string
		Desc   string
		Type   string
		Value  float64
		Labels *labelsIter
	}{
		Name:   fullName,
		Desc:   desc,
		Labels: &labelsIter{Dict: labels},
		Value:  value,
		Type:   valueType,
	})
	require.NoError(t, err)

	err = testutil.CollectAndCompare(collector, &expected, fullName)
	require.NoError(t, err)
}
