package tenant_test

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kv"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/influxdata/influxdb/v2/tenant"
	itesting "github.com/influxdata/influxdb/v2/testing"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// type Organization struct {
// 	ID          ID     `json:"id,omitempty"`
// 	Name        string `json:"name"`
// 	Description string `json:"description"`
// 	CRUDLog
// }

const (
	firstOrgID platform.ID = (iota + 1)
	secondOrgID
	thirdOrgID
	fourthOrgID
	fifthOrgID
)

func TestOrg(t *testing.T) {
	var (
		aTime    = time.Date(2020, 7, 23, 10, 0, 0, 0, time.UTC)
		testOrgs = func(count int, visit ...func(*influxdb.Organization)) (orgs []*influxdb.Organization) {
			for i := 1; i <= count; i++ {
				org := &influxdb.Organization{
					ID:          platform.ID(i),
					Name:        fmt.Sprintf("org%d", i),
					Description: "words",
				}

				if len(visit) > 0 {
					visit[0](org)
				}

				orgs = append(orgs, org)
			}

			return
		}

		withCrudLog = func(o *influxdb.Organization) {
			o.CRUDLog = influxdb.CRUDLog{
				CreatedAt: aTime,
				UpdatedAt: aTime,
			}
		}

		simpleSetup = func(t *testing.T, store *tenant.Store, tx kv.Tx) {
			store.OrgIDGen = mock.NewIncrementingIDGenerator(1)
			for _, org := range testOrgs(10) {
				require.NoError(t, store.CreateOrg(context.Background(), tx, org))
			}
		}

		over20Setup = func(t *testing.T, store *tenant.Store, tx kv.Tx) {
			store.OrgIDGen = mock.NewIncrementingIDGenerator(1)
			for _, org := range testOrgs(25) {
				require.NoError(t, store.CreateOrg(context.Background(), tx, org))
			}
		}
	)

	st := []struct {
		name    string
		setup   func(*testing.T, *tenant.Store, kv.Tx)
		update  func(*testing.T, *tenant.Store, kv.Tx)
		results func(*testing.T, *tenant.Store, kv.Tx)
	}{
		{
			name:  "create",
			setup: simpleSetup,
			results: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				orgs, err := store.ListOrgs(context.Background(), tx)
				if err != nil {
					t.Fatal(err)
				}

				assert.Len(t, orgs, 10)

				expected := testOrgs(10, withCrudLog)
				assert.Equal(t, expected, orgs)
			},
		},
		{
			name:  "get",
			setup: simpleSetup,
			results: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				org, err := store.GetOrg(context.Background(), tx, fifthOrgID)
				if err != nil {
					t.Fatal(err)
				}

				expected := &influxdb.Organization{
					ID:          fifthOrgID,
					Name:        "org5",
					Description: "words",
					CRUDLog: influxdb.CRUDLog{
						CreatedAt: org.CreatedAt,
						UpdatedAt: org.UpdatedAt,
					},
				}
				require.Equal(t, expected, org)

				org, err = store.GetOrgByName(context.Background(), tx, "org5")
				if err != nil {
					t.Fatal(err)
				}
				require.Equal(t, expected, org)

				if _, err := store.GetOrg(context.Background(), tx, 500); !errors.Is(err, tenant.ErrOrgNotFound) {
					t.Fatal("failed to get correct error when looking for invalid org by id")
				}

				if _, err := store.GetOrgByName(context.Background(), tx, "notaorg"); err.Error() != tenant.OrgNotFoundByName("notaorg").Error() {
					t.Fatal("failed to get correct error when looking for invalid org by name")
				}

			},
		},
		{
			name:  "list",
			setup: simpleSetup,
			results: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				orgs, err := store.ListOrgs(context.Background(), tx)
				if err != nil {
					t.Fatal(err)
				}

				require.Len(t, orgs, 10)

				expected := testOrgs(10, withCrudLog)
				require.Equal(t, expected, orgs)
				orgs, err = store.ListOrgs(context.Background(), tx, influxdb.FindOptions{Limit: 4})
				require.NoError(t, err)
				assert.Len(t, orgs, 4)
				assert.Equal(t, expected[:4], orgs)

				orgs, err = store.ListOrgs(context.Background(), tx, influxdb.FindOptions{Offset: 3})
				require.NoError(t, err)
				assert.Len(t, orgs, 7)
				assert.Equal(t, expected[3:], orgs)
			},
		},
		{
			name:  "listOver20",
			setup: over20Setup,
			results: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				orgs, err := store.ListOrgs(context.Background(), tx)
				if err != nil {
					t.Fatal(err)
				}

				require.Len(t, orgs, 25)
			},
		},
		{
			name:  "update",
			setup: simpleSetup,
			update: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				org5 := "org5"
				_, err := store.UpdateOrg(context.Background(), tx, thirdOrgID, influxdb.OrganizationUpdate{Name: &org5})
				if err.Error() != tenant.OrgAlreadyExistsError(org5).Error() {
					t.Fatal("failed to error on duplicate orgname")
				}

				org30 := "org30"
				_, err = store.UpdateOrg(context.Background(), tx, thirdOrgID, influxdb.OrganizationUpdate{Name: &org30})
				require.NoError(t, err)

				description := "notWords"
				_, err = store.UpdateOrg(context.Background(), tx, thirdOrgID, influxdb.OrganizationUpdate{Description: &description})
				require.NoError(t, err)
			},
			results: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				orgs, err := store.ListOrgs(context.Background(), tx)
				require.NoError(t, err)

				assert.Len(t, orgs, 10)

				expected := testOrgs(10, withCrudLog)
				expected[2].Name = "org30"
				expected[2].Description = "notWords"
				require.Equal(t, expected, orgs)
			},
		},
		{
			name:  "delete",
			setup: simpleSetup,
			update: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				err := store.DeleteOrg(context.Background(), tx, firstOrgID)
				require.NoError(t, err)

				err = store.DeleteOrg(context.Background(), tx, firstOrgID)
				if !errors.Is(err, tenant.ErrOrgNotFound) {
					t.Fatal("invalid error when deleting org that has already been deleted", err)
				}

				err = store.DeleteOrg(context.Background(), tx, thirdOrgID)
				require.NoError(t, err)
			},
			results: func(t *testing.T, store *tenant.Store, tx kv.Tx) {
				orgs, err := store.ListOrgs(context.Background(), tx)
				require.NoError(t, err)
				assert.Len(t, orgs, 8)

				all := testOrgs(10, withCrudLog)
				// deleted first and third item
				expected := append(all[1:2], all[3:]...)
				require.Equal(t, expected, orgs)
			},
		},
	}
	for _, testScenario := range st {
		t.Run(testScenario.name, func(t *testing.T) {
			s := itesting.NewTestInmemStore(t)
			ts := tenant.NewStore(s, tenant.WithNow(func() time.Time {
				return aTime
			}))

			// setup
			if testScenario.setup != nil {
				err := ts.Update(context.Background(), func(tx kv.Tx) error {
					testScenario.setup(t, ts, tx)
					return nil
				})

				if err != nil {
					t.Fatal(err)
				}
			}

			// update
			if testScenario.update != nil {
				err := ts.Update(context.Background(), func(tx kv.Tx) error {
					testScenario.update(t, ts, tx)
					return nil
				})

				if err != nil {
					t.Fatal(err)
				}
			}

			// results
			if testScenario.results != nil {
				err := ts.View(context.Background(), func(tx kv.Tx) error {
					testScenario.results(t, ts, tx)
					return nil
				})

				if err != nil {
					t.Fatal(err)
				}
			}
		})
	}
}
