package query_test

import (
	"bytes"
	"compress/gzip"
	"context"
	"io"
	"net/http"
	"strings"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/influxdata/influx-cli/v2/api"
	"github.com/influxdata/influx-cli/v2/clients"
	"github.com/influxdata/influx-cli/v2/clients/query"
	"github.com/influxdata/influx-cli/v2/config"
	"github.com/influxdata/influx-cli/v2/internal/mock"
	"github.com/stretchr/testify/assert"
	tmock "github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

func TestRawResultPrinter_PrintQueryResults(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		rawTable string
	}{
		{
			name:     "empty",
			rawTable: "",
		},
		{
			name: "single table",
			rawTable: `#group,false,false,true,true,false,false,true,true,true,true
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string
#default,_result,,,,,,,,
,result,table,_start,_stop,_time,_value,_field,_measurement,bar
,,0,1921-05-08T15:29:32.475078Z,2021-05-08T15:29:32.475078Z,2021-05-04T18:29:52.764702Z,12345,qux,foo,"""baz"""
,,0,1921-05-08T15:29:32.475078Z,2021-05-08T15:29:32.475078Z,2021-05-04T19:30:59.67555Z,12345,qux,foo,"""baz"""
,,0,1921-05-08T15:29:32.475078Z,2021-05-08T15:29:32.475078Z,2021-05-04T19:31:01.876079Z,12345,qux,foo,"""baz"""
,,0,1921-05-08T15:29:32.475078Z,2021-05-08T15:29:32.475078Z,2021-05-04T19:31:02.499461Z,12345,qux,foo,"""baz"""
`,
		},
		{
			name: "multi table",
			rawTable: `#group,false,false,true,true,false,false,true,true,true
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string
#default,_result,,,,,,,,
,result,table,_start,_stop,_time,_value,_field,_measurement,bar
,,0,1921-05-08T15:42:58.218436Z,2021-05-08T15:42:58.218436Z,2021-05-04T18:29:52.764702Z,12345,qux,foo,"""baz"""
,,0,1921-05-08T15:42:58.218436Z,2021-05-08T15:42:58.218436Z,2021-05-04T19:30:59.67555Z,12345,qux,foo,"""baz"""
,,0,1921-05-08T15:42:58.218436Z,2021-05-08T15:42:58.218436Z,2021-05-04T19:31:01.876079Z,12345,qux,foo,"""baz"""
,,0,1921-05-08T15:42:58.218436Z,2021-05-08T15:42:58.218436Z,2021-05-04T19:31:02.499461Z,12345,qux,foo,"""baz"""

#group,false,false,true,true,false,false,true,true,true,true
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
#default,_result,,,,,,,,,
,result,table,_start,_stop,_time,_value,_field,_measurement,bar,is_foo
,,1,1921-05-08T15:42:58.218436Z,2021-05-08T15:42:58.218436Z,2021-05-08T15:42:19.567667Z,12345,qux,foo,"""baz""",t

#group,false,false,true,false,false,false,false,false,false,false
#datatype,string,long,string,string,string,long,long,long,long,double
#default,_profiler,,,,,,,,,
,result,table,_measurement,Type,Label,Count,MinDuration,MaxDuration,DurationSum,MeanDuration
,,0,profiler/operator,*influxdb.readFilterSource,ReadRange2,1,367331,367331,367331,367331
`,
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			in := io.NopCloser(strings.NewReader(tc.rawTable))
			out := bytes.Buffer{}
			require.NoError(t, query.RawResultPrinter.PrintQueryResults(in, &out))
			require.Equal(t, tc.rawTable, out.String())
		})
	}
}

func TestQuery(t *testing.T) {
	t.Parallel()

	// Use dummy data here + the raw output printer to keep things
	// focused on the business logic of the Query command; details
	// of how results are formatted are tested elsewhere.
	fakeQuery := query.BuildDefaultAST("I'm a query!")
	fakeResults := "data data data"

	orgID := "1111111111111111"

	testCases := []struct {
		name                 string
		params               query.Params
		configOrgName        string
		registerExpectations func(t *testing.T, queryApi *mock.MockQueryApi)
		expectInErr          string
	}{
		{
			name: "by org ID",
			params: query.Params{
				OrgParams: clients.OrgParams{
					OrgID: orgID,
				},
				Query: fakeQuery.Query,
			},
			configOrgName: "default-org",
			registerExpectations: func(t *testing.T, queryApi *mock.MockQueryApi) {
				queryApi.EXPECT().PostQuery(gomock.Any()).Return(api.ApiPostQueryRequest{ApiService: queryApi})
				queryApi.EXPECT().PostQueryExecute(tmock.MatchedBy(func(in api.ApiPostQueryRequest) bool {
					body := in.GetQuery()
					return assert.NotNil(t, body) &&
						assert.Equal(t, fakeQuery, *body) &&
						assert.Equal(t, orgID, *in.GetOrgID()) &&
						assert.Nil(t, in.GetOrg())
				})).Return(&http.Response{Body: io.NopCloser(strings.NewReader(fakeResults))}, nil)
			},
		},
		{
			name: "by org name",
			params: query.Params{
				OrgParams: clients.OrgParams{
					OrgName: "my-org",
				},
				Query: fakeQuery.Query,
			},
			configOrgName: "default-org",
			registerExpectations: func(t *testing.T, queryApi *mock.MockQueryApi) {
				queryApi.EXPECT().PostQuery(gomock.Any()).Return(api.ApiPostQueryRequest{ApiService: queryApi})
				queryApi.EXPECT().PostQueryExecute(tmock.MatchedBy(func(in api.ApiPostQueryRequest) bool {
					body := in.GetQuery()
					return assert.NotNil(t, body) &&
						assert.Equal(t, fakeQuery, *body) &&
						assert.Equal(t, "my-org", *in.GetOrg()) &&
						assert.Nil(t, in.GetOrgID())
				})).Return(&http.Response{Body: io.NopCloser(strings.NewReader(fakeResults))}, nil)
			},
		},
		{
			name: "by org name from config",
			params: query.Params{
				OrgParams: clients.OrgParams{},
				Query:     fakeQuery.Query,
			},
			configOrgName: "default-org",
			registerExpectations: func(t *testing.T, queryApi *mock.MockQueryApi) {
				queryApi.EXPECT().PostQuery(gomock.Any()).Return(api.ApiPostQueryRequest{ApiService: queryApi})
				queryApi.EXPECT().PostQueryExecute(tmock.MatchedBy(func(in api.ApiPostQueryRequest) bool {
					body := in.GetQuery()
					return assert.NotNil(t, body) &&
						assert.Equal(t, fakeQuery, *body) &&
						assert.Equal(t, "default-org", *in.GetOrg()) &&
						assert.Nil(t, in.GetOrgID())
				})).Return(&http.Response{Body: io.NopCloser(strings.NewReader(fakeResults))}, nil)
			},
		},
		{
			name: "no org specified",
			params: query.Params{
				OrgParams: clients.OrgParams{},
				Query:     fakeQuery.Query,
			},
			expectInErr: clients.ErrMustSpecifyOrg.Error(),
		},
		{
			name: "with profilers",
			params: query.Params{
				OrgParams: clients.OrgParams{},
				Query:     fakeQuery.Query,
				Profilers: []string{"foo", "bar"},
			},
			configOrgName: "default-org",
			registerExpectations: func(t *testing.T, queryApi *mock.MockQueryApi) {
				queryApi.EXPECT().PostQuery(gomock.Any()).Return(api.ApiPostQueryRequest{ApiService: queryApi})

				expectedBody := fakeQuery
				expectedBody.Extern = query.BuildExternAST([]string{"foo", "bar"})

				queryApi.EXPECT().PostQueryExecute(tmock.MatchedBy(func(in api.ApiPostQueryRequest) bool {
					body := in.GetQuery()
					return assert.NotNil(t, body) &&
						assert.Equal(t, expectedBody, *body) &&
						assert.Equal(t, "default-org", *in.GetOrg()) &&
						assert.Nil(t, in.GetOrgID())
				})).Return(&http.Response{Body: io.NopCloser(strings.NewReader(fakeResults))}, nil)
			},
		},
		{
			name: "gzipped response",
			params: query.Params{
				OrgParams: clients.OrgParams{
					OrgID: orgID,
				},
				Query: fakeQuery.Query,
			},
			configOrgName: "default-org",
			registerExpectations: func(t *testing.T, queryApi *mock.MockQueryApi) {
				queryApi.EXPECT().PostQuery(gomock.Any()).Return(api.ApiPostQueryRequest{ApiService: queryApi})
				queryApi.EXPECT().PostQueryExecute(tmock.MatchedBy(func(in api.ApiPostQueryRequest) bool {
					body := in.GetQuery()
					return assert.NotNil(t, body) &&
						assert.Equal(t, fakeQuery, *body) &&
						assert.Equal(t, orgID, *in.GetOrgID()) &&
						assert.Nil(t, in.GetOrg())
				})).DoAndReturn(func(api.ApiPostQueryRequest) (*http.Response, error) {
					pr, pw := io.Pipe()
					gw := gzip.NewWriter(pw)

					go func() {
						_, err := gw.Write([]byte(fakeResults))
						gw.Close()
						pw.Close()
						require.NoError(t, err)
					}()

					resp := http.Response{Body: pr, Header: http.Header{"Content-Encoding": []string{"gzip"}}}
					return &resp, nil
				})
			},
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctrl := gomock.NewController(t)
			stdio := mock.NewMockStdIO(ctrl)
			writtenBytes := bytes.Buffer{}
			stdio.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes()

			queryApi := mock.NewMockQueryApi(ctrl)
			if tc.registerExpectations != nil {
				tc.registerExpectations(t, queryApi)
			}
			cli := query.Client{
				CLI:           clients.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio},
				QueryApi:      queryApi,
				ResultPrinter: query.RawResultPrinter,
			}

			err := cli.Query(context.Background(), &tc.params)
			if tc.expectInErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.expectInErr)
				require.Empty(t, writtenBytes.String())
				return
			}
			require.NoError(t, err)
			require.Equal(t, fakeResults, writtenBytes.String())
		})
	}
}
