package redis

import (
	"context"
	"reflect"
	"testing"
	"time"
)

// captureCmdable returns a cmdable that records the Cmder passed to it without
// dispatching the command. Used to assert that command builders produce the
// expected argument list.
func captureCmdable(out *Cmder) cmdable {
	return func(_ context.Context, cmd Cmder) error {
		*out = cmd
		return nil
	}
}

func TestIncrEXInt_Args(t *testing.T) {
	tests := []struct {
		name string
		args IncrEXIntArgs
		want []interface{}
	}{
		{
			name: "default",
			args: IncrEXIntArgs{},
			want: []interface{}{"increx", "key"},
		},
		{
			name: "byint",
			args: IncrEXIntArgs{By: 4, HasBy: true},
			want: []interface{}{"increx", "key", "byint", int64(4)},
		},
		{
			name: "byint_zero_explicit",
			args: IncrEXIntArgs{By: 0, HasBy: true},
			want: []interface{}{"increx", "key", "byint", int64(0)},
		},
		{
			name: "lbound_ubound",
			args: IncrEXIntArgs{
				By: 2, HasBy: true,
				LBound: 0, HasLBound: true,
				UBound: 20, HasUBound: true,
			},
			want: []interface{}{"increx", "key", "byint", int64(2), "lbound", int64(0), "ubound", int64(20)},
		},
		{
			name: "lbound_zero_is_sent",
			args: IncrEXIntArgs{
				LBound: 0, HasLBound: true,
			},
			want: []interface{}{"increx", "key", "lbound", int64(0)},
		},
		{
			name: "ex_and_enx",
			args: IncrEXIntArgs{
				By: 2, HasBy: true,
				Expiration: &ExpirationOption{Mode: EX, Value: 60},
				ENX:        true,
			},
			want: []interface{}{"increx", "key", "byint", int64(2), "ex", int64(60), "enx"},
		},
		{
			name: "px",
			args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: PX, Value: 1500}},
			want: []interface{}{"increx", "key", "px", int64(1500)},
		},
		{
			name: "exat",
			args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: EXAT, Value: 1753265054}},
			want: []interface{}{"increx", "key", "exat", int64(1753265054)},
		},
		{
			name: "pxat",
			args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: PXAT, Value: 1753265054014}},
			want: []interface{}{"increx", "key", "pxat", int64(1753265054014)},
		},
		{
			name: "persist",
			args: IncrEXIntArgs{Expiration: &ExpirationOption{Mode: PERSIST}},
			want: []interface{}{"increx", "key", "persist"},
		},
		{
			name: "saturate_with_lbound",
			args: IncrEXIntArgs{
				By: 1, HasBy: true,
				LBound: 10, HasLBound: true,
				Saturate: true,
			},
			want: []interface{}{"increx", "key", "byint", int64(1), "lbound", int64(10), "saturate"},
		},
		{
			name: "saturate_with_ubound",
			args: IncrEXIntArgs{
				By: 5, HasBy: true,
				UBound: 12, HasUBound: true,
				Saturate: true,
			},
			want: []interface{}{"increx", "key", "byint", int64(5), "ubound", int64(12), "saturate"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var captured Cmder
			c := captureCmdable(&captured)
			cmd := c.IncrEXInt(context.Background(), "key", tt.args)
			if cmd == nil {
				t.Fatalf("IncrEXInt returned nil")
			}
			if !reflect.DeepEqual(cmd.Args(), tt.want) {
				t.Errorf("args mismatch\n got: %#v\nwant: %#v", cmd.Args(), tt.want)
			}
		})
	}
}

func TestIncrEXFloat_Args(t *testing.T) {
	tests := []struct {
		name string
		args IncrEXFloatArgs
		want []interface{}
	}{
		{
			name: "default_zero_by",
			args: IncrEXFloatArgs{},
			want: []interface{}{"increx", "key", "byfloat", float64(0)},
		},
		{
			name: "byfloat",
			args: IncrEXFloatArgs{By: 0.25},
			want: []interface{}{"increx", "key", "byfloat", 0.25},
		},
		{
			name: "bounds_and_saturate",
			args: IncrEXFloatArgs{
				By:     0.7,
				UBound: 2, HasUBound: true,
				Saturate: true,
			},
			want: []interface{}{"increx", "key", "byfloat", 0.7, "ubound", float64(2), "saturate"},
		},
		{
			name: "lbound_ubound",
			args: IncrEXFloatArgs{
				By:     0.5,
				LBound: -1.5, HasLBound: true,
				UBound: 1.5, HasUBound: true,
			},
			want: []interface{}{"increx", "key", "byfloat", 0.5, "lbound", -1.5, "ubound", 1.5},
		},
		{
			name: "ex_and_enx",
			args: IncrEXFloatArgs{
				By:         1.5,
				Expiration: &ExpirationOption{Mode: EX, Value: 30},
				ENX:        true,
			},
			want: []interface{}{"increx", "key", "byfloat", 1.5, "ex", int64(30), "enx"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var captured Cmder
			c := captureCmdable(&captured)
			cmd := c.IncrEXFloat(context.Background(), "key", tt.args)
			if cmd == nil {
				t.Fatalf("IncrEXFloat returned nil")
			}
			if !reflect.DeepEqual(cmd.Args(), tt.want) {
				t.Errorf("args mismatch\n got: %#v\nwant: %#v", cmd.Args(), tt.want)
			}
		})
	}
}

func TestSetArgs_ExpireAt(t *testing.T) {
	tests := []struct {
		name     string
		expireAt time.Time
		want     []interface{}
	}{
		{
			name:     "whole_seconds",
			expireAt: time.Unix(1700000000, 0),
			want:     []interface{}{"set", "key", "value", "exat", int64(1700000000)},
		},
		{
			name:     "sub_millisecond_is_truncated",
			expireAt: time.Unix(1700000000, 500_000),
			want:     []interface{}{"set", "key", "value", "exat", int64(1700000000)},
		},
		{
			name:     "milliseconds",
			expireAt: time.UnixMilli(1700000000123),
			want:     []interface{}{"set", "key", "value", "pxat", int64(1700000000123)},
		},
		{
			name:     "beyond_nanosecond_range",
			expireAt: time.Date(2270, time.January, 2, 3, 4, 5, 123000000, time.UTC),
			want:     []interface{}{"set", "key", "value", "pxat", int64(9467204645123)},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var cmd Cmder
			c := captureCmdable(&cmd)
			c.SetArgs(context.Background(), "key", "value", SetArgs{ExpireAt: tt.expireAt})
			if got := cmd.Args(); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("args = %v, want %v", got, tt.want)
			}
		})
	}
}
