// Copyright © 2016 Abcum Ltd
//
// 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 db

import (
	"context"
	"sync"
	"testing"

	"github.com/abcum/surreal/util/data"
	. "github.com/smartystreets/goconvey/convey"
)

type stringer struct{}

func (this stringer) String() string {
	return "test"
}

func TestMutex(t *testing.T) {

	var n = 10

	Convey("Context diving works correctly", t, func() {

		ctx := context.Background()

		So(vers(ctx), ShouldEqual, 0)

		for i := vers(ctx); i <= maxRecursiveQueries; i++ {
			So(func() { ctx = dive(ctx) }, ShouldNotPanic)
			So(vers(ctx), ShouldEqual, i+1)
		}

		So(func() { dive(ctx) }, ShouldPanicWith, errRecursiveOverload)

	})

	Convey("Allow basic mutex", t, func() {

		m := new(mutex)
		ctx := context.Background()

		m.Lock(ctx, new(stringer))
		m.Unlock(ctx, new(stringer))

	})

	Convey("Allow concurrent mutex", t, func() {

		m := new(mutex)
		wg := new(sync.WaitGroup)
		ctx := context.Background()

		wg.Add(n)

		for i := 0; i < n; i++ {
			go func() {
				defer wg.Done()
				m.Lock(ctx, new(stringer))
				m.Unlock(ctx, new(stringer))
			}()
		}

		wg.Wait()

		So(nil, ShouldBeNil)

	})

	Convey("Allow fixed-level mutex", t, func() {

		m := new(mutex)
		ctx := context.Background()

		for i := 0; i < n; i++ {
			ctx = dive(ctx)
			So(func() { m.Lock(ctx, new(stringer)) }, ShouldNotPanic)
			So(func() { m.Unlock(ctx, new(stringer)) }, ShouldNotPanic)
		}

		So(nil, ShouldBeNil)

	})

	Convey("Prevent nested-recursive mutex", t, func() {

		m := new(mutex)
		ctx := context.Background()

		m.Lock(ctx, new(stringer))
		ctx = dive(ctx)
		So(func() { m.Lock(ctx, new(stringer)) }, ShouldPanic)
		So(func() { m.Unlock(ctx, new(stringer)) }, ShouldNotPanic)
		So(func() { m.Unlock(ctx, new(stringer)) }, ShouldNotPanic)

		So(nil, ShouldBeNil)

	})

	Convey("Ensure document locking when multiple events attempt to write to the same document", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		DEFINE EVENT created ON person WHEN $method = "CREATE" THEN (UPDATE $after.fk SET fks += $this);
		DEFINE EVENT deleted ON person WHEN $method = "DELETE" THEN (UPDATE $before.fk SET fks -= $this);
		UPDATE |person:1..100| SET fk = other:test;
		SELECT * FROM other;
		DELETE FROM person;
		SELECT * FROM other;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 7)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[2].Status, ShouldEqual, "OK")
		So(res[3].Status, ShouldEqual, "OK")
		So(res[3].Result, ShouldHaveLength, 100)
		So(res[4].Status, ShouldEqual, "OK")
		So(res[4].Result, ShouldHaveLength, 1)
		So(data.Consume(res[4].Result[0]).Get("fks").Data(), ShouldHaveLength, 100)
		So(res[5].Status, ShouldEqual, "OK")
		So(res[5].Result, ShouldHaveLength, 0)
		So(res[6].Status, ShouldEqual, "OK")
		So(res[6].Result, ShouldHaveLength, 1)
		So(data.Consume(res[6].Result[0]).Get("fks").Data(), ShouldHaveLength, 0)

	})

	Convey("Ability to select the same document in a SELECT subquery", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		CREATE person:test;
		SELECT * FROM (SELECT * FROM (SELECT * FROM person));
		SELECT * FROM person;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 4)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[1].Result, ShouldHaveLength, 1)
		So(res[2].Status, ShouldEqual, "OK")
		So(res[3].Status, ShouldEqual, "OK")
		So(res[3].Result, ShouldHaveLength, 1)

	})

	Convey("Ability to update the same document in a SELECT subquery", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		CREATE person:test;
		SELECT * FROM (UPDATE person SET test=true);
		SELECT * FROM person;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 4)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[1].Result, ShouldHaveLength, 1)
		So(res[2].Status, ShouldEqual, "OK")
		So(data.Consume(res[2].Result[0]).Get("temp").Data(), ShouldBeNil)
		So(res[3].Status, ShouldEqual, "OK")
		So(res[3].Result, ShouldHaveLength, 1)
		So(data.Consume(res[3].Result[0]).Get("temp").Data(), ShouldBeNil)
		So(data.Consume(res[3].Result[0]).Get("test").Data(), ShouldEqual, true)

	})

	Convey("Ability to update the same document in a SELECT subquery", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		CREATE person:test;
		SELECT *, (UPDATE $parent.id SET test=true) AS test FROM person;
		SELECT * FROM person;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 4)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[1].Result, ShouldHaveLength, 1)
		So(res[2].Status, ShouldEqual, "OK")
		So(res[2].Result, ShouldHaveLength, 1)
		So(res[3].Status, ShouldEqual, "OK")
		So(res[3].Result, ShouldHaveLength, 1)
		So(data.Consume(res[3].Result[0]).Get("temp").Data(), ShouldBeNil)
		So(data.Consume(res[3].Result[0]).Get("test").Data(), ShouldEqual, true)

	})

	Convey("Inability to update the same document in an UPDATE subquery", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		CREATE person:test;
		UPDATE person SET temp = (UPDATE person SET test=true);
		SELECT * FROM person;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 4)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[1].Result, ShouldHaveLength, 1)
		So(res[2].Status, ShouldEqual, "ERR")
		So(res[2].Detail, ShouldEqual, "Failed to update the same document recursively")
		So(res[3].Status, ShouldEqual, "OK")
		So(res[3].Result, ShouldHaveLength, 1)
		So(data.Consume(res[3].Result[0]).Get("test").Data(), ShouldBeNil)
		So(data.Consume(res[3].Result[0]).Get("temp").Data(), ShouldBeNil)

	})

	Convey("Ability to update the same document in an event", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		DEFINE EVENT test ON person WHEN $before.test != $after.test THEN (UPDATE $this SET temp = true);
		UPDATE person:test SET test=true;
		SELECT * FROM person;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 4)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[2].Status, ShouldEqual, "OK")
		So(data.Consume(res[2].Result[0]).Get("temp").Data(), ShouldBeNil)
		So(data.Consume(res[2].Result[0]).Get("test").Data(), ShouldEqual, true)
		So(res[3].Status, ShouldEqual, "OK")
		So(data.Consume(res[3].Result[0]).Get("temp").Data(), ShouldEqual, true)
		So(data.Consume(res[3].Result[0]).Get("test").Data(), ShouldEqual, true)

	})

	Convey("Subqueries for an event should be on the same level", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		DEFINE EVENT test ON person WHEN $method = "CREATE" THEN (CREATE tester);
		CREATE |person:100|;
		SELECT * FROM person;
		SELECT * FROM tester;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 5)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[2].Status, ShouldEqual, "OK")
		So(res[2].Result, ShouldHaveLength, 100)
		So(res[3].Status, ShouldEqual, "OK")
		So(res[3].Result, ShouldHaveLength, 100)
		So(res[4].Status, ShouldEqual, "OK")
		So(res[4].Result, ShouldHaveLength, 100)

	})

	Convey("Subqueries for an event on a different level create an infinite loop", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		DEFINE EVENT test ON person WHEN $method = "CREATE" THEN (CREATE person);
		CREATE person:test;
		SELECT * FROM person;
		SELECT * FROM tester;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 5)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[2].Status, ShouldEqual, "ERR")
		So(res[2].Detail, ShouldEqual, "Infinite loop when running recursive subqueries")
		So(res[3].Status, ShouldEqual, "OK")
		So(res[3].Result, ShouldHaveLength, 0)
		So(res[4].Status, ShouldEqual, "OK")
		So(res[4].Result, ShouldHaveLength, 0)

	})

	Convey("Subqueries for recursive events on a different level create an infinite loop", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		DEFINE EVENT test ON person WHEN $method = "UPDATE" THEN (UPDATE tester SET temp=time.now());
		DEFINE EVENT test ON tester WHEN $method = "UPDATE" THEN (UPDATE person SET temp=time.now());
		CREATE person:test, tester:test SET temp=time.now();
		UPDATE person:test SET temp=time.now();
		SELECT * FROM person;
		SELECT * FROM tester;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 7)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[2].Status, ShouldEqual, "OK")
		So(res[3].Status, ShouldEqual, "OK")
		So(res[4].Status, ShouldEqual, "ERR")
		So(res[4].Detail, ShouldEqual, "Infinite loop when running recursive subqueries")
		So(res[5].Status, ShouldEqual, "OK")
		So(res[5].Result, ShouldHaveLength, 1)
		So(res[6].Status, ShouldEqual, "OK")
		So(res[6].Result, ShouldHaveLength, 1)

	})

	Convey("Ability to define complex dependent events which should run consecutively and succeed", t, func() {

		setupDB(workerCount)

		txt := `
		USE NS test DB test;
		CREATE global:test SET tests=[], temps=[];
		DEFINE EVENT test ON tester WHEN $after.global != EMPTY THEN (
			UPDATE $after.global SET tests+=$this;
			UPDATE temper SET tester=$this, global=$after.global;
		);
		DEFINE EVENT test ON temper WHEN $after.global != EMPTY THEN (
			UPDATE $after.global SET temps+=$this;
		);
		CREATE |temper:1..5|;
		CREATE tester:test SET global=global:test;
		SELECT * FROM global;
		SELECT * FROM tester;
		SELECT * FROM temper;
		`

		res, err := Execute(permsKV(), txt, nil)
		So(err, ShouldBeNil)
		So(res, ShouldHaveLength, 9)
		So(res[1].Status, ShouldEqual, "OK")
		So(res[1].Result, ShouldHaveLength, 1)
		So(res[2].Status, ShouldEqual, "OK")
		So(res[3].Status, ShouldEqual, "OK")
		So(res[4].Status, ShouldEqual, "OK")
		So(res[4].Result, ShouldHaveLength, 5)
		So(res[5].Status, ShouldEqual, "OK")
		So(res[5].Result, ShouldHaveLength, 1)
		So(res[6].Status, ShouldEqual, "OK")
		So(res[6].Result, ShouldHaveLength, 1)
		So(res[7].Status, ShouldEqual, "OK")
		So(res[7].Result, ShouldHaveLength, 1)
		So(res[8].Status, ShouldEqual, "OK")
		So(res[8].Result, ShouldHaveLength, 5)

	})

}