package query

import (
	"bytes"
	"fmt"
	"testing"

	smithytesting "github.com/aws/smithy-go/testing"
)

func TestEncode(t *testing.T) {
	cases := map[string]struct {
		Encode func(*Encoder) error
		Expect []byte
	}{
		"object": {
			Encode: func(e *Encoder) error {
				e.Object().Key("foo").String("bar")
				return e.Encode()
			},
			Expect: []byte(`foo=bar`),
		},
		"nested object": {
			Encode: func(e *Encoder) error {
				e.Object().Key("foo").Object().Key("bar").String("baz")
				return e.Encode()
			},
			Expect: []byte(`foo.bar=baz`),
		},
		"list": {
			Encode: func(e *Encoder) error {
				list := e.Object().Key("list").Array("spam")
				list.Value().String("spam")
				list.Value().String("eggs")
				return e.Encode()
			},
			Expect: []byte(`list.spam.1=spam&list.spam.2=eggs`),
		},
		"empty list": {
			Encode: func(e *Encoder) error {
				e.Object().Key("list").Array("spam")
				return e.Encode()
			},
			Expect: []byte(`list=`),
		},
		"flat list": {
			Encode: func(e *Encoder) error {
				list := e.Object().FlatKey("list").Array("spam")
				list.Value().String("spam")
				list.Value().String("eggs")
				return e.Encode()
			},
			Expect: []byte(`list.1=spam&list.2=eggs`),
		},
		"empty flat list": {
			Encode: func(e *Encoder) error {
				e.Object().FlatKey("list").Array("spam")
				return e.Encode()
			},
			Expect: []byte(`list=`),
		},
		"map": {
			Encode: func(e *Encoder) error {
				mapValue := e.Object().Key("map").Map("key", "value")
				mapValue.Key("bar").String("baz")
				mapValue.Key("foo").String("bin")
				return e.Encode()
			},
			Expect: []byte(`map.entry.1.key=bar&map.entry.1.value=baz&map.entry.2.key=foo&map.entry.2.value=bin`),
		},
		"flat map": {
			Encode: func(e *Encoder) error {
				mapValue := e.Object().FlatKey("map").Map("key", "value")
				mapValue.Key("bar").String("baz")
				mapValue.Key("foo").String("bin")
				return e.Encode()
			},
			Expect: []byte(`map.1.key=bar&map.1.value=baz&map.2.key=foo&map.2.value=bin`),
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			var buff bytes.Buffer
			encoder := NewEncoder(&buff)
			if err := c.Encode(encoder); err != nil {
				t.Fatalf("failed to encode, %v", err)
			}
			smithytesting.AssertURLFormEqual(t, c.Expect, buff.Bytes())
		})
	}
}

// limitedWriter exists to isolate WriteString to ensure that any writer
// can actually be used
type limitedWriter struct {
	writer *bytes.Buffer
}

func (lw limitedWriter) Write(v []byte) (int, error) {
	return lw.writer.Write(v)
}

func TestEncodeHandlesBareIoWriter(t *testing.T) {
	buff := limitedWriter{writer: bytes.NewBuffer(nil)}
	encoder := NewEncoder(buff)
	encoder.Object().Key("foo").String("bar")
	if err := encoder.Encode(); err != nil {
		t.Fatal(err)
	}
	smithytesting.AssertURLFormEqual(t, []byte(`foo=bar`), buff.writer.Bytes())
}

// stringWriter exists to ensure that WriteString is called when
// available.
type stringWriter struct {
	writer *bytes.Buffer
}

func (w stringWriter) Write(v []byte) (int, error) {
	return 0, fmt.Errorf("the WriteString method should be used when available")
}

func (w stringWriter) WriteString(v string) (int, error) {
	return w.writer.WriteString(v)
}

func TestEncodeUsesWriteString(t *testing.T) {
	buff := stringWriter{writer: bytes.NewBuffer(nil)}
	encoder := NewEncoder(buff)
	encoder.Object().Key("foo").String("bar")
	if err := encoder.Encode(); err != nil {
		t.Fatal(err)
	}
	smithytesting.AssertURLFormEqual(t, []byte(`foo=bar`), buff.writer.Bytes())
}
