package integration_tests

import (
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/ethereum-optimism/optimism/proxyd"
	"github.com/stretchr/testify/require"
)

const (
	notWhitelistedResponse        = `{"jsonrpc":"2.0","error":{"code":-32001,"message":"rpc method is not whitelisted custom message"},"id":999}`
	parseErrResponse              = `{"jsonrpc":"2.0","error":{"code":-32700,"message":"parse error"},"id":null}`
	invalidJSONRPCVersionResponse = `{"error":{"code":-32600,"message":"invalid JSON-RPC version"},"id":null,"jsonrpc":"2.0"}`
	invalidIDResponse             = `{"error":{"code":-32600,"message":"invalid ID"},"id":null,"jsonrpc":"2.0"}`
	invalidMethodResponse         = `{"error":{"code":-32600,"message":"no method specified"},"id":null,"jsonrpc":"2.0"}`
	invalidBatchLenResponse       = `{"error":{"code":-32600,"message":"must specify at least one batch call"},"id":null,"jsonrpc":"2.0"}`
)

func TestSingleRPCValidation(t *testing.T) {
	goodBackend := NewMockBackend(BatchedResponseHandler(200, goodResponse))
	defer goodBackend.Close()

	require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", goodBackend.URL()))

	config := ReadConfig("whitelist")
	client := NewProxydClient("http://127.0.0.1:8545")
	_, shutdown, err := proxyd.Start(config)
	require.NoError(t, err)
	defer shutdown()

	tests := []struct {
		name string
		body string
		res  string
		code int
	}{
		{
			"body not JSON",
			"this ain't an RPC call",
			parseErrResponse,
			400,
		},
		{
			"body not RPC",
			"{\"not\": \"rpc\"}",
			invalidJSONRPCVersionResponse,
			400,
		},
		{
			"body missing RPC ID",
			"{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23]}",
			invalidIDResponse,
			400,
		},
		{
			"body has array ID",
			"{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": []}",
			invalidIDResponse,
			400,
		},
		{
			"body has object ID",
			"{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": {}}",
			invalidIDResponse,
			400,
		},
		{
			"bad method",
			"{\"jsonrpc\": \"2.0\", \"method\": 7, \"params\": [42, 23], \"id\": 1}",
			parseErrResponse,
			400,
		},
		{
			"bad JSON-RPC",
			"{\"jsonrpc\": \"1.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": 1}",
			invalidJSONRPCVersionResponse,
			400,
		},
		{
			"omitted method",
			"{\"jsonrpc\": \"2.0\", \"params\": [42, 23], \"id\": 1}",
			invalidMethodResponse,
			400,
		},
		{
			"not whitelisted method",
			"{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": 999}",
			notWhitelistedResponse,
			403,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			res, code, err := client.SendRequest([]byte(tt.body))
			require.NoError(t, err)
			RequireEqualJSON(t, []byte(tt.res), res)
			require.Equal(t, tt.code, code)
			require.Equal(t, 0, len(goodBackend.Requests()))
		})
	}
}

func TestBatchRPCValidation(t *testing.T) {
	goodBackend := NewMockBackend(BatchedResponseHandler(200, goodResponse))
	defer goodBackend.Close()

	require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", goodBackend.URL()))

	config := ReadConfig("whitelist")
	client := NewProxydClient("http://127.0.0.1:8545")
	_, shutdown, err := proxyd.Start(config)
	require.NoError(t, err)
	defer shutdown()

	tests := []struct {
		name     string
		body     string
		res      string
		code     int
		reqCount int
	}{
		{
			"empty batch",
			"[]",
			invalidBatchLenResponse,
			400,
			0,
		},
		{
			"bad json",
			"[{,]",
			parseErrResponse,
			400,
			0,
		},
		{
			"not object in batch",
			"[123]",
			asArray(parseErrResponse),
			200,
			0,
		},
		{
			"body not RPC",
			"[{\"not\": \"rpc\"}]",
			asArray(invalidJSONRPCVersionResponse),
			200,
			0,
		},
		{
			"body missing RPC ID",
			"[{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23]}]",
			asArray(invalidIDResponse),
			200,
			0,
		},
		{
			"body has array ID",
			"[{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": []}]",
			asArray(invalidIDResponse),
			200,
			0,
		},
		{
			"body has object ID",
			"[{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": {}}]",
			asArray(invalidIDResponse),
			200,
			0,
		},
		// this happens because we can't deserialize the method into a non
		// string value, and it blows up the parsing for the whole request.
		{
			"bad method",
			"[{\"error\":{\"code\":-32600,\"message\":\"invalid request\"},\"id\":null,\"jsonrpc\":\"2.0\"}]",
			asArray(invalidMethodResponse),
			200,
			0,
		},
		{
			"bad JSON-RPC",
			"[{\"jsonrpc\": \"1.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": 1}]",
			asArray(invalidJSONRPCVersionResponse),
			200,
			0,
		},
		{
			"omitted method",
			"[{\"jsonrpc\": \"2.0\", \"params\": [42, 23], \"id\": 1}]",
			asArray(invalidMethodResponse),
			200,
			0,
		},
		{
			"not whitelisted method",
			"[{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": 999}]",
			asArray(notWhitelistedResponse),
			200,
			0,
		},
		{
			"mixed",
			asArray(
				"{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": 999}",
				"{\"jsonrpc\": \"2.0\", \"method\": \"eth_chainId\", \"params\": [], \"id\": 123}",
				"123",
			),
			asArray(
				notWhitelistedResponse,
				goodResponse,
				parseErrResponse,
			),
			200,
			1,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			res, code, err := client.SendRequest([]byte(tt.body))
			require.NoError(t, err)
			RequireEqualJSON(t, []byte(tt.res), res)
			require.Equal(t, tt.code, code)
			require.Equal(t, tt.reqCount, len(goodBackend.Requests()))
		})
	}
}

func TestSizeLimits(t *testing.T) {
	goodBackend := NewMockBackend(BatchedResponseHandler(200, goodResponse))
	defer goodBackend.Close()

	require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", goodBackend.URL()))

	config := ReadConfig("size_limits")
	client := NewProxydClient("http://127.0.0.1:8545")
	_, shutdown, err := proxyd.Start(config)
	require.NoError(t, err)
	defer shutdown()

	payload := strings.Repeat("barf", 1024*1024)
	out, code, err := client.SendRequest([]byte(fmt.Sprintf(`{"jsonrpc": "2.0", "method": "eth_chainId", "params": [%s], "id": 1}`, payload)))
	require.NoError(t, err)
	require.Equal(t, `{"jsonrpc":"2.0","error":{"code":-32021,"message":"request body too large"},"id":null}`, strings.TrimSpace(string(out)))
	require.Equal(t, 413, code)

	// The default response is already over the size limit in size_limits.toml.
	out, code, err = client.SendRequest([]byte(`{"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1}`))
	require.NoError(t, err)
	require.Equal(t, `{"jsonrpc":"2.0","error":{"code":-32020,"message":"backend response too large"},"id":1}`, strings.TrimSpace(string(out)))
	require.Equal(t, 500, code)
}

func asArray(in ...string) string {
	return "[" + strings.Join(in, ",") + "]"
}
