package pkger_test

import (
	"context"
	"testing"
	"time"

	"github.com/influxdata/influxdb/v2/inmem"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/kv/migration/all"
	"github.com/influxdata/influxdb/v2/pkger"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
)

func TestStoreKV(t *testing.T) {
	inMemStore := inmem.NewKVStore()

	// run all migrations against store
	if err := all.Up(context.Background(), zaptest.NewLogger(t), inMemStore); err != nil {
		t.Fatal(err)
	}

	stackStub := func(id, orgID platform.ID) pkger.Stack {
		now := time.Time{}.Add(10 * 365 * 24 * time.Hour)
		urls := []string{
			"http://example.com",
			"http://abc.gov",
		}
		return pkger.Stack{
			ID:        id,
			OrgID:     orgID,
			CreatedAt: now,
			Events: []pkger.StackEvent{
				{
					EventType:    pkger.StackEventCreate,
					Name:         "threeve",
					Description:  "desc",
					UpdatedAt:    now.Add(time.Hour),
					Sources:      urls,
					TemplateURLs: urls,
					Resources: []pkger.StackResource{
						{
							APIVersion: pkger.APIVersion,
							ID:         9000,
							Kind:       pkger.KindBucket,
							MetaName:   "buzz lightyear",
							Associations: []pkger.StackResourceAssociation{{
								Kind:     pkger.KindLabel,
								MetaName: "foo_label",
							}},
						},
						{
							APIVersion: pkger.APIVersion,
							ID:         333,
							Kind:       pkger.KindBucket,
							MetaName:   "beyond",
						},
					},
				},
			},
		}
	}

	t.Run("create a stack", func(t *testing.T) {
		defer inMemStore.Flush(context.Background())

		storeKV := pkger.NewStoreKV(inMemStore)

		const orgID = 333
		seedEntities(t, storeKV, pkger.Stack{
			ID:    1,
			OrgID: orgID,
		})

		t.Run("with no ID collisions creates successfully", func(t *testing.T) {
			expected := stackStub(3, orgID)

			err := storeKV.CreateStack(context.Background(), expected)
			require.NoError(t, err)

			readStackEqual(t, storeKV, expected)
		})

		t.Run("with ID collisions fails with conflict error", func(t *testing.T) {
			for _, id := range []platform.ID{2, 3} {
				err := storeKV.CreateStack(context.Background(), pkger.Stack{
					ID:    1,
					OrgID: orgID,
				})
				require.Errorf(t, err, "id=%d", id)
				assert.Equalf(t, errors.EConflict, errors.ErrorCode(err), "id=%d", id)
			}
		})
	})

	t.Run("list stacks", func(t *testing.T) {
		defer inMemStore.Flush(context.Background())

		storeKV := pkger.NewStoreKV(inMemStore)

		const orgID1 = 1
		const orgID2 = 2
		seedEntities(t, storeKV,
			pkger.Stack{
				ID:    1,
				OrgID: orgID1,
				Events: []pkger.StackEvent{{
					Name: "first_name",
				}},
			},
			pkger.Stack{
				ID:    2,
				OrgID: orgID2,
				Events: []pkger.StackEvent{{
					Name: "first_name",
				}},
			},
			pkger.Stack{
				ID:    3,
				OrgID: orgID1,
				Events: []pkger.StackEvent{{
					Name: "second_name",
				}},
			},
			pkger.Stack{
				ID:    4,
				OrgID: orgID2,
				Events: []pkger.StackEvent{{
					Name: "second_name",
				}},
			},
		)

		tests := []struct {
			name     string
			orgID    platform.ID
			filter   pkger.ListFilter
			expected []pkger.Stack
		}{
			{
				name:  "by org id",
				orgID: orgID1,
				expected: []pkger.Stack{
					{
						ID:    1,
						OrgID: orgID1,
						Events: []pkger.StackEvent{{
							Name: "first_name",
						}},
					},
					{
						ID:    3,
						OrgID: orgID1,
						Events: []pkger.StackEvent{{
							Name: "second_name",
						}},
					},
				},
			},
			{
				name:  "by stack ids",
				orgID: orgID1,
				filter: pkger.ListFilter{
					StackIDs: []platform.ID{1, 3},
				},
				expected: []pkger.Stack{
					{
						ID:    1,
						OrgID: orgID1,
						Events: []pkger.StackEvent{{
							Name: "first_name",
						}},
					},
					{
						ID:    3,
						OrgID: orgID1,
						Events: []pkger.StackEvent{{
							Name: "second_name",
						}},
					},
				},
			},
			{
				name:  "by stack ids skips ids that belong to different organization",
				orgID: orgID1,
				filter: pkger.ListFilter{
					StackIDs: []platform.ID{1, 2, 4},
				},
				expected: []pkger.Stack{{
					ID:    1,
					OrgID: orgID1,
					Events: []pkger.StackEvent{{
						Name: "first_name",
					}},
				}},
			},
			{
				name:  "stack ids that do not exist are skipped",
				orgID: orgID1,
				filter: pkger.ListFilter{
					StackIDs: []platform.ID{1, 9000},
				},
				expected: []pkger.Stack{{
					ID:    1,
					OrgID: orgID1,
					Events: []pkger.StackEvent{{
						Name: "first_name",
					}},
				}},
			},
			{
				name:  "by name",
				orgID: orgID1,
				filter: pkger.ListFilter{
					Names: []string{"first_name"},
				},
				expected: []pkger.Stack{{
					ID:    1,
					OrgID: orgID1,
					Events: []pkger.StackEvent{{
						Name: "first_name",
					}},
				}},
			},
			{
				name:  "by name and id",
				orgID: orgID1,
				filter: pkger.ListFilter{
					StackIDs: []platform.ID{3},
					Names:    []string{"first_name"},
				},
				expected: []pkger.Stack{
					{
						ID:    1,
						OrgID: orgID1,
						Events: []pkger.StackEvent{{
							Name: "first_name",
						}},
					},
					{
						ID:    3,
						OrgID: orgID1,
						Events: []pkger.StackEvent{{
							Name: "second_name",
						}},
					},
				},
			},
		}

		for _, tt := range tests {
			fn := func(t *testing.T) {
				stacks, err := storeKV.ListStacks(context.Background(), tt.orgID, tt.filter)
				require.NoError(t, err)
				assert.Equal(t, tt.expected, stacks)
			}

			t.Run(tt.name, fn)
		}
	})

	t.Run("read a stack", func(t *testing.T) {
		defer inMemStore.Flush(context.Background())

		storeKV := pkger.NewStoreKV(inMemStore)

		expected := stackStub(1, 3)

		seedEntities(t, storeKV, expected)

		t.Run("with valid ID returns stack successfully", func(t *testing.T) {
			readStackEqual(t, storeKV, expected)
		})

		t.Run("when no match found fails with not found error", func(t *testing.T) {
			unmatchedID := platform.ID(3000)
			_, err := storeKV.ReadStackByID(context.Background(), unmatchedID)
			require.Error(t, err)
			assert.Equal(t, errors.ENotFound, errors.ErrorCode(err))
		})
	})

	t.Run("update a stack", func(t *testing.T) {
		defer inMemStore.Flush(context.Background())

		storeKV := pkger.NewStoreKV(inMemStore)

		const orgID = 3
		const id = 3
		expected := stackStub(id, orgID)

		seedEntities(t, storeKV, expected)

		t.Run("with valid ID updates stack successfully", func(t *testing.T) {
			expected := stackStub(id, orgID)
			event := expected.LatestEvent()
			event.EventType = pkger.StackEventUpdate
			event.UpdatedAt = event.UpdatedAt.Add(time.Hour)
			event.Resources = append(event.Resources, pkger.StackResource{
				APIVersion: pkger.APIVersion,
				ID:         333,
				Kind:       pkger.KindBucket,
				MetaName:   "beyond",
			})
			expected.Events = append(expected.Events, event)

			err := storeKV.UpdateStack(context.Background(), expected)
			require.NoError(t, err)

			readStackEqual(t, storeKV, expected)
		})

		t.Run("when no match found fails with not found error", func(t *testing.T) {
			unmatchedID := platform.ID(3000)
			err := storeKV.UpdateStack(context.Background(), pkger.Stack{
				ID:    unmatchedID,
				OrgID: orgID,
			})
			require.Error(t, err)
			assert.Equalf(t, errors.ENotFound, errors.ErrorCode(err), "err: %s", err)
		})

		t.Run("when org id does not match fails with unprocessable entity error", func(t *testing.T) {
			err := storeKV.UpdateStack(context.Background(), pkger.Stack{
				ID:    id,
				OrgID: orgID + 9000,
			})
			require.Error(t, err)
			assert.Equalf(t, errors.EUnprocessableEntity, errors.ErrorCode(err), "err: %s", err)
		})
	})

	t.Run("delete a stack", func(t *testing.T) {
		defer inMemStore.Flush(context.Background())

		storeKV := pkger.NewStoreKV(inMemStore)

		const orgID = 3
		expected := stackStub(1, orgID)

		seedEntities(t, storeKV, expected)

		t.Run("with valid ID deletes stack successfully", func(t *testing.T) {
			err := storeKV.DeleteStack(context.Background(), expected.ID)
			require.NoError(t, err)

			_, err = storeKV.ReadStackByID(context.Background(), expected.ID)
			require.Error(t, err)
			errCodeEqual(t, errors.ENotFound, err)
		})

		t.Run("when no match found fails with not found error", func(t *testing.T) {
			unmatchedID := platform.ID(3000)
			err := storeKV.DeleteStack(context.Background(), unmatchedID)
			require.Error(t, err)
			errCodeEqual(t, errors.ENotFound, err)
		})
	})
}

func readStackEqual(t *testing.T, store pkger.Store, expected pkger.Stack) {
	t.Helper()

	stack, err := store.ReadStackByID(context.Background(), expected.ID)
	require.NoError(t, err)
	assert.Equal(t, expected, stack)
}

func errCodeEqual(t *testing.T, expected string, actual error) {
	t.Helper()

	assert.Equalf(t, expected, errors.ErrorCode(actual), "err: %s", actual)
}

func seedEntities(t *testing.T, store pkger.Store, first pkger.Stack, rest ...pkger.Stack) {
	t.Helper()

	for _, st := range append(rest, first) {
		err := store.CreateStack(context.Background(), st)
		require.NoError(t, err)
	}
}
