// Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved.

package jrpc2_test

import (
	"context"
	"errors"
	"strings"
	"sync"
	"testing"
	"testing/synctest"
	"time"

	"github.com/creachadair/jrpc2"
	"github.com/creachadair/jrpc2/channel"
	"github.com/creachadair/jrpc2/handler"
	"github.com/creachadair/jrpc2/server"
	"github.com/google/go-cmp/cmp"
)

// Verify that a notification handler will not deadlock with the dispatcher on
// holding the server lock. See: https://github.com/creachadair/jrpc2/issues/27
func TestLockRaceRegression(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		local := server.NewLocal(handler.Map{
			// Do some busy-work and then try to get the server lock, in this case
			// via the CancelRequest helper.
			"Kill": handler.New(func(ctx context.Context, req *jrpc2.Request) error {
				var id string
				if err := req.UnmarshalParams(&handler.Args{&id}); err != nil {
					return err
				}
				time.Sleep(10 * time.Second)
				t.Logf("Notification handler woke up; cancelling request ID %q", id)
				jrpc2.ServerFromContext(ctx).CancelRequest(id)
				return nil
			}),

			// Block indefinitely, just to give the dispatcher something to do.
			"Stall": handler.New(func(ctx context.Context) error {
				<-ctx.Done()
				return ctx.Err()
			}),
		}, nil)
		defer local.Close()

		local.Client.Notify(t.Context(), "Kill", handler.Args{"1"})
		synctest.Wait() // till the notification handler is "busy"

		// This should return once cancellation completes. But if we deadlocked
		// on the server mutex, it will never return and the receive on done
		// below will block. Note that we can't just synctest.Wait, because a
		// mutex does not count as durably blocking. However, the channel read
		// pending a close by our goroutine does count.
		done := make(chan struct{})
		go func() {
			defer close(done)
			if rsp, err := local.Client.Call(ctx, "Stall", nil); !errors.Is(err, context.Canceled) {
				t.Errorf("Call Stall: got (%v, %v), want %v", rsp, err, context.Canceled)
			}
		}()
		<-done
	})
}

// Verify that if a callback handler panics, the client will report an error
// back to the server. See https://github.com/creachadair/jrpc2/issues/41.
func TestOnCallbackPanicRegression(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		const panicString = "the devil you say"

		loc := server.NewLocal(handler.Map{
			"Test": handler.New(func(ctx context.Context) error {
				rsp, err := jrpc2.ServerFromContext(ctx).Callback(ctx, "Poke", nil)
				if err == nil {
					t.Errorf("Callback unexpectedly succeeded: %#q", rsp.ResultString())
				} else if !strings.HasSuffix(err.Error(), panicString) {
					t.Errorf("Callback reported unexpected error: %v", err)
				} else {
					t.Logf("Callback reported expected error: %v", err)
				}
				return nil
			}),
		}, &server.LocalOptions{
			Server: &jrpc2.ServerOptions{
				AllowPush: true,
			},
			Client: &jrpc2.ClientOptions{
				OnCallback: func(ctx context.Context, req *jrpc2.Request) (any, error) {
					t.Log("Entering callback handler; about to panic")
					panic(panicString)
				},
			},
		})
		defer loc.Close()

		if _, err := loc.Client.Call(t.Context(), "Test", nil); err != nil {
			t.Errorf("Call unexpectedly failed: %v", err)
		}
	})
}

// Verify that a duplicate request ID that arrives while a task is in flight
// does not cause the existing task to be cancelled.
func TestDuplicateIDCancellation(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		tctx, cancel := context.WithCancel(t.Context())
		defer cancel()

		cch, sch := channel.Direct()
		srv := jrpc2.NewServer(handler.Map{
			"Test": handler.New(func(ctx context.Context, req *jrpc2.Request) error {
				t.Logf("Test method running, request ID %q", req.ID())
				select {
				case <-tctx.Done():
					t.Log("Request ending normally (test signal)")
				case <-ctx.Done():
					t.Error("Request was unexpected cancelled by the server")
				}
				return nil
			}),
		}, nil).Start(sch)

		send := func(s string) {
			if err := cch.Send([]byte(s)); err != nil {
				t.Errorf("Send %#q failed: %v", s, err)
			}
		}
		expect := func(s string) {
			bits, err := cch.Recv()
			if err != nil {
				t.Errorf("Recv failed: %v", err)
			} else if got := string(bits); got != s {
				t.Errorf("Recv: got %#q, want %#q", got, s)
			}
		}

		const duplicateReq = `{"jsonrpc":"2.0", "id":1, "method":"Test"}`

		// Send the first request and wait for the handler to start.
		send(duplicateReq)
		synctest.Wait()

		// Send the duplicate, which should report an error.
		send(duplicateReq)
		expect(`{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"duplicate request ID","data":"1"}}`)

		// Unblock the handler, which should now complete. If the duplicate request
		// caused the handler to cancel, it will have logged an error to fail the test.
		cancel()
		expect(`{"jsonrpc":"2.0","id":1,"result":null}`)

		cch.Close()
		srv.Wait()
	})
}

func TestCheckBatchDuplicateID(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		srv, cli := channel.Direct()
		s := jrpc2.NewServer(handler.Map{
			"Test": testOK,
		}, nil).Start(srv)
		defer func() {
			cli.Close()
			if err := s.Wait(); err != nil {
				t.Errorf("Server wait: unexpected error: %v", err)
			}
		}()

		// A batch of requests containing two calls with the same ID.
		const input = `[
  {"jsonrpc": "2.0", "id": 1, "method": "Test"},
  {"jsonrpc": "2.0", "id": 1, "method": "Test"},
  {"jsonrpc": "2.0", "id": 2, "method": "Test"}
]
`
		const errorReply = `{` +
			`"jsonrpc":"2.0",` +
			`"id":1,` +
			`"error":{"code":-32600,"message":"duplicate request ID","data":"1"}` +
			`}`
		const want = `[` + errorReply + `,` + errorReply + `,` + `{"jsonrpc":"2.0","id":2,"result":"OK"}]`

		if err := cli.Send([]byte(input)); err != nil {
			t.Fatalf("Send %d bytes failed: %v", len(input), err)
		}
		rsp, err := cli.Recv()
		if err != nil {
			t.Fatalf("Recv failed: %v", err)
		}
		if diff := cmp.Diff(want, string(rsp)); diff != "" {
			t.Errorf("Server response: (-want, +got)\n%s", diff)
		}
	})
}

// Verify that callbacks from notification handlers cannot deadlock on delivery
// of their own replies. Reported in #78, test case courtesy of @radeksimko.
func TestServer_NotificationCallbackDeadlock(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		var wg sync.WaitGroup

		loc := server.NewLocal(handler.Map{
			"NotifyMe": handler.New(func(ctx context.Context) error {
				defer wg.Done()
				time.Sleep(1 * time.Second) // work, work
				if _, err := jrpc2.ServerFromContext(ctx).Callback(ctx, "succeed", nil); err != nil {
					t.Errorf("Callback failed: %v", err)
				}
				return nil
			}),
		}, &server.LocalOptions{
			Server: &jrpc2.ServerOptions{AllowPush: true},
			Client: &jrpc2.ClientOptions{
				OnCallback: func(ctx context.Context, req *jrpc2.Request) (any, error) {
					switch req.Method() {
					case "succeed":
						return true, nil
					}
					panic("broken test: you should not see this")
				},
			},
		})
		defer loc.Close()

		// Call the notification method that posts a callback, twice.
		wg.Add(2)

		if err := loc.Client.Notify(t.Context(), "NotifyMe", nil); err != nil {
			t.Fatalf("Notify: unexpected error: %v", err)
		}
		if err := loc.Client.Notify(t.Context(), "NotifyMe", nil); err != nil {
			t.Fatalf("Notify: unexpected error: %v", err)
		}
		synctest.Wait() // wait for the handlers to be busy
		wg.Wait()       // wait for the handlers to be done
	})
}
