package bucket_test

import (
	"bytes"
	"context"
	"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/bucket"
	"github.com/influxdata/influx-cli/v2/config"
	"github.com/influxdata/influx-cli/v2/internal/mock"
	"github.com/influxdata/influx-cli/v2/internal/testutils"
	"github.com/stretchr/testify/assert"
	tmock "github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

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

	var testCases = []struct {
		name                       string
		configOrgName              string
		params                     bucket.BucketsCreateParams
		registerOrgExpectations    func(*testing.T, *mock.MockOrganizationsApi)
		registerBucketExpectations func(*testing.T, *mock.MockBucketsApi)
		expectedStdoutPattern      string
		expectedInErr              string
	}{
		{
			name: "minimal",
			params: bucket.BucketsCreateParams{
				OrgParams: clients.OrgParams{OrgID: "123"},
				Name:      "my-bucket",
			},
			registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) {
				bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi})
				bucketsApi.EXPECT().
					PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool {
						body := in.GetPostBucketRequest()
						return assert.NotNil(t, body) &&
							assert.Equal(t, "123", body.OrgID) &&
							assert.Equal(t, "my-bucket", body.Name) &&
							assert.Nil(t, body.Description) &&
							assert.Empty(t, body.RetentionRules)
					})).
					Return(api.Bucket{
						Id:             api.PtrString("456"),
						OrgID:          api.PtrString("123"),
						Name:           "my-bucket",
						RetentionRules: nil,
					}, nil)
			},
			expectedStdoutPattern: `456\s+my-bucket\s+infinite\s+n/a\s+123`,
		},
		{
			name: "fully specified",
			params: bucket.BucketsCreateParams{
				OrgParams:          clients.OrgParams{OrgID: "123"},
				Name:               "my-bucket",
				Description:        "my cool bucket",
				Retention:          "24h",
				ShardGroupDuration: "1h",
			},
			registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) {
				bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi})
				bucketsApi.EXPECT().
					PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool {
						body := in.GetPostBucketRequest()
						retentionRules := *body.RetentionRules
						return assert.NotNil(t, body) &&
							assert.Equal(t, "123", body.OrgID) &&
							assert.Equal(t, "my-bucket", body.Name) &&
							assert.Equal(t, "my cool bucket", *body.Description) &&
							assert.Len(t, retentionRules, 1) &&
							assert.Equal(t, int64(86400), retentionRules[0].EverySeconds) &&
							assert.Equal(t, int64(3600), *retentionRules[0].ShardGroupDurationSeconds)
					})).
					Return(api.Bucket{
						Id:    api.PtrString("456"),
						OrgID: api.PtrString("123"),
						Name:  "my-bucket",
						RetentionRules: []api.RetentionRule{
							{EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)},
						},
					}, nil)
			},
			expectedStdoutPattern: `456\s+my-bucket\s+24h0m0s\s+1h0m0s\s+123`,
		},
		{
			name: "retention but not shard-group duration",
			params: bucket.BucketsCreateParams{
				OrgParams: clients.OrgParams{OrgID: "123"},
				Name:      "my-bucket",
				Retention: "24h",
			},
			registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) {
				bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi})
				bucketsApi.EXPECT().
					PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool {
						body := in.GetPostBucketRequest()
						retentionRules := *body.RetentionRules
						return assert.NotNil(t, body) &&
							assert.Equal(t, "123", body.OrgID) &&
							assert.Equal(t, "my-bucket", body.Name) &&
							assert.Nil(t, body.Description) &&
							assert.Len(t, *body.RetentionRules, 1) &&
							assert.Equal(t, int64(86400), retentionRules[0].EverySeconds) &&
							assert.Nil(t, retentionRules[0].ShardGroupDurationSeconds)
					})).
					Return(api.Bucket{
						Id:             api.PtrString("456"),
						OrgID:          api.PtrString("123"),
						Name:           "my-bucket",
						RetentionRules: []api.RetentionRule{{EverySeconds: 86400}},
					}, nil)
			},
		},
		{
			name: "create bucket with explicit schema",
			params: bucket.BucketsCreateParams{
				OrgParams:  clients.OrgParams{OrgID: "123"},
				Name:       "my-bucket",
				SchemaType: api.SCHEMATYPE_EXPLICIT,
			},
			registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) {
				bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi})
				bucketsApi.EXPECT().
					PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool {
						body := in.GetPostBucketRequest()
						return assert.NotNil(t, body) &&
							assert.Equal(t, "123", body.OrgID) &&
							assert.Equal(t, "my-bucket", body.Name) &&
							assert.Nil(t, body.Description) &&
							assert.Empty(t, body.RetentionRules) &&
							assert.Equal(t, api.SCHEMATYPE_EXPLICIT, *body.SchemaType)
					})).
					Return(api.Bucket{
						Id:         api.PtrString("456"),
						OrgID:      api.PtrString("123"),
						Name:       "my-bucket",
						SchemaType: api.SCHEMATYPE_EXPLICIT.Ptr(),
					}, nil)
			},
			expectedStdoutPattern: `456\s+my-bucket\s+infinite\s+n/a\s+123\s+explicit`,
		},
		{
			name: "look up org by name",
			params: bucket.BucketsCreateParams{
				OrgParams:          clients.OrgParams{OrgName: "my-org"},
				Name:               "my-bucket",
				Description:        "my cool bucket",
				Retention:          "24h",
				ShardGroupDuration: "1h",
			},
			registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) {
				orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi})
				orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool {
					return assert.Equal(t, "my-org", *in.GetOrg())
				})).
					Return(api.Organizations{
						Orgs: &[]api.Organization{{Id: api.PtrString("123")}},
					}, nil)
			},
			registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) {
				bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi})
				bucketsApi.EXPECT().
					PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool {
						body := in.GetPostBucketRequest()
						retentionRules := *body.RetentionRules
						return assert.NotNil(t, body) &&
							assert.Equal(t, "123", body.OrgID) &&
							assert.Equal(t, "my-bucket", body.Name) &&
							assert.Equal(t, "my cool bucket", *body.Description) &&
							assert.Len(t, *body.RetentionRules, 1) &&
							assert.Equal(t, int64(86400), retentionRules[0].EverySeconds) &&
							assert.Equal(t, int64(3600), *retentionRules[0].ShardGroupDurationSeconds)
					})).
					Return(api.Bucket{
						Id:    api.PtrString("456"),
						OrgID: api.PtrString("123"),
						Name:  "my-bucket",
						RetentionRules: []api.RetentionRule{
							{EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)},
						},
					}, nil)
			},
			expectedStdoutPattern: `456\s+my-bucket\s+24h0m0s\s+1h0m0s\s+123`,
		},
		{
			name: "look up org by name from config",
			params: bucket.BucketsCreateParams{
				Name:               "my-bucket",
				Description:        "my cool bucket",
				Retention:          "24h",
				ShardGroupDuration: "1h",
			},
			configOrgName: "my-org",
			registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) {
				orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi})
				orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool {
					return assert.Equal(t, "my-org", *in.GetOrg())
				})).
					Return(api.Organizations{
						Orgs: &[]api.Organization{{Id: api.PtrString("123")}},
					}, nil)
			},
			registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) {
				bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi})
				bucketsApi.EXPECT().
					PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool {
						body := in.GetPostBucketRequest()
						retentionRules := *body.RetentionRules
						return assert.NotNil(t, body) &&
							assert.Equal(t, "123", body.OrgID) &&
							assert.Equal(t, "my-bucket", body.Name) &&
							assert.Equal(t, "my cool bucket", *body.Description) &&
							assert.Len(t, *body.RetentionRules, 1) &&
							assert.Equal(t, int64(86400), retentionRules[0].EverySeconds) &&
							assert.Equal(t, int64(3600), *retentionRules[0].ShardGroupDurationSeconds)
					})).
					Return(api.Bucket{
						Id:    api.PtrString("456"),
						OrgID: api.PtrString("123"),
						Name:  "my-bucket",
						RetentionRules: []api.RetentionRule{
							{EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)},
						},
					}, nil)
			},
			expectedStdoutPattern: `456\s+my-bucket\s+24h0m0s\s+1h0m0s\s+123`,
		},
		{
			name: "no org specified",
			params: bucket.BucketsCreateParams{
				Name:               "my-bucket",
				Description:        "my cool bucket",
				Retention:          "24h",
				ShardGroupDuration: "1h",
			},
			expectedInErr: "must specify org ID or org name",
		},
		{
			name: "no such org",
			params: bucket.BucketsCreateParams{
				OrgParams:          clients.OrgParams{OrgName: "fake-org"},
				Name:               "my-bucket",
				Description:        "my cool bucket",
				Retention:          "24h",
				ShardGroupDuration: "1h",
			},
			registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) {
				orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi})
				orgApi.EXPECT().GetOrgsExecute(gomock.Any()).Return(api.Organizations{}, nil)
			},
			expectedInErr: "no organization with name \"fake-org\"",
		},
	}

	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()

			orgApi := mock.NewMockOrganizationsApi(ctrl)
			if tc.registerOrgExpectations != nil {
				tc.registerOrgExpectations(t, orgApi)
			}
			bucketApi := mock.NewMockBucketsApi(ctrl)
			if tc.registerBucketExpectations != nil {
				tc.registerBucketExpectations(t, bucketApi)
			}
			cli := bucket.Client{
				CLI:              clients.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio},
				OrganizationsApi: orgApi,
				BucketsApi:       bucketApi,
			}

			err := cli.Create(context.Background(), &tc.params)
			if tc.expectedInErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.expectedInErr)
				require.Empty(t, writtenBytes.String())
				return
			}
			require.NoError(t, err)
			testutils.MatchLines(t, []string{
				`ID\s+Name\s+Retention\s+Shard group duration\s+Organization ID\s+Schema Type`,
				tc.expectedStdoutPattern,
			}, strings.Split(writtenBytes.String(), "\n"))
		})
	}
}
