/*
 *
 * Copyright 2020 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

// Package fakeserver provides a fake implementation of the RouteLookupService,
// to be used in unit tests.
package fakeserver

import (
	"context"
	"errors"
	"fmt"
	"net"
	"time"

	"google.golang.org/grpc"
	rlsgrpc "google.golang.org/grpc/balancer/rls/internal/proto/grpc_lookup_v1"
	rlspb "google.golang.org/grpc/balancer/rls/internal/proto/grpc_lookup_v1"
	"google.golang.org/grpc/internal/testutils"
)

const (
	defaultDialTimeout       = 5 * time.Second
	defaultRPCTimeout        = 5 * time.Second
	defaultChannelBufferSize = 50
)

// Response wraps the response protobuf (xds/LRS) and error that the Server
// should send out to the client through a call to stream.Send()
type Response struct {
	Resp *rlspb.RouteLookupResponse
	Err  error
}

// Server is a fake implementation of RLS. It exposes channels to send/receive
// RLS requests and responses.
type Server struct {
	rlsgrpc.UnimplementedRouteLookupServiceServer
	RequestChan  *testutils.Channel
	ResponseChan chan Response
	Address      string
}

// Start makes a new Server which uses the provided net.Listener. If lis is nil,
// it creates a new net.Listener on a local port. The returned cancel function
// should be invoked by the caller upon completion of the test.
func Start(lis net.Listener, opts ...grpc.ServerOption) (*Server, func(), error) {
	if lis == nil {
		var err error
		lis, err = net.Listen("tcp", "localhost:0")
		if err != nil {
			return nil, func() {}, fmt.Errorf("net.Listen() failed: %v", err)
		}
	}
	s := &Server{
		// Give the channels a buffer size of 1 so that we can setup
		// expectations for one lookup call, without blocking.
		RequestChan:  testutils.NewChannelWithSize(defaultChannelBufferSize),
		ResponseChan: make(chan Response, 1),
		Address:      lis.Addr().String(),
	}

	server := grpc.NewServer(opts...)
	rlsgrpc.RegisterRouteLookupServiceServer(server, s)
	go server.Serve(lis)

	return s, func() { server.Stop() }, nil
}

// RouteLookup implements the RouteLookupService.
func (s *Server) RouteLookup(ctx context.Context, req *rlspb.RouteLookupRequest) (*rlspb.RouteLookupResponse, error) {
	s.RequestChan.Send(req)

	// The leakchecker fails if we don't exit out of here in a reasonable time.
	timer := time.NewTimer(defaultRPCTimeout)
	select {
	case <-timer.C:
		return nil, errors.New("default RPC timeout exceeded")
	case resp := <-s.ResponseChan:
		timer.Stop()
		return resp.Resp, resp.Err
	}
}

// ClientConn returns a grpc.ClientConn connected to the fakeServer.
func (s *Server) ClientConn() (*grpc.ClientConn, func(), error) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultDialTimeout)
	defer cancel()

	cc, err := grpc.DialContext(ctx, s.Address, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		return nil, nil, fmt.Errorf("grpc.DialContext(%s) failed: %v", s.Address, err)
	}
	return cc, func() { cc.Close() }, nil
}
