package testing

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

	"github.com/google/go-cmp/cmp"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/authorizer"
	icontext "github.com/influxdata/influxdb/v2/context"
	"github.com/influxdata/influxdb/v2/kv"
	"github.com/influxdata/influxdb/v2/mock"
	"go.uber.org/zap/zaptest"
)

// NewDocumentIntegrationTest will test the documents related funcs.
func NewDocumentIntegrationTest(store kv.Store) func(t *testing.T) {
	return func(t *testing.T) {
		ctx := context.Background()
		kvsvc := kv.NewService(zaptest.NewLogger(t), store)
		mockTimeGen := new(mock.TimeGenerator)
		if err := kvsvc.Initialize(ctx); err != nil {
			t.Fatalf("failed to initialize service: %v", err)
		}

		kvsvc.TimeGenerator = mockTimeGen
		svc := authorizer.NewDocumentService(kvsvc)

		s, err := svc.CreateDocumentStore(ctx, "testing")
		if err != nil {
			t.Fatalf("failed to create document store: %v", err)
		}

		ss, err := svc.FindDocumentStore(ctx, "testing")
		if err != nil {
			t.Fatalf("failed to find document store: %v", err)
		}

		l1 := &influxdb.Label{Name: "l1", OrgID: MustIDBase16("41a9f7288d4e2d64")}
		l2 := &influxdb.Label{Name: "l2", OrgID: MustIDBase16("41a9f7288d4e2d64")}
		MustCreateLabels(ctx, kvsvc, l1, l2)
		lBad := &influxdb.Label{ID: MustIDBase16(oneID), Name: "bad"}

		o1 := &influxdb.Organization{Name: "foo"}
		o2 := &influxdb.Organization{Name: "bar"}
		MustCreateOrgs(ctx, kvsvc, o1, o2)

		u1 := &influxdb.User{Name: "yanky"}
		u2 := &influxdb.User{Name: "doodle"}
		MustCreateUsers(ctx, kvsvc, u1, u2)

		MustMakeUsersOrgOwner(ctx, kvsvc, o1.ID, u1.ID)
		MustMakeUsersOrgOwner(ctx, kvsvc, o2.ID, u2.ID)
		MustMakeUsersOrgMember(ctx, kvsvc, o1.ID, u2.ID)

		// TODO(desa): test tokens and authorizations as well.
		now := time.Now()
		s1 := &influxdb.Session{
			CreatedAt: now,
			ExpiresAt: now.Add(1 * time.Hour),
			UserID:    u1.ID,
			Permissions: []influxdb.Permission{
				// u1 is owner of o1
				{
					Action: influxdb.ReadAction,
					Resource: influxdb.Resource{
						OrgID: &o1.ID,
						Type:  influxdb.DocumentsResourceType,
					},
				},
				{
					Action: influxdb.WriteAction,
					Resource: influxdb.Resource{
						OrgID: &o1.ID,
						Type:  influxdb.DocumentsResourceType,
					},
				},
			},
		}
		s2 := &influxdb.Session{
			CreatedAt: now,
			ExpiresAt: now.Add(1 * time.Hour),
			UserID:    u2.ID,
			Permissions: []influxdb.Permission{
				// u2 is owner of o2
				{
					Action: influxdb.ReadAction,
					Resource: influxdb.Resource{
						OrgID: &o2.ID,
						Type:  influxdb.DocumentsResourceType,
					},
				},
				{
					Action: influxdb.WriteAction,
					Resource: influxdb.Resource{
						OrgID: &o2.ID,
						Type:  influxdb.DocumentsResourceType,
					},
				},
				// u2 is member of o1
				{
					Action: influxdb.ReadAction,
					Resource: influxdb.Resource{
						OrgID: &o1.ID,
						Type:  influxdb.DocumentsResourceType,
					},
				},
			},
		}

		var d1 *influxdb.Document
		var d2 *influxdb.Document
		var d3 *influxdb.Document

		t.Run("u1 can create document for o1", func(t *testing.T) {
			d1 = &influxdb.Document{
				Meta: influxdb.DocumentMeta{
					Name:        "i1",
					Type:        "type1",
					Description: "desc1",
				},
				Content: map[string]interface{}{
					"v1": "v1",
				},
				Labels:        []*influxdb.Label{l1},
				Organizations: map[influxdb.ID]influxdb.UserType{o1.ID: influxdb.Owner},
			}
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s1)

			mockTimeGen.FakeValue = time.Date(2009, 1, 2, 3, 0, 0, 0, time.UTC)
			if err := s.CreateDocument(ctx, d1); err != nil {
				t.Errorf("failed to create document: %v", err)
			}
		})

		t.Run("u2 cannot create document for o1", func(t *testing.T) {
			d2 = &influxdb.Document{
				Meta: influxdb.DocumentMeta{
					Name: "i2",
				},
				Content: map[string]interface{}{
					"i2": "i2",
				},
				Labels:        []*influxdb.Label{l2},
				Organizations: map[influxdb.ID]influxdb.UserType{o1.ID: influxdb.Owner},
			}
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s2)

			mockTimeGen.FakeValue = time.Date(2009, 1, 2, 3, 0, 1, 0, time.UTC)
			if err := s.CreateDocument(ctx, d2); err == nil {
				t.Fatalf("should not have been authorized to create document")
			}

			mockTimeGen.FakeValue = time.Date(2009, 1, 2, 3, 0, 1, 0, time.UTC)
			d2.Organizations = map[influxdb.ID]influxdb.UserType{o2.ID: influxdb.Owner}
			if err := s.CreateDocument(ctx, d2); err != nil {
				t.Errorf("should have been authorized to create document: %v", err)
			}
		})

		t.Run("u1 cannot create document for o2", func(t *testing.T) {
			d3 = &influxdb.Document{
				Meta: influxdb.DocumentMeta{
					Name: "i2",
				},
				Content: map[string]interface{}{
					"k2": "v2",
				},
				Organizations: map[influxdb.ID]influxdb.UserType{o2.ID: influxdb.Owner},
			}
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s1)

			mockTimeGen.FakeValue = time.Date(2009, 1, 2, 3, 0, 2, 0, time.UTC)
			if err := s.CreateDocument(ctx, d3); err == nil {
				t.Errorf("should not have be authorized to create document")
			}
		})

		t.Run("can't create document with non existing label", func(t *testing.T) {
			d4 := &influxdb.Document{
				Meta: influxdb.DocumentMeta{
					Name: "i4",
				},
				Content: map[string]interface{}{
					"k4": "v4",
				},
				Labels:        []*influxdb.Label{lBad},
				Organizations: map[influxdb.ID]influxdb.UserType{o1.ID: influxdb.Owner},
			}
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s1)
			err = s.CreateDocument(ctx, d4)
			ErrorsEqual(t, err, &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  "label not found",
			})
		})

		d1.Meta.CreatedAt = time.Date(2009, 1, 2, 3, 0, 0, 0, time.UTC)
		dl1 := new(influxdb.Document)
		*dl1 = *d1
		dl1.Labels = append([]*influxdb.Label{}, l1)

		d2.Meta.CreatedAt = time.Date(2009, 1, 2, 3, 0, 1, 0, time.UTC)
		dl2 := new(influxdb.Document)
		*dl2 = *d2
		dl2.Labels = append([]*influxdb.Label{}, d2.Labels...)

		d3.Meta.CreatedAt = time.Date(2009, 1, 2, 3, 0, 2, 0, time.UTC)

		t.Run("u1 can see only o1s documents by label", func(t *testing.T) {
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s1)
			ds, err := ss.FindDocuments(
				ctx,
				influxdb.WhereOrgID(o1.ID),
				influxdb.WhereOrgID(o2.ID),
				influxdb.IncludeContent,
				influxdb.IncludeLabels,
			)
			if err != nil {
				t.Fatalf("failed to retrieve documents: %v", err)
			}

			if exp, got := []*influxdb.Document{dl1}, ds; !docsEqual(got, exp) {
				t.Errorf("documents are different -got/+want\ndiff %s", docsDiff(got, exp))
			}
		})

		t.Run("check not found err", func(t *testing.T) {
			_, err := ss.FindDocument(ctx, MustIDBase16(fourID))
			ErrorsEqual(t, err, &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  influxdb.ErrDocumentNotFound,
			})
		})

		t.Run("u2 can see o1 and o2s documents", func(t *testing.T) {
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s2)
			ds, err := ss.FindDocuments(
				ctx,
				influxdb.WhereOrgID(o1.ID),
				influxdb.WhereOrgID(o2.ID),
				influxdb.IncludeContent,
				influxdb.IncludeLabels,
			)
			if err != nil {
				t.Fatalf("failed to retrieve documents for org1: %v", err)
			}
			if exp, got := []*influxdb.Document{dl1, dl2}, ds; !docsEqual(exp, got) {
				t.Errorf("documents are different -got/+want\ndiff %s", docsDiff(exp, got))
			}
		})

		t.Run("u2 cannot update document d1", func(t *testing.T) {
			d := &influxdb.Document{
				ID: d1.ID,
				Meta: influxdb.DocumentMeta{
					Name: "updatei1",
				},
				Content: map[string]interface{}{
					"updatev1": "updatev1",
				},
			}
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s2)
			if err := s.UpdateDocument(ctx, d); err == nil {
				t.Errorf("should not have been authorized to update document")
				return
			}
		})

		t.Run("u2 can update document d2", func(t *testing.T) {
			d := &influxdb.Document{
				ID: d2.ID,
				Meta: influxdb.DocumentMeta{
					Name: "updatei2",
				},
				Content: map[string]interface{}{
					"updatev2": "updatev2",
				},
			}
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s2)
			if err := s.UpdateDocument(ctx, d); err != nil {
				t.Errorf("unexpected error updating document: %v", err)
			}
		})

		t.Run("u1 can update document d1", func(t *testing.T) {
			ctx := context.Background()
			ctx = icontext.SetAuthorizer(ctx, s1)
			if err := s.DeleteDocuments(ctx); err != nil {
				t.Errorf("unexpected error deleteing document: %v", err)
			}
		})

	}
}

func docsEqual(i1, i2 interface{}) bool {
	return cmp.Equal(i1, i2, documentCmpOptions...)
}

func docsDiff(i1, i2 interface{}) string {
	return cmp.Diff(i1, i2, documentCmpOptions...)
}

var documentCmpOptions = cmp.Options{
	cmp.Comparer(func(x, y []byte) bool {
		return bytes.Equal(x, y)
	}),
	cmp.Transformer("Sort", func(in []*influxdb.Document) []*influxdb.Document {
		out := append([]*influxdb.Document(nil), in...) // Copy input to avoid mutating it
		sort.Slice(out, func(i, j int) bool {
			return out[i].ID.String() > out[j].ID.String()
		})
		return out
	}),
}
