package redis_test

import (
	"bytes"
	"errors"
	"strconv"
	"time"

	"github.com/redis/go-redis/v9"

	. "github.com/bsm/ginkgo/v2"
	. "github.com/bsm/gomega"
)

// parseRESPType returns the RESP type character and validates basic structure
func parseRESPType(data []byte) (byte, bool) {
	if len(data) < 3 {
		return 0, false
	}
	// Check for CRLF at the end
	if !bytes.HasSuffix(data, []byte("\r\n")) {
		return 0, false
	}
	return data[0], true
}

// countRESPElements counts the number of elements in a RESP array/map response
func countRESPElements(data []byte) (int, bool) {
	if len(data) < 4 {
		return 0, false
	}
	// Find first CRLF
	idx := bytes.Index(data, []byte("\r\n"))
	if idx < 2 {
		return 0, false
	}
	count, err := strconv.Atoi(string(data[1:idx]))
	if err != nil {
		return 0, false
	}
	return count, true
}

var _ = Describe("Cmd", func() {
	var client *redis.Client

	BeforeEach(func() {
		client = redis.NewClient(redisOptions())
		Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		Expect(client.Close()).NotTo(HaveOccurred())
	})

	It("implements Stringer", func() {
		set := client.Set(ctx, "foo", "bar", 0)
		Expect(set.String()).To(Equal("set foo bar: OK"))

		get := client.Get(ctx, "foo")
		Expect(get.String()).To(Equal("get foo: bar"))
	})

	It("has val/err", func() {
		set := client.Set(ctx, "key", "hello", 0)
		Expect(set.Err()).NotTo(HaveOccurred())
		Expect(set.Val()).To(Equal("OK"))

		get := client.Get(ctx, "key")
		Expect(get.Err()).NotTo(HaveOccurred())
		Expect(get.Val()).To(Equal("hello"))

		Expect(set.Err()).NotTo(HaveOccurred())
		Expect(set.Val()).To(Equal("OK"))
	})

	It("has helpers", func() {
		set := client.Set(ctx, "key", "10", 0)
		Expect(set.Err()).NotTo(HaveOccurred())

		n, err := client.Get(ctx, "key").Int64()
		Expect(err).NotTo(HaveOccurred())
		Expect(n).To(Equal(int64(10)))

		un, err := client.Get(ctx, "key").Uint64()
		Expect(err).NotTo(HaveOccurred())
		Expect(un).To(Equal(uint64(10)))

		f, err := client.Get(ctx, "key").Float64()
		Expect(err).NotTo(HaveOccurred())
		Expect(f).To(Equal(float64(10)))
	})

	It("supports float32", func() {
		f := float32(66.97)

		err := client.Set(ctx, "float_key", f, 0).Err()
		Expect(err).NotTo(HaveOccurred())

		val, err := client.Get(ctx, "float_key").Float32()
		Expect(err).NotTo(HaveOccurred())
		Expect(val).To(Equal(f))
	})

	It("supports time.Time", func() {
		tm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC)

		err := client.Set(ctx, "time_key", tm, 0).Err()
		Expect(err).NotTo(HaveOccurred())

		s, err := client.Get(ctx, "time_key").Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(s).To(Equal("2019-01-01T09:45:10.000222125Z"))

		tm2, err := client.Get(ctx, "time_key").Time()
		Expect(err).NotTo(HaveOccurred())
		Expect(tm2).To(BeTemporally("==", tm))
	})

	It("allows to set custom error", func() {
		e := errors.New("custom error")
		cmd := redis.Cmd{}
		cmd.SetErr(e)
		_, err := cmd.Result()
		Expect(err).To(Equal(e))
	})
})

var _ = Describe("RawCmd", func() {
	var client *redis.Client

	BeforeEach(func() {
		client = redis.NewClient(redisOptions())
		Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		Expect(client.Close()).NotTo(HaveOccurred())
	})

	It("returns raw RESP bytes for simple string (PING)", func() {
		cmd := client.DoRaw(ctx, "PING")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(rawBytes)).To(Equal("+PONG\r\n"))
	})

	It("returns raw RESP bytes for bulk string (GET)", func() {
		err := client.Set(ctx, "rawtest", "hello", 0).Err()
		Expect(err).NotTo(HaveOccurred())

		cmd := client.DoRaw(ctx, "GET", "rawtest")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(rawBytes)).To(Equal("$5\r\nhello\r\n"))
	})

	It("returns raw RESP bytes for integer (INCR)", func() {
		err := client.Set(ctx, "counter", "10", 0).Err()
		Expect(err).NotTo(HaveOccurred())

		cmd := client.DoRaw(ctx, "INCR", "counter")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(rawBytes)).To(Equal(":11\r\n"))
	})

	It("returns raw RESP bytes for array (KEYS) with validation", func() {
		err := client.Set(ctx, "key1", "val1", 0).Err()
		Expect(err).NotTo(HaveOccurred())
		err = client.Set(ctx, "key2", "val2", 0).Err()
		Expect(err).NotTo(HaveOccurred())

		cmd := client.DoRaw(ctx, "KEYS", "key*")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())

		// Validate RESP structure
		respType, valid := parseRESPType(rawBytes)
		Expect(valid).To(BeTrue())
		Expect(respType).To(Equal(byte('*')), "Should be array type")

		// Verify element count
		count, ok := countRESPElements(rawBytes)
		Expect(ok).To(BeTrue())
		Expect(count).To(Equal(2))

		// Verify keys are present in the raw bytes
		rawStr := string(rawBytes)
		Expect(rawStr).To(ContainSubstring("key1"))
		Expect(rawStr).To(ContainSubstring("key2"))
	})

	It("returns raw RESP bytes for HGETALL with complete validation", func() {
		err := client.HSet(ctx, "myhash", "field1", "value1", "field2", "value2").Err()
		Expect(err).NotTo(HaveOccurred())

		cmd := client.DoRaw(ctx, "HGETALL", "myhash")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())

		// Validate RESP structure
		respType, valid := parseRESPType(rawBytes)
		Expect(valid).To(BeTrue(), "Response should be valid RESP")

		// HGETALL returns array (RESP2) or map (RESP3)
		Expect(respType).To(BeElementOf(byte('*'), byte('%')), "Should be array or map type")

		// Count elements: array has 4 elements (2 key-value pairs), map has 2 pairs
		count, ok := countRESPElements(rawBytes)
		Expect(ok).To(BeTrue())
		if respType == '*' {
			Expect(count).To(Equal(4), "Array should have 4 elements (2 key-value pairs)")
		} else {
			Expect(count).To(Equal(2), "Map should have 2 pairs")
		}

		rawStr := string(rawBytes)
		Expect(rawStr).To(ContainSubstring("field1"))
		Expect(rawStr).To(ContainSubstring("value1"))
		Expect(rawStr).To(ContainSubstring("field2"))
		Expect(rawStr).To(ContainSubstring("value2"))

		// Verify response ends with CRLF
		Expect(rawBytes[len(rawBytes)-2:]).To(Equal([]byte("\r\n")))
	})

	It("returns raw RESP bytes for nil value", func() {
		cmd := client.DoRaw(ctx, "GET", "nonexistent")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		rawStr := string(rawBytes)
		Expect(rawStr == "$-1\r\n" || rawStr == "_\r\n").To(BeTrue())
	})

	It("returns raw RESP bytes for empty array", func() {
		cmd := client.DoRaw(ctx, "KEYS", "nonexistent*")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(rawBytes)).To(Equal("*0\r\n"))
	})

	It("has Val and Bytes methods", func() {
		cmd := client.DoRaw(ctx, "PING")
		Expect(cmd.Err()).NotTo(HaveOccurred())

		val := cmd.Val()
		Expect(string(val)).To(Equal("+PONG\r\n"))

		bytes, err := cmd.Bytes()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(bytes)).To(Equal("+PONG\r\n"))
	})

	It("implements Stringer", func() {
		cmd := client.DoRaw(ctx, "PING")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		Expect(cmd.String()).To(ContainSubstring("PING"))
	})

	It("allows to set custom error", func() {
		e := errors.New("custom error")
		cmd := redis.RawCmd{}
		cmd.SetErr(e)
		_, err := cmd.Result()
		Expect(err).To(Equal(e))
	})

	It("can be cloned", func() {
		cmd := client.DoRaw(ctx, "PING")
		Expect(cmd.Err()).NotTo(HaveOccurred())

		cloned := cmd.Clone().(*redis.RawCmd)
		Expect(cloned.Val()).To(Equal(cmd.Val()))
		originalVal := cmd.Val()
		clonedVal := cloned.Val()
		Expect(&originalVal[0]).NotTo(BeIdenticalTo(&clonedVal[0]))
	})

	It("returns raw RESP bytes for error response", func() {
		cmd := client.DoRaw(ctx, "INVALIDCMD")
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(rawBytes)).To(HavePrefix("-"))
		Expect(string(rawBytes)).To(ContainSubstring("ERR"))
	})

	It("returns raw RESP bytes for empty hash (nil map)", func() {
		// HGETALL on non-existent hash returns empty array/map
		cmd := client.DoRaw(ctx, "HGETALL", "nonexistent_hash")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())

		respType, valid := parseRESPType(rawBytes)
		Expect(valid).To(BeTrue())
		// Should be empty array or empty map
		Expect(respType).To(BeElementOf(byte('*'), byte('%')))

		count, ok := countRESPElements(rawBytes)
		Expect(ok).To(BeTrue())
		Expect(count).To(Equal(0), "Empty hash should return 0 elements")
	})

	It("returns raw RESP bytes for nested array structures (SMEMBERS)", func() {
		// Create a set with multiple members
		err := client.SAdd(ctx, "myset", "member1", "member2", "member3").Err()
		Expect(err).NotTo(HaveOccurred())

		cmd := client.DoRaw(ctx, "SMEMBERS", "myset")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())

		// Validate basic RESP structure
		respType, valid := parseRESPType(rawBytes)
		Expect(valid).To(BeTrue())
		// SMEMBERS returns array (RESP2) or set (RESP3)
		Expect(respType).To(BeElementOf(byte('*'), byte('~')))

		// Verify count
		count, ok := countRESPElements(rawBytes)
		Expect(ok).To(BeTrue())
		Expect(count).To(Equal(3))

		// Verify members are present
		rawStr := string(rawBytes)
		Expect(rawStr).To(ContainSubstring("member1"))
		Expect(rawStr).To(ContainSubstring("member2"))
		Expect(rawStr).To(ContainSubstring("member3"))
	})

	It("returns raw RESP bytes for LRANGE with exact byte validation", func() {
		// Create a list with multiple elements
		err := client.RPush(ctx, "mylist", "elem1", "elem2", "elem3").Err()
		Expect(err).NotTo(HaveOccurred())

		cmd := client.DoRaw(ctx, "LRANGE", "mylist", "0", "-1")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())

		// LRANGE returns elements in order, so we can validate exact bytes
		expectedResp := "*3\r\n$5\r\nelem1\r\n$5\r\nelem2\r\n$5\r\nelem3\r\n"
		Expect(string(rawBytes)).To(Equal(expectedResp))
	})

	It("returns raw RESP bytes for LPUSH with exact integer response", func() {
		// LPUSH returns the length of the list after push
		cmd := client.DoRaw(ctx, "LPUSH", "newlist", "item")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(rawBytes)).To(Equal(":1\r\n"))
	})

	It("returns raw RESP bytes for ECHO with exact bulk string", func() {
		cmd := client.DoRaw(ctx, "ECHO", "testmsg")
		Expect(cmd.Err()).NotTo(HaveOccurred())
		rawBytes, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(string(rawBytes)).To(Equal("$7\r\ntestmsg\r\n"))
	})
})

var _ = Describe("RawWriteToCmd", func() {
	var client *redis.Client

	BeforeEach(func() {
		client = redis.NewClient(redisOptions())
		Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		Expect(client.Close()).NotTo(HaveOccurred())
	})

	It("streams raw RESP bytes for PING", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "PING")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(Equal(int64(7)))
		Expect(buf.String()).To(Equal("+PONG\r\n"))
	})

	It("streams raw RESP bytes for bulk string (GET)", func() {
		client.Set(ctx, "rawkey", "rawvalue", 0)
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "GET", "rawkey")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(BeNumerically(">", 0))
		Expect(buf.String()).To(Equal("$8\r\nrawvalue\r\n"))
	})

	It("streams raw RESP bytes for integer (INCR)", func() {
		client.Set(ctx, "writeto_counter", "10", 0)
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "INCR", "writeto_counter")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(Equal(int64(5)))
		Expect(buf.String()).To(Equal(":11\r\n"))
	})

	It("streams raw RESP bytes for nil value (GET non-existent)", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "GET", "nonexistent_key")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(Equal(int64(buf.Len())))
		rawStr := buf.String()
		// RESP2 returns $-1\r\n, RESP3 returns _\r\n
		Expect(rawStr == "$-1\r\n" || rawStr == "_\r\n").To(BeTrue())
	})

	It("streams raw RESP bytes for array (LRANGE) with exact validation", func() {
		client.RPush(ctx, "writeto_list", "a", "b", "c")
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "LRANGE", "writeto_list", "0", "-1")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(Equal(int64(buf.Len())))
		// LRANGE returns elements in order
		expectedResp := "*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"
		Expect(buf.String()).To(Equal(expectedResp))
	})

	It("streams raw RESP bytes for empty array (LRANGE empty list)", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "LRANGE", "nonexistent_list", "0", "-1")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(Equal(int64(buf.Len())))
		Expect(buf.String()).To(Equal("*0\r\n"))
	})

	It("streams raw RESP bytes for HGETALL", func() {
		client.HSet(ctx, "rawhash", "f1", "v1", "f2", "v2")
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "rawhash")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(BeNumerically(">", 0))
		raw := buf.String()
		Expect(raw).To(ContainSubstring("f1"))
		Expect(raw).To(ContainSubstring("v1"))
	})

	It("returns correct byte count", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "PING")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(Equal(int64(buf.Len())))
	})

	It("implements Stringer", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "PING")
		Expect(cmd.String()).To(ContainSubstring("PING"))
	})

	It("can be cloned", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "PING")
		clone := cmd.Clone().(*redis.RawWriteToCmd)
		Expect(clone.Val()).To(Equal(cmd.Val()))
	})

	It("streams HGETALL with complete validation", func() {
		client.HSet(ctx, "rawhash2", "key1", "val1", "key2", "val2")
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "rawhash2")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())

		// Verify written bytes match buffer length
		Expect(written).To(Equal(int64(buf.Len())))

		// Validate RESP structure
		rawBytes := buf.Bytes()
		respType, valid := parseRESPType(rawBytes)
		Expect(valid).To(BeTrue())
		Expect(respType).To(BeElementOf(byte('*'), byte('%')))

		// Verify all data present
		rawStr := buf.String()
		Expect(rawStr).To(ContainSubstring("key1"))
		Expect(rawStr).To(ContainSubstring("val1"))
		Expect(rawStr).To(ContainSubstring("key2"))
		Expect(rawStr).To(ContainSubstring("val2"))
	})

	It("streams empty hash correctly", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "nonexistent_hash")
		written, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())
		Expect(written).To(Equal(int64(buf.Len())))

		rawBytes := buf.Bytes()
		respType, valid := parseRESPType(rawBytes)
		Expect(valid).To(BeTrue())
		Expect(respType).To(BeElementOf(byte('*'), byte('%')))

		count, ok := countRESPElements(rawBytes)
		Expect(ok).To(BeTrue())
		Expect(count).To(Equal(0))
	})

	It("streams error response correctly", func() {
		var buf bytes.Buffer
		cmd := client.DoRawWriteTo(ctx, &buf, "INVALIDCMD")
		_, err := cmd.Result()
		Expect(err).NotTo(HaveOccurred())

		rawStr := buf.String()
		Expect(rawStr).To(HavePrefix("-"))
		Expect(rawStr).To(ContainSubstring("ERR"))
	})
})

var _ = Describe("NoRetry behavior", func() {
	It("RawCmd does not have NoRetry set", func() {
		cmd := redis.NewRawCmd(ctx, "PING")
		Expect(cmd.NoRetry()).To(BeFalse())
	})

	It("RawWriteToCmd has NoRetry set to prevent data corruption", func() {
		var buf bytes.Buffer
		cmd := redis.NewRawWriteToCmd(ctx, &buf, "PING")
		Expect(cmd.NoRetry()).To(BeTrue())
	})
})
