// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package jsonrpc2_test

import (
	"context"
	"errors"
	"testing"
	"time"

	"golang.org/x/exp/jsonrpc2"
	"golang.org/x/exp/jsonrpc2/internal/stack/stacktest"
)

func TestIdleTimeout(t *testing.T) {
	stacktest.NoLeak(t)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{})
	if err != nil {
		t.Fatal(err)
	}
	listener = jsonrpc2.NewIdleListener(100*time.Millisecond, listener)
	defer listener.Close()
	server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{})
	if err != nil {
		t.Fatal(err)
	}

	connect := func() *jsonrpc2.Connection {
		client, err := jsonrpc2.Dial(ctx,
			listener.Dialer(),
			jsonrpc2.ConnectionOptions{})
		if err != nil {
			t.Fatal(err)
		}
		return client
	}
	// Exercise some connection/disconnection patterns, and then assert that when
	// our timer fires, the server exits.
	conn1 := connect()
	conn2 := connect()
	if err := conn1.Close(); err != nil {
		t.Fatalf("conn1.Close failed with error: %v", err)
	}
	if err := conn2.Close(); err != nil {
		t.Fatalf("conn2.Close failed with error: %v", err)
	}
	conn3 := connect()
	if err := conn3.Close(); err != nil {
		t.Fatalf("conn3.Close failed with error: %v", err)
	}

	serverError := server.Wait()

	if !errors.Is(serverError, jsonrpc2.ErrIdleTimeout) {
		t.Errorf("run() returned error %v, want %v", serverError, jsonrpc2.ErrIdleTimeout)
	}
}

type msg struct {
	Msg string
}

type fakeHandler struct{}

func (fakeHandler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) {
	switch req.Method {
	case "ping":
		return &msg{"pong"}, nil
	default:
		return nil, jsonrpc2.ErrNotHandled
	}
}

func TestServe(t *testing.T) {
	stacktest.NoLeak(t)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	tests := []struct {
		name    string
		factory func(context.Context) (jsonrpc2.Listener, error)
	}{
		{"tcp", func(ctx context.Context) (jsonrpc2.Listener, error) {
			return jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{})
		}},
		{"pipe", func(ctx context.Context) (jsonrpc2.Listener, error) {
			return jsonrpc2.NetPipe(ctx)
		}},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			fake, err := test.factory(ctx)
			if err != nil {
				t.Fatal(err)
			}
			conn, shutdown, err := newFake(t, ctx, fake)
			if err != nil {
				t.Fatal(err)
			}
			defer shutdown()
			var got msg
			if err := conn.Call(ctx, "ping", &msg{"ting"}).Await(ctx, &got); err != nil {
				t.Fatal(err)
			}
			if want := "pong"; got.Msg != want {
				t.Errorf("conn.Call(...): returned %q, want %q", got, want)
			}
		})
	}
}

func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(), error) {
	l = jsonrpc2.NewIdleListener(100*time.Millisecond, l)
	server, err := jsonrpc2.Serve(ctx, l, jsonrpc2.ConnectionOptions{
		Handler: fakeHandler{},
	})
	if err != nil {
		return nil, nil, err
	}

	client, err := jsonrpc2.Dial(ctx,
		l.Dialer(),
		jsonrpc2.ConnectionOptions{
			Handler: fakeHandler{},
		})
	if err != nil {
		return nil, nil, err
	}
	return client, func() {
		if err := l.Close(); err != nil {
			t.Fatal(err)
		}
		if err := client.Close(); err != nil {
			t.Fatal(err)
		}
		server.Wait()
	}, nil
}
