package arangodb

import (
	"context"
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/arangodb/go-driver"
	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/util"
)

func newTestStore(t *testing.T) *ArangodbStore {
	t.Helper()

	if os.Getenv("RUN_ARANGODB_TESTS") != "1" {
		t.Skip("arangodb tests are disabled. Start an arangodb server and set RUN_ARANGODB_TESTS=1 to enable, ARANGODB_ADDR defaults to http://127.0.0.1:8529.")
	}

	addr := os.Getenv("ARANGODB_ADDR")
	if addr == "" {
		addr = "http://127.0.0.1:8529"
	}
	user := os.Getenv("ARANGODB_USER")
	if user == "" {
		user = "root"
	}

	store := &ArangodbStore{databaseName: fmt.Sprintf("seaweed_test_%d", time.Now().UnixNano())}
	store.buckets = make(map[string]driver.Collection, 3)
	if err := store.connection([]string{addr}, user, os.Getenv("ARANGODB_PASSWORD"), true); err != nil {
		t.Fatalf("connect to arangodb at %s: %v", addr, err)
	}
	t.Cleanup(func() {
		if err := store.database.Remove(context.Background()); err != nil {
			t.Errorf("drop test database: %v", err)
		}
	})
	return store
}

func insertTestEntry(t *testing.T, store *ArangodbStore, path string) {
	t.Helper()
	if err := store.InsertEntry(context.Background(), &filer.Entry{FullPath: util.FullPath(path)}); err != nil {
		t.Fatalf("insert %s: %v", path, err)
	}
}

func countDocuments(t *testing.T, store *ArangodbStore, bucket string) int64 {
	t.Helper()
	collection, err := store.ensureBucket(context.Background(), bucket)
	if err != nil {
		t.Fatalf("ensure bucket %s: %v", bucket, err)
	}
	count, err := collection.Count(context.Background())
	if err != nil {
		t.Fatalf("count %s: %v", bucket, err)
	}
	return count
}

func listNames(t *testing.T, store *ArangodbStore, dir util.FullPath, prefix string) []string {
	t.Helper()
	var names []string
	_, err := store.ListDirectoryPrefixedEntries(context.Background(), dir, "", true, 100, prefix,
		func(entry *filer.Entry) (bool, error) {
			names = append(names, entry.Name())
			return true, nil
		})
	if err != nil {
		t.Fatalf("list %s prefix %q: %v", dir, prefix, err)
	}
	return names
}

// AQL operators in a list prefix must be matched literally, never executed.
func TestListDirectoryPrefixedEntriesAqlInjection(t *testing.T) {
	store := newTestStore(t)

	insertTestEntry(t, store, "/buckets/victim/secret")
	insertTestEntry(t, store, "/buckets/tenant/regular")

	injection := "x\" && false || (FOR q IN `victim` LIMIT 1 UPDATE q WITH {injected:\"yes\"} IN `victim` RETURN \"\") || \""
	if names := listNames(t, store, util.FullPath("/buckets/tenant"), injection); len(names) != 0 {
		t.Errorf("expected no match for injected prefix, got %v", names)
	}

	var victim Model
	collection, err := store.ensureBucket(context.Background(), "victim")
	if err != nil {
		t.Fatalf("ensure bucket victim: %v", err)
	}
	if _, err := collection.ReadDocument(context.Background(), hashString("/buckets/victim/secret"), &victim); err != nil {
		t.Fatalf("read victim document: %v", err)
	}
	if victim.Name != "secret" {
		t.Errorf("victim document was modified across buckets: %+v", victim)
	}
}

// A prefix full of AQL operators must not be evaluated as a query.
func TestListDirectoryPrefixedEntriesPrefixNotEvaluated(t *testing.T) {
	store := newTestStore(t)

	insertTestEntry(t, store, "/buckets/tenant/a")
	insertTestEntry(t, store, "/buckets/tenant/b")
	insertTestEntry(t, store, "/buckets/tenant/c")

	start := time.Now()
	if names := listNames(t, store, util.FullPath("/buckets/tenant"), "x\" && false || TO_STRING(SLEEP(1)) && false || \""); len(names) != 0 {
		t.Errorf("expected no match for injected prefix, got %v", names)
	}
	if elapsed := time.Since(start); elapsed > time.Second {
		t.Errorf("SLEEP() in prefix was executed, listing took %v", elapsed)
	}
}

// Quotes and newlines are ordinary object-name characters and must round-trip.
func TestListDirectoryPrefixedEntriesQuotedNames(t *testing.T) {
	store := newTestStore(t)

	names := []string{"plain", "quo\"te", "new\nline", "back\\slash"}
	for _, name := range names {
		insertTestEntry(t, store, "/buckets/tenant/"+name)
	}

	for _, name := range names {
		if got := listNames(t, store, util.FullPath("/buckets/tenant"), name); len(got) != 1 || got[0] != name {
			t.Errorf("prefix %q listed %v, want exactly [%q]", name, got, name)
		}
	}

	if got := listNames(t, store, util.FullPath("/buckets/tenant"), ""); len(got) != len(names) {
		t.Errorf("empty prefix listed %v, want %d entries", got, len(names))
	}
}

// A directory name full of AQL operators must only drop that directory's own children.
func TestDeleteFolderChildrenAqlInjection(t *testing.T) {
	store := newTestStore(t)

	for i := 0; i < 5; i++ {
		insertTestEntry(t, store, fmt.Sprintf("/buckets/tenant/keep%d", i))
	}
	injection := "x\" || true || \""
	insertTestEntry(t, store, "/buckets/tenant/"+injection+"/child")

	if err := store.DeleteFolderChildren(context.Background(), util.FullPath("/buckets/tenant/"+injection)); err != nil {
		t.Fatalf("delete folder children: %v", err)
	}

	if got := countDocuments(t, store, "tenant"); got != 5 {
		t.Errorf("tenant collection holds %d documents, want the 5 unrelated ones", got)
	}
}

func TestDeleteFolderChildrenRemovesSubtree(t *testing.T) {
	store := newTestStore(t)

	insertTestEntry(t, store, "/buckets/tenant/dir/file")
	insertTestEntry(t, store, "/buckets/tenant/dir/sub/deep")
	insertTestEntry(t, store, "/buckets/tenant/dirX/sibling")
	insertTestEntry(t, store, "/buckets/tenant/other")

	if err := store.DeleteFolderChildren(context.Background(), util.FullPath("/buckets/tenant/dir")); err != nil {
		t.Fatalf("delete folder children: %v", err)
	}

	if got := countDocuments(t, store, "tenant"); got != 2 {
		t.Errorf("tenant collection holds %d documents, want /buckets/tenant/dirX/sibling and /buckets/tenant/other", got)
	}
}

// startFileName is request-controlled too and must not break out of the filter.
func TestListDirectoryEntriesQuotedStartFileName(t *testing.T) {
	store := newTestStore(t)

	insertTestEntry(t, store, "/buckets/tenant/a")
	insertTestEntry(t, store, "/buckets/tenant/z")

	var names []string
	_, err := store.ListDirectoryEntries(context.Background(), util.FullPath("/buckets/tenant"), "z\" || true || \"", false, 100,
		func(entry *filer.Entry) (bool, error) {
			names = append(names, entry.Name())
			return true, nil
		})
	if err != nil {
		t.Fatalf("list with quoted start file: %v", err)
	}
	if len(names) != 0 {
		t.Errorf("listed %v, want nothing sorted after the literal start name", names)
	}
}
