// Copyright 2020 Kentaro Hibino. All rights reserved.
// Use of this source code is governed by a MIT license
// that can be found in the LICENSE file.

package asynq

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	h "github.com/hibiken/asynq/internal/asynqtest"
	"github.com/hibiken/asynq/internal/base"
)

type payloadTest struct {
	data   map[string]interface{}
	key    string
	nonkey string
}

func TestPayloadString(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"name": "gopher"},
			key:    "name",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetString(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("Payload.GetString(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetString(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("With Marshaling: Payload.GetString(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetString(tc.nonkey)
		if err == nil || got != "" {
			t.Errorf("Payload.GetString(%q) = %v, %v; want '', error",
				tc.key, got, err)
		}
	}
}

func TestPayloadInt(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"user_id": 42},
			key:    "user_id",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetInt(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("Payload.GetInt(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetInt(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("With Marshaling: Payload.GetInt(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetInt(tc.nonkey)
		if err == nil || got != 0 {
			t.Errorf("Payload.GetInt(%q) = %v, %v; want 0, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadFloat64(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"pi": 3.14},
			key:    "pi",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetFloat64(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("Payload.GetFloat64(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetFloat64(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("With Marshaling: Payload.GetFloat64(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetFloat64(tc.nonkey)
		if err == nil || got != 0 {
			t.Errorf("Payload.GetFloat64(%q) = %v, %v; want 0, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadBool(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"enabled": true},
			key:    "enabled",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetBool(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("Payload.GetBool(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetBool(tc.key)
		if err != nil || got != tc.data[tc.key] {
			t.Errorf("With Marshaling: Payload.GetBool(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetBool(tc.nonkey)
		if err == nil || got != false {
			t.Errorf("Payload.GetBool(%q) = %v, %v; want false, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadStringSlice(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"names": []string{"luke", "rey", "anakin"}},
			key:    "names",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetStringSlice(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetStringSlice(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetStringSlice(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetStringSlice(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetStringSlice(tc.nonkey)
		if err == nil || got != nil {
			t.Errorf("Payload.GetStringSlice(%q) = %v, %v; want nil, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadIntSlice(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"nums": []int{9, 8, 7}},
			key:    "nums",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetIntSlice(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetIntSlice(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetIntSlice(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetIntSlice(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetIntSlice(tc.nonkey)
		if err == nil || got != nil {
			t.Errorf("Payload.GetIntSlice(%q) = %v, %v; want nil, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadStringMap(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"user": map[string]interface{}{"name": "Jon Doe", "score": 2.2}},
			key:    "user",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetStringMap(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetStringMap(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetStringMap(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetStringMap(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetStringMap(tc.nonkey)
		if err == nil || got != nil {
			t.Errorf("Payload.GetStringMap(%q) = %v, %v; want nil, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadStringMapString(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"address": map[string]string{"line": "123 Main St", "city": "San Francisco", "state": "CA"}},
			key:    "address",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetStringMapString(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetStringMapString(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetStringMapString(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetStringMapString(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetStringMapString(tc.nonkey)
		if err == nil || got != nil {
			t.Errorf("Payload.GetStringMapString(%q) = %v, %v; want nil, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadStringMapStringSlice(t *testing.T) {
	favs := map[string][]string{
		"movies":   {"forrest gump", "star wars"},
		"tv_shows": {"game of thrones", "HIMYM", "breaking bad"},
	}
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"favorites": favs},
			key:    "favorites",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetStringMapStringSlice(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetStringMapStringSlice(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetStringMapStringSlice(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetStringMapStringSlice(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetStringMapStringSlice(tc.nonkey)
		if err == nil || got != nil {
			t.Errorf("Payload.GetStringMapStringSlice(%q) = %v, %v; want nil, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadStringMapInt(t *testing.T) {
	counter := map[string]int{
		"a": 1,
		"b": 101,
		"c": 42,
	}
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"counts": counter},
			key:    "counts",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetStringMapInt(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetStringMapInt(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetStringMapInt(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetStringMapInt(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetStringMapInt(tc.nonkey)
		if err == nil || got != nil {
			t.Errorf("Payload.GetStringMapInt(%q) = %v, %v; want nil, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadStringMapBool(t *testing.T) {
	features := map[string]bool{
		"A": false,
		"B": true,
		"C": true,
	}
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"features": features},
			key:    "features",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetStringMapBool(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetStringMapBool(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetStringMapBool(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetStringMapBool(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetStringMapBool(tc.nonkey)
		if err == nil || got != nil {
			t.Errorf("Payload.GetStringMapBool(%q) = %v, %v; want nil, error",
				tc.key, got, err)
		}
	}
}

func TestPayloadTime(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"current": time.Now()},
			key:    "current",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetTime(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetTime(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetTime(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetTime(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetTime(tc.nonkey)
		if err == nil || !got.IsZero() {
			t.Errorf("Payload.GetTime(%q) = %v, %v; want %v, error",
				tc.key, got, err, time.Time{})
		}
	}
}

func TestPayloadDuration(t *testing.T) {
	tests := []payloadTest{
		{
			data:   map[string]interface{}{"duration": 15 * time.Minute},
			key:    "duration",
			nonkey: "unknown",
		},
	}

	for _, tc := range tests {
		payload := Payload{tc.data}

		got, err := payload.GetDuration(tc.key)
		diff := cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("Payload.GetDuration(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// encode and then decode task messsage.
		in := h.NewTaskMessage("testing", tc.data)
		b, err := json.Marshal(in)
		if err != nil {
			t.Fatal(err)
		}
		var out base.TaskMessage
		err = json.Unmarshal(b, &out)
		if err != nil {
			t.Fatal(err)
		}
		payload = Payload{out.Payload}
		got, err = payload.GetDuration(tc.key)
		diff = cmp.Diff(got, tc.data[tc.key])
		if err != nil || diff != "" {
			t.Errorf("With Marshaling: Payload.GetDuration(%q) = %v, %v, want %v, nil",
				tc.key, got, err, tc.data[tc.key])
		}

		// access non-existent key.
		got, err = payload.GetDuration(tc.nonkey)
		if err == nil || got != 0 {
			t.Errorf("Payload.GetDuration(%q) = %v, %v; want %v, error",
				tc.key, got, err, time.Duration(0))
		}
	}
}

func TestPayloadHas(t *testing.T) {
	payload := Payload{map[string]interface{}{
		"user_id": 123,
	}}

	if !payload.Has("user_id") {
		t.Errorf("Payload.Has(%q) = false, want true", "user_id")
	}
	if payload.Has("name") {
		t.Errorf("Payload.Has(%q) = true, want false", "name")
	}
}
