package storagepacker

import (
	"context"
	"fmt"
	"testing"

	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	log "github.com/hashicorp/go-hclog"
	uuid "github.com/hashicorp/go-uuid"
	"github.com/hashicorp/vault/helper/identity"
	"github.com/hashicorp/vault/sdk/logical"
)

func BenchmarkStoragePacker(b *testing.B) {
	storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "")
	if err != nil {
		b.Fatal(err)
	}

	ctx := context.Background()

	for i := 0; i < b.N; i++ {
		itemID, err := uuid.GenerateUUID()
		if err != nil {
			b.Fatal(err)
		}

		item := &Item{
			ID: itemID,
		}

		err = storagePacker.PutItem(ctx, item)
		if err != nil {
			b.Fatal(err)
		}

		fetchedItem, err := storagePacker.GetItem(itemID)
		if err != nil {
			b.Fatal(err)
		}

		if fetchedItem == nil {
			b.Fatalf("failed to read stored item with ID: %q, iteration: %d", item.ID, i)
		}

		if fetchedItem.ID != item.ID {
			b.Fatalf("bad: item ID; expected: %q\n actual: %q", item.ID, fetchedItem.ID)
		}

		err = storagePacker.DeleteItem(ctx, item.ID)
		if err != nil {
			b.Fatal(err)
		}

		fetchedItem, err = storagePacker.GetItem(item.ID)
		if err != nil {
			b.Fatal(err)
		}
		if fetchedItem != nil {
			b.Fatalf("failed to delete item")
		}
	}
}

func TestStoragePacker(t *testing.T) {
	storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "")
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()

	// Persist a storage entry
	item1 := &Item{
		ID: "item1",
	}

	err = storagePacker.PutItem(ctx, item1)
	if err != nil {
		t.Fatal(err)
	}

	// Verify that it can be read
	fetchedItem, err := storagePacker.GetItem(item1.ID)
	if err != nil {
		t.Fatal(err)
	}
	if fetchedItem == nil {
		t.Fatalf("failed to read the stored item")
	}

	if item1.ID != fetchedItem.ID {
		t.Fatalf("bad: item ID; expected: %q\n actual: %q\n", item1.ID, fetchedItem.ID)
	}

	// Delete item1
	err = storagePacker.DeleteItem(ctx, item1.ID)
	if err != nil {
		t.Fatal(err)
	}

	// Check that the deletion was successful
	fetchedItem, err = storagePacker.GetItem(item1.ID)
	if err != nil {
		t.Fatal(err)
	}

	if fetchedItem != nil {
		t.Fatalf("failed to delete item")
	}
}

func TestStoragePacker_SerializeDeserializeComplexItem(t *testing.T) {
	storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "")
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()

	timeNow := ptypes.TimestampNow()

	alias1 := &identity.Alias{
		ID:            "alias_id",
		CanonicalID:   "canonical_id",
		MountType:     "mount_type",
		MountAccessor: "mount_accessor",
		Metadata: map[string]string{
			"aliasmkey": "aliasmvalue",
		},
		Name:                   "alias_name",
		CreationTime:           timeNow,
		LastUpdateTime:         timeNow,
		MergedFromCanonicalIDs: []string{"merged_from_canonical_id"},
	}

	entity := &identity.Entity{
		Aliases: []*identity.Alias{alias1},
		ID:      "entity_id",
		Name:    "entity_name",
		Metadata: map[string]string{
			"testkey1": "testvalue1",
			"testkey2": "testvalue2",
		},
		CreationTime:    timeNow,
		LastUpdateTime:  timeNow,
		BucketKey:       "entity_hash",
		MergedEntityIDs: []string{"merged_entity_id1", "merged_entity_id2"},
		Policies:        []string{"policy1", "policy2"},
	}

	marshaledEntity, err := ptypes.MarshalAny(entity)
	if err != nil {
		t.Fatal(err)
	}
	err = storagePacker.PutItem(ctx, &Item{
		ID:      entity.ID,
		Message: marshaledEntity,
	})
	if err != nil {
		t.Fatal(err)
	}

	itemFetched, err := storagePacker.GetItem(entity.ID)
	if err != nil {
		t.Fatal(err)
	}

	var itemDecoded identity.Entity
	err = ptypes.UnmarshalAny(itemFetched.Message, &itemDecoded)
	if err != nil {
		t.Fatal(err)
	}

	if !proto.Equal(&itemDecoded, entity) {
		t.Fatalf("bad: expected: %#v\nactual: %#v\n", entity, itemDecoded)
	}
}

func TestStoragePacker_DeleteMultiple(t *testing.T) {
	storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "")
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()

	// Persist a storage entry
	for i := 0; i < 100; i++ {
		item := &Item{
			ID: fmt.Sprintf("item%d", i),
		}

		err = storagePacker.PutItem(ctx, item)
		if err != nil {
			t.Fatal(err)
		}

		// Verify that it can be read
		fetchedItem, err := storagePacker.GetItem(item.ID)
		if err != nil {
			t.Fatal(err)
		}
		if fetchedItem == nil {
			t.Fatalf("failed to read the stored item")
		}

		if item.ID != fetchedItem.ID {
			t.Fatalf("bad: item ID; expected: %q\n actual: %q\n", item.ID, fetchedItem.ID)
		}
	}

	itemsToDelete := make([]string, 0, 50)
	for i := 1; i < 100; i += 2 {
		itemsToDelete = append(itemsToDelete, fmt.Sprintf("item%d", i))
	}

	err = storagePacker.DeleteMultipleItems(ctx, nil, itemsToDelete)
	if err != nil {
		t.Fatal(err)
	}

	// Check that the deletion was successful
	for i := 0; i < 100; i++ {
		fetchedItem, err := storagePacker.GetItem(fmt.Sprintf("item%d", i))
		if err != nil {
			t.Fatal(err)
		}

		if i%2 == 0 && fetchedItem == nil {
			t.Fatal("expected item not found")
		}
		if i%2 == 1 && fetchedItem != nil {
			t.Fatalf("failed to delete item")
		}
	}
}

func TestStoragePacker_DeleteMultiple_ALL(t *testing.T) {
	storagePacker, err := NewStoragePacker(&logical.InmemStorage{}, log.New(&log.LoggerOptions{Name: "storagepackertest"}), "")
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()

	// Persist a storage entry
	itemsToDelete := make([]string, 0, 10000)
	for i := 0; i < 10000; i++ {
		item := &Item{
			ID: fmt.Sprintf("item%d", i),
		}

		err = storagePacker.PutItem(ctx, item)
		if err != nil {
			t.Fatal(err)
		}

		// Verify that it can be read
		fetchedItem, err := storagePacker.GetItem(item.ID)
		if err != nil {
			t.Fatal(err)
		}
		if fetchedItem == nil {
			t.Fatalf("failed to read the stored item")
		}

		if item.ID != fetchedItem.ID {
			t.Fatalf("bad: item ID; expected: %q\n actual: %q\n", item.ID, fetchedItem.ID)
		}

		itemsToDelete = append(itemsToDelete, fmt.Sprintf("item%d", i))
	}

	err = storagePacker.DeleteMultipleItems(ctx, nil, itemsToDelete)
	if err != nil {
		t.Fatal(err)
	}

	// Check that the deletion was successful
	for _, item := range itemsToDelete {
		fetchedItem, err := storagePacker.GetItem(item)
		if err != nil {
			t.Fatal(err)
		}
		if fetchedItem != nil {
			t.Fatal("item not deleted")
		}
	}
}
