/*
 *
 * Copyright 2026 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 interop

import (
	"context"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/internal"
	"google.golang.org/grpc/internal/grpctest"
	"google.golang.org/grpc/internal/stubserver"
	testgrpc "google.golang.org/grpc/interop/grpc_testing"
	testpb "google.golang.org/grpc/interop/grpc_testing"
	"google.golang.org/grpc/orca"
	"google.golang.org/grpc/resolver"
	"google.golang.org/grpc/resolver/manual"

	v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
	"google.golang.org/protobuf/testing/protocmp"
)

type s struct {
	grpctest.Tester
}

func Test(t *testing.T) {
	grpctest.RunSubTests(t, s{})
}

const defaultTestTimeout = 30 * time.Second

type orcaServer struct {
	*stubserver.StubServer
	metricsRecorder orca.ServerMetricsRecorder
}

func startORCAServer(t *testing.T) *orcaServer {
	t.Helper()

	smr := orca.NewServerMetricsRecorder()
	ts := NewTestServer(NewTestServerOptions{MetricsRecorder: smr})

	stub := &stubserver.StubServer{
		UnaryCallF: func(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
			return ts.UnaryCall(ctx, in)
		},
		FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
			return ts.FullDuplexCall(stream)
		},
	}

	sopts := []grpc.ServerOption{orca.CallMetricsServerOption(nil)}

	oso := orca.ServiceOptions{
		ServerMetricsProvider: smr,
		MinReportingInterval:  time.Second,
	}
	internal.ORCAAllowAnyMinReportingInterval.(func(so *orca.ServiceOptions))(&oso)
	sopts = append(sopts, stubserver.RegisterServiceServerOption(func(s grpc.ServiceRegistrar) {
		if err := orca.Register(s, oso); err != nil {
			t.Fatalf("Failed to register ORCA service: %v", err)
		}
	}))

	if err := stub.StartServer(sopts...); err != nil {
		t.Fatalf("Error starting server: %v", err)
	}
	t.Cleanup(stub.Stop)

	return &orcaServer{
		StubServer:      stub,
		metricsRecorder: smr,
	}
}

func orcaSvcConfig() string {
	return `{"loadBalancingConfig": [{"test_backend_metrics_load_balancer": {}}]}`
}

// TestORCAPerRPCReport verifies that per-call ORCA load reports flow from the
// server through the orcaPicker's Done callback into the context.
func (s) TestORCAPerRPCReport(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	srv := startORCAServer(t)
	if err := srv.StartClient(grpc.WithDefaultServiceConfig(orcaSvcConfig())); err != nil {
		t.Fatalf("Error starting client: %v", err)
	}

	DoORCAPerRPCTest(ctx, srv.Client)
}

// TestORCAOOBReport verifies that OOB ORCA load reports flow through
// OnLoadReport and are returned by the picker when no per-call report is
// present.
func (s) TestORCAOOBReport(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	srv := startORCAServer(t)
	if err := srv.StartClient(grpc.WithDefaultServiceConfig(orcaSvcConfig())); err != nil {
		t.Fatalf("Error starting client: %v", err)
	}

	DoORCAOOBTest(ctx, srv.Client)
}

// TestORCAOOBFallback verifies the fallback behavior: when a per-call report
// is present but has all zero fields, the picker uses the most recent OOB
// report instead.
func (s) TestORCAOOBFallback(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	srv := startORCAServer(t)
	if err := srv.StartClient(grpc.WithDefaultServiceConfig(orcaSvcConfig())); err != nil {
		t.Fatalf("Error starting client: %v", err)
	}

	stream, err := srv.Client.FullDuplexCall(ctx)
	if err != nil {
		t.Fatalf("FullDuplexCall failed: %v", err)
	}
	req := &testpb.StreamingOutputCallRequest{
		OrcaOobReport: &testpb.TestOrcaReport{
			CpuUtilization:    0.73,
			MemoryUtilization: 0.55,
			Utilization:       map[string]float64{"util": 0.42},
		},
		ResponseParameters: []*testpb.ResponseParameters{{Size: 1}},
	}
	if err := stream.Send(req); err != nil {
		t.Fatalf("stream.Send failed: %v", err)
	}
	if _, err := stream.Recv(); err != nil {
		t.Fatalf("stream.Recv failed: %v", err)
	}

	oobWant := &v3orcapb.OrcaLoadReport{
		CpuUtilization: 0.73,
		MemUtilization: 0.55,
		Utilization:    map[string]float64{"util": 0.42},
	}
	pollORCAResult(ctx, t, srv.Client, oobWant)

	// OrcaPerQueryReport is nil: per-call report has all-zero fields,
	// triggering the OOB fallback path in orcaPicker.
	orcaRes := &v3orcapb.OrcaLoadReport{}
	if _, err := srv.Client.UnaryCall(contextWithORCAResult(ctx, &orcaRes), &testpb.SimpleRequest{}); err != nil {
		t.Fatalf("UnaryCall failed: %v", err)
	}
	if diff := cmp.Diff(orcaRes, oobWant, protocmp.Transform()); diff != "" {
		t.Fatalf("ORCA load report with zero per-call mismatch (-got +want):\n%s", diff)
	}
}

// TestEndpoints_MultipleAddresses verifies client behavior when an endpoint has
// multiple addresses and the first is unreachable. It validates that the
// pick_first falls through the address list and connects via the first
// reachable address, so per-call ORCA reports flow correctly.
func (s) TestEndpoints_MultipleAddresses(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	srv := startORCAServer(t)
	if err := srv.StartClient(grpc.WithDefaultServiceConfig(orcaSvcConfig())); err != nil {
		t.Fatalf("Error starting client: %v", err)
	}

	// First address is unreachable; pick_first falls through to the good one.
	state := resolver.State{
		Endpoints: []resolver.Endpoint{
			{Addresses: []resolver.Address{
				{Addr: "bad-address"},
				{Addr: srv.Address},
			}},
		},
	}
	srv.R.UpdateState(state)
	// Transient errors while pick_first probes the bad address are expected.
	want := &v3orcapb.OrcaLoadReport{CpuUtilization: 0.42, MemUtilization: 0.21}
	for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) {
		orcaRes := &v3orcapb.OrcaLoadReport{}
		req := &testpb.SimpleRequest{
			OrcaPerQueryReport: &testpb.TestOrcaReport{
				CpuUtilization:    0.42,
				MemoryUtilization: 0.21,
			},
		}
		if _, err := srv.Client.UnaryCall(contextWithORCAResult(ctx, &orcaRes), req); err != nil {
			continue
		}
		if diff := cmp.Diff(orcaRes, want, protocmp.Transform()); diff != "" {
			t.Fatalf("Per-RPC ORCA mismatch (-got +want):\n%s", diff)
		}
		return
	}
	t.Fatalf("timed out waiting for connection through good address")
}

// TestMultipleEndpoints_OOBListeners verifies that N endpoints produce N
// independent pick_first instances and N independent OOB listeners, each
// receiving reports from its respective server.
func (s) TestMultipleEndpoints_OOBListeners(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	srvA := startORCAServer(t)
	srvB := startORCAServer(t)

	srvA.metricsRecorder.SetCPUUtilization(0.1)
	srvB.metricsRecorder.SetCPUUtilization(0.9)

	r := manual.NewBuilderWithScheme("test")
	cc, err := grpc.Dial(r.Scheme()+":///test",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithResolvers(r),
		grpc.WithDefaultServiceConfig(orcaSvcConfig()),
	)
	if err != nil {
		t.Fatalf("Failed to dial: %v", err)
	}
	defer cc.Close()
	tc := testgrpc.NewTestServiceClient(cc)

	// One endpoint per server: endpointsharding creates two independent
	// pick_first children, each with its own OOB listener.
	state := resolver.State{
		Endpoints: []resolver.Endpoint{
			{Addresses: []resolver.Address{{Addr: srvA.Address}}},
			{Addresses: []resolver.Address{{Addr: srvB.Address}}},
		},
	}
	r.UpdateState(state)
	// No per-call report triggers OOB fallback. With two active listeners,
	// b.report alternates between servers; both values must eventually appear.
	wantA := &v3orcapb.OrcaLoadReport{CpuUtilization: 0.1}
	wantB := &v3orcapb.OrcaLoadReport{CpuUtilization: 0.9}
	seenA, seenB := false, false
	for ; ctx.Err() == nil && (!seenA || !seenB); <-time.After(100 * time.Millisecond) {
		orcaRes := &v3orcapb.OrcaLoadReport{}
		if _, err := tc.UnaryCall(contextWithORCAResult(ctx, &orcaRes), &testpb.SimpleRequest{}); err != nil {
			continue
		}
		if diff := cmp.Diff(orcaRes, wantA, protocmp.Transform()); diff == "" {
			seenA = true
		} else if diff := cmp.Diff(orcaRes, wantB, protocmp.Transform()); diff == "" {
			seenB = true
		}
	}
	if ctx.Err() != nil {
		t.Fatalf("timed out waiting for OOB reports from both endpoints; seenA=%v seenB=%v", seenA, seenB)
	}
}

// TestEndpointUpdate verifies client behavior in response to a resolver update
// that changes an endpoint's address. It ensures that the client disconnects
// from the old address and connects to the new address via pick_first. It
// also validates that the OOB listener for the old connection is stopped,
// and a new OOB listener is registered for the new connection once it becomes
// READY.
func (s) TestEndpointUpdate(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	srvA := startORCAServer(t)
	srvB := startORCAServer(t)

	r := manual.NewBuilderWithScheme("test")
	cc, err := grpc.Dial(r.Scheme()+":///test",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithResolvers(r),
		grpc.WithDefaultServiceConfig(orcaSvcConfig()),
	)
	if err != nil {
		t.Fatalf("Failed to dial: %v", err)
	}
	defer cc.Close()
	tc := testgrpc.NewTestServiceClient(cc)

	state := resolver.State{
		Endpoints: []resolver.Endpoint{
			{Addresses: []resolver.Address{{Addr: srvA.Address}}},
		},
	}
	r.UpdateState(state)

	orcaRes := &v3orcapb.OrcaLoadReport{}
	req := &testpb.SimpleRequest{
		OrcaPerQueryReport: &testpb.TestOrcaReport{
			CpuUtilization:    0.11,
			MemoryUtilization: 0.22,
		},
	}
	if _, err := tc.UnaryCall(contextWithORCAResult(ctx, &orcaRes), req); err != nil {
		t.Fatalf("UnaryCall to srvA failed: %v", err)
	}
	wantA := &v3orcapb.OrcaLoadReport{CpuUtilization: 0.11, MemUtilization: 0.22}
	if diff := cmp.Diff(orcaRes, wantA, protocmp.Transform()); diff != "" {
		t.Fatalf("ORCA from srvA mismatch (-got +want):\n%s", diff)
	}

	// Distinctive OOB metric on srvB to confirm the switch via OOB fallback.
	srvB.metricsRecorder.SetCPUUtilization(0.77)

	state = resolver.State{
		Endpoints: []resolver.Endpoint{
			{Addresses: []resolver.Address{{Addr: srvB.Address}}},
		},
	}
	r.UpdateState(state)
	// No per-call report forces OOB fallback. Once srvB's metric appears,
	// all three are confirmed: pick_first reconnected, new OOB listener
	// registered for srvB, old one for srvA stopped.
	wantB := &v3orcapb.OrcaLoadReport{CpuUtilization: 0.77}
	for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) {
		orcaRes := &v3orcapb.OrcaLoadReport{}
		if _, err := tc.UnaryCall(contextWithORCAResult(ctx, &orcaRes), &testpb.SimpleRequest{}); err != nil {
			continue
		}
		if diff := cmp.Diff(orcaRes, wantB, protocmp.Transform()); diff == "" {
			return
		}
		t.Logf("ORCA after endpoint update = %v; want %v; retrying...", orcaRes, wantB)
	}
	t.Fatalf("timed out waiting for srvB OOB report after endpoint update")
}

func pollORCAResult(ctx context.Context, t *testing.T, tc testgrpc.TestServiceClient, want *v3orcapb.OrcaLoadReport) {
	t.Helper()
	for ; ctx.Err() == nil; <-time.After(time.Second) {
		orcaRes := &v3orcapb.OrcaLoadReport{}
		if _, err := tc.UnaryCall(contextWithORCAResult(ctx, &orcaRes), &testpb.SimpleRequest{}); err != nil {
			t.Fatalf("UnaryCall failed: %v", err)
		}
		if diff := cmp.Diff(orcaRes, want, protocmp.Transform()); diff == "" {
			return
		}
		t.Logf("ORCA load report = %v; want %v; retrying...", orcaRes, want)
	}
	t.Fatalf("Timed out waiting for expected ORCA load report %v", want)
}
