package rc

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"runtime"
	"strings"
	"testing"

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

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

func TestMain(m *testing.M) {
	// Pretend to be rclone version if we have a version string parameter
	if os.Args[len(os.Args)-1] == "version" {
		fmt.Printf("rclone %s\n", fs.Version)
		os.Exit(0)
	}
	// Pretend to error if we have an unknown command
	if os.Args[len(os.Args)-1] == "unknown_command" {
		fmt.Printf("rclone %s\n", fs.Version)
		fmt.Fprintf(os.Stderr, "Unknown command\n")
		os.Exit(1)
	}
	os.Exit(m.Run())
}

func TestInternalNoop(t *testing.T) {
	call := Calls.Get("rc/noop")
	assert.NotNil(t, call)
	in := Params{
		"String": "hello",
		"Int":    42,
	}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, in, out)
}

func TestInternalError(t *testing.T) {
	call := Calls.Get("rc/error")
	assert.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.Error(t, err)
	require.Nil(t, out)
}

func TestInternalList(t *testing.T) {
	call := Calls.Get("rc/list")
	assert.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, Params{"commands": Calls.List()}, out)
}

func TestCorePid(t *testing.T) {
	call := Calls.Get("core/pid")
	assert.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	pid := out["pid"]
	assert.NotEqual(t, nil, pid)
	_, ok := pid.(int)
	assert.Equal(t, true, ok)
}

func TestCoreMemstats(t *testing.T) {
	call := Calls.Get("core/memstats")
	assert.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	sys := out["Sys"]
	assert.NotEqual(t, nil, sys)
	_, ok := sys.(uint64)
	assert.Equal(t, true, ok)
}

func TestCoreGC(t *testing.T) {
	call := Calls.Get("core/gc")
	assert.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.Nil(t, out)
	assert.Equal(t, Params(nil), out)
}

func TestCoreVersion(t *testing.T) {
	call := Calls.Get("core/version")
	assert.NotNil(t, call)
	in := Params{}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, fs.Version, out["version"])
	assert.Equal(t, runtime.GOOS, out["os"])
	assert.Equal(t, runtime.GOARCH, out["arch"])
	assert.Equal(t, runtime.Version(), out["goVersion"])
	assert.True(t, strings.HasPrefix(out["osArch"].(string), runtime.GOARCH))
	assert.NotEqual(t, "", out["osVersion"].(string))
	assert.NotEqual(t, "", out["osKernel"].(string))
	_ = out["isGit"].(bool)
	v := out["decomposed"].([]int64)
	assert.True(t, len(v) >= 2)
}

func TestCoreObscure(t *testing.T) {
	call := Calls.Get("core/obscure")
	assert.NotNil(t, call)
	in := Params{
		"clear": "potato",
	}
	out, err := call.Fn(context.Background(), in)
	require.NoError(t, err)
	require.NotNil(t, out)
	assert.Equal(t, in["clear"], obscure.MustReveal(out["obscured"].(string)))
}

func TestCoreQuit(t *testing.T) {
	//The call should return an error if param exitCode is not parsed to int
	call := Calls.Get("core/quit")
	assert.NotNil(t, call)
	in := Params{
		"exitCode": "potato",
	}
	_, err := call.Fn(context.Background(), in)
	require.Error(t, err)
}

// core/command: Runs a raw rclone command
func TestCoreCommand(t *testing.T) {
	call := Calls.Get("core/command")

	test := func(command string, returnType string, wantOutput string, fail bool) {
		var rec = httptest.NewRecorder()
		var w http.ResponseWriter = rec

		in := Params{
			"command":   command,
			"opt":       map[string]string{},
			"arg":       []string{},
			"_response": w,
		}
		if returnType != "" {
			in["returnType"] = returnType
		} else {
			returnType = "COMBINED_OUTPUT"
		}
		stream := strings.HasPrefix(returnType, "STREAM")
		got, err := call.Fn(context.Background(), in)
		if stream && fail {
			assert.Error(t, err)
		} else {
			assert.NoError(t, err)
		}

		if !stream {
			assert.Equal(t, wantOutput, got["result"])
			assert.Equal(t, fail, got["error"])
		} else {
			assert.Equal(t, wantOutput, rec.Body.String())
		}
		assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
	}

	version := fmt.Sprintf("rclone %s\n", fs.Version)
	errorString := "Unknown command\n"
	t.Run("OK", func(t *testing.T) {
		test("version", "", version, false)
	})
	t.Run("Fail", func(t *testing.T) {
		test("unknown_command", "", version+errorString, true)
	})
	t.Run("Combined", func(t *testing.T) {
		test("unknown_command", "COMBINED_OUTPUT", version+errorString, true)
	})
	t.Run("Stderr", func(t *testing.T) {
		test("unknown_command", "STREAM_ONLY_STDERR", errorString, true)
	})
	t.Run("Stdout", func(t *testing.T) {
		test("unknown_command", "STREAM_ONLY_STDOUT", version, true)
	})
	t.Run("Stream", func(t *testing.T) {
		test("unknown_command", "STREAM", version+errorString, true)
	})
}
