package fs_test

import (
	"bytes"
	"context"
	"fmt"
	"testing"

	"github.com/MontFerret/ferret/pkg/runtime/core"
	"github.com/MontFerret/ferret/pkg/runtime/values"
	"github.com/MontFerret/ferret/pkg/stdlib/io/fs"

	. "github.com/smartystreets/goconvey/convey"
)

func TestWrite(t *testing.T) {

	Convey("Invalid arguments", t, func() {

		path := values.NewString("path.txt")
		data := values.NewBinary([]byte("3timeslazy"))
		params := values.NewObjectWith(
			values.NewObjectProperty("mode", values.NewString("w")),
		)
		someInt := values.NewInt(0)

		Convey("All arguments", func() {

			testCases := []struct {
				Name string
				Args []core.Value
			}{
				{
					Name: "Arguments Number: No arguments passed",
					Args: []core.Value{},
				},
				{
					Name: "Arguments Number: Only `path` passed",
					Args: []core.Value{path},
				},
				{
					Name: "Arguments Number: more than 3 arguments passed",
					Args: []core.Value{path, data, params, someInt},
				},
				{
					Name: "Arguments Type: `path` not a string",
					Args: []core.Value{someInt},
				},
				{
					Name: "Arguments Type: `data` not a binary",
					Args: []core.Value{path, someInt},
				},
				{
					Name: "Arguments Type: `params` not an object",
					Args: []core.Value{path, data, someInt},
				},
			}

			for _, tC := range testCases {
				tC := tC

				Convey(tC.Name, func() {
					none, err := fs.Write(context.Background(), tC.Args...)
					So(err, ShouldBeError)
					So(none, ShouldResemble, values.None)
				})
			}
		})

		Convey("Argument `params`", func() {

			Convey("Key `mode`", func() {

				testCases := []core.Value{
					// empty mode string
					values.NewString(""),

					// `a` and `w` cannot be used together
					values.NewString("aw"),

					// two equal letters
					values.NewString("ww"),

					// mode string is too long
					values.NewString("awx"),

					// the `x` mode only
					values.NewString("x"),

					// mode is not a string
					values.NewInt(1),
				}

				for _, mode := range testCases {
					mode := mode
					name := fmt.Sprintf("mode `%s`", mode)

					Convey(name, func() {
						params := values.NewObjectWith(
							values.NewObjectProperty("mode", mode),
						)

						none, err := fs.Write(context.Background(), path, data, params)
						So(err, ShouldBeError)
						So(none, ShouldResemble, values.None)
					})
				}
			})
		})
	})

	Convey("Error cases", t, func() {

		Convey("Write into existing file with `x` mode", func() {
			file, delFile := tempFile()
			defer delFile()

			none, err := fs.Write(
				context.Background(),
				values.NewString(file.Name()),
				values.NewBinary([]byte("3timeslazy")),
				values.NewObjectWith(
					values.NewObjectProperty("mode", values.NewString("wx")),
				),
			)
			So(none, ShouldResemble, values.None)
			So(err, ShouldBeError)
		})

		Convey("Filepath is empty", func() {
			none, err := fs.Write(
				context.Background(),
				values.NewString(""),
				values.NewBinary([]byte("3timeslazy")),
			)
			So(none, ShouldResemble, values.None)
			So(err, ShouldBeError)
		})
	})

	Convey("Success cases", t, func() {

		Convey("Mode `w` should truncate file", func() {
			file, delFile := tempFile()
			defer delFile()

			data := values.NewBinary([]byte("3timeslazy"))
			fpath := values.NewString(file.Name())
			params := values.NewObjectWith(
				values.NewObjectProperty("mode", values.NewString("w")),
			)

			for _ = range [2]struct{}{} {
				// at first iteration check that `Write` creates file and writes `data`.
				// At second iteration check that `Write` truncates the file and
				// writes `data` again

				_, err := fs.Write(context.Background(), fpath, data, params)
				So(err, ShouldBeNil)

				read, err := fs.Read(context.Background(), fpath)
				So(err, ShouldBeNil)
				So(read, ShouldResemble, data)
			}
		})

		Convey("Mode `a` should append into file", func() {
			file, delFile := tempFile()
			defer delFile()

			data := values.NewBinary([]byte("3timeslazy"))
			fpath := values.NewString(file.Name())
			params := values.NewObjectWith(
				values.NewObjectProperty("mode", values.NewString("a")),
			)

			for i := range [2]struct{}{} {
				// at first iteration check that `Write` creates file and appends `data`
				// into it.
				// At second iteration check that `Write` appends `data` into file
				// one more time using bytes.Repeat

				_, err := fs.Write(context.Background(), fpath, data, params)
				So(err, ShouldBeNil)

				read, err := fs.Read(context.Background(), fpath)
				So(err, ShouldBeNil)

				readBytes := read.Unwrap().([]byte)
				So(readBytes, ShouldResemble, bytes.Repeat(data, i+1))
			}
		})
	})
}
