package testing

import (
	"bytes"
	"context"
	"fmt"
	"sort"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/mock"
)

const (
	bucketOneID   = "020f755c3c082000"
	bucketTwoID   = "020f755c3c082001"
	bucketThreeID = "9565493717473001"
)

var bucketCmpOptions = cmp.Options{
	cmp.Comparer(func(x, y []byte) bool {
		return bytes.Equal(x, y)
	}),
	cmp.Comparer(func(x, y *influxdb.Bucket) bool {
		if x == nil && y == nil {
			return true
		}
		if x != nil && y == nil || y != nil && x == nil {
			return false
		}

		return x.OrgID == y.OrgID &&
			x.Type == y.Type &&
			x.Description == y.Description &&
			x.RetentionPolicyName == y.RetentionPolicyName &&
			x.RetentionPeriod == y.RetentionPeriod &&
			x.Name == y.Name
	}),
	cmp.Transformer("Sort", func(in []*influxdb.Bucket) []*influxdb.Bucket {
		out := append([]*influxdb.Bucket(nil), in...) // Copy input to avoid mutating it
		sort.Slice(out, func(i, j int) bool {
			return out[i].Name > out[j].Name
		})
		return out
	}),
}

type BucketSvcOpts struct {
	NoHooks bool
}

// WithoutHooks allows the test suite to be run without being able to hook into the underlying implementation of theservice
// in most cases that is to remove specific id generation controls.
func WithoutHooks() BucketSvcOpts {
	return BucketSvcOpts{
		NoHooks: true,
	}
}

// BucketFields will include the IDGenerator, and buckets
type BucketFields struct {
	IDGenerator   influxdb.IDGenerator
	OrgBucketIDs  influxdb.IDGenerator
	TimeGenerator influxdb.TimeGenerator
	Buckets       []*influxdb.Bucket
	Organizations []*influxdb.Organization
}

type bucketServiceF func(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
)

// BucketService tests all the service functions.
func BucketService(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
	opts ...BucketSvcOpts) {
	tests := []struct {
		name string
		fn   bucketServiceF
	}{
		{
			name: "CreateBucket",
			fn:   CreateBucket,
		},
		{
			name: "IDUnique",
			fn:   IDUnique,
		},
		{
			name: "FindBucketByID",
			fn:   FindBucketByID,
		},
		{
			name: "FindBuckets",
			fn:   FindBuckets,
		},
		{
			name: "FindBucket",
			fn:   FindBucket,
		},
		{
			name: "UpdateBucket",
			fn:   UpdateBucket,
		},
		{
			name: "DeleteBucket",
			fn:   DeleteBucket,
		},
	}
	for _, tt := range tests {
		if tt.name == "IDUnique" && len(opts) > 0 && opts[0].NoHooks {
			continue
		}
		t.Run(tt.name, func(t *testing.T) {
			tt.fn(init, t)
		})
	}
}

// CreateBucket testing
func CreateBucket(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
) {
	type args struct {
		bucket *influxdb.Bucket
	}
	type wants struct {
		err     error
		buckets []*influxdb.Bucket
	}

	tests := []struct {
		name   string
		fields BucketFields
		args   args
		wants  wants
	}{
		{
			name: "create buckets with empty set",
			fields: BucketFields{
				IDGenerator:   mock.NewIDGenerator(bucketOneID, t),
				OrgBucketIDs:  mock.NewIDGenerator(bucketOneID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Buckets:       []*influxdb.Bucket{},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				bucket: &influxdb.Bucket{
					Name:        "name1",
					OrgID:       MustIDBase16(orgOneID),
					Description: "desc1",
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						Name:        "name1",
						ID:          MustIDBase16(bucketOneID),
						OrgID:       MustIDBase16(orgOneID),
						Description: "desc1",
						CRUDLog: influxdb.CRUDLog{
							CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
							UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
						},
					},
				},
			},
		},
		{
			name: "basic create bucket",
			fields: BucketFields{
				IDGenerator:   mock.NewIDGenerator(bucketTwoID, t),
				OrgBucketIDs:  mock.NewIDGenerator(bucketTwoID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
			},
			args: args{
				bucket: &influxdb.Bucket{
					Name:  "bucket2",
					OrgID: MustIDBase16(orgTwoID),
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						Name:  "bucket2",
						OrgID: MustIDBase16(orgTwoID),
						CRUDLog: influxdb.CRUDLog{
							CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
							UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
						},
					},
				},
			},
		},
		{
			name: "names should be unique within an organization",
			fields: BucketFields{
				IDGenerator:   mock.NewIDGenerator(bucketTwoID, t),
				OrgBucketIDs:  mock.NewIDGenerator(bucketTwoID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
			},
			args: args{
				bucket: &influxdb.Bucket{
					Name:  "bucket1",
					OrgID: MustIDBase16(orgOneID),
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
				err: &influxdb.Error{
					Code: influxdb.EConflict,
					Op:   influxdb.OpCreateBucket,
					Msg:  fmt.Sprintf("bucket with name bucket1 already exists"),
				},
			},
		},
		{
			name: "names should not be unique across organizations",
			fields: BucketFields{
				IDGenerator:   mock.NewIDGenerator(bucketTwoID, t),
				OrgBucketIDs:  mock.NewIDGenerator(bucketTwoID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				bucket: &influxdb.Bucket{
					Name:  "bucket1",
					OrgID: MustIDBase16(orgTwoID),
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgTwoID),
						CRUDLog: influxdb.CRUDLog{
							CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
							UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
						},
					},
				},
			},
		},
		{
			name: "create bucket with orgID not exist",
			fields: BucketFields{
				IDGenerator:   mock.NewIDGenerator(bucketOneID, t),
				OrgBucketIDs:  mock.NewIDGenerator(bucketOneID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Buckets:       []*influxdb.Bucket{},
				Organizations: []*influxdb.Organization{},
			},
			args: args{
				bucket: &influxdb.Bucket{
					Name:  "name1",
					OrgID: MustIDBase16(orgOneID),
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{},
				err: &influxdb.Error{
					Code: influxdb.ENotFound,
					Msg:  "organization not found",
					Op:   influxdb.OpCreateBucket,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			err := s.CreateBucket(ctx, tt.args.bucket)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			// Delete only newly created buckets - ie., with a not nil ID
			// if tt.args.bucket.ID.Valid() {
			defer s.DeleteBucket(ctx, tt.args.bucket.ID)
			// }

			buckets, _, err := s.FindBuckets(ctx, influxdb.BucketFilter{})
			if err != nil {
				t.Fatalf("failed to retrieve buckets: %v", err)
			}

			// remove system buckets
			filteredBuckets := []*influxdb.Bucket{}
			for _, b := range buckets {
				if b.Type != influxdb.BucketTypeSystem {
					filteredBuckets = append(filteredBuckets, b)
				}
			}

			if diff := cmp.Diff(filteredBuckets, tt.wants.buckets, bucketCmpOptions...); diff != "" {
				t.Errorf("buckets are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// CreateBucket testing
func IDUnique(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
) {
	type args struct {
		bucket *influxdb.Bucket
	}
	type wants struct {
		err     error
		buckets []*influxdb.Bucket
	}

	tests := []struct {
		name   string
		fields BucketFields
		args   args
		wants  wants
	}{
		{
			name: "ids should be unique",
			fields: BucketFields{
				IDGenerator:   mock.NewIDGenerator(bucketOneID, t),
				OrgBucketIDs:  mock.NewIDGenerator(bucketOneID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				bucket: &influxdb.Bucket{
					ID:    MustIDBase16(bucketOneID),
					Name:  "bucket2",
					OrgID: MustIDBase16(orgOneID),
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
				err: &influxdb.Error{
					Code: influxdb.EInternal,
					Msg:  fmt.Sprintf("unable to generate valid id"),
				},
			},
		},
		{
			name: "reserved ids should not be created",
			fields: BucketFields{
				IDGenerator:   mock.NewIDGenerator("000000000000000a", t),
				OrgBucketIDs:  mock.NewIDGenerator("000000000000000a", t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				bucket: &influxdb.Bucket{
					Name:  "bucket2",
					OrgID: MustIDBase16(orgOneID),
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						Name:  "bucket1",
						OrgID: MustIDBase16(orgOneID),
					},
				},
				err: &influxdb.Error{
					Code: influxdb.EInternal,
					Msg:  fmt.Sprintf("unable to generate valid id"),
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			err := s.CreateBucket(ctx, tt.args.bucket)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			// Delete only newly created buckets - ie., with a not nil ID
			// if tt.args.bucket.ID.Valid() {
			defer s.DeleteBucket(ctx, tt.args.bucket.ID)
			// }

			buckets, _, err := s.FindBuckets(ctx, influxdb.BucketFilter{})
			if err != nil {
				t.Fatalf("failed to retrieve buckets: %v", err)
			}

			// remove system buckets
			filteredBuckets := []*influxdb.Bucket{}
			for _, b := range buckets {
				if b.Type != influxdb.BucketTypeSystem {
					filteredBuckets = append(filteredBuckets, b)
				}
			}

			if diff := cmp.Diff(filteredBuckets, tt.wants.buckets, bucketCmpOptions...); diff != "" {
				t.Errorf("buckets are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// FindBucketByID testing
func FindBucketByID(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
) {
	type args struct {
		id influxdb.ID
	}
	type wants struct {
		err    error
		bucket *influxdb.Bucket
	}

	tests := []struct {
		name   string
		fields BucketFields
		args   args
		wants  wants
	}{
		{
			name: "basic find bucket by id",
			fields: BucketFields{
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				id: MustIDBase16(bucketTwoID),
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:    MustIDBase16(bucketTwoID),
					OrgID: MustIDBase16(orgOneID),
					Name:  "bucket2",
				},
			},
		},
		{
			name: "find bucket by id not exist",
			fields: BucketFields{
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				id: MustIDBase16(threeID),
			},
			wants: wants{
				err: &influxdb.Error{
					Code: influxdb.ENotFound,
					Op:   influxdb.OpFindBucketByID,
					Msg:  "bucket not found",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()

			bucket, err := s.FindBucketByID(ctx, tt.args.id)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			if diff := cmp.Diff(bucket, tt.wants.bucket, bucketCmpOptions...); diff != "" {
				t.Errorf("bucket is different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// FindBuckets testing
func FindBuckets(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
) {
	type args struct {
		ID             influxdb.ID
		name           string
		organization   string
		organizationID influxdb.ID
		findOptions    influxdb.FindOptions
	}

	type wants struct {
		buckets []*influxdb.Bucket
		err     error
	}
	tests := []struct {
		name   string
		fields BucketFields
		args   args
		wants  wants
	}{
		{
			name: "find all buckets",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgTwoID),
						Name:  "xyz",
					},
				},
			},
			args: args{},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgTwoID),
						Name:  "xyz",
					},
				},
			},
		},
		{
			name: "find all buckets by offset and limit",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "def",
					},
					{
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "xyz",
					},
				},
			},
			args: args{
				findOptions: influxdb.FindOptions{
					Offset: 1,
					Limit:  1,
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "def",
					},
				},
			},
		},
		{
			name: "find all buckets by descending",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "def",
					},
					{
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "xyz",
					},
				},
			},
			args: args{
				findOptions: influxdb.FindOptions{
					Offset:     1,
					Descending: true,
				},
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "def",
					},
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
				},
			},
		},
		{
			name: "find buckets by organization name",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgTwoID),
						Name:  "xyz",
					},
					{
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "123",
					},
				},
			},
			args: args{
				organization: "theorg",
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "123",
					},
				},
			},
		},
		{
			name: "find buckets by organization id",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgTwoID),
						Name:  "xyz",
					},
					{
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "123",
					},
				},
			},
			args: args{
				organizationID: MustIDBase16(orgOneID),
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "123",
					},
				},
			},
		},
		{
			name: "find bucket by name",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "xyz",
					},
				},
			},
			args: args{
				name: "xyz",
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "xyz",
					},
				},
			},
		},
		{
			name: "missing bucket returns no buckets",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{},
			},
			args: args{
				name: "xyz",
			},
			wants: wants{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()

			filter := influxdb.BucketFilter{}
			if tt.args.ID.Valid() {
				filter.ID = &tt.args.ID
			}
			if tt.args.organizationID.Valid() {
				filter.OrganizationID = &tt.args.organizationID
			}
			if tt.args.organization != "" {
				filter.Org = &tt.args.organization
			}
			if tt.args.name != "" {
				filter.Name = &tt.args.name
			}

			buckets, _, err := s.FindBuckets(ctx, filter, tt.args.findOptions)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			// remove system buckets
			filteredBuckets := []*influxdb.Bucket{}
			for _, b := range buckets {
				if b.Type != influxdb.BucketTypeSystem {
					filteredBuckets = append(filteredBuckets, b)
				}
			}

			if diff := cmp.Diff(filteredBuckets, tt.wants.buckets, bucketCmpOptions...); diff != "" {
				t.Errorf("buckets are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// DeleteBucket testing
func DeleteBucket(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
) {
	type args struct {
		ID string
	}
	type wants struct {
		err     error
		buckets []*influxdb.Bucket
	}

	tests := []struct {
		name   string
		fields BucketFields
		args   args
		wants  wants
	}{
		{
			name: "delete buckets using exist id",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						Name:  "A",
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
					},
					{
						Name:  "B",
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				ID: bucketOneID,
			},
			wants: wants{
				buckets: []*influxdb.Bucket{
					{
						Name:  "B",
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
					},
				},
			},
		},
		{
			name: "delete buckets using id that does not exist",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						Name:  "A",
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
					},
					{
						Name:  "B",
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				ID: "1234567890654321",
			},
			wants: wants{
				err: &influxdb.Error{
					Op:   influxdb.OpDeleteBucket,
					Msg:  "bucket not found",
					Code: influxdb.ENotFound,
				},
				buckets: []*influxdb.Bucket{
					{
						Name:  "A",
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
					},
					{
						Name:  "B",
						ID:    MustIDBase16(bucketThreeID),
						OrgID: MustIDBase16(orgOneID),
					},
				},
			},
		},
		{
			name: "delete system buckets",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						Name:  "A",
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Type:  influxdb.BucketTypeSystem,
					},
				},
			},
			args: args{
				ID: bucketOneID,
			},
			wants: wants{
				err: &influxdb.Error{
					Op:   influxdb.OpDeleteBucket,
					Msg:  "system buckets cannot be deleted",
					Code: influxdb.EInvalid,
				},
				buckets: []*influxdb.Bucket{
					{
						Name:  "A",
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Type:  influxdb.BucketTypeSystem,
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			err := s.DeleteBucket(ctx, MustIDBase16(tt.args.ID))
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			filter := influxdb.BucketFilter{}
			buckets, _, err := s.FindBuckets(ctx, filter)
			if err != nil {
				t.Fatalf("failed to retrieve buckets: %v", err)
			}

			// remove built in system buckets
			filteredBuckets := []*influxdb.Bucket{}
			for _, b := range buckets {
				if b.Name != influxdb.TasksSystemBucketName && b.Name != influxdb.MonitoringSystemBucketName {
					filteredBuckets = append(filteredBuckets, b)
				}
			}

			if diff := cmp.Diff(filteredBuckets, tt.wants.buckets, bucketCmpOptions...); diff != "" {
				t.Errorf("buckets are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// FindBucket testing
func FindBucket(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
) {
	type args struct {
		name           string
		organizationID influxdb.ID
		id             influxdb.ID
	}

	type wants struct {
		bucket *influxdb.Bucket
		err    error
	}

	tests := []struct {
		name   string
		fields BucketFields
		args   args
		wants  wants
	}{
		{
			name: "find bucket by name",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "xyz",
					},
				},
			},
			args: args{
				name:           "abc",
				organizationID: MustIDBase16(orgOneID),
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:    MustIDBase16(bucketOneID),
					OrgID: MustIDBase16(orgOneID),
					Name:  "abc",
				},
			},
		},
		{
			name: "find bucket by id",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "abc",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "xyz",
					},
				},
			},
			args: args{
				id:             MustIDBase16(bucketOneID),
				organizationID: MustIDBase16(orgOneID),
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:    MustIDBase16(bucketOneID),
					OrgID: MustIDBase16(orgOneID),
					Name:  "abc",
				},
			},
		},
		{
			name: "missing bucket returns error",
			fields: BucketFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{},
			},
			args: args{
				name:           "xyz",
				organizationID: MustIDBase16(orgOneID),
			},
			wants: wants{
				err: &influxdb.Error{
					Code: influxdb.ENotFound,
					Op:   influxdb.OpFindBucket,
					Msg:  "bucket \"xyz\" not found",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			filter := influxdb.BucketFilter{}
			if tt.args.name != "" {
				filter.Name = &tt.args.name
			}
			if tt.args.id.Valid() {
				filter.ID = &tt.args.id
			}
			if tt.args.organizationID.Valid() {
				filter.OrganizationID = &tt.args.organizationID
			}

			bucket, err := s.FindBucket(ctx, filter)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			if diff := cmp.Diff(bucket, tt.wants.bucket, bucketCmpOptions...); diff != "" {
				t.Errorf("buckets are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// UpdateBucket testing
func UpdateBucket(
	init func(BucketFields, *testing.T) (influxdb.BucketService, string, func()),
	t *testing.T,
) {
	type args struct {
		name        string
		id          influxdb.ID
		retention   int
		description *string
	}
	type wants struct {
		err    error
		bucket *influxdb.Bucket
	}

	tests := []struct {
		name   string
		fields BucketFields
		args   args
		wants  wants
	}{
		{
			name: "update name",
			fields: BucketFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
			},
			args: args{
				id:   MustIDBase16(bucketOneID),
				name: "changed",
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:    MustIDBase16(bucketOneID),
					OrgID: MustIDBase16(orgOneID),
					Name:  "changed",
					CRUDLog: influxdb.CRUDLog{
						UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "update name unique",
			fields: BucketFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
			},
			args: args{
				id:   MustIDBase16(bucketOneID),
				name: "bucket2",
			},
			wants: wants{
				err: &influxdb.Error{
					Code: influxdb.EConflict,
					Msg:  "bucket name is not unique",
				},
			},
		},
		{
			name: "update system bucket name",
			fields: BucketFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Type:  influxdb.BucketTypeSystem,
						Name:  "bucket1",
					},
				},
			},
			args: args{
				id:   MustIDBase16(bucketOneID),
				name: "bucket2",
			},
			wants: wants{
				err: &influxdb.Error{
					Code: influxdb.EInvalid,
					Msg:  "system buckets cannot be renamed",
				},
			},
		},
		{
			name: "update retention",
			fields: BucketFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
			},
			args: args{
				id:        MustIDBase16(bucketOneID),
				retention: 100,
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:              MustIDBase16(bucketOneID),
					OrgID:           MustIDBase16(orgOneID),
					Name:            "bucket1",
					RetentionPeriod: 100 * time.Minute,
					CRUDLog: influxdb.CRUDLog{
						UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "update description",
			fields: BucketFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
			},
			args: args{
				id:          MustIDBase16(bucketOneID),
				description: stringPtr("desc1"),
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:          MustIDBase16(bucketOneID),
					OrgID:       MustIDBase16(orgOneID),
					Name:        "bucket1",
					Description: "desc1",
					CRUDLog: influxdb.CRUDLog{
						UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "update retention and name",
			fields: BucketFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
			},
			args: args{
				id:        MustIDBase16(bucketTwoID),
				retention: 101,
				name:      "changed",
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:              MustIDBase16(bucketTwoID),
					OrgID:           MustIDBase16(orgOneID),
					Name:            "changed",
					RetentionPeriod: 101 * time.Minute,
					CRUDLog: influxdb.CRUDLog{
						UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
					},
				},
			},
		},
		{
			name: "update retention and same name",
			fields: BucketFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Buckets: []*influxdb.Bucket{
					{
						ID:    MustIDBase16(bucketOneID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket1",
					},
					{
						ID:    MustIDBase16(bucketTwoID),
						OrgID: MustIDBase16(orgOneID),
						Name:  "bucket2",
					},
				},
			},
			args: args{
				id:        MustIDBase16(bucketTwoID),
				retention: 101,
				name:      "bucket2",
			},
			wants: wants{
				bucket: &influxdb.Bucket{
					ID:              MustIDBase16(bucketTwoID),
					OrgID:           MustIDBase16(orgOneID),
					Name:            "bucket2",
					RetentionPeriod: 101 * time.Minute,
					CRUDLog: influxdb.CRUDLog{
						UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()

			upd := influxdb.BucketUpdate{}
			if tt.args.name != "" {
				upd.Name = &tt.args.name
			}
			if tt.args.retention != 0 {
				d := time.Duration(tt.args.retention) * time.Minute
				upd.RetentionPeriod = &d
			}

			upd.Description = tt.args.description

			bucket, err := s.UpdateBucket(ctx, tt.args.id, upd)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			if diff := cmp.Diff(bucket, tt.wants.bucket, bucketCmpOptions...); diff != "" {
				t.Errorf("bucket is different -got/+want\ndiff %s", diff)
			}
		})
	}
}
