/*
 *
 * Copyright 2024 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 test

import (
	"context"
	"fmt"
	"net"
	"sync"
	"testing"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/internal/stubserver"
	"google.golang.org/grpc/interop"
	testgrpc "google.golang.org/grpc/interop/grpc_testing"
	testpb "google.golang.org/grpc/interop/grpc_testing"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/stats"
)

// TestPeerForClientStatsHandler configures a stats handler that
// verifies that peer is sent all stats handler callouts instead
// of Begin and PickerUpdated.
func (s) TestPeerForClientStatsHandler(t *testing.T) {
	psh := &peerStatsHandler{}

	// Stats callouts & peer object population.
	// Note:
	// * Begin stats lack peer info (RPC starts pre-resolution).
	// * PickerUpdated: no peer info (picker lacks transport details).
	expectedCallouts := map[stats.RPCStats]bool{
		&stats.OutPayload{}:    true,
		&stats.InHeader{}:      true,
		&stats.OutHeader{}:     true,
		&stats.InTrailer{}:     true,
		&stats.OutTrailer{}:    true,
		&stats.End{}:           true,
		&stats.Begin{}:         false,
		&stats.PickerUpdated{}: false,
	}

	// Start server.
	l, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		t.Fatal(err)
	}
	ss := &stubserver.StubServer{
		Listener: l,
		EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
			return &testpb.Empty{}, nil
		},
		S: grpc.NewServer(),
	}
	stubserver.StartTestService(t, ss)
	defer ss.S.Stop()

	// Create client with stats handler and do some calls.
	cc, err := grpc.NewClient(
		l.Addr().String(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithStatsHandler(psh))
	if err != nil {
		t.Fatal(err)
	}
	defer cc.Close()
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()
	client := testgrpc.NewTestServiceClient(cc)
	interop.DoEmptyUnaryCall(ctx, client)

	psh.mu.Lock()
	pshArgs := psh.args
	psh.mu.Unlock()

	// Fetch the total unique stats handlers with peer != nil
	uniqueStatsTypes := make(map[string]struct{})
	for _, callbackArgs := range pshArgs {
		key := fmt.Sprintf("%T", callbackArgs.rpcStats)
		if _, exists := uniqueStatsTypes[key]; exists {
			continue
		}
		uniqueStatsTypes[fmt.Sprintf("%T", callbackArgs.rpcStats)] = struct{}{}
	}
	if len(uniqueStatsTypes) != len(expectedCallouts) {
		t.Errorf("Unexpected number of stats handler callouts. Got %v, want %v", len(uniqueStatsTypes), len(expectedCallouts))
	}

	for _, callbackArgs := range pshArgs {
		expectedPeer, found := expectedCallouts[callbackArgs.rpcStats]
		// In case expectation is set to false and still we got the peer,
		// then it's good to have it. So no need to assert those conditions.
		if found && expectedPeer && callbackArgs.peer != nil {
			continue
		} else if expectedPeer && callbackArgs.peer == nil {
			t.Errorf("peer not populated for: %T", callbackArgs.rpcStats)
		}
	}
}

type peerStats struct {
	rpcStats stats.RPCStats
	peer     *peer.Peer
}

type peerStatsHandler struct {
	args []peerStats
	mu   sync.Mutex
}

func (h *peerStatsHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
	return ctx
}

func (h *peerStatsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
	p, _ := peer.FromContext(ctx)
	h.mu.Lock()
	defer h.mu.Unlock()
	h.args = append(h.args, peerStats{rs, p})
}

func (h *peerStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
	return ctx
}

func (h *peerStatsHandler) HandleConn(context.Context, stats.ConnStats) {}
