// Copyright (C) 2016-2021  Nexedi SA and Contributors.
//                          Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.

package neonet

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net"
	"reflect"
	"runtime"
	"testing"
	"time"

	"lab.nexedi.com/kirr/go123/exc"
	"lab.nexedi.com/kirr/go123/xerr"
	"lab.nexedi.com/kirr/go123/xsync"

	"lab.nexedi.com/kirr/neo/go/internal/packed"

	"lab.nexedi.com/kirr/neo/go/neo/proto"
	"lab.nexedi.com/kirr/neo/go/zodb"

	"github.com/tinylib/msgp/msgp"

	"github.com/kylelemons/godebug/pretty"
	"github.com/pkg/errors"
)

// T is neonet testing environment.
type T struct {
	*testing.T
	enc proto.Encoding // encoding to use for messages exchange
}

// Verify tests f for all possible environments.
func Verify(t *testing.T, f func(*T)) {
	// for each encoding
	for _, enc := range []proto.Encoding{'N', 'M'} {
		t.Run(fmt.Sprintf("enc=%c", enc), func(t *testing.T) {
			f(&T{t, enc})
		})
	}
}

// bin returns payload for raw binary data as it would-be encoded by t.enc .
func (t *T) bin(data string) []byte {
	switch t.enc {
	case 'N': return []byte(data)
	case 'M': return msgp.AppendBytes(nil, []byte(data))
	default:  panic("bug")
	}
}


func xclose(c io.Closer) {
	err := c.Close()
	exc.Raiseif(err)
}

func xnewconn(nl *NodeLink) *Conn {
	c, err := nl.NewConn()
	exc.Raiseif(err)
	return c
}

func xaccept(nl *NodeLink) *Conn {
	c, err := nl.Accept()
	exc.Raiseif(err)
	return c
}

func xsendPkt(c interface{ sendPkt(*pktBuf) error }, pkt *pktBuf) {
	err := c.sendPkt(pkt)
	exc.Raiseif(err)
}

func xrecvPkt(c interface{ recvPkt() (*pktBuf, error) }) *pktBuf {
	pkt, err := c.recvPkt()
	exc.Raiseif(err)
	return pkt
}

func xwait(w interface{ Wait() error }) {
	err := w.Wait()
	exc.Raiseif(err)
}

func gox(wg interface { Go(func(context.Context) error) }, xf func(context.Context)) {
	wg.Go(func(ctx context.Context) error {
		return exc.Funcx(func() {
			xf(ctx)
		})()
	})
}

// xlinkError verifies that err is *LinkError and returns err.Err .
func xlinkError(err error) error {
	le, ok := err.(*LinkError)
	if !ok {
		exc.Raisef("%#v is not *LinkError", err)
	}
	return le.Err
}

// xconnError verifies that err is *ConnError and returns err.Err .
func xconnError(err error) error {
	ce, ok := err.(*ConnError)
	if !ok {
		exc.Raisef("%#v is not *ConnError", err)
	}
	return ce.Err
}

// Prepare pktBuf with content.
func _mkpkt(enc proto.Encoding, connid uint32, msgcode uint16, payload []byte) *pktBuf {
	// XXX -> enc.PktHeadEncode + enc.MsgEncode
	switch enc {
	case 'N':
		pkt := &pktBuf{make([]byte, proto.PktHeaderLenN+len(payload))}
		h := pkt.HeaderN()
		h.ConnId  = packed.Hton32(connid)
		h.MsgCode = packed.Hton16(msgcode)
		h.MsgLen  = packed.Hton32(uint32(len(payload)))
		copy(pkt.PayloadN(), payload)
		return pkt

	case 'M':
		var b []byte
		b = msgp.AppendArrayHeader (b, 3)
		b = msgp.AppendUint32      (b, connid)
		b = msgp.AppendUint16      (b, msgcode)
		// NOTE payload is assumed to be valid msgpack-encoded object.
		b = append                 (b, payload...)
		return &pktBuf{b}

	default:
		panic("bug")
	}
}

func (c *Conn) mkpkt(msgcode uint16, payload []byte) *pktBuf {
	// in Conn exchange connid is automatically set by Conn.sendPkt
	return _mkpkt(c.link.enc, c.connId, msgcode, payload)
}

// Verify pktBuf is as expected.
func (t *T) xverifyPkt(pkt *pktBuf, connid uint32, msgcode uint16, payload []byte) {
	errv := xerr.Errorv{}

	pktConnID, pktMsgCode, pktPayload, err := pktDecodeHead(t.enc, pkt)
	exc.Raiseif(err)

	// TODO include caller location
	if pktConnID != connid {
		errv.Appendf("header: unexpected connid %v  (want %v)", pktConnID, connid)
	}
	if pktMsgCode != msgcode {
		errv.Appendf("header: unexpected msgcode %v  (want %v)", pktMsgCode, msgcode)
	}
	if !bytes.Equal(pktPayload, payload) {
		errv.Appendf("payload differ:\n%s",
			pretty.Compare(string(payload), string(pktPayload)))
	}

	exc.Raiseif(errv.Err())
}

// Verify pktBuf to match expected message.
func (t *T) xverifyPktMsg(pkt *pktBuf, connid uint32, msg proto.Msg) {
	data := make([]byte, t.enc.MsgEncodedLen(msg))
	t.enc.MsgEncode(msg, data)
	t.xverifyPkt(pkt, connid, proto.MsgCode(msg), data)
}

// delay a bit.
//
// needed e.g. to test Close interaction with waiting read or write
// (we cannot easily sync and make sure e.g. read is started and became asleep)
//
// XXX JM suggested to really wait till syscall starts this way:
// - via polling get traceback for thread that is going to call syscall and eventually block
// - if from that traceback we can see that blocking syscall is already called
//   -> this way we can know that it is already blocking and thus sleep-hack can be avoided
// this can be done via runtime/pprof -> "goroutine" predefined profile
func tdelay() {
	time.Sleep(1 * time.Millisecond)
}

// create NodeLinks connected via net.Pipe; messages are encoded via t.enc.
func (t *T) _nodeLinkPipe(flags1, flags2 _LinkRole) (nl1, nl2 *NodeLink) {
	node1, node2 := net.Pipe()
	nl1 = newNodeLink(node1, t.enc, _LinkClient|flags1, nil)
	nl2 = newNodeLink(node2, t.enc, _LinkServer|flags2, nil)
	return nl1, nl2
}

func (t *T) nodeLinkPipe() (nl1, nl2 *NodeLink) {
	return t._nodeLinkPipe(0, 0)
}

func TestNodeLink(t *testing.T) {
	Verify(t, _TestNodeLink)
}
func _TestNodeLink(t *T) {
	// TODO catch exception -> add proper location from it -> t.Fatal (see git-backup)
	b := t.bin
	bg := context.Background()

	// Close vs recvPkt
	nl1, nl2 := t._nodeLinkPipe(linkNoRecvSend, linkNoRecvSend)
	wg := xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		tdelay()
		xclose(nl1)
	})
	pkt, err := nl1.recvPkt()
	if !(pkt == nil && err == io.ErrClosedPipe) {
		t.Fatalf("NodeLink.recvPkt() after close: pkt = %v  err = %v", pkt, err)
	}
	xwait(wg)
	xclose(nl2)

	// Close vs sendPkt
	nl1, nl2 = t._nodeLinkPipe(linkNoRecvSend, linkNoRecvSend)
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		tdelay()
		xclose(nl1)
	})
	pkt = &pktBuf{[]byte("data")}
	err = nl1.sendPkt(pkt)
	if err != io.ErrClosedPipe {
		t.Fatalf("NodeLink.sendPkt() after close: err = %v", err)
	}
	xwait(wg)
	xclose(nl2)

	// {Close,CloseAccept} vs Accept
	nl1, nl2 = t._nodeLinkPipe(linkNoRecvSend, linkNoRecvSend)
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		tdelay()
		xclose(nl2)
	})
	c, err := nl2.Accept()
	if !(c == nil && xlinkError(err) == ErrLinkClosed) {
		t.Fatalf("NodeLink.Accept() after close: conn = %v, err = %v", c, err)
	}
	gox(wg, func(_ context.Context) {
		tdelay()
		nl1.CloseAccept()
	})
	c, err = nl1.Accept()
	if !(c == nil && xlinkError(err) == ErrLinkNoListen) {
		t.Fatalf("NodeLink.Accept() after CloseAccept: conn = %v, err = %v", c, err)
	}
	xwait(wg)
	// nl1 is now not accepting connections - because it was CloseAccept'ed
	// check further Accept behaviour.
	c, err = nl1.Accept()
	if !(c == nil && xlinkError(err) == ErrLinkNoListen) {
		t.Fatalf("NodeLink.Accept() on non-listening node link: conn = %v, err = %v", c, err)
	}
	xclose(nl1)

	// Close vs recvPkt on another side
	nl1, nl2 = t._nodeLinkPipe(linkNoRecvSend, linkNoRecvSend)
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		tdelay()
		xclose(nl2)
	})
	pkt, err = nl1.recvPkt()
	if !(pkt == nil && err == io.EOF) { // NOTE io.EOF on Read per io.Pipe
		t.Fatalf("NodeLink.recvPkt() after peer shutdown: pkt = %v  err = %v", pkt, err)
	}
	xwait(wg)
	xclose(nl1)

	// Close vs sendPkt on another side
	nl1, nl2 = t._nodeLinkPipe(linkNoRecvSend, linkNoRecvSend)
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		tdelay()
		xclose(nl2)
	})
	pkt = &pktBuf{[]byte("data")}
	err = nl1.sendPkt(pkt)
	if err != io.ErrClosedPipe { // NOTE io.ErrClosedPipe on Write per io.Pipe
		t.Fatalf("NodeLink.sendPkt() after peer shutdown: pkt = %v  err = %v", pkt, err)
	}
	xwait(wg)
	xclose(nl1)

	// raw exchange
	nl1, nl2 = t._nodeLinkPipe(linkNoRecvSend, linkNoRecvSend)

	wg = xsync.NewWorkGroup(bg)
	okch := make(chan int, 2)
	gox(wg, func(_ context.Context) {
		// send ping; wait for pong
		pkt := _mkpkt(t.enc, 1, 2, b("ping"))
		xsendPkt(nl1, pkt)
		pkt = xrecvPkt(nl1)
		t.xverifyPkt(pkt, 3, 4, b("pong"))
		okch <- 1
	})
	gox(wg, func(_ context.Context) {
		// wait for ping; send pong
		pkt = xrecvPkt(nl2)
		t.xverifyPkt(pkt, 1, 2, b("ping"))
		pkt = _mkpkt(t.enc, 3, 4, b("pong"))
		xsendPkt(nl2, pkt)
		okch <- 2
	})

	// close nodelinks either when checks are done, or upon first error
	gox(wg, func(ctx context.Context) {
		nok := 0
	loop:
		for {
			select {
			case <-ctx.Done():
				break loop
			case <-okch:
				nok++
				if nok == 2 {
					break loop
				}
			}
		}

		xclose(nl1)
		xclose(nl2)
	})

	xwait(wg)

	// ---- connections on top of nodelink ----

	// Close vs recvPkt
	nl1, nl2 = t._nodeLinkPipe(0, linkNoRecvSend)
	c = xnewconn(nl1)
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		tdelay()
		xclose(c)
	})
	pkt, err = c.recvPkt()
	if !(pkt == nil && xconnError(err) == ErrClosedConn) {
		t.Fatalf("Conn.recvPkt() after close: pkt = %v  err = %v", pkt, err)
	}
	xwait(wg)
	xclose(nl1)
	xclose(nl2)

	// Close vs sendPkt
	nl1, nl2 = t._nodeLinkPipe(0, linkNoRecvSend)
	c = xnewconn(nl1)
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		tdelay()
		xclose(c)
	})
	pkt = c.mkpkt(0, b("data"))
	err = c.sendPkt(pkt)
	if xconnError(err) != ErrClosedConn {
		t.Fatalf("Conn.sendPkt() after close: err = %v", err)
	}
	xwait(wg)

	// NodeLink.Close vs Conn.sendPkt/recvPkt
	c11 := xnewconn(nl1)
	c12 := xnewconn(nl1)
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		pkt, err := c11.recvPkt()
		if !(pkt == nil && xconnError(err) == ErrLinkClosed) {
			exc.Raisef("Conn.recvPkt() after NodeLink close: pkt = %v  err = %v", pkt, err)
		}
	})
	gox(wg, func(_ context.Context) {
		pkt := c12.mkpkt(0, b("data"))
		err := c12.sendPkt(pkt)
		if xconnError(err) != ErrLinkClosed {
			exc.Raisef("Conn.sendPkt() after NodeLink close: err = %v", err)
		}
	})
	tdelay()
	xclose(nl1)
	xwait(wg)
	xclose(c11)
	xclose(c12)
	xclose(nl2)

	// NodeLink.Close vs Conn.sendPkt/recvPkt and Accept on another side
	nl1, nl2 = t._nodeLinkPipe(linkNoRecvSend, 0)
	c21 := xnewconn(nl2)
	c22 := xnewconn(nl2)
	c23 := xnewconn(nl2)
	wg = xsync.NewWorkGroup(bg)
	var errRecv error
	gox(wg, func(_ context.Context) {
		pkt, err := c21.recvPkt()
		want1 := io.EOF           // if recvPkt wakes up due to peer close
		want2 := io.ErrClosedPipe // if recvPkt wakes up due to sendPkt wakes up first and closes nl1
		cerr := xconnError(err)
		if !(pkt == nil && (cerr == want1 || cerr == want2)) {
			exc.Raisef("Conn.recvPkt after peer NodeLink shutdown: pkt = %v  err = %v", pkt, err)
		}

		errRecv = cerr
	})
	gox(wg, func(_ context.Context) {
		pkt := c22.mkpkt(0, b("data"))
		err := c22.sendPkt(pkt)
		want := io.ErrClosedPipe // always this in both due to peer close or recvPkt waking up and closing nl2
		if xconnError(err) != want {
			exc.Raisef("Conn.sendPkt after peer NodeLink shutdown: %v", err)
		}

	})
	gox(wg, func(_ context.Context) {
		conn, err := nl2.Accept()
		if !(conn == nil && xlinkError(err) == ErrLinkDown) {
			exc.Raisef("Accept after peer NodeLink shutdown: conn = %v  err = %v", conn, err)
		}
	})
	tdelay()
	xclose(nl1)
	xwait(wg)

	// XXX denoise vvv

	// NewConn after NodeLink shutdown
	c, err = nl2.NewConn()
	if xlinkError(err) != ErrLinkDown {
		t.Fatalf("NewConn after NodeLink shutdown: %v", err)
	}

	// Accept after NodeLink shutdown
	c, err = nl2.Accept()
	if xlinkError(err) != ErrLinkDown {
		t.Fatalf("Accept after NodeLink shutdown: conn = %v  err = %v", c, err)
	}

	// recvPkt/sendPkt on another Conn
	pkt, err = c23.recvPkt()
	if !(pkt == nil && xconnError(err) == errRecv) {
		t.Fatalf("Conn.recvPkt 2 after peer NodeLink shutdown: pkt = %v  err = %v", pkt, err)
	}
	err = c23.sendPkt(c23.mkpkt(0, b("data")))
	if xconnError(err) != ErrLinkDown {
		t.Fatalf("Conn.sendPkt 2 after peer NodeLink shutdown: %v", err)
	}

	// recvPkt/sendPkt error on second call
	pkt, err = c21.recvPkt()
	if !(pkt == nil && xconnError(err) == ErrLinkDown) {
		t.Fatalf("Conn.recvPkt after NodeLink shutdown: pkt = %v  err = %v", pkt, err)
	}
	err = c22.sendPkt(c22.mkpkt(0, b("data")))
	if xconnError(err) != ErrLinkDown {
		t.Fatalf("Conn.sendPkt after NodeLink shutdown: %v", err)
	}

	xclose(c23)
	// recvPkt/sendPkt on closed Conn but not closed NodeLink
	pkt, err = c23.recvPkt()
	if !(pkt == nil && xconnError(err) == ErrClosedConn) {
		t.Fatalf("Conn.recvPkt after close but only stopped NodeLink: pkt = %v  err = %v", pkt, err)
	}
	err = c23.sendPkt(c23.mkpkt(0, b("data")))
	if xconnError(err) != ErrClosedConn {
		t.Fatalf("Conn.sendPkt after close but only stopped NodeLink: %v", err)
	}

	xclose(nl2)
	// recvPkt/sendPkt NewConn/Accept error after NodeLink close
	pkt, err = c21.recvPkt()
	if !(pkt == nil && xconnError(err) == ErrLinkClosed) {
		t.Fatalf("Conn.recvPkt after NodeLink shutdown: pkt = %v  err = %v", pkt, err)
	}
	err = c22.sendPkt(c22.mkpkt(0, b("data")))
	if xconnError(err) != ErrLinkClosed {
		t.Fatalf("Conn.sendPkt after NodeLink shutdown: %v", err)
	}

	c, err = nl2.NewConn()
	if xlinkError(err) != ErrLinkClosed {
		t.Fatalf("NewConn after NodeLink close: %v", err)
	}
	c, err = nl2.Accept()
	if xlinkError(err) != ErrLinkClosed {
		t.Fatalf("Accept after NodeLink close: %v", err)
	}

	xclose(c21)
	xclose(c22)
	// recvPkt/sendPkt error after Close & NodeLink shutdown
	pkt, err = c21.recvPkt()
	if !(pkt == nil && xconnError(err) == ErrClosedConn) {
		t.Fatalf("Conn.recvPkt after close and NodeLink close: pkt = %v  err = %v", pkt, err)
	}
	err = c22.sendPkt(c22.mkpkt(0, b("data")))
	if xconnError(err) != ErrClosedConn {
		t.Fatalf("Conn.sendPkt after close and NodeLink close: %v", err)
	}


	saveKeepClosed := connKeepClosed
	connKeepClosed = 10 * time.Millisecond

	// Conn accept + exchange
	nl1, nl2 = t.nodeLinkPipe()
	nl1.CloseAccept()
	wg = xsync.NewWorkGroup(bg)
	closed := make(chan int)
	gox(wg, func(_ context.Context) {
		c := xaccept(nl2)

		pkt := xrecvPkt(c)
		t.xverifyPkt(pkt, c.connId, 33, b("ping"))

		// change pkt a bit and send it back
		xsendPkt(c, c.mkpkt(34, b("pong")))

		// one more time
		pkt = xrecvPkt(c)
		t.xverifyPkt(pkt, c.connId, 35, b("ping2"))
		xsendPkt(c, c.mkpkt(36, b("pong2")))

		xclose(c)
		closed <- 1

		// once again as ^^^ but finish only with CloseRecv
		c2 := xaccept(nl2)
		pkt = xrecvPkt(c2)
		t.xverifyPkt(pkt, c2.connId, 41, b("ping5"))
		xsendPkt(c2, c2.mkpkt(42, b("pong5")))

		c2.CloseRecv()
		closed <- 2

		// "connection refused" when trying to connect to not-listening peer
		c = xnewconn(nl2) // XXX should get error here?
		xsendPkt(c, c.mkpkt(38, b("pong3")))
		pkt = xrecvPkt(c)
		t.xverifyPktMsg(pkt, c.connId, errConnRefused)
		xsendPkt(c, c.mkpkt(40, b("pong4"))) // once again
		pkt = xrecvPkt(c)
		t.xverifyPktMsg(pkt, c.connId, errConnRefused)

		xclose(c)

	})

	c1 := xnewconn(nl1)
	xsendPkt(c1, c1.mkpkt(33, b("ping")))
	pkt = xrecvPkt(c1)
	t.xverifyPkt(pkt, c1.connId, 34, b("pong"))
	xsendPkt(c1, c1.mkpkt(35, b("ping2")))
	pkt = xrecvPkt(c1)
	t.xverifyPkt(pkt, c1.connId, 36, b("pong2"))

	// "connection closed" after peer closed its end
	<-closed
	xsendPkt(c1, c1.mkpkt(37, b("ping3")))
	pkt = xrecvPkt(c1)
	t.xverifyPktMsg(pkt, c1.connId, errConnClosed)
	xsendPkt(c1, c1.mkpkt(39, b("ping4"))) // once again
	pkt = xrecvPkt(c1)
	t.xverifyPktMsg(pkt, c1.connId, errConnClosed)
	// XXX also should get EOF on recv

	// one more time but now peer does only .CloseRecv()
	c2 := xnewconn(nl1)
	xsendPkt(c2, c2.mkpkt(41, b("ping5")))
	pkt = xrecvPkt(c2)
	t.xverifyPkt(pkt, c2.connId, 42, b("pong5"))
	<-closed
	xsendPkt(c2, c2.mkpkt(41, b("ping6")))
	pkt = xrecvPkt(c2)
	t.xverifyPktMsg(pkt, c2.connId, errConnClosed)

	xwait(wg)

	// make sure entry for closed nl2.1 stays in nl2.connTab
	nl2.connMu.Lock()
	if cnl2 := nl2.connTab[1]; cnl2 == nil {
		t.Fatal("nl2.connTab[1] == nil  ; want \"closed\" entry")
	}
	nl2.connMu.Unlock()

	// make sure "closed" entry goes away after its time
	time.Sleep(3*connKeepClosed)
	nl2.connMu.Lock()
	if cnl2 := nl2.connTab[1]; cnl2 != nil {
		t.Fatalf("nl2.connTab[1] == %v after close time window  ; want nil", cnl2)
	}
	nl2.connMu.Unlock()

	xclose(c1)
	xclose(c2)
	xclose(nl1)
	xclose(nl2)
	connKeepClosed = saveKeepClosed

	// test 2 channels with replies coming in reversed time order
	nl1, nl2 = t.nodeLinkPipe()
	wg = xsync.NewWorkGroup(bg)
	replyOrder := map[uint16]struct { // "order" in which to process requests
		start chan struct{} // processing starts when start chan is ready
		next  uint16        // after processing this switch to next
	}{
		2: {make(chan struct{}), 1},
		1: {make(chan struct{}), 0},
	}
	close(replyOrder[2].start)

	gox(wg, func(_ context.Context) {
		for _ = range replyOrder {
			c := xaccept(nl2)

			gox(wg, func(_ context.Context) {
				pkt := xrecvPkt(c)
				_, msgCode, _, err := pktDecodeHead(t.enc, pkt)
				exc.Raiseif(err)
				x := replyOrder[msgCode]

				// wait before it is our turn & echo pkt back
				<-x.start
				xsendPkt(c, pkt)

				xclose(c)

				// tell next it can start
				if x.next != 0 {
					close(replyOrder[x.next].start)
				}
			})
		}
	})

	c1 = xnewconn(nl1)
	c2 = xnewconn(nl1)
	xsendPkt(c1, c1.mkpkt(1, b("")))
	xsendPkt(c2, c2.mkpkt(2, b("")))

	// replies must be coming in reverse order
	xechoWait := func(c *Conn, msgCode uint16) {
		pkt := xrecvPkt(c)
		t.xverifyPkt(pkt, c.connId, msgCode, b(""))
	}
	xechoWait(c2, 2)
	xechoWait(c1, 1)
	xwait(wg)

	xclose(c1)
	xclose(c2)
	xclose(nl1)
	xclose(nl2)
}


// ---- recv1 mode ----

func xSend(c *Conn, msg proto.Msg) {
	err := c.Send(msg)
	exc.Raiseif(err)
}

func xRecv(c *Conn) proto.Msg {
	msg, err := c.Recv()
	exc.Raiseif(err)
	return msg
}

func xRecv1(l *NodeLink) Request {
	req, err := l.Recv1()
	exc.Raiseif(err)
	return req
}

func xSend1(l *NodeLink, msg proto.Msg) {
	err := l.Send1(msg)
	exc.Raiseif(err)
}

func xverifyMsg(msg1, msg2 proto.Msg) {
	if !reflect.DeepEqual(msg1, msg2) {
		exc.Raisef("messages differ:\n%s", pretty.Compare(msg1, msg2))
	}
}

func TestRecv1Mode(t *testing.T) {
	Verify(t, _TestRecv1Mode)
}
func _TestRecv1Mode(t *T) {
	bg := context.Background()

	// Send1
	nl1, nl2 := t.nodeLinkPipe()
	wg := xsync.NewWorkGroup(bg)
	sync := make(chan int)
	gox(wg, func(_ context.Context) {
		defer func() {
			if e := recover(); e != nil {
				panic(e)
			}
		}()

		c := xaccept(nl2)
		msg := xRecv(c)
		xverifyMsg(msg, &proto.Ping{})
		xSend(c, &proto.Pong{})
		msg = xRecv(c)
		xverifyMsg(msg, errConnClosed)
		xclose(c)

		sync <- 1

		c = xaccept(nl2)
		msg = xRecv(c)
		xverifyMsg(msg, &proto.Error{proto.ACK, "hello up there"})
		xSend(c, &proto.Error{proto.ACK, "hello to you too"})
		msg = xRecv(c)
		xverifyMsg(msg, errConnClosed)
		xclose(c)
	})

	xSend1(nl1, &proto.Ping{})

	// before next Send1 wait till peer receives errConnClosed from us
	// otherwise peer could be already in accept while our errConnClosed is received
	// and there is only one receiving thread there ^^^
	<-sync
	xSend1(nl1, &proto.Error{proto.ACK, "hello up there"})
	xwait(wg)

	// Recv1: further packets with same connid are rejected with "connection closed"
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		c := xnewconn(nl2)

		xSend(c, &proto.Ping{})
		xSend(c, &proto.Ping{})
		msg := xRecv(c)
		xverifyMsg(msg, errConnClosed)
	})

	_ = xRecv1(nl1)
	xwait(wg)

	// TODO link.Close vs Recv1
}

// test possible race-condition between link.shutdown and conn.lightClose:
//
// link.shutdown sets link.connTab=nil and under link.connMu and then iterates
// read connTab without link.connMu held. If conn.lightClose does
// conn.release() in parallel to link.shutdown() iterating connTab they can be
// both writing/using e.g. conn.rxdownOnce.
//
// bug triggers under -race.
func TestLightCloseVsLinkShutdown(t *testing.T) {
	Verify(t, _TestLightCloseVsLinkShutdown)
}
func _TestLightCloseVsLinkShutdown(t *T) {
	nl1, nl2 := t.nodeLinkPipe()
	wg := xsync.NewWorkGroup(context.Background())

	c := xnewconn(nl1)
	nl1.shutdown()
	gox(wg, func(_ context.Context) {
		c.lightClose()
	})

	xwait(wg)
	xclose(nl1)
	xclose(nl2)
}

// ---- benchmarks ----

// rtt over chan - for comparison as base.
func benchmarkChanRTT(b *testing.B, c12, c21 chan byte) {
	go func() {
		for {
			c, ok := <-c12
			if !ok {
				break
			}

			c21 <- c
		}
	}()

	for i := 0; i < b.N; i++ {
		c := byte(i)
		c12 <- c
		cc := <-c21
		if cc != c {
			b.Fatalf("sent %q != got %q", c, cc)
		}
	}

	close(c12)
}

func BenchmarkSyncChanRTT(b *testing.B) {
	benchmarkChanRTT(b, make(chan byte), make(chan byte))
}

func BenchmarkBufChanRTT(b *testing.B) {
	benchmarkChanRTT(b, make(chan byte, 1), make(chan byte, 1))
}

// rtt over (acceptq, rxq) & ack channels - base comparison for link.Accept + conn.Recv .
func BenchmarkBufChanAXRXRTT(b *testing.B) {
	axq := make(chan chan byte)
	ack := make(chan byte)
	go func() {
		for {
			// accept
			rxq, ok := <-axq
			if !ok {
				break
			}

			// recv
			c := <-rxq

			// send back
			ack <- c
		}
	}()

	rxq := make(chan byte, 1) // buffered
	for i := 0; i < b.N; i++ {
		c := byte(i)
		axq <- rxq
		rxq <- c
		cc := <-ack
		if cc != c {
			b.Fatalf("sent %q != got %q", c, cc)
		}
	}

	close(axq)
}


var gosched = make(chan struct{})

// GoschedLocal is like runtime.Gosched but queues current goroutine on P-local
// runqueue instead of global runqueue.
// FIXME does not work - in the end goroutines appear on different Ps/Ms
func GoschedLocal() {
	go func() {
		gosched <- struct{}{}
	}()
	<-gosched
}

// rtt over net.Conn Read/Write
// if serveRecv=t - do RX path with additional serveRecv-style goroutine
func benchmarkNetConnRTT(b *testing.B, c1, c2 net.Conn, serveRecv bool, ghandoff bool) {
	buf1 := make([]byte, 1)
	buf2 := make([]byte, 1)

	// make func to recv from c into buf via selected rx strategy
	mkrecv := func(c net.Conn, buf []byte) func() (int, error) {
		var recv func() (int, error)
		if serveRecv {
			type rx struct {
				n   int
				erx error
			}
			rxq := make(chan rx, 1)
			rxghandoff := make(chan struct{})
			var serveRx func()
			serveRx = func() {
				for {
					n, erx := io.ReadFull(c, buf)
					//fmt.Printf("(go) %p rx -> %v %v\n", c, n, erx)
					rxq <- rx{n, erx}

					// good: reduce switch to receiver G latency
					// see comment about rxghandoff in serveRecv
					// in case of TCP/loopback saves ~5μs
					if ghandoff {
						<-rxghandoff
					}

					// stop on first error
					if erx != nil {
						return
					}

					if false {
						// bad - puts G in global runq and so it changes M
						runtime.Gosched()
					}
					if false {
						// bad - same as runtime.Gosched
						GoschedLocal()
					}

					if false {
						// bad - in the end Gs appear on different Ms
						go serveRx()
						return
					}
				}
			}

			go serveRx()

			recv = func() (int, error) {
				r := <-rxq
				if ghandoff {
					rxghandoff <- struct{}{}
				}
				return r.n, r.erx
			}

		} else {
			recv = func() (int, error) {
				return io.ReadFull(c, buf)
			}
		}
		return recv
	}

	recv1 := mkrecv(c1, buf1)
	recv2 := mkrecv(c2, buf2)


	b.ResetTimer()

	go func() {
		defer func() {
			//fmt.Printf("2: close\n")
			xclose(c2)
		}()

		for {
			n, erx := recv2()
			//fmt.Printf("2: rx %q\n", buf2[:n])
			if n > 0 {
				if n != len(buf2) {
					b.Fatalf("read -> %d bytes  ; want %d", n, len(buf2))
				}

				//fmt.Printf("2: tx %q\n", buf2)
				_, etx := c2.Write(buf2)
				if etx != nil {
					b.Fatal(etx)
				}
			}

			switch erx {
			case nil:
				// ok

			case io.ErrClosedPipe, io.EOF: // net.Pipe, TCP
				return

			default:
				b.Fatal(erx) // XXX cannot call b.Fatal from non-main goroutine?
			}
		}
	}()

	for i := 0; i < b.N; i++ {
		c := byte(i)
		buf1[0] = c
		//fmt.Printf("1: tx %q\n", buf1)
		_, err := c1.Write(buf1)
		if err != nil {
			b.Fatal(err)
		}

		n, err := recv1()
		//fmt.Printf("1: rx %q\n", buf1[:n])
		if !(n == len(buf1) && err == nil) {
			b.Fatalf("read back: n=%v  err=%v", n, err)
		}

		if buf1[0] != c {
			b.Fatalf("sent %q != got %q", c, buf1[0])
		}
	}

	//fmt.Printf("1: close\n")
	xclose(c1)
}

// rtt over net.Pipe - for comparison as base.
func BenchmarkNetPipeRTT(b *testing.B) {
	c1, c2 := net.Pipe()
	benchmarkNetConnRTT(b, c1, c2, false, false)
}

func BenchmarkNetPipeRTTsr(b *testing.B) {
	c1, c2 := net.Pipe()
	benchmarkNetConnRTT(b, c1, c2, true, false)
}

func BenchmarkNetPipeRTTsrho(b *testing.B) {
	c1, c2 := net.Pipe()
	benchmarkNetConnRTT(b, c1, c2, true, true)
}

// xtcpPipe creates two TCP connections connected to each other via loopback.
func xtcpPipe() (*net.TCPConn, *net.TCPConn) {
	// NOTE go sets TCP_NODELAY by default for TCP sockets
	l, err := net.Listen("tcp", "localhost:")
	exc.Raiseif(err)

	c1, err := net.Dial("tcp", l.Addr().String())
	exc.Raiseif(err)

	c2, err := l.Accept()
	exc.Raiseif(err)

	xclose(l)
	return c1.(*net.TCPConn), c2.(*net.TCPConn)
}

// rtt over TCP/loopback - for comparison as base.
func BenchmarkTCPlo(b *testing.B) {
	c1, c2 := xtcpPipe()
	benchmarkNetConnRTT(b, c1, c2, false, false)
}

func BenchmarkTCPlosr(b *testing.B) {
	c1, c2 := xtcpPipe()
	benchmarkNetConnRTT(b, c1, c2, true, false)
}

func BenchmarkTCPlosrho(b *testing.B) {
	c1, c2 := xtcpPipe()
	benchmarkNetConnRTT(b, c1, c2, true, true)
}


// rtt over NodeLink via Ask1/Recv1
func benchmarkLinkRTT(b *testing.B, l1, l2 *NodeLink) {
	b.ResetTimer()

	go func() {
		defer xclose(l2)

		for {
			req, err := l2.Recv1()
			if err != nil {
				switch errors.Cause(err) {
				case ErrLinkDown:
					return

				default:
					b.Fatal(err)
				}
			}

			switch msg := req.Msg.(type) {
			default:
				b.Fatalf("read -> unexpected message %T", msg)

			case *proto.GetObject:
				err = req.Reply(&proto.AnswerObject{
					Oid:        msg.Oid,
					Serial:     msg.At,
					DataSerial: msg.Before,
				})
				if err != nil {
					b.Fatal(err)
				}
			}

			req.Close()
		}
	}()

	for i := 0; i < b.N; i++ {
		// NOTE keeping inside loop to simulate what happens in real Load
		get := &proto.GetObject{}
		obj := &proto.AnswerObject{}

		get.Oid = zodb.Oid(i)
		get.At = zodb.Tid(i+1)
		get.Before = zodb.Tid(i+2)

		err := l1.Ask1(get, obj)
		if err != nil {
			b.Fatal(err)
		}

		if !(obj.Oid == get.Oid && obj.Serial == get.At && obj.DataSerial == get.Before) {
			b.Fatalf("read back: %v  ; requested %v", obj, get)
		}

		// XXX must be obj.Release
		obj.Data.XRelease()
	}

	xclose(l1)
}

// XXX RTT over Conn.Send/Recv		(no msg encoding/decoding)
// XXX RTT over link.sendPkt/recvPkt	(no conn route)

// xlinkPipe creates two links interconnected to each other via c1 and c2.
//
// XXX c1, c2 -> piper (who creates c1, c2) ?
// XXX overlap with nodeLinkPipe
func xlinkPipe(c1, c2 net.Conn) (*NodeLink, *NodeLink) {
	var l1, l2 *NodeLink

	wg := xsync.NewWorkGroup(context.Background())
	gox(wg, func(ctx context.Context) {
		l, err := _HandshakeClient(ctx, c1)
		exc.Raiseif(err)
		l1 = l
	})
	gox(wg, func(ctx context.Context) {
		l, err := _HandshakeServer(ctx, c2)
		exc.Raiseif(err)
		l2 = l
	})
	xwait(wg)

	return l1, l2
}

func BenchmarkLinkNetPipeRTT(b *testing.B) {
	c1, c2 := net.Pipe()
	l1, l2 := xlinkPipe(c1, c2)
	benchmarkLinkRTT(b, l1, l2)
}

func BenchmarkLinkTCPRTT(b *testing.B) {
	c1, c2 := xtcpPipe()
	l1, l2 := xlinkPipe(c1, c2)
	benchmarkLinkRTT(b, l1, l2)
}
