package integration_tests

import (
	"net/http"
	"os"
	"testing"

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

func TestBatching(t *testing.T) {
	config := ReadConfig("batching")

	chainIDResponse1 := `{"jsonrpc": "2.0", "result": "hello1", "id": 1}`
	chainIDResponse2 := `{"jsonrpc": "2.0", "result": "hello2", "id": 2}`
	chainIDResponse3 := `{"jsonrpc": "2.0", "result": "hello3", "id": 3}`
	netVersionResponse1 := `{"jsonrpc": "2.0", "result": "1.0", "id": 1}`
	callResponse1 := `{"jsonrpc": "2.0", "result": "ekans1", "id": 1}`

	type mockResult struct {
		method string
		id     string
		result interface{}
	}

	chainIDMock1 := mockResult{"eth_chainId", "1", "hello1"}
	chainIDMock2 := mockResult{"eth_chainId", "2", "hello2"}
	chainIDMock3 := mockResult{"eth_chainId", "3", "hello3"}
	netVersionMock1 := mockResult{"net_version", "1", "1.0"}
	callMock1 := mockResult{"eth_call", "1", "ekans1"}

	tests := []struct {
		name                string
		handler             http.Handler
		mocks               []mockResult
		reqs                []*proxyd.RPCReq
		expectedRes         string
		maxBatchSize        int
		numExpectedForwards int
	}{
		{
			name:  "backend returns batches out of order",
			mocks: []mockResult{chainIDMock1, chainIDMock2, chainIDMock3},
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("2", "eth_chainId", nil),
				NewRPCReq("3", "eth_chainId", nil),
			},
			expectedRes:         asArray(chainIDResponse1, chainIDResponse2, chainIDResponse3),
			maxBatchSize:        2,
			numExpectedForwards: 2,
		},
		{
			// infura behavior
			name:    "backend returns single RPC response object as error",
			handler: SingleResponseHandler(500, `{"jsonrpc":"2.0","error":{"code":-32001,"message":"internal server error"},"id":1}`),
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("2", "eth_chainId", nil),
			},
			expectedRes: asArray(
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":1,"jsonrpc":"2.0"}`,
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":2,"jsonrpc":"2.0"}`,
			),
			maxBatchSize:        10,
			numExpectedForwards: 1,
		},
		{
			name:    "backend returns single RPC response object for minibatches",
			handler: SingleResponseHandler(500, `{"jsonrpc":"2.0","error":{"code":-32001,"message":"internal server error"},"id":1}`),
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("2", "eth_chainId", nil),
			},
			expectedRes: asArray(
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":1,"jsonrpc":"2.0"}`,
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":2,"jsonrpc":"2.0"}`,
			),
			maxBatchSize:        1,
			numExpectedForwards: 2,
		},
		{
			name: "duplicate request ids are on distinct batches",
			mocks: []mockResult{
				netVersionMock1,
				chainIDMock2,
				chainIDMock1,
				callMock1,
			},
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "net_version", nil),
				NewRPCReq("2", "eth_chainId", nil),
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("1", "eth_call", nil),
			},
			expectedRes:         asArray(netVersionResponse1, chainIDResponse2, chainIDResponse1, callResponse1),
			maxBatchSize:        2,
			numExpectedForwards: 3,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			config.Server.MaxUpstreamBatchSize = tt.maxBatchSize

			handler := tt.handler
			if handler == nil {
				router := NewBatchRPCResponseRouter()
				for _, mock := range tt.mocks {
					router.SetRoute(mock.method, mock.id, mock.result)
				}
				handler = router
			}

			goodBackend := NewMockBackend(handler)
			defer goodBackend.Close()
			require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", goodBackend.URL()))

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

			res, statusCode, err := client.SendBatchRPC(tt.reqs...)
			require.NoError(t, err)
			require.Equal(t, http.StatusOK, statusCode)
			RequireEqualJSON(t, []byte(tt.expectedRes), res)

			if tt.numExpectedForwards != 0 {
				require.Equal(t, tt.numExpectedForwards, len(goodBackend.Requests()))
			}

			if handler, ok := handler.(*BatchRPCResponseRouter); ok {
				for i, mock := range tt.mocks {
					require.Equal(t, 1, handler.GetNumCalls(mock.method, mock.id), i)
				}
			}
		})
	}
}
