package httpd_test

import (
	"bytes"
	"encoding/json"
	"fmt"
	"math"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/influxdata/influxdb/models"
	"github.com/influxdata/influxdb/query"
	"github.com/influxdata/influxdb/services/httpd"
	"github.com/tinylib/msgp/msgp"
)

func TestResponseWriter_CSV(t *testing.T) {
	tableTest := []struct {
		header string
	}{
		{header: "*/csv"},
		{header: "text/*"},
		{header: "text/csv"},
		{header: "text/csv,application/json"},
		{header: "text/csv;q=1,application/json"},
		{header: "text/csv;q=0.9,application/json;q=0.8"},
		{header: "application/json;q=0.8,text/csv;q=0.9"},
	}

	for _, testCase := range tableTest {
		testCase := testCase
		t.Run(testCase.header, func(t *testing.T) {
			t.Parallel()
			header := make(http.Header)
			header.Set("Accept", testCase.header)
			r := &http.Request{
				Header: header,
				URL:    &url.URL{},
			}
			w := httptest.NewRecorder()

			writer := httpd.NewResponseWriter(w, r)
			n, err := writer.WriteResponse(httpd.Response{
				Results: []*query.Result{
					{
						StatementID: 0,
						Series: []*models.Row{
							{
								Name: "cpu",
								Tags: map[string]string{
									"host":   "server01",
									"region": "uswest",
								},
								Columns: []string{"time", "value"},
								Values: [][]interface{}{
									{time.Unix(0, 10), float64(2.5)},
									{time.Unix(0, 20), int64(5)},
									{time.Unix(0, 30), nil},
									{time.Unix(0, 40), "foobar"},
									{time.Unix(0, 50), true},
									{time.Unix(0, 60), false},
									{time.Unix(0, 70), uint64(math.MaxInt64 + 1)},
								},
							},
							{
								Name: "cpu",
								Tags: map[string]string{
									"host":   "",
									"region": "",
								},
								Columns: []string{"time", "value"},
								Values: [][]interface{}{
									{time.Unix(0, 10), float64(2.5)},
									{time.Unix(0, 20), int64(5)},
									{time.Unix(0, 30), nil},
									{time.Unix(0, 40), "foobar"},
									{time.Unix(0, 50), true},
									{time.Unix(0, 60), false},
									{time.Unix(0, 70), uint64(math.MaxInt64 + 1)},
								},
							},
						},
					},
				},
			})
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if got, want := w.Body.String(), `name,tags,time,value
cpu,"host=server01,region=uswest",10,2.5
cpu,"host=server01,region=uswest",20,5
cpu,"host=server01,region=uswest",30,
cpu,"host=server01,region=uswest",40,foobar
cpu,"host=server01,region=uswest",50,true
cpu,"host=server01,region=uswest",60,false
cpu,"host=server01,region=uswest",70,9223372036854775808
cpu,,10,2.5
cpu,,20,5
cpu,,30,
cpu,,40,foobar
cpu,,50,true
cpu,,60,false
cpu,,70,9223372036854775808
`; got != want {
				t.Errorf("unexpected output:\n\ngot=%v\nwant=%s", got, want)
			} else if got, want := n, len(want); got != want {
				t.Errorf("unexpected output length: got=%d want=%d", got, want)
			}
		})
	}
}

func TestResponseWriter_MessagePack(t *testing.T) {
	tableTest := []struct {
		header string
	}{
		{header: "*/x-msgpack"},
		{header: "application/x-msgpack"},
		{header: "application/x-msgpack,application/json"},
		{header: "application/x-msgpack;q=1,application/json"},
		{header: "application/x-msgpack;q=0.9,application/json;q=0.8"},
		{header: "application/json;q=0.8,application/x-msgpack;q=0.9"},
	}

	for _, testCase := range tableTest {
		testCase := testCase
		t.Run(testCase.header, func(t *testing.T) {
			t.Parallel()
			header := make(http.Header)
			header.Set("Accept", testCase.header)
			r := &http.Request{
				Header: header,
				URL:    &url.URL{},
			}
			w := httptest.NewRecorder()

			writer := httpd.NewResponseWriter(w, r)
			_, err := writer.WriteResponse(httpd.Response{
				Results: []*query.Result{
					{
						StatementID: 0,
						Series: []*models.Row{
							{
								Name: "cpu",
								Tags: map[string]string{
									"host": "server01",
								},
								Columns: []string{"time", "value"},
								Values: [][]interface{}{
									{time.Unix(0, 10), float64(2.5)},
									{time.Unix(0, 20), int64(5)},
									{time.Unix(0, 30), nil},
									{time.Unix(0, 40), "foobar"},
									{time.Unix(0, 50), true},
									{time.Unix(0, 60), false},
									{time.Unix(0, 70), uint64(math.MaxInt64 + 1)},
								},
							},
						},
					},
				},
			})
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			// The reader always reads times as time.Local so encode the expected response
			// as JSON and insert it into the expected values.
			values, err := json.Marshal([][]interface{}{
				{time.Unix(0, 10).Local(), float64(2.5)},
				{time.Unix(0, 20).Local(), int64(5)},
				{time.Unix(0, 30).Local(), nil},
				{time.Unix(0, 40).Local(), "foobar"},
				{time.Unix(0, 50).Local(), true},
				{time.Unix(0, 60).Local(), false},
				{time.Unix(0, 70).Local(), uint64(math.MaxInt64 + 1)},
			})
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			reader := msgp.NewReader(w.Body)
			var buf bytes.Buffer
			if _, err := reader.WriteToJSON(&buf); err != nil {
				t.Fatalf("unexpected error: %s", err)
			}
			want := fmt.Sprintf(`{"results":[{"statement_id":0,"series":[{"name":"cpu","tags":{"host":"server01"},"columns":["time","value"],"values":%s}]}]}`, string(values))
			if got := strings.TrimSpace(buf.String()); got != want {
				t.Fatalf("unexpected output:\n\ngot=%v\nwant=%v", got, want)
			}
		})
	}
}

func TestResponseWriter_MessagePack_Error(t *testing.T) {
	tableTest := []struct {
		header string
	}{
		{header: "application/x-msgpack"},
		{header: "application/x-msgpack,application/json"},
		{header: "application/x-msgpack;q=1,application/json"},
		{header: "application/x-msgpack;q=0.9,application/json;q=0.8"},
		{header: "application/json;q=0.8,application/x-msgpack;q=0.9"},
	}

	for _, testCase := range tableTest {
		testCase := testCase
		t.Run(testCase.header, func(t *testing.T) {
			t.Parallel()
			header := make(http.Header)
			header.Set("Accept", testCase.header)
			r := &http.Request{
				Header: header,
				URL:    &url.URL{},
			}
			w := httptest.NewRecorder()

			writer := httpd.NewResponseWriter(w, r)
			writer.WriteResponse(httpd.Response{
				Err: fmt.Errorf("test error"),
			})

			reader := msgp.NewReader(w.Body)
			var buf bytes.Buffer
			if _, err := reader.WriteToJSON(&buf); err != nil {
				t.Fatalf("unexpected error: %s", err)
			}
			want := fmt.Sprintf(`{"error":"test error"}`)
			if have := strings.TrimSpace(buf.String()); have != want {
				t.Fatalf("unexpected output: %s != %s", have, want)
			}
		})
	}
}

func TestResponseWriter_CSV_DifferentColumns(t *testing.T) {
	header := make(http.Header)
	header.Set("Accept", "text/csv")
	r := &http.Request{
		Header: header,
		URL:    &url.URL{},
	}
	w := httptest.NewRecorder()

	writer := httpd.NewResponseWriter(w, r)
	writer.WriteResponse(httpd.Response{
		Results: []*query.Result{
			{
				StatementID: 0,
				Series: []*models.Row{
					{
						Name:    "network",
						Columns: []string{"hostname"},
						Values: [][]interface{}{
							{"localhost"},
						},
					},
					{
						Name:    "runtime",
						Columns: []string{"GOARCH", "GOMAXPROCS", "GOOS", "version"},
						Values: [][]interface{}{
							{"amd64", int64(8), "darwin", "go1.12"},
						},
					},
				},
			},
		},
	})

	if got, want := w.Body.String(), `name,tags,hostname
network,,localhost

name,tags,GOARCH,GOMAXPROCS,GOOS,version
runtime,,amd64,8,darwin,go1.12
`; got != want {
		t.Errorf("unexpected output:\n\ngot=%v\nwant=%s", got, want)
	}
}
