// Copyright 2015 flannel authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package etcdv2

import (
	"fmt"
	"testing"

	etcd "github.com/coreos/etcd/client"
	"golang.org/x/net/context"
)

func expectSuccess(t *testing.T, r *etcd.Response, err error, expected *etcd.Response, expectedValue string) {
	if err != nil {
		t.Fatalf("Failed to get etcd keys: %v", err)
	}
	if r == nil {
		t.Fatal("etcd response was nil")
	}
	if r.Action != expected.Action {
		t.Fatalf("Unexpected action %s (expected %s)", r.Action, expected.Action)
	}
	if r.Index < expected.Index {
		t.Fatalf("Unexpected response index %v (expected >= %v)", r.Index, expected.Index)
	}
	if expected.Node != nil {
		if expected.Node.Key != r.Node.Key {
			t.Fatalf("Unexpected response node %s key %s (expected %s)", r.Node.Key, r.Node.Key, expected.Node.Key)
		}
		if expected.Node.Value != r.Node.Value {
			t.Fatalf("Unexpected response node %s value %s (expected %s)", r.Node.Key, r.Node.Value, expected.Node.Value)
		}
		if expected.Node.Dir != r.Node.Dir {
			t.Fatalf("Unexpected response node %s dir %v (expected %v)", r.Node.Key, r.Node.Dir, expected.Node.Dir)
		}
		if expected.Node.CreatedIndex != r.Node.CreatedIndex {
			t.Fatalf("Unexpected response node %s CreatedIndex %v (expected %v)", r.Node.Key, r.Node.CreatedIndex, expected.Node.CreatedIndex)
		}
		if expected.Node.ModifiedIndex > r.Node.ModifiedIndex {
			t.Fatalf("Unexpected response node %s ModifiedIndex %v (expected %v)", r.Node.Key, r.Node.ModifiedIndex, expected.Node.ModifiedIndex)
		}
	}
	if expectedValue != "" {
		if r.Node == nil {
			t.Fatalf("Unexpected empty response node")
		}
		if r.Node.Value != expectedValue {
			t.Fatalf("Unexpected response node %s value %s (expected %s)", r.Node.Key, r.Node.Value, expectedValue)
		}
	}
}

func watchMockEtcd(ctx context.Context, watcher etcd.Watcher, result chan error) {
	type evt struct {
		key      string
		event    string
		received bool
	}

	expected := []evt{
		{"/coreos.com/network/foobar/config", "create", false},
		{"/coreos.com/network/blah/config", "create", false},
		{"/coreos.com/network/blah/config", "update", false},
		{"/coreos.com/network/foobar/config", "delete", false},
		{"/coreos.com/network/foobar", "delete", false},
	}

	// Wait for delete events on /coreos.com/network/foobar and its
	// 'config' child, and for the update event on
	// /coreos.com/network/foobar (for 'config' delete) and on
	// /coreos.com/network (for 'foobar' delete)
	numEvents := 0
	for {
		resp, err := watcher.Next(ctx)

		if err != nil {
			if err == context.Canceled {
				break
			}
			result <- fmt.Errorf("Unexpected error watching for event: %v", err)
			break
		}
		if resp.Node == nil {
			result <- fmt.Errorf("Unexpected empty node watching for event")
			break
		}
		found := false
		for i, e := range expected {
			if e.key == resp.Node.Key && e.event == resp.Action {
				if expected[i].received != true {
					expected[i].received = true
					found = true
					numEvents += 1
				}
				break
			}
		}
		if found == false {
			result <- fmt.Errorf("Received unexpected or already received event %v", resp)
			break
		}

		if numEvents == len(expected) {
			result <- nil
			break
		}
	}
}

func TestMockEtcd(t *testing.T) {
	m := newMockEtcd()

	ctx, _ := context.WithCancel(context.Background())

	// Sanity tests for our mock etcd

	// Ensure no entries yet exist
	opts := &etcd.GetOptions{Recursive: true, Quorum: true}
	r, err := m.Get(ctx, "/", opts)
	e := &etcd.Response{Action: "get", Index: 1000, Node: m.nodes["/"]}
	expectSuccess(t, r, err, e, "")

	// Create base test keys
	sopts := &etcd.SetOptions{Dir: true}
	r, err = m.Set(ctx, "/coreos.com/network", "", sopts)
	e = &etcd.Response{Action: "create", Index: 1002}
	expectSuccess(t, r, err, e, "")

	wopts := &etcd.WatcherOptions{AfterIndex: m.index, Recursive: true}
	watcher := m.Watcher("/coreos.com/network", wopts)

	result := make(chan error, 1)
	go watchMockEtcd(ctx, watcher, result)

	// Populate etcd with some keys
	netKey1 := "/coreos.com/network/foobar/config"
	netValue := "{ \"Network\": \"10.1.0.0/16\", \"Backend\": { \"Type\": \"host-gw\" } }"
	r, err = m.Create(ctx, netKey1, netValue)
	e = &etcd.Response{Action: "create", Index: 1004}
	expectSuccess(t, r, err, e, netValue)

	netKey2 := "/coreos.com/network/blah/config"
	netValue = "{ \"Network\": \"10.1.1.0/16\", \"Backend\": { \"Type\": \"host-gw\" } }"
	r, err = m.Create(ctx, netKey2, netValue)
	e = &etcd.Response{Action: "create", Index: 1006}
	expectSuccess(t, r, err, e, netValue)

	// Get it again
	expectedNode := r.Node
	opts = &etcd.GetOptions{Recursive: false, Quorum: true}
	r, err = m.Get(ctx, netKey2, opts)
	e = &etcd.Response{Action: "get", Index: m.index, Node: expectedNode}
	expectSuccess(t, r, err, e, netValue)

	// Update it
	netValue = "ReallyCoolValue"
	r, err = m.Update(ctx, netKey2, netValue)
	e = &etcd.Response{Action: "update", Index: m.index}
	expectSuccess(t, r, err, e, netValue)

	// Get it again
	opts = &etcd.GetOptions{Recursive: false, Quorum: true}
	r, err = m.Get(ctx, netKey2, opts)
	e = &etcd.Response{Action: "get", Index: m.index}
	expectSuccess(t, r, err, e, netValue)

	// test directory listing
	opts = &etcd.GetOptions{Recursive: true, Quorum: true}
	r, err = m.Get(ctx, "/coreos.com/network/", opts)
	e = &etcd.Response{Action: "get", Index: 1007}
	expectSuccess(t, r, err, e, "")

	if len(r.Node.Nodes) != 2 {
		t.Fatalf("Unexpected %d children in response (expected 2)", len(r.Node.Nodes))
	}
	node1Found := false
	node2Found := false
	for _, child := range r.Node.Nodes {
		if child.Dir != true {
			t.Fatalf("Unexpected non-directory child %s", child.Key)
		}
		if child.Key == "/coreos.com/network/foobar" {
			node1Found = true
		} else if child.Key == "/coreos.com/network/blah" {
			node2Found = true
		} else {
			t.Fatalf("Unexpected child %s found", child.Key)
		}
		if len(child.Nodes) != 1 {
			t.Fatalf("Unexpected %d children in response (expected 2)", len(r.Node.Nodes))
		}
	}
	if node1Found == false || node2Found == false {
		t.Fatalf("Failed to find expected children")
	}

	// Delete a key
	dopts := &etcd.DeleteOptions{Recursive: true, Dir: false}
	r, err = m.Delete(ctx, "/coreos.com/network/foobar", dopts)
	if err == nil {
		t.Fatalf("Unexpected success deleting a directory")
	}

	// Delete a key
	dopts = &etcd.DeleteOptions{Recursive: true, Dir: true}
	r, err = m.Delete(ctx, "/coreos.com/network/foobar", dopts)
	e = &etcd.Response{Action: "delete", Index: 1010}
	expectSuccess(t, r, err, e, "")

	// Get it again; should fail
	opts = &etcd.GetOptions{Recursive: false, Quorum: true}
	r, err = m.Get(ctx, netKey1, opts)
	if err == nil {
		t.Fatalf("Get of %s after delete unexpectedly succeeded", netKey1)
	}
	if r != nil {
		t.Fatalf("Unexpected non-nil response to get after delete %v", r)
	}

	// Check errors from watch goroutine
	watchResult := <-result
	if watchResult != nil {
		t.Fatalf("Error watching keys: %v", watchResult)
	}
}
