package devices

import (
	"fmt"
	"testing"
	"time"

	"code.secondbit.org/uuid.hg"
	"golang.org/x/net/context"
)

type StorerFactory interface {
	NewStorer(ctx context.Context) (Storer, error)
	TeardownStorer(storer Storer, ctx context.Context) error
}

var storerFactories []StorerFactory

const (
	changeName = 1 << iota
	changeOwner
	changeType
	changeLastSeen
	changePushToken
	changeVariations
)

func compareDevices(device1, device2 Device) (ok bool, field string, expected, result interface{}) {
	if !device1.ID.Equal(device2.ID) {
		return false, "ID", device1.ID, device2.ID
	}
	if device1.Name != device2.Name {
		return false, "Name", device1.Name, device2.Name
	}
	if !device1.Owner.Equal(device2.Owner) {
		return false, "Owner", device1.Owner, device2.Owner
	}
	if device1.Type != device2.Type {
		return false, "Type", device1.Type, device2.Type
	}
	if !device1.Created.Equal(device2.Created) {
		return false, "Created", device1.Created, device2.Created
	}
	if !device1.LastSeen.Equal(device2.LastSeen) {
		return false, "LastSeen", device1.LastSeen, device2.LastSeen
	}
	if device1.PushToken != device2.PushToken {
		return false, "PushToken", device1.PushToken, device2.PushToken
	}
	return true, "", nil, nil
}

func TestCreateAndGetDevices(t *testing.T) {
	for _, factory := range storerFactories {
		ctx := context.Background()
		storer, err := factory.NewStorer(ctx)
		if err != nil {
			t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
		}

		devices := []Device{
			{ID: uuid.NewID(), Name: "Test 1", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 2", Owner: uuid.NewID(), Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 3", Owner: uuid.NewID(), Type: TypeChromeExtension, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
		}

		err = storer.CreateDevices(devices, ctx)
		if err != nil {
			t.Errorf("Error creating devices in %T: %+v\n", storer, err)
		}

		ids := make([]uuid.ID, 0, len(devices))
		for _, device := range devices {
			ids = append(ids, device.ID)
		}

		results, err := storer.GetDevices(ids, ctx)
		if err != nil {
			t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
		}
		for _, device := range devices {
			d, returned := results[device.ID.String()]
			if !returned {
				t.Errorf("Expected device %s to be in results from %T, but wasn't present\n", device.Name, storer)
			}
			ok, field, expected, result := compareDevices(device, d)
			if !ok {
				t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, device.Name, expected, result, storer)
			}
		}
		err = factory.TeardownStorer(storer, ctx)
		if err != nil {
			t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
		}
	}
}

func TestGetDevicesNoErrorForMissing(t *testing.T) {
	for _, factory := range storerFactories {
		ctx := context.Background()
		storer, err := factory.NewStorer(ctx)
		if err != nil {
			t.Fatalf("Fatal error creatng Storer from %T: %+v\n", factory, err)
		}

		results, err := storer.GetDevices([]uuid.ID{uuid.NewID()}, ctx)
		if err != nil {
			t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
		}
		if len(results) != 0 {
			t.Errorf("Expected results to be empty, got %+v from %T instead\n", results, storer)
		}
		err = factory.TeardownStorer(storer, ctx)
		if err != nil {
			t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
		}
	}
}

func TestCreateDevicesDuplicates(t *testing.T) {
	for _, factory := range storerFactories {
		ctx := context.Background()
		storer, err := factory.NewStorer(ctx)
		if err != nil {
			t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
		}

		devices := []Device{
			{ID: uuid.NewID(), Name: "Test 1", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 2", Owner: uuid.NewID(), Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 3", Owner: uuid.NewID(), Type: TypeChromeExtension, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
		}

		err = storer.CreateDevices(devices, ctx)
		if err != nil {
			t.Errorf("Unexpected error creating devices in %T: %+v\n", storer, err)
		}

		newDevices := []Device{
			{ID: uuid.NewID(), Name: "Test 4", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 5", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
		}

		err = storer.CreateDevices([]Device{newDevices[0], devices[1], newDevices[1]}, ctx)
		daeErr, ok := err.(ErrDeviceAlreadyExists)
		if !ok {
			t.Errorf("Expected ErrDeviceAlreadyExists creating duplicate device in %T, got %+v\n", storer, err)
		}
		if !uuid.ID(daeErr).Equal(devices[1].ID) {
			t.Errorf("Expected ErrDeviceAlreadyExists to be %+v, got %+v from %T\n", devices[1].ID, daeErr, storer)
		}

		// inserts should be a transaction; they either all make it, or none do
		results, err := storer.GetDevices([]uuid.ID{newDevices[0].ID, newDevices[1].ID}, ctx)
		if err != nil {
			t.Errorf("Error retrieving devices from %T: %+v\n", storer, err)
		}
		if len(results) != 0 {
			t.Errorf("Expected new inserts to not be in results, got %+v from %T\n", results, storer)
		}

		err = factory.TeardownStorer(storer, ctx)
		if err != nil {
			t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
		}
	}
}

func TestCreateAndListDevicesByOwner(t *testing.T) {
	for _, factory := range storerFactories {
		ctx := context.Background()
		storer, err := factory.NewStorer(ctx)
		if err != nil {
			t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
		}

		owner1, owner2 := uuid.NewID(), uuid.NewID()
		devices := []Device{
			{ID: uuid.NewID(), Name: "Test 1", Owner: owner1, Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 2", Owner: owner2, Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 3", Owner: owner1, Type: TypeChromeExtension, Created: time.Now().Add(time.Minute), LastSeen: time.Now(), PushToken: "test token"},
		}

		err = storer.CreateDevices(devices, ctx)
		if err != nil {
			t.Errorf("Error creating devices in %T: %+v\n", storer, err)
		}

		results, err := storer.ListDevicesByOwner(owner1, ctx)
		if err != nil {
			t.Errorf("Error listing devices for owner1 from %T: %+v\n", storer, err)
		}
		if len(results) != 2 {
			t.Errorf("Expected %d results for owner1, got %d from %T\n", 2, len(results), storer)
		}
		resultMap := ToMap(results)
		d, ok := resultMap[devices[0].ID.String()]
		if !ok {
			t.Errorf("Expected to get %s in results, got %+v from %T\n", devices[0].Name, results, storer)
		}
		ok, field, expected, result := compareDevices(devices[0], d)
		if !ok {
			t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, devices[0].Name, expected, result, storer)
		}
		d, ok = resultMap[devices[2].ID.String()]
		if !ok {
			t.Errorf("Expected to get %s in results, got %+v from %T\n", devices[2].Name, results, storer)
		}
		ok, field, expected, result = compareDevices(devices[2], d)
		if !ok {
			t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, devices[2].Name, expected, result, storer)
		}

		results, err = storer.ListDevicesByOwner(owner2, ctx)
		if err != nil {
			t.Errorf("Error listing devices for owner2 from %T: %+v\n", storer, err)
		}
		if len(results) != 1 {
			t.Errorf("Expected %d results for owner2, got %d from %T\n", 1, len(results), storer)
		}
		ok, field, expected, result = compareDevices(devices[1], results[0])
		if !ok {
			t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, devices[1].Name, expected, result, storer)
		}

		err = factory.TeardownStorer(storer, ctx)
		if err != nil {
			t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
		}
	}
}

func TestUpdateDevicesHappyPath(t *testing.T) {
	device := Device{
		ID:        uuid.NewID(),
		Name:      "Test 1",
		Owner:     uuid.NewID(),
		Type:      TypeAndroidPhone,
		Created:   time.Now(),
		LastSeen:  time.Now(),
		PushToken: "test token",
	}
	for _, factory := range storerFactories {
		ctx := context.Background()
		storer, err := factory.NewStorer(ctx)
		if err != nil {
			t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
		}
		for i := 1; i < changeVariations; i++ {
			var change DeviceChange
			var owner uuid.ID
			var name, pushToken string
			var lastSeen time.Time
			var deviceType DeviceType

			device.ID = uuid.NewID()

			expectation := device
			result := device

			change.DeviceID = device.ID

			if i&changeName != 0 {
				name = fmt.Sprintf("name-%d", i)
				change.Name = &name
				expectation.Name = name
			}
			if i&changeOwner != 0 {
				owner = uuid.NewID()
				change.Owner = &owner
				expectation.Owner = owner
			}
			if i&changeType != 0 {
				deviceType = DeviceType(fmt.Sprintf("type-%d", i))
				change.Type = &deviceType
				expectation.Type = deviceType
			}
			if i&changeLastSeen != 0 {
				lastSeen = time.Now().Add(time.Minute * time.Duration(i*-1))
				change.LastSeen = &lastSeen
				expectation.LastSeen = lastSeen
			}
			if i&changePushToken != 0 {
				pushToken = fmt.Sprintf("push-token-%d", i)
				change.PushToken = &pushToken
				expectation.PushToken = pushToken
			}
			result = ApplyChange(result, change)
			ok, field, expectedVal, resultVal := compareDevices(expectation, result)
			if !ok {
				t.Errorf("Expected %s of %s to be %v, got %v after applying DeviceChange %+v\n", field, device.Name, expectedVal, resultVal, change)
			}

			err = storer.CreateDevices([]Device{device}, ctx)
			if err != nil {
				t.Errorf("Unexpected error creating devices in %T: %+v\n", storer, err)
			}

			err = storer.UpdateDevice(change, ctx)
			if err != nil {
				t.Errorf("Unexpected error updating device in %T: %+v\n", storer, err)
			}

			retrieved, err := storer.GetDevices([]uuid.ID{device.ID}, ctx)
			if err != nil {
				t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
			}
			retrievedDevice, ok := retrieved[device.ID.String()]
			if !ok {
				t.Errorf("Expected retrieved devices to contain %s, got %+v from %T\n", device.Name, retrieved, storer)
			}
			ok, field, expectedVal, resultVal = compareDevices(expectation, retrievedDevice)
			if !ok {
				t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, device.Name, expectedVal, resultVal, storer)
			}
		}

		err = factory.TeardownStorer(storer, ctx)
		if err != nil {
			t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
		}
	}
}

func TestUpdateDeviceNotFound(t *testing.T) {
	for _, factory := range storerFactories {
		ctx := context.Background()
		storer, err := factory.NewStorer(ctx)
		if err != nil {
			t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
		}

		deviceID := uuid.NewID()
		name := "my new name"
		change := DeviceChange{DeviceID: deviceID, Name: &name}

		err = storer.UpdateDevice(change, ctx)
		if err != ErrDeviceNotFound {
			t.Errorf("Expected error to be ErrDeviceNotFound, %T returned %+v\n", storer, err)
		}

		results, err := storer.GetDevices([]uuid.ID{deviceID}, ctx)
		if err != nil {
			t.Errorf("Error retrieving devices from %T: %+v\n", storer, err)
		}
		if len(results) != 0 {
			t.Errorf("Expected no devices in %T, got %+v\n", storer, results)
		}

		err = factory.TeardownStorer(storer, ctx)
		if err != nil {
			t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
		}
	}
}

func TestDeleteDevicesHappyPath(t *testing.T) {
	for _, factory := range storerFactories {
		ctx := context.Background()
		storer, err := factory.NewStorer(ctx)
		if err != nil {
			t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
		}

		owner1, owner2 := uuid.NewID(), uuid.NewID()

		devices := []Device{
			{ID: uuid.NewID(), Name: "Test 1", Owner: owner1, Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 2", Owner: owner2, Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
			{ID: uuid.NewID(), Name: "Test 3", Owner: owner1, Type: TypeChromeExtension, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
		}

		err = storer.CreateDevices(devices, ctx)
		if err != nil {
			t.Errorf("Error creating devices in %T: %+v\n", storer, err)
		}

		err = storer.DeleteDevices([]uuid.ID{devices[0].ID, devices[1].ID}, ctx)
		if err != nil {
			t.Errorf("Error deleting devices from %T: %+v\n", storer, err)
		}

		results, err := storer.GetDevices([]uuid.ID{devices[0].ID, devices[1].ID, devices[2].ID}, ctx)
		if err != nil {
			t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
		}

		if len(results) != 1 {
			t.Errorf("Expected %d results, got %d from %T\n", 1, len(results), storer)
		}

		device, ok := results[devices[0].ID.String()]
		if ok {
			t.Errorf("Retrieved first device (which was deleted!) from %T: %+v\n", storer, device)
		}

		device, ok = results[devices[1].ID.String()]
		if ok {
			t.Errorf("Retrieved second device (which was deleted!) from %T: %+v\n", storer, device)
		}

		device, ok = results[devices[2].ID.String()]
		if !ok {
			t.Errorf("Didn't retrieve third device (which wasn't deleted!) from %T. Got %+v\n", storer, results)
		}

		err = factory.TeardownStorer(storer, ctx)
		if err != nil {
			t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
		}
	}
}
