package view

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"local/dndex/config"
	"local/dndex/storage"
	"local/dndex/storage/entity"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
)

func TestWho(t *testing.T) {
	os.Args = os.Args[:1]
	if _, ok := os.LookupEnv("DBURI"); !ok {
		f, err := ioutil.TempFile(os.TempDir(), "pattern*")
		if err != nil {
			t.Fatal(err)
		}
		f.Close()
		defer os.Remove(f.Name())
		os.Setenv("DBURI", f.Name())
	}

	t.Logf("config: %+v", config.New())

	g := storage.NewGraph()
	ones := fillDB(t, g)
	want := ones[len(ones)-1]

	handler := jsonHandler(g)

	t.Log(handler, want)

	t.Run("get no namespace is 404", func(t *testing.T) {
		iwant := want
		r := httptest.NewRequest(http.MethodGet, "/who?id="+iwant.Name, nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusNotFound {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	t.Run("get fake", func(t *testing.T) {
		iwant := want
		r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id=FAKER"+iwant.Name, nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusNotFound {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	t.Run("get real", func(t *testing.T) {
		iwant := want
		r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id="+iwant.Name, nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
		o := entity.One{}
		if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
			t.Fatal(err)
		}
		if fmt.Sprint(o) == fmt.Sprint(iwant) {
			t.Fatal(o, iwant)
		}
		if len(o.Connections) != len(iwant.Connections) {
			t.Fatal(len(o.Connections), len(iwant.Connections))
		}
		iwant.Connections = o.Connections
		iwant.Modified = 0
		o.Modified = 0
		if fmt.Sprint(o) != fmt.Sprint(iwant) {
			t.Fatalf("after resolving connections and modified, iwant and got differ: \nwant %+v\n got %+v", iwant, o)
		}
		b, _ := json.MarshalIndent(o, "", "   ")
		t.Logf("POST GET:\n%s", b)
	})

	t.Run("put fake", func(t *testing.T) {
		iwant := want
		r := httptest.NewRequest(http.MethodPut, "/who?namespace=col&id=FAKER"+iwant.Name, strings.NewReader(`{"title":"this should fail to find someone"}`))
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusNotFound {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	t.Run("put real", func(t *testing.T) {
		iwant := want
		r := httptest.NewRequest(http.MethodPut, "/who?namespace=col&id="+iwant.Name, strings.NewReader(`{"title":"this should work"}`))
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
		o := entity.One{}
		if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
			t.Fatal(err)
		}
		if len(o.Connections) != len(iwant.Connections) {
			t.Fatalf("wrong number of connections returned: want %v, got %v", len(iwant.Connections), len(o.Connections))
		}
		if o.Title != "this should work" {
			t.Fatalf("failed to PUT a new title: %+v", o)
		}
		b, _ := json.MarshalIndent(o, "", "   ")
		t.Logf("POST PUT:\n%s", b)
	})

	t.Run("post exists", func(t *testing.T) {
		iwant := want
		iwant.Name = ""
		r := httptest.NewRequest(http.MethodPost, "/who?namespace=col&id="+want.Name, strings.NewReader(`{"title":"this should fail to insert"}`))
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusConflict {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	t.Run("post real", func(t *testing.T) {
		iwant := want
		iwant.Name = ""
		b, err := json.Marshal(iwant)
		if err != nil {
			t.Fatal(err)
		}
		r := httptest.NewRequest(http.MethodPost, "/who?namespace=col&id=NEWBIE"+want.Name, bytes.NewReader(b))
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
		o := entity.One{}
		if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
			t.Fatal(err)
		}
		if len(o.Connections) != len(iwant.Connections) {
			t.Fatalf("wrong number of connections returned: want %v, got %v", len(iwant.Connections), len(o.Connections))
		}
		if o.Name != "NEWBIE"+want.Name {
			t.Fatalf("failed to POST specified name: %+v", o)
		}
		b, _ = json.MarshalIndent(o, "", "   ")
		t.Logf("POST POST:\n%s", b)
	})

	t.Run("delete real", func(t *testing.T) {
		r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=NEWBIE"+want.Name, nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
		r = httptest.NewRequest(http.MethodGet, "/who?namespace=col&id=NEWBIE"+want.Name, nil)
		w = httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusNotFound {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	t.Run("delete fake", func(t *testing.T) {
		r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=FAKER"+want.Name, nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	t.Run("patch fake", func(t *testing.T) {
		r := httptest.NewRequest(http.MethodPatch, "/who?namespace=col&id=FAKER"+want.Name, nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code == http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	want = ones[2]
	t.Run("patch real", func(t *testing.T) {
		iwant := want
		iwant.Relationship = "spawn"
		iwant.Name = "child of " + want.Name
		b, err := json.Marshal(iwant)
		if err != nil {
			t.Fatal(err)
		}
		r := httptest.NewRequest(http.MethodPatch, "/who?namespace=col&id="+want.Name, bytes.NewReader(b))
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
		got := entity.One{}
		if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
			t.Fatal(err)
		}
		if fmt.Sprint(got) == fmt.Sprint(entity.One{}) {
			t.Fatal(got)
		}
		iwant = want
		if len(got.Connections) != len(want.Connections)+1 {
			t.Fatal(len(got.Connections), len(want.Connections)+1)
		}
		got.Connections = want.Connections
		got.Modified = 0
		want.Modified = 0
		if fmt.Sprint(got) != fmt.Sprint(want) {
			t.Fatalf("without connections and modified, got != want: want \n   %+v, got \n   %+v", want, got)
		}
		t.Logf("%s", w.Body.Bytes())
	})

	t.Run("trace fake", func(t *testing.T) {
		r := httptest.NewRequest(http.MethodTrace, "/who?namespace=notcol", nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
	})

	t.Run("trace real", func(t *testing.T) {
		r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
		var v []string
		if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
			t.Fatalf("%v: %s", err, w.Body.Bytes())
		}
		if len(v) < 5 {
			t.Fatal(len(v))
		}
		t.Logf("%+v", v)
	})

	t.Run("get without id == trace real", func(t *testing.T) {
		r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		if w.Code != http.StatusOK {
			t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
		}
		var v []string
		if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
			t.Fatalf("%v: %s", err, w.Body.Bytes())
		}
		if len(v) < 5 {
			t.Fatal(len(v))
		}
		t.Logf("%+v", v)
	})

	t.Run("trace real sorted asc/desc name", func(t *testing.T) {
		for _, order := range []string{"1", "-1"} {
			r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col&sort=name&order="+order, nil)
			w := httptest.NewRecorder()
			handler.ServeHTTP(w, r)
			if w.Code != http.StatusOK {
				t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
			}
			var v []string
			if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
				t.Fatalf("%v: %s", err, w.Body.Bytes())
			}
			if len(v) < 5 {
				t.Fatal(len(v))
			}
			for i := range v {
				if i == 0 {
					continue
				}
				if (v[i] < v[i-1] && order == "1") || (v[i] > v[i-1] && order == "-1") {
					t.Fatalf("not sorted: %s: %+v", order, v)
				}
			}
		}
	})
}

func fillDB(t *testing.T, g storage.Graph) []entity.One {
	ones := make([]entity.One, 5)
	for i := range ones {
		ones[i] = randomOne()
		if i > 0 {
			ones[i].Connections[ones[i-1].Name] = entity.One{
				Name:         ones[i-1].Name,
				Relationship: ":D",
			}
		}
	}
	for i := range ones {
		if err := g.Insert(context.TODO(), "col", ones[i]); err != nil {
			t.Fatal(err)
		}
		if results, err := g.List(context.TODO(), "col", ones[i].Name); err != nil {
			t.Fatal(err)
		} else if len(results) != 1 {
			t.Fatal(len(results))
		} else if len(results[0].Connections) != len(ones[i].Connections) {
			t.Fatal(len(results[0].Connections), len(ones[i].Connections))
		} else if len(results[0].Connections) > 0 {
			for k, v := range results[0].Connections {
				if k == "" || v.Name == "" {
					t.Fatalf("name is gone: %q:%+v", k, v)
				}
			}
		}
	}
	return ones
}

func randomOne() entity.One {
	return entity.One{
		Name:        "name-" + uuid.New().String()[:5],
		Type:        "type-" + uuid.New().String()[:5],
		Title:       "titl-" + uuid.New().String()[:5],
		Text:        "text-" + uuid.New().String()[:5],
		Modified:    time.Now().UnixNano(),
		Connections: map[string]entity.One{},
	}
}

//func sortOnes(ones []entity.One, r *http.Request) []entity.One {
func TestSortOnes(t *testing.T) {
	oneA := entity.One{Name: "A", Title: "c", Modified: 2}
	oneB := entity.One{Name: "B", Title: "b", Modified: 1}
	oneC := entity.One{Name: "C", Title: "a", Modified: 3}

	cases := map[string]struct {
		ones  []entity.One
		sort  string
		order string
		test  func(entity.One, entity.One) bool
	}{
		"nothing to sort": {},
		"all the same": {
			ones: []entity.One{oneA, oneA, oneA},
			test: func(a, b entity.One) bool { return fmt.Sprint(a) == fmt.Sprint(b) },
		},
		"default: modified desc, but already ordered": {
			ones: []entity.One{oneC, oneB, oneA},
			test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
		},
		"default: modified desc": {
			ones: []entity.One{oneA, oneB, oneC},
			test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
		},
		"default=modified set=desc": {
			ones:  []entity.One{oneA, oneB, oneC},
			order: "-1",
			test:  func(a, b entity.One) bool { return a.Modified >= b.Modified },
		},
		"set=modified default=desc": {
			ones: []entity.One{oneA, oneB, oneC},
			sort: entity.Modified,
			test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
		},
		"set=modified set=asc": {
			ones:  []entity.One{oneA, oneB, oneC},
			sort:  entity.Modified,
			order: "1",
			test:  func(a, b entity.One) bool { return a.Modified <= b.Modified },
		},
		"set=title set=desc": {
			ones:  []entity.One{oneA, oneB, oneC},
			sort:  entity.Title,
			order: "-1",
			test:  func(a, b entity.One) bool { return a.Title >= b.Title },
		},
		"set=title set=asc": {
			ones:  []entity.One{oneA, oneB, oneC},
			sort:  entity.Title,
			order: "1",
			test:  func(a, b entity.One) bool { return a.Title <= b.Title },
		},
		"set=name set=desc": {
			ones:  []entity.One{oneA, oneB, oneC},
			sort:  entity.Name,
			order: "-1",
			test:  func(a, b entity.One) bool { return a.Name >= b.Name },
		},
		"set=name set=asc": {
			ones:  []entity.One{oneA, oneB, oneC},
			sort:  entity.Name,
			order: "1",
			test:  func(a, b entity.One) bool { return a.Name <= b.Name },
		},
	}

	for name, d := range cases {
		c := d
		t.Run(name, func(t *testing.T) {
			q := url.Values{}
			q.Set("sort", c.sort)
			q.Set("order", c.order)
			url := url.URL{
				Path:     "/",
				RawQuery: q.Encode(),
			}
			r := httptest.NewRequest("GET", url.String(), nil)
			ones := sortOnes(c.ones, r)
			for i := range ones {
				if i == 0 {
					continue
				}
				if ok := c.test(ones[i-1], ones[i]); !ok {
					t.Fatal(ok, ones[i-1], ones[i])
				}
			}
		})
	}
}
