// Copyright (C) 2016-2023  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 (
	"context"
	"errors"
	"io"
	"net"
	"reflect"
	"testing"

	"lab.nexedi.com/kirr/go123/exc"
	"lab.nexedi.com/kirr/go123/xsync"
	"lab.nexedi.com/kirr/neo/go/neo/proto"
)

// _xhandshakeClient handshakes as client with specified version and encoding and verifies that server accepts it.
func _xhandshakeClient(ctx context.Context, c net.Conn, version uint32, encoding proto.Encoding) {
	_, err := _handshakeClient(ctx, c, version, encoding)
	exc.Raiseif(err)
}

// _xhandshakeServer handshakes as server and verifies negotiated encoding to be encOK.
func _xhandshakeServer(ctx context.Context, c net.Conn, version uint32, encOK proto.Encoding) {
	enc, _, err := _handshakeServer(ctx, c, version)
	exc.Raiseif(err)
	if enc != encOK {
		exc.Raisef("enc (%c) != encOK (%c)", enc, encOK)
	}
}

func TestHandshake(t *testing.T) {
	Verify(t, _TestHandshake)
}
func _TestHandshake(t *T) {
	bg := context.Background()
	// handshake ok
	p1, p2 := net.Pipe()
	wg := xsync.NewWorkGroup(bg)
	gox(wg, func(ctx context.Context) {
		_xhandshakeClient(ctx, p1, 1, t.enc)
	})
	gox(wg, func(ctx context.Context) {
		_xhandshakeServer(ctx, p2, 1, t.enc)
	})
	xwait(wg)
	xclose(p1)
	xclose(p2)

	// version mismatch
	p1, p2 = net.Pipe()
	var err1, err2 error
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(ctx context.Context) {
		_, err1 = _handshakeClient(ctx, p1, 1, t.enc)
	})
	gox(wg, func(ctx context.Context) {
		_, _, err2 = _handshakeServer(ctx, p2, 2)
	})
	xwait(wg)
	xclose(p1)
	xclose(p2)

	e1ok := &_HandshakeError{
		LocalRole:   _LinkClient,
		LocalAddr:   p1.LocalAddr(),
		RemoteAddr:  p1.RemoteAddr(),
		Err:         &_VersionMismatchError{
			LocalVer:  1,
			RemoteVer: 2,
		},
	}
	e2ok := &_HandshakeError{
		LocalRole:   _LinkServer,
		LocalAddr:   p2.LocalAddr(),
		RemoteAddr:  p2.RemoteAddr(),
		Err:         &_VersionMismatchError{
			LocalVer:  2,
			RemoteVer: 1,
		},
	}

	if !reflect.DeepEqual(err1, e1ok) {
		t.Errorf("handshake ver mismatch: p1: unexpected error:\nhave: %#v\n     %q\nwant: %#v\n     %q",
			 err1, estr(err1), e1ok, e1ok.Error())
	}
	if !reflect.DeepEqual(err2, e2ok) {
		t.Errorf("handshake ver mismatch: p2: unexpected error:\nhave: %#v\n     %q\nwant: %#v\n     %q",
			 err2, estr(err2), e2ok, e2ok.Error())
	}

	// encoding mismatch (mimic behaviour of NEO/py server who does not accept client-proposed encoding)
	p1, p2 = net.Pipe()
	var err error
	var srvEnc proto.Encoding
	switch t.enc {
	case 'N': srvEnc = 'M'
	case 'M': srvEnc = 'N'
	default:  panic("bug")
	}
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(ctx context.Context) {
		_, err = _handshakeClient(ctx, p1, 1, t.enc)
	})
	gox(wg, func(ctx context.Context) {
		wg := xsync.NewWorkGroup(ctx)
		gox(wg, func(ctx context.Context) {
			err := txHello("tx hello", p2, 1, srvEnc)
			exc.Raiseif(err)
		})

		gox(wg, func(ctx context.Context) {
			rxbuf := newXBufReader(p2, 1024)
			_, _, err := rxHello("rx hello", rxbuf)
			exc.Raiseif(err)
		})
		xwait(wg)
	})
	xwait(wg)
	xclose(p1)
	xclose(p2)

	eok := &_HandshakeError{
		LocalRole:  _LinkClient,
		LocalAddr:  p1.LocalAddr(),
		RemoteAddr: p1.RemoteAddr(),
		Err:        &_EncodingMismatchError{
			LocalEnc:  t.enc,
			RemoteEnc: srvEnc,
		},
	}
	if !reflect.DeepEqual(err, eok) {
		t.Errorf("handshake encoding mismatch: client: unexpected error:\nhave: %#v\n     %q\nwant: %#v\n     %q",
			 err, estr(err), eok, eok.Error())
	}

	// tx & rx problem (client)
	p1, p2 = net.Pipe()
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(ctx context.Context) {
		_, err = _handshakeClient(ctx, p1, 1, t.enc)
	})
	gox(wg, func(_ context.Context) {
		xclose(p2)
	})
	xwait(wg)
	xclose(p1)

	err_, ok := err.(*_HandshakeError)
	if !ok || !(errors.Is(err_.Err, io.ErrClosedPipe /* on Write */) || errors.Is(err_.Err, io.ErrUnexpectedEOF /* on Read */)) {
		t.Errorf("handshake peer close: unexpected error: %#v", err)
	}

	// tx & rx problem (server)
	p1, p2 = net.Pipe()
	wg = xsync.NewWorkGroup(bg)
	gox(wg, func(_ context.Context) {
		xclose(p1)
	})
	gox(wg, func(ctx context.Context) {
		_, _, err = _handshakeServer(ctx, p2, 1)
	})
	xwait(wg)
	xclose(p2)

	err_, ok = err.(*_HandshakeError)
	if !ok || !(errors.Is(err_.Err, io.ErrClosedPipe /* on Write */) || errors.Is(err_.Err, io.ErrUnexpectedEOF /* on Read */)) {
		t.Errorf("handshake peer close: unexpected error: %#v", err)
	}

	// ctx cancel (client)
	p1, p2 = net.Pipe()
	ctx, cancel := context.WithCancel(bg)
	wg = xsync.NewWorkGroup(ctx)
	gox(wg, func(ctx context.Context) {
		_, err = _handshakeClient(ctx, p1, 1, t.enc)
	})
	tdelay()
	cancel()
	xwait(wg)
	xclose(p1)
	xclose(p2)

	err_, ok = err.(*_HandshakeError)
	if !ok || !(err_.Err == context.Canceled) {
		t.Errorf("handshake (client): cancel: unexpected error: %#v", err)
	}

	// ctx cancel (server)
	p1, p2 = net.Pipe()
	ctx, cancel = context.WithCancel(bg)
	wg = xsync.NewWorkGroup(ctx)
	gox(wg, func(ctx context.Context) {
		_, _, err = _handshakeServer(ctx, p2, 1)
	})
	tdelay()
	cancel()
	xwait(wg)
	xclose(p1)
	xclose(p2)

	err_, ok = err.(*_HandshakeError)
	if !ok || !(err_.Err == context.Canceled) {
		t.Errorf("handshake (server): cancel: unexpected error: %#v", err)
	}
}

// estr returns err.Error() or "<nil>".
func estr(err error) string {
	if err == nil {
		return "<nil>"
	} else {
		return err.Error()
	}
}
