package influxdb

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

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf/plugins/parsers/influx"
	"github.com/influxdata/telegraf/testutil"
)

func TestBasic(t *testing.T) {
	fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/endpoint" {
			if _, err := w.Write([]byte(basicJSON)); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer fakeServer.Close()

	plugin := &InfluxDB{
		URLs: []string{fakeServer.URL + "/endpoint"},
	}

	var acc testutil.Accumulator
	require.NoError(t, acc.GatherError(plugin.Gather))

	require.Len(t, acc.Metrics, 3)
	fields := map[string]interface{}{
		// JSON will truncate floats to integer representations.
		// Since there's no distinction in JSON, we can't assume it's an int.
		"i": -1.0,
		"f": 0.5,
		"b": true,
		"s": "string",
	}
	tags := map[string]string{
		"id":  "ex1",
		"url": fakeServer.URL + "/endpoint",
	}
	acc.AssertContainsTaggedFields(t, "influxdb_foo", fields, tags)

	fields = map[string]interface{}{
		"x": "x",
	}
	tags = map[string]string{
		"id":  "ex2",
		"url": fakeServer.URL + "/endpoint",
	}
	acc.AssertContainsTaggedFields(t, "influxdb_bar", fields, tags)

	acc.AssertContainsTaggedFields(t, "influxdb",
		map[string]interface{}{
			"n_shards": 0,
		}, map[string]string{})
}

func TestInfluxDB(t *testing.T) {
	influxReturn, err := os.ReadFile("./testdata/influx_return.json")
	require.NoError(t, err)

	fakeInfluxServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/endpoint" {
			if _, err := w.Write(influxReturn); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer fakeInfluxServer.Close()

	plugin := &InfluxDB{
		URLs: []string{fakeInfluxServer.URL + "/endpoint"},
	}

	var acc testutil.Accumulator
	require.NoError(t, acc.GatherError(plugin.Gather))

	require.Len(t, acc.Metrics, 36)

	fields := map[string]interface{}{
		"heap_inuse":      int64(18046976),
		"heap_released":   int64(3473408),
		"mspan_inuse":     int64(97440),
		"total_alloc":     int64(201739016),
		"sys":             int64(38537464),
		"mallocs":         int64(570251),
		"frees":           int64(381008),
		"heap_idle":       int64(15802368),
		"pause_total_ns":  int64(5132914),
		"pause_ns":        int64(127053),
		"lookups":         int64(77),
		"heap_sys":        int64(33849344),
		"mcache_sys":      int64(16384),
		"next_gc":         int64(20843042),
		"gc_cpu_fraction": float64(4.287178819113636e-05),
		"other_sys":       int64(1229737),
		"alloc":           int64(17034016),
		"stack_inuse":     int64(753664),
		"stack_sys":       int64(753664),
		"buck_hash_sys":   int64(1461583),
		"gc_sys":          int64(1112064),
		"num_gc":          int64(27),
		"heap_alloc":      int64(17034016),
		"heap_objects":    int64(189243),
		"mspan_sys":       int64(114688),
		"mcache_inuse":    int64(4800),
		"last_gc":         int64(1460434886475114239),
	}

	tags := map[string]string{
		"url": fakeInfluxServer.URL + "/endpoint",
	}
	acc.AssertContainsTaggedFields(t, "influxdb_memstats", fields, tags)

	fields = map[string]interface{}{
		"current_time": "2023-01-11T16:51:52.723166944Z",
		"started":      "2023-01-11T16:51:23.355766023Z",
		"uptime":       uint64(29),
	}
	acc.AssertContainsTaggedFields(t, "influxdb_system", fields, tags)

	acc.AssertContainsTaggedFields(t, "influxdb",
		map[string]interface{}{
			"n_shards": 1,
		}, map[string]string{})
}

func TestInfluxDB2(t *testing.T) {
	// InfluxDB 1.0+ with tags: null instead of tags: {}.
	influxReturn2, err := os.ReadFile("./testdata/influx_return2.json")
	require.NoError(t, err)

	fakeInfluxServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/endpoint" {
			if _, err := w.Write(influxReturn2); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer fakeInfluxServer.Close()

	plugin := &InfluxDB{
		URLs: []string{fakeInfluxServer.URL + "/endpoint"},
	}

	var acc testutil.Accumulator
	require.NoError(t, acc.GatherError(plugin.Gather))

	require.Len(t, acc.Metrics, 36)

	acc.AssertContainsTaggedFields(t, "influxdb",
		map[string]interface{}{
			"n_shards": 1,
		}, map[string]string{})

	fields := map[string]interface{}{
		"current_time": "2023-01-11T17:04:59.928454705Z",
		"started":      "2023-01-11T16:51:23.355766023Z",
		"uptime":       uint64(816),
	}
	tags := map[string]string{
		"url": fakeInfluxServer.URL + "/endpoint",
	}
	acc.AssertContainsTaggedFields(t, "influxdb_system", fields, tags)
}

func TestCloud1(t *testing.T) {
	// Setup a fake endpoint with the input data
	input, err := os.ReadFile("./testdata/cloud1.json")
	require.NoError(t, err)

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/endpoint" {
			if _, err := w.Write(input); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer server.Close()

	// Setup the plugin
	plugin := &InfluxDB{
		URLs: []string{server.URL + "/endpoint"},
	}

	// Gather the data
	var acc testutil.Accumulator
	require.NoError(t, acc.GatherError(plugin.Gather))

	// Read the expected data
	parser := &influx.Parser{}
	require.NoError(t, parser.Init())

	buf, err := os.ReadFile("./testdata/cloud1.influx")
	require.NoError(t, err)
	expected, err := parser.Parse(buf)
	require.NoError(t, err)

	// Check the output
	opts := []cmp.Option{testutil.IgnoreTags("url"), testutil.IgnoreTime()}
	actual := acc.GetTelegrafMetrics()
	testutil.RequireMetricsEqual(t, expected, actual, opts...)
}

func TestErrorHandling(t *testing.T) {
	badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/endpoint" {
			if _, err := w.Write([]byte("not json")); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer badServer.Close()

	plugin := &InfluxDB{
		URLs: []string{badServer.URL + "/endpoint"},
	}

	var acc testutil.Accumulator
	require.Error(t, acc.GatherError(plugin.Gather))
}

func TestErrorHandling404(t *testing.T) {
	badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/endpoint" {
			if _, err := w.Write([]byte(basicJSON)); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				t.Error(err)
				return
			}
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer badServer.Close()

	plugin := &InfluxDB{
		URLs: []string{badServer.URL},
	}

	var acc testutil.Accumulator
	require.Error(t, acc.GatherError(plugin.Gather))
}

func TestErrorResponse(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
		if _, err := w.Write([]byte(`{"error": "unable to parse authentication credentials"}`)); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			t.Error(err)
			return
		}
	}))
	defer ts.Close()

	plugin := &InfluxDB{
		URLs: []string{ts.URL},
	}

	var acc testutil.Accumulator
	err := plugin.Gather(&acc)
	require.NoError(t, err)

	expected := []error{
		&apiError{
			StatusCode:  http.StatusUnauthorized,
			Reason:      fmt.Sprintf("%d %s", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)),
			Description: "unable to parse authentication credentials",
		},
	}
	require.Equal(t, expected, acc.Errors)
}

const basicJSON = `
{
  "_1": {
    "name": "foo",
    "tags": {
      "id": "ex1"
    },
    "values": {
      "i": -1,
      "f": 0.5,
      "b": true,
      "s": "string"
    }
  },
  "ignored": {
    "willBeRecorded": false
  },
  "ignoredAndNested": {
    "hash": {
      "is": "nested"
    }
  },
  "array": [
   "makes parsing more difficult than necessary"
  ],
  "string": "makes parsing more difficult than necessary",
  "_2": {
    "name": "bar",
    "tags": {
      "id": "ex2"
    },
    "values": {
      "x": "x"
    }
  },
  "pointWithoutFields_willNotBeIncluded": {
    "name": "asdf",
    "tags": {
      "id": "ex3"
    },
    "values": {}
  }
}
`
