package aurora

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/influxdata/telegraf/testutil"
	"github.com/stretchr/testify/require"
)

type (
	TestHandlerFunc func(t *testing.T, w http.ResponseWriter, r *http.Request)
	CheckFunc       func(t *testing.T, err error, acc *testutil.Accumulator)
)

func TestAurora(t *testing.T) {
	ts := httptest.NewServer(http.NotFoundHandler())
	defer ts.Close()

	u, err := url.Parse(fmt.Sprintf("http://%s", ts.Listener.Addr().String()))
	require.NoError(t, err)

	tests := []struct {
		name         string
		plugin       *Aurora
		schedulers   []string
		roles        []string
		leaderhealth TestHandlerFunc
		varsjson     TestHandlerFunc
		check        CheckFunc
	}{
		{
			name: "minimal",
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			},
			varsjson: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				body := `{
					"variable_scrape_events": 2958,
					"variable_scrape_events_per_sec": 1.0,
					"variable_scrape_micros_per_event": 1484.0,
					"variable_scrape_micros_total": 4401084,
					"variable_scrape_micros_total_per_sec": 1485.0
				}`
				w.WriteHeader(http.StatusOK)
				_, err := w.Write([]byte(body))
				require.NoError(t, err)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.Equal(t, 1, len(acc.Metrics))
				acc.AssertContainsTaggedFields(t,
					"aurora",
					map[string]interface{}{
						"variable_scrape_events":               int64(2958),
						"variable_scrape_events_per_sec":       1.0,
						"variable_scrape_micros_per_event":     1484.0,
						"variable_scrape_micros_total":         int64(4401084),
						"variable_scrape_micros_total_per_sec": 1485.0,
					},
					map[string]string{
						"scheduler": u.String(),
						"role":      "leader",
					},
				)
			},
		},
		{
			name:  "disabled role",
			roles: []string{"leader"},
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusServiceUnavailable)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.NoError(t, acc.FirstError())
				require.Equal(t, 0, len(acc.Metrics))
			},
		},
		{
			name: "no metrics available",
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			},
			varsjson: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
				_, err := w.Write([]byte("{}"))
				require.NoError(t, err)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.NoError(t, acc.FirstError())
				require.Equal(t, 0, len(acc.Metrics))
			},
		},
		{
			name: "string metrics skipped",
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			},
			varsjson: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				body := `{
					"foo": "bar"
				}`
				w.WriteHeader(http.StatusOK)
				_, err := w.Write([]byte(body))
				require.NoError(t, err)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.NoError(t, acc.FirstError())
				require.Equal(t, 0, len(acc.Metrics))
			},
		},
		{
			name: "float64 unparseable",
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			},
			varsjson: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				// too large
				body := `{
					"foo": 1e309
				}`
				w.WriteHeader(http.StatusOK)
				_, err := w.Write([]byte(body))
				require.NoError(t, err)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.Error(t, acc.FirstError())
				require.Equal(t, 0, len(acc.Metrics))
			},
		},
		{
			name: "int64 unparseable",
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			},
			varsjson: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				// too large
				body := `{
					"foo": 9223372036854775808
				}`
				w.WriteHeader(http.StatusOK)
				_, err := w.Write([]byte(body))
				require.NoError(t, err)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.Error(t, acc.FirstError())
				require.Equal(t, 0, len(acc.Metrics))
			},
		},
		{
			name: "bad json",
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			},
			varsjson: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				body := `{]`
				w.WriteHeader(http.StatusOK)
				_, err := w.Write([]byte(body))
				require.NoError(t, err)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.Error(t, acc.FirstError())
				require.Equal(t, 0, len(acc.Metrics))
			},
		},
		{
			name: "wrong status code",
			leaderhealth: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			},
			varsjson: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
				body := `{
					"value": 42
				}`
				w.WriteHeader(http.StatusServiceUnavailable)
				_, err := w.Write([]byte(body))
				require.NoError(t, err)
			},
			check: func(t *testing.T, err error, acc *testutil.Accumulator) {
				require.NoError(t, err)
				require.Error(t, acc.FirstError())
				require.Equal(t, 0, len(acc.Metrics))
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				switch r.URL.Path {
				case "/leaderhealth":
					tt.leaderhealth(t, w, r)
				case "/vars.json":
					tt.varsjson(t, w, r)
				default:
					w.WriteHeader(http.StatusNotFound)
				}
			})

			var acc testutil.Accumulator
			plugin := &Aurora{}
			plugin.Schedulers = []string{u.String()}
			plugin.Roles = tt.roles
			err := plugin.Gather(&acc)
			tt.check(t, err, &acc)
		})
	}
}

func TestBasicAuth(t *testing.T) {
	ts := httptest.NewServer(http.NotFoundHandler())
	defer ts.Close()

	u, err := url.Parse(fmt.Sprintf("http://%s", ts.Listener.Addr().String()))
	require.NoError(t, err)

	tests := []struct {
		name     string
		username string
		password string
	}{
		{
			name: "no auth",
		},
		{
			name:     "basic auth",
			username: "username",
			password: "pa$$word",
		},
		{
			name:     "username only",
			username: "username",
		},
		{
			name:     "password only",
			password: "pa$$word",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				username, password, _ := r.BasicAuth()
				require.Equal(t, tt.username, username)
				require.Equal(t, tt.password, password)
				w.WriteHeader(http.StatusOK)
				_, err := w.Write([]byte("{}"))
				require.NoError(t, err)
			})

			var acc testutil.Accumulator
			plugin := &Aurora{}
			plugin.Schedulers = []string{u.String()}
			plugin.Username = tt.username
			plugin.Password = tt.password
			err := plugin.Gather(&acc)
			require.NoError(t, err)
		})
	}
}
