package auth

import (
	"fmt"
	"net/url"
	"testing"
	"time"

	"sort"
	"secondbit.org/uuid"
)

const (
	clientChangeSecret = 1 << iota
	clientChangeOwnerID
	clientChangeName
	clientChangeLogo
	clientChangeWebsite
)

var clientStores = []ClientStore{NewMemstore()}

func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
	if !client1.ID.Equal(client2.ID) {
		return false, "ID", client1.ID, client2.ID
	}
	if client1.Secret != client2.Secret {
		return false, "secret", client1.Secret, client2.Secret
	}
	if !client1.OwnerID.Equal(client2.OwnerID) {
		return false, "owner ID", client1.OwnerID, client2.OwnerID
	}
	if client1.Name != client2.Name {
		return false, "name", client1.Name, client2.Name
	}
	if client1.Logo != client2.Logo {
		return false, "logo", client1.Logo, client2.Logo
	}
	if client1.Website != client2.Website {
		return false, "website", client1.Website, client2.Website
	}
	if client1.Type != client2.Type {
		return false, "type", client1.Type, client2.Type
	}
	return true, "", nil, nil
}

func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
	if !endpoint1.ID.Equal(endpoint2.ID) {
		return false, "ID", endpoint1.ID, endpoint2.ID
	}
	if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
		return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
	}
	if !endpoint1.Added.Equal(endpoint2.Added) {
		return false, "Added", endpoint1.Added, endpoint2.Added
	}
	if endpoint1.URI.String() != endpoint2.URI.String() {
		return false, "URI", endpoint1.URI, endpoint2.URI
	}
	return true, "", nil, nil
}

func TestClientStoreSuccess(t *testing.T) {
	t.Parallel()
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	for _, store := range clientStores {
		err := store.SaveClient(client)
		if err != nil {
			t.Fatalf("Error saving client to %T: %s", store, err)
		}
		err = store.SaveClient(client)
		if err != ErrClientAlreadyExists {
			t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
		}
		retrieved, err := store.GetClient(client.ID)
		if err != nil {
			t.Fatalf("Error retrieving client from %T: %s", store, err)
		}
		success, field, expectation, result := compareClients(client, retrieved)
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		clients, err := store.ListClientsByOwner(client.OwnerID, 25, 0)
		if err != nil {
			t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
		}
		if len(clients) != 1 {
			t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
		}
		success, field, expectation, result = compareClients(client, clients[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = store.DeleteClient(client.ID)
		if err != nil {
			t.Fatalf("Error deleting client from %T: %s", store, err)
		}
		err = store.DeleteClient(client.ID)
		if err != ErrClientNotFound {
			t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store)
		}
		retrieved, err = store.GetClient(client.ID)
		if err != ErrClientNotFound {
			t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
		}
		clients, err = store.ListClientsByOwner(client.OwnerID, 25, 0)
		if err != nil {
			t.Fatalf("Error listing clients by owner from %T: %s", store, err)
		}
		if len(clients) != 0 {
			t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
		}
	}
}

func TestEndpointStoreSuccess(t *testing.T) {
	t.Parallel()
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	uri1, _ := url.Parse("https://www.example.com/")
	uri2, _ := url.Parse("https://www.example.com/my/full/path")
	endpoint1 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now(),
		URI:      *uri1,
	}
	endpoint2 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now(),
		URI:      *uri2,
	}
	for _, store := range clientStores {
		err := store.SaveClient(client)
		if err != nil {
			t.Fatalf("Error saving client to %T: %s", store, err)
		}
		err = store.AddEndpoint(client.ID, endpoint1)
		if err != nil {
			t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
		}
		endpoints, err := store.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
		}
		if len(endpoints) != 1 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
		}
		success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = store.AddEndpoint(client.ID, endpoint2)
		if err != nil {
			t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
		}
		endpoints, err = store.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
		}
		if len(endpoints) != 2 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
		}
		sortedEnd := sortedEndpoints(endpoints)
		sort.Sort(sortedEnd)
		endpoints = []Endpoint(sortedEnd)
		success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = store.RemoveEndpoint(client.ID, endpoint1.ID)
		if err != nil {
			t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
		}
		endpoints, err = store.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error listing endpoints in %T: %s", store, err)
		}
		if len(endpoints) != 1 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
		}
		success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = store.RemoveEndpoint(client.ID, endpoint2.ID)
		if err != nil {
			t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
		}
		endpoints, err = store.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error listing endpoints in %T: %s", store, err)
		}
		if len(endpoints) != 0 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
		}
	}
}

func TestClientUpdates(t *testing.T) {
	t.Parallel()
	variations := 1 << 5
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	for i := 0; i < variations; i++ {
		var secret, name, logo, website string
		change := ClientChange{}
		expectation := client
		result := client
		if i&clientChangeSecret != 0 {
			secret = fmt.Sprintf("secret-%d", i)
			change.Secret = &secret
			expectation.Secret = secret
		}
		if i&clientChangeOwnerID != 0 {
			change.OwnerID = uuid.NewID()
			expectation.OwnerID = change.OwnerID
		}
		if i&clientChangeName != 0 {
			name = fmt.Sprintf("name-%d", i)
			change.Name = &name
			expectation.Name = name
		}
		if i&clientChangeLogo != 0 {
			logo = fmt.Sprintf("logo-%d", i)
			change.Logo = &logo
			expectation.Logo = logo
		}
		if i&clientChangeWebsite != 0 {
			website = fmt.Sprintf("website-%d", i)
			change.Website = &website
			expectation.Website = website
		}
		result.ApplyChange(change)
		match, field, expected, got := compareClients(expectation, result)
		if !match {
			t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
		}
		for _, store := range clientStores {
			err := store.SaveClient(client)
			if err != nil {
				t.Fatalf("Error saving client in %T: %s", store, err)
			}
			err = store.UpdateClient(client.ID, change)
			if err != nil {
				t.Fatalf("Error updating client in %T: %s", store, err)
			}
			retrieved, err := store.GetClient(client.ID)
			if err != nil {
				t.Fatalf("Error getting profile from %T: %s", store, err)
			}
			match, field, expected, got = compareClients(expectation, retrieved)
			if !match {
				t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
			}
			err = store.DeleteClient(client.ID)
			if err != nil {
				t.Fatalf("Error deleting client from %T: %s", store, err)
			}
			err = store.UpdateClient(client.ID, change)
			if err != ErrClientNotFound {
				t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store)
			}
		}
	}
}

func TestClientEndpointChecks(t *testing.T) {
	t.Parallel()
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	uri1, _ := url.Parse("https://www.example.com/first")
	uri2, _ := url.Parse("https://www.example.com/my/full/path")
	endpoint1 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now(),
		URI:      *uri1,
	}
	endpoint2 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now(),
		URI:      *uri2,
	}
	candidates := map[string]bool{
		"https://www.example.com/":                 false,
		"https://www.example.com/first":            true,
		"https://www.example.com/first/extra/path": true,
		"https://www.example.com/my":               false,
		"https://www.example.com/my/full/path":     true,
	}
	for _, store := range clientStores {
		err := store.SaveClient(client)
		if err != nil {
			t.Fatalf("Error saving client in %T: %s", store, err)
		}
		err = store.AddEndpoint(client.ID, endpoint1)
		if err != nil {
			t.Fatalf("Error saving endpoint in %T: %s", store, err)
		}
		err = store.AddEndpoint(client.ID, endpoint2)
		if err != nil {
			t.Fatalf("Error saving endpoint in %T: %s", store, err)
		}
		for candidate, expectation := range candidates {
			result, err := store.CheckEndpoint(client.ID, candidate)
			if err != nil {
				t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
			}
			if result != expectation {
				expectStr := "no"
				resultStr := "a"
				if expectation {
					expectStr = "a"
					resultStr = "no"
				}
				t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
			}
		}
	}
}
