package grpcdemo

import (
	"context"
	"io"
	"net"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	pb "google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/grpc/test/bufconn"
)

type service struct{ pb.UnimplementedHealthServer }

func (service) Check(ctx context.Context, r *pb.HealthCheckRequest) (*pb.HealthCheckResponse, error) {
	if _, ok := ctx.Deadline(); !ok {
		return nil, status.Error(codes.InvalidArgument, "deadline required")
	}
	if r.Service == "wait" {
		<-ctx.Done()
		return nil, status.FromContextError(ctx.Err()).Err()
	}
	return &pb.HealthCheckResponse{Status: pb.HealthCheckResponse_SERVING}, nil
}
func (service) Watch(r *pb.HealthCheckRequest, s pb.Health_WatchServer) error {
	for _, v := range []pb.HealthCheckResponse_ServingStatus{pb.HealthCheckResponse_SERVING, pb.HealthCheckResponse_NOT_SERVING} {
		if err := s.Send(&pb.HealthCheckResponse{Status: v}); err != nil {
			return err
		}
	}
	return nil
}
func unary(ctx context.Context, req any, info *grpc.UnaryServerInfo, next grpc.UnaryHandler) (any, error) {
	md, _ := metadata.FromIncomingContext(ctx)
	if v := md.Get("x-demo"); len(v) != 1 || v[0] != "test" {
		return nil, status.Error(codes.Unauthenticated, "test metadata missing")
	}
	return next(ctx, req)
}
func TestDeadlineMetadataAndStream(t *testing.T) {
	lis := bufconn.Listen(1 << 20)
	srv := grpc.NewServer(grpc.UnaryInterceptor(unary))
	pb.RegisterHealthServer(srv, service{})
	done := make(chan error, 1)
	go func() { done <- srv.Serve(lis) }()
	defer func() { srv.Stop(); lis.Close(); <-done }()
	cc, err := grpc.NewClient("passthrough:///local", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }))
	if err != nil {
		t.Fatal(err)
	}
	defer cc.Close()
	client := pb.NewHealthClient(cc)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	if _, err = client.Check(ctx, &pb.HealthCheckRequest{}); status.Code(err) != codes.Unauthenticated {
		t.Fatalf("code=%v err=%v", status.Code(err), err)
	}
	ctx = metadata.AppendToOutgoingContext(ctx, "x-demo", "test")
	resp, err := client.Check(ctx, &pb.HealthCheckRequest{})
	if err != nil || resp.GetStatus() != pb.HealthCheckResponse_SERVING {
		t.Fatalf("%v %v", resp, err)
	}
	stream, err := client.Watch(ctx, &pb.HealthCheckRequest{})
	if err != nil {
		t.Fatal(err)
	}
	for _, want := range []pb.HealthCheckResponse_ServingStatus{pb.HealthCheckResponse_SERVING, pb.HealthCheckResponse_NOT_SERVING} {
		got, e := stream.Recv()
		if e != nil || got.GetStatus() != want {
			t.Fatalf("%v %v", got, e)
		}
	}
	if _, err = stream.Recv(); err != io.EOF {
		t.Fatalf("stream terminal=%v", err)
	}
	short, stop := context.WithTimeout(ctx, 30*time.Millisecond)
	defer stop()
	if _, err = client.Check(short, &pb.HealthCheckRequest{Service: "wait"}); status.Code(err) != codes.DeadlineExceeded {
		t.Fatalf("deadline error=%v", err)
	}
	t.Log("unary metadata/interceptor, incoming deadline, server stream ordering and EOF, DeadlineExceeded verified")
}
