// 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 servertest provides utilities for running tests against a remote LSP
// server.
package servertest

import (
	"context"
	"fmt"
	"net"
	"strings"
	"sync"

	"cuelang.org/go/internal/golangorgx/tools/jsonrpc2"
)

// Connector is the interface used to connect to a server.
type Connector interface {
	Connect(context.Context) jsonrpc2.Conn
}

// TCPServer is a helper for executing tests against a remote jsonrpc2
// connection. Once initialized, its Addr field may be used to connect a
// jsonrpc2 client.
type TCPServer struct {
	*connList

	Addr string

	ln     net.Listener
	framer jsonrpc2.Framer
}

// NewTCPServer returns a new test server listening on local tcp port and
// serving incoming jsonrpc2 streams using the provided stream server. It
// panics on any error.
func NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *TCPServer {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(fmt.Sprintf("servertest: failed to listen: %v", err))
	}
	if framer == nil {
		framer = jsonrpc2.NewHeaderStream
	}
	go jsonrpc2.Serve(ctx, ln, server, 0)
	return &TCPServer{Addr: ln.Addr().String(), ln: ln, framer: framer, connList: &connList{}}
}

// Connect dials the test server and returns a jsonrpc2 Connection that is
// ready for use.
func (s *TCPServer) Connect(_ context.Context) jsonrpc2.Conn {
	netConn, err := net.Dial("tcp", s.Addr)
	if err != nil {
		panic(fmt.Sprintf("servertest: failed to connect to test instance: %v", err))
	}
	conn := jsonrpc2.NewConn(s.framer(netConn))
	s.add(conn)
	return conn
}

// PipeServer is a test server that handles connections over io.Pipes.
type PipeServer struct {
	*connList
	server jsonrpc2.StreamServer
	framer jsonrpc2.Framer
}

// NewPipeServer returns a test server that can be connected to via io.Pipes.
func NewPipeServer(server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer {
	if framer == nil {
		framer = jsonrpc2.NewRawStream
	}
	return &PipeServer{server: server, framer: framer, connList: &connList{}}
}

// Connect creates new io.Pipes and binds them to the underlying StreamServer.
func (s *PipeServer) Connect(ctx context.Context) jsonrpc2.Conn {
	sPipe, cPipe := net.Pipe()
	serverStream := s.framer(sPipe)
	serverConn := jsonrpc2.NewConn(serverStream)
	s.add(serverConn)
	go s.server.ServeStream(ctx, serverConn)

	clientStream := s.framer(cPipe)
	clientConn := jsonrpc2.NewConn(clientStream)
	s.add(clientConn)
	return clientConn
}

// connList tracks closers to run when a testserver is closed.  This is a
// convenience, so that callers don't have to worry about closing each
// connection.
type connList struct {
	mu    sync.Mutex
	conns []jsonrpc2.Conn
}

func (l *connList) add(conn jsonrpc2.Conn) {
	l.mu.Lock()
	defer l.mu.Unlock()
	l.conns = append(l.conns, conn)
}

func (l *connList) Close() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	var errmsgs []string
	for _, conn := range l.conns {
		if err := conn.Close(); err != nil {
			errmsgs = append(errmsgs, err.Error())
		}
	}
	if len(errmsgs) > 0 {
		return fmt.Errorf("closing errors:\n%s", strings.Join(errmsgs, "\n"))
	}
	return nil
}
