package rc

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

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

func clearOptionBlock() func() {
	oldOptionBlock := optionBlock
	optionBlock = map[string]interface{}{}
	return func() {
		optionBlock = oldOptionBlock
	}
}

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

func TestAddOption(t *testing.T) {
	defer clearOptionBlock()()
	assert.Equal(t, len(optionBlock), 0)
	AddOption("potato", &testOptions)
	assert.Equal(t, len(optionBlock), 1)
	assert.Equal(t, len(optionReload), 0)
	assert.Equal(t, &testOptions, optionBlock["potato"])
}

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

func TestOptionsBlocks(t *testing.T) {
	defer clearOptionBlock()()
	AddOption("potato", &testOptions)
	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()()
	AddOption("potato", &testOptions)
	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)
}

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

	// Add some real options
	AddOption("http", &httplib.DefaultOpt)
	AddOption("main", ci)
	AddOption("rc", &DefaultOpt)

	// 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 TestOptionsSet(t *testing.T) {
	defer clearOptionBlock()()
	var reloaded int
	AddOptionReload("potato", &testOptions, func(ctx context.Context) error {
		if reloaded > 0 {
			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, "hello", testOptions.String)
	assert.Equal(t, 1, 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")

}