// Copyright 2016 The go-ethereum Authors
// (original work)
// Copyright 2024 The Erigon Authors
// (modifications)
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package rpc

import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strings"
	"sync"
	"testing"
	"testing/synctest"
	"time"

	"github.com/davecgh/go-spew/spew"

	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/log/v3"
)

func TestClientRequest(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	var resp echoResult
	if err := client.Call(&resp, "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(resp, echoResult{"hello", 10, &echoArgs{"world"}}) {
		t.Errorf("incorrect result %#v", resp)
	}
}

func TestNullResponse(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	var result json.RawMessage
	if err := client.Call(&result, "test_returnNull"); err != nil {
		t.Fatal(err)
	}
	if string(result) != "null" {
		t.Errorf("expected null, got %s", result)
	}
}

func TestClientResponseType(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	if err := client.Call(nil, "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
		t.Errorf("Passing nil as result should be fine, but got an error: %v", err)
	}
	var resultVar echoResult
	// Note: passing the var, not a ref
	err := client.Call(resultVar, "test_echo", "hello", 10, &echoArgs{"world"})
	if err == nil {
		t.Error("Passing a var as result should be an error")
	}
}

// This test checks that server-returned errors with code and data come out of Client.Call.
func TestClientErrorData(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	var resp any
	err := client.Call(&resp, "test_returnError")
	if err == nil {
		t.Fatal("expected error")
	}

	// Check code.
	if e, ok := err.(Error); !ok {
		t.Fatalf("client did not return rpc.Error, got %#v", e)
	} else if e.ErrorCode() != (testError{}.ErrorCode()) {
		t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), testError{}.ErrorCode())
	}
	// Check data.
	if e, ok := err.(DataError); !ok {
		t.Fatalf("client did not return rpc.DataError, got %#v", e)
	} else if e.ErrorData() != (testError{}.ErrorData()) {
		t.Fatalf("wrong error data %#v, want %#v", e.ErrorData(), testError{}.ErrorData())
	}
}

func TestClientBatchRequest(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	batch := []BatchElem{
		{
			Method: "test_echo",
			Args:   []any{"hello", 10, &echoArgs{"world"}},
			Result: new(echoResult),
		},
		{
			Method: "test_echo",
			Args:   []any{"hello2", 11, &echoArgs{"world"}},
			Result: new(echoResult),
		},
		{
			Method: "no_such_method",
			Args:   []any{1, 2, 3},
			Result: new(int),
		},
	}
	if err := client.BatchCall(batch); err != nil {
		t.Fatal(err)
	}
	wantResult := []BatchElem{
		{
			Method: "test_echo",
			Args:   []any{"hello", 10, &echoArgs{"world"}},
			Result: &echoResult{"hello", 10, &echoArgs{"world"}},
		},
		{
			Method: "test_echo",
			Args:   []any{"hello2", 11, &echoArgs{"world"}},
			Result: &echoResult{"hello2", 11, &echoArgs{"world"}},
		},
		{
			Method: "no_such_method",
			Args:   []any{1, 2, 3},
			Result: new(int),
			Error:  &jsonError{Code: -32601, Message: "the method no_such_method does not exist/is not available"},
		},
	}
	if !reflect.DeepEqual(batch, wantResult) {
		t.Errorf("batch results mismatch:\ngot %swant %s", spew.Sdump(batch), spew.Sdump(wantResult))
	}
}

func TestClientNotify(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	if err := client.Notify(context.Background(), "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
		t.Fatal(err)
	}
}

// func TestClientCancelInproc(t *testing.T) { testClientCancel("inproc", t) }
func TestClientCancelWebsocket(t *testing.T) { testClientCancel("ws", t, log.New()) }
func TestClientCancelHTTP(t *testing.T)      { testClientCancel("http", t, log.New()) }

// testClientCancel checks that requests made through CallContext can be canceled
// by canceling the context at various stages of request processing:
//   - cancel during dial
//   - cancel while performing an HTTP request
//   - cancel while waiting for a response
//
// The HTTP transport uses synctest with in-memory connections for deterministic timing.
// The WebSocket transport uses real TCP because its long-lived server goroutines
// (pingLoop, ServeCodec, dispatch) have complex shutdown dependencies incompatible
// with synctest's requirement that all bubble goroutines exit.
func testClientCancel(transport string, t *testing.T, logger log.Logger) {
	if testing.Short() {
		t.Skip()
	}

	switch transport {
	case "http":
		testClientCancelSynctest(t, logger)
	case "ws":
		testClientCancelTCP(t, logger)
	default:
		panic("unknown transport: " + transport)
	}
}

// runCancelCallers spawns ncallers goroutines, each making nreqs calls to test_block
// with random context cancellation. No call should ever succeed because test_block
// blocks until the context is canceled.
func runCancelCallers(t *testing.T, client *Client, maxContextCancelTimeout time.Duration) {
	var (
		wg       sync.WaitGroup
		nreqs    = 10
		ncallers = 10
	)
	caller := func(index int) {
		defer wg.Done()
		for i := 0; i < nreqs; i++ {
			var (
				ctx     context.Context
				cancel  func()
				timeout = time.Duration(rand.Int63n(int64(maxContextCancelTimeout)))
			)
			if index < ncallers/2 {
				// Half the callers cancel via explicit cancel func.
				ctx, cancel = context.WithCancel(context.Background())
				time.AfterFunc(timeout, cancel)
			} else {
				// Other half use context deadline, which also sets the
				// socket write deadline — a different cancellation path.
				ctx, cancel = context.WithTimeout(context.Background(), timeout)
			}
			err := client.CallContext(ctx, nil, "test_block")
			if err == nil {
				_, hasDeadline := ctx.Deadline()
				t.Errorf("no error for call with %v wait time (deadline: %v)", timeout, hasDeadline)
			}
			cancel()
		}
	}
	wg.Add(ncallers)
	for i := 0; i < ncallers; i++ {
		go caller(i)
	}
	wg.Wait()
}

// testClientCancelSynctest runs the cancel test using synctest with in-memory
// connections. All time.Sleep/time.AfterFunc use fake time, so it runs instantly
// regardless of CI machine speed.
func testClientCancelSynctest(t *testing.T, logger log.Logger) {
	synctest.Test(t, func(t *testing.T) {
		server := newTestServer(logger)

		// To trigger cancels at various stages, the times are chosen such that
		// connections are killed within the deadline for every other call
		// (maxKillTimeout is 2x maxCancelTimeout).
		// Once a connection is dead, there is a fair chance it won't connect
		// successfully because the accept is delayed by 1s.
		fl := &flakeyListener{
			maxAcceptDelay: 1 * time.Second,
			maxKillTimeout: 600 * time.Millisecond,
		}

		client, hs := memHTTPTestClient(server, fl)
		defer hs.Close()
		defer server.Stop()

		runCancelCallers(t, client, 300*time.Millisecond)
	})
}

// testClientCancelTCP runs the cancel test over real TCP connections.
func testClientCancelTCP(t *testing.T, logger log.Logger) {
	t.Parallel()

	server := newTestServer(logger)
	defer server.Stop()

	// To trigger cancels at various stages, the times are chosen such that
	// connections are killed within the deadline for every other call
	// (maxKillTimeout is 2x maxCancelTimeout).
	// Once a connection is dead, there is a fair chance it won't connect
	// successfully because the accept is delayed by 1s.
	fl := &flakeyListener{
		maxAcceptDelay: 1 * time.Second,
		maxKillTimeout: 600 * time.Millisecond,
	}

	client, hs := httpTestClient(server, "ws", fl)
	defer hs.Close()
	defer client.Close()

	runCancelCallers(t, client, 300*time.Millisecond)
}

func TestClientSubscribeInvalidArg(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	check := func(shouldPanic bool, arg any) {
		defer func() {
			err := recover()
			if shouldPanic && err == nil {
				t.Errorf("EthSubscribe should've panicked for %#v", arg)
			}
			if !shouldPanic && err != nil {
				t.Errorf("EthSubscribe shouldn't have panicked for %#v", arg)
				t.Error(err)
				t.Error(dbg.Stack())
			}
		}()
		client.EthSubscribe(context.Background(), arg, "foo_bar")
	}
	check(true, nil)
	check(true, 1)
	check(true, (chan int)(nil))
	check(true, make(<-chan int))
	check(false, make(chan int))
	check(false, make(chan<- int))
}

func TestClientSubscribe(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	nc := make(chan int)
	count := 10
	sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", count, 0)
	if err != nil {
		t.Fatal("can't subscribe:", err)
	}
	for i := 0; i < count; i++ {
		if val := <-nc; val != i {
			t.Fatalf("value mismatch: got %d, want %d", val, i)
		}
	}

	sub.Unsubscribe()
	select {
	case v := <-nc:
		t.Fatal("received value after unsubscribe:", v)
	case err := <-sub.Err():
		if err != nil {
			t.Fatalf("Err returned a non-nil error after explicit unsubscribe: %q", err)
		}
	case <-time.After(1 * time.Second):
		t.Fatalf("subscription not closed within 1s after unsubscribe")
	}
}

// In this test, the connection drops while Subscribe is waiting for a response.
func TestClientSubscribeClose(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	service := &notificationTestService{
		gotHangSubscriptionReq:  make(chan struct{}),
		unblockHangSubscription: make(chan struct{}),
	}
	if err := server.RegisterName("nftest2", service); err != nil {
		t.Fatal(err)
	}

	defer server.Stop()
	client := DialInProc(server, logger)
	defer client.Close()

	var (
		nc   = make(chan int)
		errc = make(chan error, 1)
		sub  *ClientSubscription
		err  error
	)
	go func() {
		sub, err = client.Subscribe(context.Background(), "nftest2", nc, "hangSubscription", 999)
		errc <- err
	}()

	<-service.gotHangSubscriptionReq
	client.Close()
	service.unblockHangSubscription <- struct{}{}

	select {
	case err := <-errc:
		if err == nil {
			t.Errorf("Subscribe returned nil error after Close")
		}
		if sub != nil {
			t.Error("Subscribe returned non-nil subscription after Close")
		}
	case <-time.After(1 * time.Second):
		t.Fatalf("Subscribe did not return within 1s after Close")
	}
}

// This test reproduces https://github.com/erigontech/erigon/issues/17837 where the
// client hangs during shutdown when Unsubscribe races with Client.Close.
func TestClientCloseUnsubscribeRace(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()

	for i := 0; i < 20; i++ {
		client := DialInProc(server, logger)
		nc := make(chan int)
		sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", 3, 1)
		if err != nil {
			t.Fatal(err)
		}
		go client.Close()
		go sub.Unsubscribe()
		select {
		case <-sub.Err():
		case <-time.After(5 * time.Second):
			t.Fatal("subscription not closed within timeout")
		}
	}
}

// This test checks that Client doesn't lock up when a single subscriber
// doesn't read subscription events.
func TestClientNotificationStorm(t *testing.T) {
	if testing.Short() {
		t.Skip("slow test")
	}
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()

	doTest := func(count int, wantError bool) {
		client := DialInProc(server, logger)
		defer client.Close()
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		// Subscribe on the server. It will start sending many notifications
		// very quickly.
		nc := make(chan int)
		sub, err := client.Subscribe(ctx, "nftest", nc, "someSubscription", count, 0)
		if err != nil {
			t.Fatal("can't subscribe:", err)
		}
		defer sub.Unsubscribe()

		// Process each notification, try to run a call in between each of them.
		for i := 0; i < count; i++ {
			select {
			case val := <-nc:
				if val != i {
					t.Fatalf("(%d/%d) unexpected value %d", i, count, val)
				}
			case err := <-sub.Err():
				if wantError && err != ErrSubscriptionQueueOverflow {
					t.Fatalf("(%d/%d) got error %q, want %q", i, count, err, ErrSubscriptionQueueOverflow)
				} else if !wantError {
					t.Fatalf("(%d/%d) got unexpected error %q", i, count, err)
				}
				return
			}
			var r int
			err := client.CallContext(ctx, &r, "nftest_echo", i)
			if err != nil {
				if !wantError {
					t.Fatalf("(%d/%d) call error: %v", i, count, err)
				}
				return
			}
		}
		if wantError {
			t.Fatalf("didn't get expected error")
		}
	}

	doTest(8000, false)
	doTest(30000, true)
}

func TestClientSetHeader(t *testing.T) {
	logger := log.New()
	var gotHeader bool
	srv := newTestServer(logger)
	httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("test") == "ok" {
			gotHeader = true
		}
		srv.ServeHTTP(w, r)
	}))
	defer httpsrv.Close()
	defer srv.Stop()

	client, err := Dial(httpsrv.URL, logger)
	if err != nil {
		t.Fatal(err)
	}
	defer client.Close()

	client.SetHeader("test", "ok")
	if _, err1 := client.SupportedModules(); err1 != nil {
		t.Fatal(err1)
	}
	if !gotHeader {
		t.Fatal("client did not set custom header")
	}

	// Check that Content-Type can be replaced.
	client.SetHeader("content-type", "application/x-garbage")
	_, err = client.SupportedModules()
	if err == nil {
		t.Fatal("no error for invalid content-type header")
	} else if !strings.Contains(err.Error(), "Unsupported Media Type") {
		t.Fatalf("error is not related to content-type: %q", err)
	}
}

func TestClientHTTP(t *testing.T) {
	logger := log.New()
	server := newTestServer(logger)
	defer server.Stop()

	client, hs := httpTestClient(server, "http", nil)
	defer hs.Close()
	defer client.Close()

	// Launch concurrent requests.
	var (
		results    = make([]echoResult, 100)
		errc       = make(chan error, len(results))
		wantResult = echoResult{"a", 1, new(echoArgs)}
	)
	defer client.Close()
	for i := range results {
		go func() {
			errc <- client.Call(&results[i], "test_echo", wantResult.String, wantResult.Int, wantResult.Args)
		}()
	}

	// Wait for all of them to complete.
	timeout := time.NewTimer(5 * time.Second)
	defer timeout.Stop()
	for i := range results {
		select {
		case err := <-errc:
			if err != nil {
				t.Fatal(err)
			}
		case <-timeout.C:
			t.Fatalf("timeout (got %d/%d) results)", i+1, len(results))
		}
	}

	// Check results.
	for i := range results {
		if !reflect.DeepEqual(results[i], wantResult) {
			t.Errorf("result %d mismatch: got %#v, want %#v", i, results[i], wantResult)
		}
	}
}

func TestClientReconnect(t *testing.T) {
	if testing.Short() {
		t.Skip()
	}

	logger := log.New()
	startServer := func(addr string) (*Server, net.Listener) {
		srv := newTestServer(logger)
		l, err := net.Listen("tcp", addr)
		if err != nil {
			t.Fatal("can't listen:", err)
		}
		go http.Serve(l, srv.WebsocketHandler([]string{"*"}, nil, false, logger))
		return srv, l
	}

	ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
	defer cancel()

	// Start a server and corresponding client.
	s1, l1 := startServer("127.0.0.1:0")
	client, err := DialContext(ctx, "ws://"+l1.Addr().String(), logger)
	if err != nil {
		t.Fatal("can't dial", err)
	}

	// Perform a call. This should work because the server is up.
	var resp echoResult
	if err := client.CallContext(ctx, &resp, "test_echo", "", 1, nil); err != nil {
		t.Fatal(err)
	}

	// Shut down the server and allow for some cool down time so we can listen on the same
	// address again.
	l1.Close()
	s1.Stop()
	time.Sleep(2 * time.Second)

	// Try calling again. It shouldn't work.
	if err := client.CallContext(ctx, &resp, "test_echo", "", 2, nil); err == nil {
		t.Error("successful call while the server is down")
		t.Logf("resp: %#v", resp)
	}

	// Start it up again and call again. The connection should be reestablished.
	// We spawn multiple calls here to check whether this hangs somehow.
	s2, l2 := startServer(l1.Addr().String())
	defer l2.Close()
	defer s2.Stop()

	start := make(chan struct{})
	errors := make(chan error, 20)
	for i := 0; i < cap(errors); i++ {
		go func() {
			<-start
			var resp echoResult
			errors <- client.CallContext(ctx, &resp, "test_echo", "", 3, nil)
		}()
	}
	close(start)
	errcount := 0
	for i := 0; i < cap(errors); i++ {
		if err = <-errors; err != nil {
			errcount++
		}
	}
	t.Logf("%d errors, last error: %v", errcount, err)
	if errcount > 1 {
		t.Errorf("expected one error after disconnect, got %d", errcount)
	}
}

func httpTestClient(srv *Server, transport string, fl *flakeyListener) (*Client, *httptest.Server) {
	logger := log.New()
	// Create the HTTP server.
	var hs *httptest.Server
	switch transport {
	case "ws":
		hs = httptest.NewUnstartedServer(srv.WebsocketHandler([]string{"*"}, nil, false, logger))
	case "http":
		hs = httptest.NewUnstartedServer(srv)
	default:
		panic("unknown HTTP transport: " + transport)
	}
	// Wrap the listener if required.
	if fl != nil {
		fl.Listener = hs.Listener
		hs.Listener = fl
	}
	// Connect the client.
	hs.Start()
	client, err := Dial(transport+"://"+hs.Listener.Addr().String(), logger)
	if err != nil {
		panic(err)
	}
	return client, hs
}

// flakeyListener kills accepted connections after a random timeout.
type flakeyListener struct {
	net.Listener
	maxKillTimeout time.Duration
	maxAcceptDelay time.Duration
}

func (l *flakeyListener) Accept() (net.Conn, error) {
	delay := time.Duration(rand.Int63n(int64(l.maxAcceptDelay)))
	time.Sleep(delay)

	c, err := l.Listener.Accept()
	if err == nil {
		timeout := time.Duration(rand.Int63n(int64(l.maxKillTimeout)))
		time.AfterFunc(timeout, func() {
			log.Trace(fmt.Sprintf("killing conn %v after %v", c.LocalAddr(), timeout))
			c.Close()
		})
	}
	return c, err
}

// memListener is an in-memory net.Listener backed by net.Pipe.
// It is compatible with synctest because it uses no real I/O.
type memListener struct {
	connCh chan net.Conn
	done   chan struct{}
	once   sync.Once
}

func newMemListener() *memListener {
	return &memListener{
		connCh: make(chan net.Conn),
		done:   make(chan struct{}),
	}
}

func (l *memListener) Accept() (net.Conn, error) {
	select {
	case c := <-l.connCh:
		return c, nil
	case <-l.done:
		return nil, net.ErrClosed
	}
}

func (l *memListener) Close() error {
	l.once.Do(func() { close(l.done) })
	return nil
}

func (l *memListener) Addr() net.Addr {
	return memAddr{}
}

// Dial creates a new in-memory connection to this listener.
func (l *memListener) Dial(ctx context.Context, _, _ string) (net.Conn, error) {
	server, client := net.Pipe()
	select {
	case l.connCh <- server:
		return client, nil
	case <-l.done:
		server.Close()
		client.Close()
		return nil, net.ErrClosed
	case <-ctx.Done():
		server.Close()
		client.Close()
		return nil, ctx.Err()
	}
}

type memAddr struct{}

func (memAddr) Network() string { return "mem" }
func (memAddr) String() string  { return "mem" }

// memHTTPTestClient creates an HTTP test client using in-memory connections
// (no real TCP). Compatible with synctest.
func memHTTPTestClient(srv *Server, fl *flakeyListener) (*Client, *http.Server) {
	logger := log.New()
	ml := newMemListener()

	var listener net.Listener = ml
	if fl != nil {
		fl.Listener = ml
		listener = fl
	}

	hs := &http.Server{Handler: srv}
	go hs.Serve(listener)

	httpClient := &http.Client{
		Transport: &http.Transport{
			DialContext: ml.Dial,
		},
	}

	client, err := DialHTTPWithClient("http://mem", httpClient, logger)
	if err != nil {
		panic(err)
	}
	return client, hs
}
