package graphdb

import (
	"database/sql"
	"fmt"
	"os"
	"path"
	"runtime"
	"strconv"
	"testing"

	_ "github.com/mattn/go-sqlite3"
)

func newTestDb(t *testing.T) (*Database, string) {
	p := path.Join(os.TempDir(), "sqlite.db")
	conn, err := sql.Open("sqlite3", p)
	db, err := NewDatabase(conn)
	if err != nil {
		t.Fatal(err)
	}
	return db, p
}

func destroyTestDb(dbPath string) {
	os.Remove(dbPath)
}

func TestNewDatabase(t *testing.T) {
	db, dbpath := newTestDb(t)
	if db == nil {
		t.Fatal("Database should not be nil")
	}
	db.Close()
	defer destroyTestDb(dbpath)
}

func TestCreateRootEntity(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)
	root := db.RootEntity()
	if root == nil {
		t.Fatal("Root entity should not be nil")
	}
}

func TestGetRootEntity(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	e := db.Get("/")
	if e == nil {
		t.Fatal("Entity should not be nil")
	}
	if e.ID() != "0" {
		t.Fatalf("Entity id should be 0, got %s", e.ID())
	}
}

func TestSetEntityWithDifferentName(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/test", "1")
	if _, err := db.Set("/other", "1"); err != nil {
		t.Fatal(err)
	}
}

func TestSetDuplicateEntity(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	if _, err := db.Set("/foo", "42"); err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/foo", "43"); err == nil {
		t.Fatalf("Creating an entry with a duplicate path did not cause an error")
	}
}

func TestCreateChild(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	child, err := db.Set("/db", "1")
	if err != nil {
		t.Fatal(err)
	}
	if child == nil {
		t.Fatal("Child should not be nil")
	}
	if child.ID() != "1" {
		t.Fail()
	}
}

func TestParents(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	for i := 1; i < 6; i++ {
		a := strconv.Itoa(i)
		if _, err := db.Set("/"+a, a); err != nil {
			t.Fatal(err)
		}
	}

	for i := 6; i < 11; i++ {
		a := strconv.Itoa(i)
		p := strconv.Itoa(i - 5)

		key := fmt.Sprintf("/%s/%s", p, a)

		if _, err := db.Set(key, a); err != nil {
			t.Fatal(err)
		}

		parents, err := db.Parents(key)
		if err != nil {
			t.Fatal(err)
		}

		if len(parents) != 1 {
			t.Fatalf("Expected 1 entry for %s got %d", key, len(parents))
		}

		if parents[0] != p {
			t.Fatalf("ID %s received, %s expected", parents[0], p)
		}
	}
}

func TestChildren(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	str := "/"
	for i := 1; i < 6; i++ {
		a := strconv.Itoa(i)
		if _, err := db.Set(str+a, a); err != nil {
			t.Fatal(err)
		}

		str = str + a + "/"
	}

	str = "/"
	for i := 10; i < 30; i++ { // 20 entities
		a := strconv.Itoa(i)
		if _, err := db.Set(str+a, a); err != nil {
			t.Fatal(err)
		}

		str = str + a + "/"
	}
	entries, err := db.Children("/", 5)
	if err != nil {
		t.Fatal(err)
	}

	if len(entries) != 11 {
		t.Fatalf("Expect 11 entries for / got %d", len(entries))
	}

	entries, err = db.Children("/", 20)
	if err != nil {
		t.Fatal(err)
	}

	if len(entries) != 25 {
		t.Fatalf("Expect 25 entries for / got %d", len(entries))
	}
}

func TestListAllRootChildren(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}

	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	for i := 1; i < 6; i++ {
		a := strconv.Itoa(i)
		if _, err := db.Set("/"+a, a); err != nil {
			t.Fatal(err)
		}
	}
	entries := db.List("/", -1)
	if len(entries) != 5 {
		t.Fatalf("Expect 5 entries for / got %d", len(entries))
	}
}

func TestListAllSubChildren(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	_, err := db.Set("/webapp", "1")
	if err != nil {
		t.Fatal(err)
	}
	child2, err := db.Set("/db", "2")
	if err != nil {
		t.Fatal(err)
	}
	child4, err := db.Set("/logs", "4")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/db/logs", child4.ID()); err != nil {
		t.Fatal(err)
	}

	child3, err := db.Set("/sentry", "3")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
		t.Fatal(err)
	}

	entries := db.List("/webapp", 1)
	if len(entries) != 3 {
		t.Fatalf("Expect 3 entries for / got %d", len(entries))
	}

	entries = db.List("/webapp", 0)
	if len(entries) != 2 {
		t.Fatalf("Expect 2 entries for / got %d", len(entries))
	}
}

func TestAddSelfAsChild(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	child, err := db.Set("/test", "1")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/test/other", child.ID()); err == nil {
		t.Fatal("Error should not be nil")
	}
}

func TestAddChildToNonExistentRoot(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	if _, err := db.Set("/myapp", "1"); err != nil {
		t.Fatal(err)
	}

	if _, err := db.Set("/myapp/proxy/db", "2"); err == nil {
		t.Fatal("Error should not be nil")
	}
}

func TestWalkAll(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)
	_, err := db.Set("/webapp", "1")
	if err != nil {
		t.Fatal(err)
	}
	child2, err := db.Set("/db", "2")
	if err != nil {
		t.Fatal(err)
	}
	child4, err := db.Set("/db/logs", "4")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/logs", child4.ID()); err != nil {
		t.Fatal(err)
	}

	child3, err := db.Set("/sentry", "3")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
		t.Fatal(err)
	}

	child5, err := db.Set("/gograph", "5")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
		t.Fatal(err)
	}

	if err := db.Walk("/", func(p string, e *Entity) error {
		t.Logf("Path: %s Entity: %s", p, e.ID())
		return nil
	}, -1); err != nil {
		t.Fatal(err)
	}
}

func TestGetEntityByPath(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)
	_, err := db.Set("/webapp", "1")
	if err != nil {
		t.Fatal(err)
	}
	child2, err := db.Set("/db", "2")
	if err != nil {
		t.Fatal(err)
	}
	child4, err := db.Set("/logs", "4")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/db/logs", child4.ID()); err != nil {
		t.Fatal(err)
	}

	child3, err := db.Set("/sentry", "3")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
		t.Fatal(err)
	}

	child5, err := db.Set("/gograph", "5")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
		t.Fatal(err)
	}

	entity := db.Get("/webapp/db/logs")
	if entity == nil {
		t.Fatal("Entity should not be nil")
	}
	if entity.ID() != "4" {
		t.Fatalf("Expected to get entity with id 4, got %s", entity.ID())
	}
}

func TestEnitiesPaths(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)
	_, err := db.Set("/webapp", "1")
	if err != nil {
		t.Fatal(err)
	}
	child2, err := db.Set("/db", "2")
	if err != nil {
		t.Fatal(err)
	}
	child4, err := db.Set("/logs", "4")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/db/logs", child4.ID()); err != nil {
		t.Fatal(err)
	}

	child3, err := db.Set("/sentry", "3")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
		t.Fatal(err)
	}

	child5, err := db.Set("/gograph", "5")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
		t.Fatal(err)
	}

	out := db.List("/", -1)
	for _, p := range out.Paths() {
		t.Log(p)
	}
}

func TestDeleteRootEntity(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	if err := db.Delete("/"); err == nil {
		t.Fatal("Error should not be nil")
	}
}

func TestDeleteEntity(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)
	_, err := db.Set("/webapp", "1")
	if err != nil {
		t.Fatal(err)
	}
	child2, err := db.Set("/db", "2")
	if err != nil {
		t.Fatal(err)
	}
	child4, err := db.Set("/logs", "4")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/db/logs", child4.ID()); err != nil {
		t.Fatal(err)
	}

	child3, err := db.Set("/sentry", "3")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
		t.Fatal(err)
	}

	child5, err := db.Set("/gograph", "5")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
		t.Fatal(err)
	}

	if err := db.Delete("/webapp/sentry"); err != nil {
		t.Fatal(err)
	}
	entity := db.Get("/webapp/sentry")
	if entity != nil {
		t.Fatal("Entity /webapp/sentry should be nil")
	}
}

func TestCountRefs(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/webapp", "1")

	if db.Refs("1") != 1 {
		t.Fatal("Expect reference count to be 1")
	}

	db.Set("/db", "2")
	db.Set("/webapp/db", "2")
	if db.Refs("2") != 2 {
		t.Fatal("Expect reference count to be 2")
	}
}

func TestPurgeId(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}

	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/webapp", "1")

	if c := db.Refs("1"); c != 1 {
		t.Fatalf("Expect reference count to be 1, got %d", c)
	}

	db.Set("/db", "2")
	db.Set("/webapp/db", "2")

	count, err := db.Purge("2")
	if err != nil {
		t.Fatal(err)
	}
	if count != 2 {
		t.Fatalf("Expected 2 references to be removed, got %d", count)
	}
}

// Regression test https://github.com/docker/docker/issues/12334
func TestPurgeIdRefPaths(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/webapp", "1")
	db.Set("/db", "2")

	db.Set("/db/webapp", "1")

	if c := db.Refs("1"); c != 2 {
		t.Fatalf("Expected 2 reference for webapp, got %d", c)
	}
	if c := db.Refs("2"); c != 1 {
		t.Fatalf("Expected 1 reference for db, got %d", c)
	}

	if rp := db.RefPaths("2"); len(rp) != 1 {
		t.Fatalf("Expected 1 reference path for db, got %d", len(rp))
	}

	count, err := db.Purge("2")
	if err != nil {
		t.Fatal(err)
	}

	if count != 2 {
		t.Fatalf("Expected 2 rows to be removed, got %d", count)
	}

	if c := db.Refs("2"); c != 0 {
		t.Fatalf("Expected 0 reference for db, got %d", c)
	}
	if c := db.Refs("1"); c != 1 {
		t.Fatalf("Expected 1 reference for webapp, got %d", c)
	}
}

func TestRename(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/webapp", "1")

	if db.Refs("1") != 1 {
		t.Fatal("Expect reference count to be 1")
	}

	db.Set("/db", "2")
	db.Set("/webapp/db", "2")

	if db.Get("/webapp/db") == nil {
		t.Fatal("Cannot find entity at path /webapp/db")
	}

	if err := db.Rename("/webapp/db", "/webapp/newdb"); err != nil {
		t.Fatal(err)
	}
	if db.Get("/webapp/db") != nil {
		t.Fatal("Entity should not exist at /webapp/db")
	}
	if db.Get("/webapp/newdb") == nil {
		t.Fatal("Cannot find entity at path /webapp/newdb")
	}

}

func TestCreateMultipleNames(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}

	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/db", "1")
	if _, err := db.Set("/myapp", "1"); err != nil {
		t.Fatal(err)
	}

	db.Walk("/", func(p string, e *Entity) error {
		t.Logf("%s\n", p)
		return nil
	}, -1)
}

func TestRefPaths(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/webapp", "1")

	db.Set("/db", "2")
	db.Set("/webapp/db", "2")

	refs := db.RefPaths("2")
	if len(refs) != 2 {
		t.Fatalf("Expected reference count to be 2, got %d", len(refs))
	}
}

func TestExistsTrue(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/testing", "1")

	if !db.Exists("/testing") {
		t.Fatalf("/tesing should exist")
	}
}

func TestExistsFalse(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/toerhe", "1")

	if db.Exists("/testing") {
		t.Fatalf("/tesing should not exist")
	}

}

func TestGetNameWithTrailingSlash(t *testing.T) {
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	db.Set("/todo", "1")

	e := db.Get("/todo/")
	if e == nil {
		t.Fatalf("Entity should not be nil")
	}
}

func TestConcurrentWrites(t *testing.T) {
	// TODO Windows: Port this test
	if runtime.GOOS == "windows" {
		t.Skip("Needs porting to Windows")
	}
	db, dbpath := newTestDb(t)
	defer destroyTestDb(dbpath)

	errs := make(chan error, 2)

	save := func(name string, id string) {
		if _, err := db.Set(fmt.Sprintf("/%s", name), id); err != nil {
			errs <- err
		}
		errs <- nil
	}
	purge := func(id string) {
		if _, err := db.Purge(id); err != nil {
			errs <- err
		}
		errs <- nil
	}

	save("/1", "1")

	go purge("1")
	go save("/2", "2")

	any := false
	for i := 0; i < 2; i++ {
		if err := <-errs; err != nil {
			any = true
			t.Log(err)
		}
	}
	if any {
		t.Fail()
	}
}
