package redis

import (
	"bytes"
	"fmt"
	"reflect"
	"strings"
	"testing"
)

// fakeIO is a simple fake Reader/Writer. Read pulls data from the output
// channel; Writes are NOOPs.
type fakeIO struct {
	output chan byte
}

// Read from output until it would block.
func (fakeio *fakeIO) Read(buf []byte) (int, error) {
	// read what can be read from the channel without blocking
	for i := 0; i < len(buf); {
		select {
		case b := <-fakeio.output:
			buf[i] = b
			i++
		default:
			return i, nil
		}
	}
	return len(buf), nil
}

// Write does nothing.
func (fakeio *fakeIO) Write(buf []byte) (int, error) {
	return len(buf), nil
}

// Provide data to the output channel.
func (fakeio *fakeIO) Provide(buf []byte) {
	go func() {
		// send new bytes as the buffer clears
		for _, v := range buf {
			fakeio.output <- v
		}
	}()
}

var bigSimpleString = strings.Repeat("simple,", 1024*1024)
var bigBulkString = "--- BEGIN BULK STRING ---\r\n" + bigSimpleString + "\r\n--- END BULK STRING---\r\n"

// simpleStrings maps the string value to its encoding
var simpleStrings = map[string]string{
	"":                 "+\r\n",
	"foo":              "+foo\r\n",
	"0123456789abcdef": "+0123456789abcdef\r\n",
	bigSimpleString:    "+" + bigSimpleString + "\r\n",
}

// bulkStrings maps the string value to its encoding
var bulkStrings = map[string]string{
	"":                 "$0\r\n\r\n",
	"foo":              "$3\r\nfoo\r\n",
	"0123456789abcdef": "$16\r\n0123456789abcdef\r\n",
	bigBulkString:      fmt.Sprintf("$%d\r\n%s\r\n", len(bigBulkString), bigBulkString),
	"\r\n\n\r\r\n":     "$6\r\n\r\n\n\r\r\n\r\n",
}

// integers maps integer values to their encoding
var integers = map[int64]string{
	0:  ":0\r\n",
	1:  ":1\r\n",
	-1: ":-1\r\n",
	// Largest signed 64-bit integer
	(1 << 63) - 1: ":9223372036854775807\r\n",
	// Smallest signed 64-bit integer
	-(1 << 63): ":-9223372036854775808\r\n",
	12345:      ":12345\r\n",
}

// redisErrors maps error strings to their encoding
var redisErrors = map[string]string{
	"": "-\r\n",
	"ERR something went wrong": "-ERR something went wrong\r\n",
	"singleword":               "-singleword\r\n",
}

// redisErrors maps error strings to their prefixes
var redisErrorPrefixes = map[string]string{
	"": "",
	"ERR something went wrong": "ERR",
	"singleword":               "singleword",
}

// redisErrorMessages maps error strings to their "messages"
var redisErrorMessages = map[string]string{
	"": "",
	"ERR something went wrong": "something went wrong",
	"singleword":               "singleword",
}

// redisArrays maps encoded array values to the corresponding array (Note: reverse key/value order from other maps)
var redisArrays = map[string]RedisArray{
	"*0\r\n":                                  RedisArray{},
	"*1\r\n+\r\n":                             RedisArray{SimpleString("")},
	"*2\r\n*1\r\n*0\r\n*1\r\n$5\r\n12345\r\n": RedisArray{RedisArray{RedisArray{}}, RedisArray{BulkString("12345")}},
	"*5\r\n" +
		"+simpleString\r\n" +
		"-ERR error message\r\n" +
		":12345\r\n" +
		"$47\r\n*5\r\n+simpleString\r\n-ERR error message\r\n:12345\r\n\r\n" +
		"*0\r\n": RedisArray{
		SimpleString("simpleString"),
		ErrorMessage("ERR error message"),
		Integer(12345),
		BulkString([]byte("*5\r\n+simpleString\r\n-ERR error message\r\n:12345\r\n")),
		RedisArray{},
	},
}

// getConnection backed by a fakeIO, and the fakeIO instance.
func getConnection() (*Connection, *fakeIO) {
	fakeio := fakeIO{
		output: make(chan byte, 1024),
	}
	conn := Connection{conn: &fakeio}
	return &conn, &fakeio
}

// strip s down to 32 chars
func strip(s interface{}) string {
	var stringVal string
	switch v := s.(type) {
	case SimpleString:
		stringVal = string(v)
	case string:
		stringVal = v
	case BulkString:
		stringVal = string([]byte(v))
	case []byte:
		stringVal = string(v)
	default:
		stringVal = fmt.Sprintf("%v", s)
	}
	if len(stringVal) > 32 {
		return stringVal[0:20] + "..." + stringVal[len(stringVal)-10:]
	}
	return stringVal
}

// Check that the two strings are equivalent, and if not, log the expected/actual
func assertEquals(t *testing.T, actual string, expected string) {
	if actual != expected {
		t.Errorf("Expected [<%s>], got [<%s>]", strip(expected), strip(actual))
	}
}

// Read a value from the connection, or throw a fatal error
func rawRead(t *testing.T, conn *Connection) RedisValue {
	ret, err := conn.ReadRedisValue()
	if err != nil {
		t.Fatalf("Error reading value: %v", err)
	}
	return ret
}

// Read a value from the connection and convert it to a string for easy comparison
func read(t *testing.T, conn *Connection) string {
	ret := rawRead(t, conn)
	b, ok := ret.(BulkString)
	if ok {
		return string(b)
	}
	return fmt.Sprintf("%v", ret)
}

// Encode a value and return a string for easy comparison
func encode(a RedisValue) string {
	return string(a.Encode())
}

// Recursively compare two arrays, with the given path prefix
func _compareArrays(a, b RedisArray, path string) error {
	if len(a) != len(b) {
		return fmt.Errorf("Length mismatch (%d != %d)", len(a), len(b))
	}
	for i, ai := range a {
		subPath := fmt.Sprintf("%s[%d]", path, i)
		bi := b[i]
		aType := reflect.TypeOf(ai)
		bType := reflect.TypeOf(bi)
		if aType != bType {
			return fmt.Errorf("Type mismatch at %s: %s != %s", subPath, aType.Name(), bType.Name())
		}
		switch ait := ai.(type) {
		case RedisArray:
			if err := _compareArrays(ait, bi.(RedisArray), subPath); err != nil {
				return err
			}
		case BulkString:
			if !bytes.Equal(ait, bi.(BulkString)) {
				return fmt.Errorf("Bulk string mismatch at %s: %s != %s", subPath, strip(ai), strip(bi))
			}
		default:
			if ai != bi {
				return fmt.Errorf("Mismatch at %s: %s != %s", subPath, strip(ai), strip(bi))
			}
		}
	}
	return nil
}

// Compare two RedisValues directly (without using their encodings)
func compareRedisValues(a, b RedisValue) error {
	if reflect.TypeOf(a) != reflect.TypeOf(b) {
		return fmt.Errorf("different types (%s != %s)", reflect.TypeOf(a).Name(), reflect.TypeOf(b).Name())
	}
	switch c := a.(type) {
	case RedisArray:
		return compareArrays(c, b.(RedisArray))
	case BulkString:
		if !bytes.Equal(c, b.(BulkString)) {
			return fmt.Errorf("byte array mismatch (%s != %s)", strip(a), strip(b))
		}
		return nil
	case NullType:
		return nil
	case Integer:
		if int64(c) != int64(b.(Integer)) {
			return fmt.Errorf("int mismatch (%d != %d)", c, int64(b.(Integer)))
		}
		return nil
	default:
		if fmt.Sprintf("%v", a) != fmt.Sprintf("%v", b) {
			return fmt.Errorf("Generic mismatch (%s != %s)", strip(a), strip(b))
		}
		return nil
	}
}

// Push the encoding to the fake IO, then read it off and compare it to the expected value
func writeThenRead(t *testing.T, conn *Connection, io *fakeIO, encoded string, expected RedisValue) {
	assertEquals(t, encoded, encode(expected))
	io.Provide([]byte(encoded))
	decoded, err := conn.ReadRedisValue()
	if err != nil {
		t.Fatalf("Error decoding value for %s: %v", strip(string(encoded)), err)
	}
	if err = compareRedisValues(decoded, expected); err != nil {
		t.Errorf("Read value did not match original value: %s != %s: %v", strip(decoded), strip(expected), err)
	}
}

// TestSimpleString checks that SimpleStrings are encoded/decoded as expected.
func TestSimpleString(t *testing.T) {
	conn, io := getConnection()
	for str, encoding := range simpleStrings {
		writeThenRead(t, conn, io, encoding, SimpleString(str))
	}
}

// TestInteger checks that Integers are encoded/decoded to the expected values.
func TestInteger(t *testing.T) {
	conn, io := getConnection()
	for val, encoding := range integers {
		writeThenRead(t, conn, io, encoding, Integer(val))
	}
}

// TestBulkString checks that SimpleStrings are encoded/decoded as expected.
func TestBulkString(t *testing.T) {
	conn, io := getConnection()
	for str, encoding := range bulkStrings {
		writeThenRead(t, conn, io, encoding, BulkString([]byte(str)))
	}
	writeThenRead(t, conn, io, "$-1\r\n", NullValue)
}

// TestIsNullValue checks that only the null value causes IsNullValue to return true
func TestIsNullValue(t *testing.T) {
	if !IsNullValue(NullValue) {
		t.Errorf("!IsNullValue(&global RedisNull)")
	}
	for str, array := range redisArrays {
		if IsNullValue(array) {
			t.Errorf("Non-null array '%s' returned IsNullValue", strip(str))
		}
	}
	for err := range redisErrors {
		if IsNullValue(ErrorMessage(err)) {
			t.Errorf("Non-null error message '%s' returned IsNullValue", strip(err))
		}
	}
	for val := range integers {
		if IsNullValue(Integer(val)) {
			t.Errorf("Non-null integer '%d' returned IsNullValue", val)
		}
	}
	for str := range bulkStrings {
		bulk := BulkString([]byte(str))
		if IsNullValue(bulk) {
			t.Errorf("Non-null bulk string '%s' returned IsNullValue", strip(str))
		}
	}
	for str := range simpleStrings {
		if IsNullValue(SimpleString(str)) {
			t.Errorf("Non-null simple string '%s' returned IsNullValue", strip(str))
		}
	}
}

// TestErrorMessage checks that ErrorMessages are encoded/decoded as expected.
func TestErrorMessage(t *testing.T) {
	conn, io := getConnection()
	for str, encoding := range redisErrors {
		assertEquals(t, ErrorMessage(str).ErrorPrefix(), redisErrorPrefixes[str])
		assertEquals(t, ErrorMessage(str).ErrorMessage(), redisErrorMessages[str])
		writeThenRead(t, conn, io, encoding, ErrorMessage(str))
	}
}

// compareArrays recursively compares two arrays (loops not supported)
func compareArrays(a, b RedisArray) error {
	return _compareArrays(a, b, "")
}

// TestRedisArray checks that RedisArray encoding and decoding match the expected values.
func TestRedisArray(t *testing.T) {
	conn, io := getConnection()

	// Slowly build up array, checking its encoding after each element is added
	var array RedisArray
	e0 := SimpleString("foo")
	e1 := BulkString([]byte(bigBulkString))
	e2 := Integer(0)
	e3 := Integer((1 << 62))
	e4 := make(RedisArray, 0)
	e5 := ErrorMessage("ERR some error")
	writeThenRead(t, conn, io, "*0\r\n", array)
	array = append(array, e0)
	writeThenRead(t, conn, io, "*1\r\n"+encode(e0), array)
	array = append(array, e1)
	writeThenRead(t, conn, io, "*2\r\n"+encode(e0)+encode(e1), array)
	array = append(array, e2)
	writeThenRead(t, conn, io, "*3\r\n"+encode(e0)+encode(e1)+encode(e2), array)
	array = append(array, e3)
	writeThenRead(t, conn, io, "*4\r\n"+encode(e0)+encode(e1)+encode(e2)+encode(e3), array)
	array = append(array, e4)
	writeThenRead(t, conn, io, "*5\r\n"+encode(e0)+encode(e1)+encode(e2)+encode(e3)+encode(e4), array)
	array = append(array, e5)
	writeThenRead(t, conn, io, "*6\r\n"+encode(e0)+encode(e1)+encode(e2)+encode(e3)+encode(e4)+encode(e5), array)

	// Check calculated values
	for expectedEncoding, redisValue := range redisArrays {
		writeThenRead(t, conn, io, expectedEncoding, redisValue)
	}
}
