package changefeed

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/dataflow-service/app/controllers/open"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"google.golang.org/grpc/metadata"
)

// TestCheckPermission provides comprehensive unit tests for the rbacChecker.checkPermission method.
// It uses a table-driven approach to cover all major scenarios.
func TestCheckPermission(t *testing.T) {
	const (
		mockOrgID     uint64 = 1
		mockProjectID uint64 = 10
		mockClusterID uint64 = 100
		mockAPIKey           = "test-api-key"
	)

	// Create a context with valid API key metadata, which simulates a typical gRPC request.
	md := metadata.New(map[string]string{strings.ToLower(open.OpenAPIPublicKey): mockAPIKey})
	validCtx := metadata.NewIncomingContext(context.Background(), md)

	// Define test cases for various scenarios.
	testCases := []struct {
		name          string
		ctx           context.Context
		rbacClient    RBACClient // Use the interface to allow different mock implementations.
		expectedError bizerr.BizError
	}{
		{
			name: "success - permission granted",
			ctx:  validCtx,
			// Mock an RBAC client that successfully grants permission.
			rbacClient: RBACClientFunc(func(ctx context.Context, params *auth.CheckPermissionParam) (*auth.CheckPermissionRet, error) {
				// Assert that the correct API key from the context is used in the check.
				require.Equal(t, mockAPIKey, params.IdentityVal)
				return &auth.CheckPermissionRet{HasPermission: true}, nil
			}),
			expectedError: nil, // Expect no error.
		},
		{
			name: "failure - permission denied by rbac service",
			ctx:  validCtx,
			// Mock an RBAC client that explicitly denies permission.
			rbacClient: RBACClientFunc(func(ctx context.Context, params *auth.CheckPermissionParam) (*auth.CheckPermissionRet, error) {
				return &auth.CheckPermissionRet{HasPermission: false, Message: "forbidden by policy"}, nil
			}),
			expectedError: bizerr.New(bizerr.ErrCodePermission, "no permission %s", "forbidden by policy"),
		},
		{
			name: "failure - rbac client returns a technical error",
			ctx:  validCtx,
			// Mock an RBAC client that fails due to a technical issue (e.g., network error).
			rbacClient: RBACClientFunc(func(ctx context.Context, params *auth.CheckPermissionParam) (*auth.CheckPermissionRet, error) {
				return nil, errors.New("network connection failed")
			}),
			expectedError: bizerr.New(bizerr.ErrCodeGRPC, "failed to check permission"),
		},
		{
			name:          "failure - missing all metadata from context",
			ctx:           context.Background(), // Use a plain context with no gRPC metadata.
			rbacClient:    nil,                  // The client should not be called in this case.
			expectedError: bizerr.New(bizerr.ErrCodePermission, "missing permission metadata"),
		},
		{
			name:          "failure - api key header is missing in metadata",
			ctx:           metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"other-key": "some-value"})),
			rbacClient:    nil,
			expectedError: bizerr.New(bizerr.ErrCodePermission, "invalid permission metadata"),
		},
		{
			name: "failure - multiple api keys found in metadata",
			// Create a context where the API key header has more than one value, which is invalid.
			ctx:           metadata.NewIncomingContext(context.Background(), metadata.MD{strings.ToLower(open.OpenAPIPublicKey): []string{"key1", "key2"}}),
			rbacClient:    nil,
			expectedError: bizerr.New(bizerr.ErrCodePermission, "invalid permission metadata"),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Arrange: Create the checker with the mocked client for the specific test case.
			checker := nwRBACChecker(tc.rbacClient)

			// Act: Call the method under test.
			err := checker.checkPermission(tc.ctx, GetChangefeedReq, mockOrgID, mockProjectID, mockClusterID)

			// Assert: Check if the returned error matches the expected error.
			if tc.expectedError == nil {
				require.Nil(t, err, "expected no error but got one")
			} else {
				require.NotNil(t, err, "expected an error but got nil")
				require.Equal(t, tc.expectedError.ErrCode(), err.ErrCode(), "error code mismatch")
				require.Equal(t, tc.expectedError.ErrMsg(), err.ErrMsg(), "error message mismatch")
			}
		})
	}
}
