package rc

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"testing"

	"github.com/rclone/rclone/fs"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func clearOptionBlock() func() {
	oldOptionBlock := fs.OptionsRegistry
	fs.OptionsRegistry = map[string]fs.OptionsInfo{}
	return func() {
		fs.OptionsRegistry = oldOptionBlock
	}
}

var testInfo = fs.Options{{
	Name:    "string",
	Default: "str",
	Help:    "It is a string",
}, {
	Name:    "int",
	Default: 17,
	Help:    "It is an int",
}}

var testOptions = struct {
	String string
	Int    int
}{
	String: "hello",
	Int:    42,
}

func registerTestOptions() {
	fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "potato", Opt: &testOptions, Options: testInfo})
}

func registerTestOptionsReload(reload func(context.Context) error) {
	fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "potato", Opt: &testOptions, Options: testInfo, Reload: reload})
}

func TestAddOption(t *testing.T) {
	defer clearOptionBlock()()
	assert.Equal(t, len(fs.OptionsRegistry), 0)
	registerTestOptions()
	assert.Equal(t, len(fs.OptionsRegistry), 1)
	assert.Equal(t, &testOptions, fs.OptionsRegistry["potato"].Opt)
}

func TestAddOptionReload(t *testing.T) {
	defer clearOptionBlock()()
	assert.Equal(t, len(fs.OptionsRegistry), 0)
	reload := func(ctx context.Context) error { return nil }
	registerTestOptionsReload(reload)
	assert.Equal(t, len(fs.OptionsRegistry), 1)
	assert.Equal(t, &testOptions, fs.OptionsRegistry["potato"].Opt)
	assert.Equal(t, fmt.Sprintf("%p", reload), fmt.Sprintf("%p", fs.OptionsRegistry["potato"].Reload))
}

func TestOptionsBlocks(t *testing.T) {
	defer clearOptionBlock()()
	registerTestOptions()
	call := Calls.Get("options/blocks")
	require.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{"options": []string{"potato"}}, out)
}

func TestOptionsGet(t *testing.T) {
	defer clearOptionBlock()()
	registerTestOptions()
	call := Calls.Get("options/get")
	require.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{"potato": &testOptions}, out)
	in = Params{"blocks": "sausage,potato,rhubarb"}
	out, err = call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{"potato": &testOptions}, out)
	in = Params{"blocks": "sausage"}
	out, err = call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{}, out)
}

func TestOptionsGetMarshal(t *testing.T) {
	defer clearOptionBlock()()
	ctx := context.Background()
	ci := fs.GetConfig(ctx)

	// Add some real options
	fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "main", Opt: ci, Options: nil})
	fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "rc", Opt: &Opt, Options: nil})

	// get them
	call := Calls.Get("options/get")
	require.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)

	// Check that they marshal
	_, err = json.Marshal(out)
	require.NoError(t, err)
}

func TestOptionsInfo(t *testing.T) {
	defer clearOptionBlock()()
	registerTestOptions()
	call := Calls.Get("options/info")
	require.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{"potato": testInfo}, out)
	in = Params{"blocks": "sausage,potato,rhubarb"}
	out, err = call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{"potato": testInfo}, out)
	in = Params{"blocks": "sausage"}
	out, err = call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{}, out)
}

func TestOptionsSet(t *testing.T) {
	defer clearOptionBlock()()
	var reloaded int
	registerTestOptionsReload(func(ctx context.Context) error {
		if reloaded > 1 {
			return errors.New("error while reloading")
		}
		reloaded++
		return nil
	})
	call := Calls.Get("options/set")
	require.NotNil(t, call)

	in := Params{
		"potato": Params{
			"Int": 50,
		},
	}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.Nil(t, out)
	assert.Equal(t, 50, testOptions.Int)
	assert.Equal(t, "str", testOptions.String)
	assert.Equal(t, 2, reloaded)

	// error from reload
	_, err = call.Fn(context.Background(), in)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "error while reloading")

	// unknown option block
	in = Params{
		"sausage": Params{
			"Int": 50,
		},
	}
	_, err = call.Fn(context.Background(), in)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "unknown option block")

	// bad shape
	in = Params{
		"potato": []string{"a", "b"},
	}
	_, err = call.Fn(context.Background(), in)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to write options")

}