// Most of that is copied from go/src/net/rpc/jsonrpc/all_test.go

package rpc

import (
	"errors"
	"io"
	"net"
	"net/rpc"
	"testing"
	"time"

	"github.com/google/uuid"
	"go.cypherpunks.su/keks"
)

type Args struct {
	A, B int
}

type Reply struct {
	C int
}

type Arith int

func (t *Arith) Add(args *Args, reply *Reply) error {
	reply.C = args.A + args.B
	return nil
}

func (t *Arith) Mul(args *Args, reply *Reply) error {
	reply.C = args.A * args.B
	return nil
}

func (t *Arith) Div(args *Args, reply *Reply) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	reply.C = args.A / args.B
	return nil
}

func (t *Arith) Error(args *Args, reply *Reply) error {
	panic("ERROR")
}

func init() {
	rpc.Register(new(Arith))
}

func TestServerNoParams(t *testing.T) {
	cli, srv := net.Pipe()
	defer cli.Close()
	srv.SetDeadline(time.Now().Add(10 * time.Millisecond))
	go ServeConn(srv)
	_, err := keks.Encode(cli, []any{123, "Arith.Add"}, nil)
	if err != nil {
		panic(err)
	}
	_, err = keks.NewDecoderFromReader(cli, nil).Decode()
	if err != io.EOF {
		t.Fatal("no EOF")
	}
}

func TestServerEmptyMessage(t *testing.T) {
	cli, srv := net.Pipe()
	defer cli.Close()
	go ServeConn(srv)
	_, err := keks.Encode(cli, struct{}{}, nil)
	if err != nil {
		panic(err)
	}
	_, err = keks.NewDecoderFromReader(cli, nil).Decode()
	if err != io.EOF {
		t.Fatal("no EOF")
	}
}

func TestServer(t *testing.T) {
	cli, srv := net.Pipe()
	defer cli.Close()
	go ServeConn(srv)
	for i := range 10 {
		id, err := uuid.NewRandom()
		if err != nil {
			panic(err)
		}
		_, err = keks.Encode(cli, []any{"c", id, "Arith.Add", Args{i, i + 1}}, nil)
		if err != nil {
			panic(err)
		}
		reply, err := keks.NewDecoderFromReader(cli, nil).Decode()
		if err != nil {
			t.Fatal(err)
		}
		list := reply.([]any)
		if list[0].(string) != "r" {
			t.Fatal("bad r")
		}
		if list[1].(*keks.Hexlet).UUID() != id {
			t.Fatal("bad id")
		}
		if list[2].(string) != "" {
			t.Fatal("has err", list[2])
		}
		var r Reply
		err = keks.Map2Struct(&r, list[3].(map[string]any))
		if err != nil {
			t.Fatal(err)
		}
		if r.C != 2*i+1 {
			t.Fatal("bad result", r.C)
		}
	}
}

func TestClient(t *testing.T) {
	cli, srv := net.Pipe()
	go ServeConn(srv)
	client := NewClient(cli)
	defer client.Close()

	args := &Args{7, 8}
	reply := new(Reply)
	err := client.Call("Arith.Add", args, reply)
	if err != nil {
		t.Error(err.Error())
	}
	if reply.C != args.A+args.B {
		t.Error(reply.C, args.A+args.B)
	}

	args = &Args{7, 8}
	reply = new(Reply)
	err = client.Call("Arith.Mul", args, reply)
	if err != nil {
		t.Error(err.Error())
	}
	if reply.C != args.A*args.B {
		t.Error(reply.C, args.A*args.B)
	}

	// Out of order.
	args = &Args{7, 8}
	mulReply := new(Reply)
	mulCall := client.Go("Arith.Mul", args, mulReply, nil)
	addReply := new(Reply)
	addCall := client.Go("Arith.Add", args, addReply, nil)

	addCall = <-addCall.Done
	if addCall.Error != nil {
		t.Error(addCall.Error.Error())
	}
	if addReply.C != args.A+args.B {
		t.Error(addReply.C, args.A+args.B)
	}

	mulCall = <-mulCall.Done
	if mulCall.Error != nil {
		t.Error(mulCall.Error.Error())
	}
	if mulReply.C != args.A*args.B {
		t.Error(mulReply.C, args.A*args.B)
	}

	// Error test
	args = &Args{7, 0}
	reply = new(Reply)
	err = client.Call("Arith.Div", args, reply)
	// expect an error: zero divide
	if err == nil {
		t.Error("expected error")
	} else if err.Error() != "divide by zero" {
		t.Error("expected divide by zero error; got", err)
	}

	args = &Args{7, 7}
	reply = new(Reply)
	err = client.Call("Arith.Div", args, reply)
	if err != nil {
		t.Error("unexpected error")
	}
}

func TestMalformedInput(t *testing.T) {
	cli, srv := net.Pipe()
	go cli.Write([]byte(`{id:1}`)) // invalid json
	ServeConn(srv)                 // must return, not loop
}

func TestMalformedOutput(t *testing.T) {
	cli, srv := net.Pipe()
	go func() {
		buf0, err := keks.EncodeBuf([]any{1, nil}, nil)
		if err != nil {
			panic(err)
		}
		buf1, err := keks.EncodeBuf(struct{}{}, nil)
		if err != nil {
			panic(err)
		}
		srv.Write(append(buf0, buf1...))
	}()
	go io.ReadAll(srv)

	client := NewClient(cli)
	defer client.Close()

	args := &Args{7, 8}
	reply := new(Reply)
	err := client.Call("Arith.Add", args, reply)
	if err == nil {
		t.Error("expected error")
	}
}
