package core_test

import (
	"testing"

	"github.com/MontFerret/ferret/pkg/runtime/core"
	"github.com/MontFerret/ferret/pkg/runtime/values"
	. "github.com/smartystreets/goconvey/convey"
)

func TestScope(t *testing.T) {
	Convey(".SetVariable", t, func() {
		Convey("Should set a new variable", func() {
			rs, cf := core.NewRootScope()

			So(cf, ShouldNotBeNil)

			err := rs.SetVariable("foo", values.NewString("bar"))

			So(err, ShouldBeNil)
		})

		Convey("Should return an error when a variable is already defined", func() {
			rs, cf := core.NewRootScope()

			So(cf, ShouldNotBeNil)

			err := rs.SetVariable("foo", values.NewString("bar"))
			So(err, ShouldBeNil)

			err = rs.SetVariable("foo", values.NewString("bar"))
			So(err, ShouldHaveSameTypeAs, core.ErrNotUnique)
		})
	})

	Convey(".GetVariable", t, func() {
		Convey("Should set and get a variable", func() {
			rs, cf := core.NewRootScope()

			So(cf, ShouldNotBeNil)

			err := rs.SetVariable("foo", values.NewString("bar"))
			So(err, ShouldBeNil)

			v, err := rs.GetVariable("foo")

			So(err, ShouldBeNil)
			So(v, ShouldEqual, "bar")
		})

		Convey("Should return an error when variable is not defined", func() {
			rs, cf := core.NewRootScope()

			So(cf, ShouldNotBeNil)

			_, err := rs.GetVariable("foo")

			So(err, ShouldNotBeNil)
		})
	})

	Convey(".HasVariable", t, func() {
		Convey("Should return TRUE when a variable exists", func() {
			rs, cf := core.NewRootScope()

			So(cf, ShouldNotBeNil)

			err := rs.SetVariable("foo", values.NewString("bar"))
			So(err, ShouldBeNil)

			exists := rs.HasVariable("foo")

			So(exists, ShouldBeTrue)
		})

		Convey("Should return FALSE when a variable exists", func() {
			rs, cf := core.NewRootScope()

			So(cf, ShouldNotBeNil)

			exists := rs.HasVariable("foo")

			So(exists, ShouldBeFalse)
		})
	})

	Convey(".Fork", t, func() {
		Convey("Should create a nested scope", func() {
			Convey("Should set a variable only in a child scope", func() {
				rs, cf := core.NewRootScope()
				So(cf, ShouldNotBeNil)

				cs := rs.Fork()
				cs.SetVariable("foo", values.NewString("bar"))

				exists := rs.HasVariable("foo")

				So(exists, ShouldBeFalse)
			})

			Convey("Should return a variable defined only in a child scope", func() {
				rs, cf := core.NewRootScope()
				So(cf, ShouldNotBeNil)

				cs := rs.Fork()
				err := cs.SetVariable("foo", values.NewString("bar"))
				So(err, ShouldBeNil)

				v, err := cs.GetVariable("foo")

				So(err, ShouldBeNil)
				So(v, ShouldEqual, "bar")
			})

			Convey("Should return a variable defined only in a parent scope", func() {
				rs, cf := core.NewRootScope()
				So(cf, ShouldNotBeNil)

				cs := rs.Fork()
				err := cs.SetVariable("foo", values.NewString("bar"))
				So(err, ShouldBeNil)

				err = rs.SetVariable("faz", values.NewString("qaz"))
				So(err, ShouldBeNil)

				v, err := cs.GetVariable("faz")

				So(err, ShouldBeNil)
				So(v, ShouldEqual, "qaz")
			})

			Convey("Should set a new variable with a same name defined in a parent scope", func() {
				rs, cf := core.NewRootScope()
				So(cf, ShouldNotBeNil)

				err := rs.SetVariable("foo", values.NewString("bar"))
				So(err, ShouldBeNil)

				cs := rs.Fork()
				err = cs.SetVariable("foo", values.NewString("faz"))
				So(err, ShouldBeNil)

				rsV, err := rs.GetVariable("foo")
				So(err, ShouldBeNil)

				csV, err := cs.GetVariable("foo")
				So(err, ShouldBeNil)

				So(csV, ShouldNotEqual, rsV)
			})
		})
	})
}

func BenchmarkScope(b *testing.B) {
	root, _ := core.NewRootScope()

	for n := 0; n < b.N; n++ {
		root.Fork()
	}
}

type (
	TestCloserType struct{}

	TestCloserValue struct {
		closed bool
	}
)

func (tct TestCloserType) ID() int64 {
	return 99
}

func (tct TestCloserType) String() string {
	return "TestCloser"
}

func (tct TestCloserType) Equals(other core.Type) bool {
	return other.ID() == tct.ID()
}

func (tc *TestCloserValue) MarshalJSON() ([]byte, error) {
	return nil, core.ErrNotImplemented
}

func (tc *TestCloserValue) Type() core.Type {
	return TestCloserType{}
}

func (tc *TestCloserValue) String() string {
	return ""
}

func (tc *TestCloserValue) Compare(other core.Value) int64 {
	return 0
}

func (tc *TestCloserValue) Unwrap() interface{} {
	return tc
}

func (tc *TestCloserValue) Hash() uint64 {
	return 0
}

func (tc *TestCloserValue) Copy() core.Value {
	return &TestCloserValue{}
}

func (tc *TestCloserValue) Close() error {
	if tc.closed {
		return core.Error(core.ErrInvalidOperation, "already closed")
	}

	tc.closed = true

	return nil
}

func TestCloseFunc(t *testing.T) {
	Convey("Should close root scope and close all io.Closer values", t, func() {
		rs, cf := core.NewRootScope()

		tc := &TestCloserValue{}

		rs.SetVariable("disposable", tc)
		So(tc.closed, ShouldBeFalse)

		err := cf()
		So(err, ShouldBeNil)

		So(tc.closed, ShouldBeTrue)
	})

	Convey("Should return error if it's already closed", t, func() {
		rs, cf := core.NewRootScope()

		tc := &TestCloserValue{}

		rs.SetVariable("disposable", tc)
		So(tc.closed, ShouldBeFalse)

		err := cf()
		So(err, ShouldBeNil)

		So(tc.closed, ShouldBeTrue)

		err = cf()
		So(err, ShouldHaveSameTypeAs, core.ErrInvalidOperation)
	})
}
