package rc

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/rclone/rclone/fs"
)

func TestErrParamNotFoundError(t *testing.T) {
	e := ErrParamNotFound("key")
	assert.Equal(t, "Didn't find key \"key\" in input", e.Error())
}

func TestIsErrParamNotFound(t *testing.T) {
	assert.Equal(t, true, IsErrParamNotFound(ErrParamNotFound("key")))
	assert.Equal(t, false, IsErrParamNotFound(nil))
	assert.Equal(t, false, IsErrParamNotFound(errors.New("potato")))
}

func TestNotErrParamNotFound(t *testing.T) {
	assert.Equal(t, false, NotErrParamNotFound(ErrParamNotFound("key")))
	assert.Equal(t, false, NotErrParamNotFound(nil))
	assert.Equal(t, true, NotErrParamNotFound(errors.New("potato")))
}

func TestIsErrParamInvalid(t *testing.T) {
	e := ErrParamInvalid{errors.New("potato")}
	assert.Equal(t, true, IsErrParamInvalid(e))
	assert.Equal(t, false, IsErrParamInvalid(nil))
	assert.Equal(t, false, IsErrParamInvalid(errors.New("potato")))
}

func TestReshape(t *testing.T) {
	in := Params{
		"String": "hello",
		"Float":  4.2,
	}
	var out struct {
		String string
		Float  float64
	}
	require.NoError(t, Reshape(&out, in))
	assert.Equal(t, "hello", out.String)
	assert.Equal(t, 4.2, out.Float)
	var inCopy = Params{}
	require.NoError(t, Reshape(&inCopy, out))
	assert.Equal(t, in, inCopy)

	// Now a failure to marshal
	var in2 func()
	require.Error(t, Reshape(&inCopy, in2))

	// Now a failure to unmarshal
	require.Error(t, Reshape(&out, "string"))

}

func TestParamsCopy(t *testing.T) {
	in := Params{
		"ok":  1,
		"x":   "seventeen",
		"nil": nil,
	}
	out := in.Copy()
	assert.Equal(t, in, out)
	if &in == &out {
		t.Error("didn't copy")
	}
}

func TestParamsGet(t *testing.T) {
	in := Params{
		"ok": 1,
	}
	v1, e1 := in.Get("ok")
	assert.NoError(t, e1)
	assert.Equal(t, 1, v1)
	v2, e2 := in.Get("notOK")
	assert.Error(t, e2)
	assert.Equal(t, nil, v2)
	assert.Equal(t, ErrParamNotFound("notOK"), e2)
}

func TestParamsGetString(t *testing.T) {
	in := Params{
		"string":    "one",
		"notString": 17,
	}
	v1, e1 := in.GetString("string")
	assert.NoError(t, e1)
	assert.Equal(t, "one", v1)
	v2, e2 := in.GetString("notOK")
	assert.Error(t, e2)
	assert.Equal(t, "", v2)
	assert.Equal(t, ErrParamNotFound("notOK"), e2)
	v3, e3 := in.GetString("notString")
	assert.Error(t, e3)
	assert.Equal(t, "", v3)
	assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}

func TestParamsGetInt64(t *testing.T) {
	for _, test := range []struct {
		value     any
		result    int64
		errString string
	}{
		{"123", 123, ""},
		{"123x", 0, "couldn't parse"},
		{int(12), 12, ""},
		{int64(13), 13, ""},
		{float64(14), 14, ""},
		{float64(9.3e18), 0, "overflows int64"},
		{float64(-9.3e18), 0, "overflows int64"},
	} {
		t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
			in := Params{
				"key": test.value,
			}
			v1, e1 := in.GetInt64("key")
			if test.errString == "" {
				require.NoError(t, e1)
				assert.Equal(t, test.result, v1)
			} else {
				require.NotNil(t, e1)
				require.Error(t, e1)
				assert.Contains(t, e1.Error(), test.errString)
				assert.Equal(t, int64(0), v1)
			}
		})
	}
	in := Params{
		"notInt64": []string{"a", "b"},
	}
	v2, e2 := in.GetInt64("notOK")
	assert.Error(t, e2)
	assert.Equal(t, int64(0), v2)
	assert.Equal(t, ErrParamNotFound("notOK"), e2)
	v3, e3 := in.GetInt64("notInt64")
	assert.Error(t, e3)
	assert.Equal(t, int64(0), v3)
	assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}

func TestParamsGetFloat64(t *testing.T) {
	for _, test := range []struct {
		value     any
		result    float64
		errString string
	}{
		{"123.1", 123.1, ""},
		{"123x1", 0, "couldn't parse"},
		{int(12), 12, ""},
		{int64(13), 13, ""},
		{float64(14), 14, ""},
	} {
		t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
			in := Params{
				"key": test.value,
			}
			v1, e1 := in.GetFloat64("key")
			if test.errString == "" {
				require.NoError(t, e1)
				assert.Equal(t, test.result, v1)
			} else {
				require.NotNil(t, e1)
				require.Error(t, e1)
				assert.Contains(t, e1.Error(), test.errString)
				assert.Equal(t, float64(0), v1)
			}
		})
	}
	in := Params{
		"notFloat64": []string{"a", "b"},
	}
	v2, e2 := in.GetFloat64("notOK")
	assert.Error(t, e2)
	assert.Equal(t, float64(0), v2)
	assert.Equal(t, ErrParamNotFound("notOK"), e2)
	v3, e3 := in.GetFloat64("notFloat64")
	assert.Error(t, e3)
	assert.Equal(t, float64(0), v3)
	assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}

func TestParamsGetDuration(t *testing.T) {
	for _, test := range []struct {
		value     any
		result    time.Duration
		errString string
	}{
		{"86400", time.Hour * 24, ""},
		{"1y", time.Hour * 24 * 365, ""},
		{"60", time.Minute * 1, ""},
		{"0", 0, ""},
		{"-45", -time.Second * 45, ""},
		{"2", time.Second * 2, ""},
		{"2h4m7s", time.Hour*2 + 4*time.Minute + 7*time.Second, ""},
		{"3d", time.Hour * 24 * 3, ""},
		{"off", time.Duration(fs.DurationOff), ""},
		{"", 0, "parse duration"},
		{12, 0, "expecting string"},
		{"34y", time.Hour * 24 * 365 * 34, ""},
		{"30d", time.Hour * 24 * 30, ""},
		{"2M", time.Hour * 24 * 60, ""},
		{"wrong", 0, "parse duration"},
	} {
		t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
			in := Params{
				"key": test.value,
			}
			v1, e1 := in.GetDuration("key")
			if test.errString == "" {
				require.NoError(t, e1)
				assert.Equal(t, test.result, v1)
			} else {
				require.NotNil(t, e1)
				require.Error(t, e1)
				assert.Contains(t, e1.Error(), test.errString)
				assert.Equal(t, time.Duration(0), v1)
			}
		})
	}
	in := Params{
		"notDuration": []string{"a", "b"},
	}
	v2, e2 := in.GetDuration("notOK")
	assert.Error(t, e2)
	assert.Equal(t, time.Duration(0), v2)
	assert.Equal(t, ErrParamNotFound("notOK"), e2)
	v3, e3 := in.GetDuration("notDuration")
	assert.Error(t, e3)
	assert.Equal(t, time.Duration(0), v3)
	assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}

func TestParamsGetBool(t *testing.T) {
	for _, test := range []struct {
		value     any
		result    bool
		errString string
	}{
		{true, true, ""},
		{false, false, ""},
		{"true", true, ""},
		{"false", false, ""},
		{"fasle", false, "couldn't parse"},
		{int(12), true, ""},
		{int(0), false, ""},
		{int64(13), true, ""},
		{int64(0), false, ""},
		{float64(14), true, ""},
		{float64(0), false, ""},
	} {
		t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
			in := Params{
				"key": test.value,
			}
			v1, e1 := in.GetBool("key")
			if test.errString == "" {
				require.NoError(t, e1)
				assert.Equal(t, test.result, v1)
			} else {
				require.NotNil(t, e1)
				require.Error(t, e1)
				assert.Contains(t, e1.Error(), test.errString)
				assert.Equal(t, false, v1)
			}
		})
	}
	in := Params{
		"notBool": []string{"a", "b"},
	}
	v2, e2 := Params{}.GetBool("notOK")
	assert.Error(t, e2)
	assert.Equal(t, false, v2)
	assert.Equal(t, ErrParamNotFound("notOK"), e2)
	v3, e3 := in.GetBool("notBool")
	assert.Error(t, e3)
	assert.Equal(t, false, v3)
	assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}

func TestParamsGetStruct(t *testing.T) {
	in := Params{
		"struct": Params{
			"String": "one",
			"Float":  4.2,
		},
	}
	var out struct {
		String string
		Float  float64
	}
	e1 := in.GetStruct("struct", &out)
	assert.NoError(t, e1)
	assert.Equal(t, "one", out.String)
	assert.Equal(t, 4.2, out.Float)

	e2 := in.GetStruct("notOK", &out)
	assert.Error(t, e2)
	assert.Equal(t, "one", out.String)
	assert.Equal(t, 4.2, out.Float)
	assert.Equal(t, ErrParamNotFound("notOK"), e2)

	in["struct"] = "string"
	e3 := in.GetStruct("struct", &out)
	assert.Error(t, e3)
	assert.Equal(t, "one", out.String)
	assert.Equal(t, 4.2, out.Float)
	assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}

func TestParamsGetStructString(t *testing.T) {
	in := Params{
		"struct": `{"String": "one", "Float": 4.2}`,
	}
	var out struct {
		String string
		Float  float64
	}
	e1 := in.GetStruct("struct", &out)
	assert.NoError(t, e1)
	assert.Equal(t, "one", out.String)
	assert.Equal(t, 4.2, out.Float)
}

func TestParamsGetStructMissingOK(t *testing.T) {
	in := Params{
		"struct": Params{
			"String": "one",
			"Float":  4.2,
		},
	}
	var out struct {
		String string
		Float  float64
	}
	e1 := in.GetStructMissingOK("struct", &out)
	assert.NoError(t, e1)
	assert.Equal(t, "one", out.String)
	assert.Equal(t, 4.2, out.Float)

	e2 := in.GetStructMissingOK("notOK", &out)
	assert.NoError(t, e2)
	assert.Equal(t, "one", out.String)
	assert.Equal(t, 4.2, out.Float)

	in["struct"] = "string"
	e3 := in.GetStructMissingOK("struct", &out)
	assert.Error(t, e3)
	assert.Equal(t, "one", out.String)
	assert.Equal(t, 4.2, out.Float)
	assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}

func TestParamsGetHTTPRequest(t *testing.T) {
	in := Params{}
	req, err := in.GetHTTPRequest()
	assert.Nil(t, req)
	assert.Error(t, err)
	assert.Equal(t, true, IsErrParamNotFound(err), err.Error())

	in = Params{
		"_request": 42,
	}
	req, err = in.GetHTTPRequest()
	assert.Nil(t, req)
	assert.Error(t, err)
	assert.Equal(t, true, IsErrParamInvalid(err), err.Error())

	r := new(http.Request)
	in = Params{
		"_request": r,
	}
	req, err = in.GetHTTPRequest()
	assert.NotNil(t, req)
	assert.NoError(t, err)
	assert.Equal(t, r, req)
}

func TestParamsGetHTTPResponseWriter(t *testing.T) {
	in := Params{}
	wr, err := in.GetHTTPResponseWriter()
	assert.Nil(t, wr)
	assert.Error(t, err)
	assert.Equal(t, true, IsErrParamNotFound(err), err.Error())

	in = Params{
		"_response": 42,
	}
	wr, err = in.GetHTTPResponseWriter()
	assert.Nil(t, wr)
	assert.Error(t, err)
	assert.Equal(t, true, IsErrParamInvalid(err), err.Error())

	var w http.ResponseWriter = httptest.NewRecorder()
	in = Params{
		"_response": w,
	}
	wr, err = in.GetHTTPResponseWriter()
	assert.NotNil(t, wr)
	assert.NoError(t, err)
	assert.Equal(t, w, wr)
}
