@ApiOperation(value = "Create Or update Tenant Profile (saveTenantProfile)",
            notes = "Create or update the Tenant Profile. When creating tenant profile, platform generates Tenant Profile Id as " + UUID_WIKI_LINK +
                    "The newly created Tenant Profile Id will be present in the response. " +
                    "Specify existing Tenant Profile Id id to update the Tenant Profile. " +
                    "Referencing non-existing Tenant Profile Id will cause 'Not Found' error. " +
                    "\n\nUpdate of the tenant profile configuration will cause immediate recalculation of API limits for all affected Tenants. " +
                    "\n\nThe **'profileData'** object is the part of Tenant Profile that defines API limits and Rate limits. " +
                    "\n\nYou have an ability to define maximum number of devices ('maxDevice'), assets ('maxAssets') and other entities. " +
                    "You may also define maximum number of messages to be processed per month ('maxTransportMessages', 'maxREExecutions', etc). " +
                    "The '*RateLimit' defines the rate limits using simple syntax. For example, '1000:1,20000:60' means up to 1000 events per second but no more than 20000 event per minute. " +
                    "Let's review the example of tenant profile data below: " +
                    "\n\n" + MARKDOWN_CODE_BLOCK_START +
                    "{\n" +
                    "  \"name\": \"Your name\",\n" +
                    "  \"description\": \"Your description\",\n" +
                    "  \"isolatedTbRuleEngine\": false,\n" +
                    "  \"profileData\": {\n" +
                    "    \"configuration\": {\n" +
                    "      \"type\": \"DEFAULT\",\n" +
                    "      \"maxDevices\": 0,\n" +
                    "      \"maxAssets\": 0,\n" +
                    "      \"maxCustomers\": 0,\n" +
                    "      \"maxUsers\": 0,\n" +
                    "      \"maxDashboards\": 0,\n" +
                    "      \"maxRuleChains\": 0,\n" +
                    "      \"maxResourcesInBytes\": 0,\n" +
                    "      \"maxOtaPackagesInBytes\": 0,\n" +
                    "      \"maxResourceSize\": 0,\n" +
                    "      \"transportTenantMsgRateLimit\": \"1000:1,20000:60\",\n" +
                    "      \"transportTenantTelemetryMsgRateLimit\": \"1000:1,20000:60\",\n" +
                    "      \"transportTenantTelemetryDataPointsRateLimit\": \"1000:1,20000:60\",\n" +
                    "      \"transportDeviceMsgRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportGatewayMsgRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportGatewayTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportGatewayTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportGatewayDeviceMsgRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportGatewayDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
                    "      \"transportGatewayDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
                    "      \"maxTransportMessages\": 10000000,\n" +
                    "      \"maxTransportDataPoints\": 10000000,\n" +
                    "      \"maxREExecutions\": 4000000,\n" +
                    "      \"maxJSExecutions\": 5000000,\n" +
                    "      \"maxDPStorageDays\": 0,\n" +
                    "      \"maxRuleNodeExecutionsPerMessage\": 50,\n" +
                    "      \"maxEmails\": 0,\n" +
                    "      \"maxSms\": 0,\n" +
                    "      \"maxCreatedAlarms\": 1000,\n" +
                    "      \"defaultStorageTtlDays\": 0,\n" +
                    "      \"alarmsTtlDays\": 0,\n" +
                    "      \"rpcTtlDays\": 0,\n" +
                    "      \"queueStatsTtlDays\": 0,\n" +
                    "      \"ruleEngineExceptionsTtlDays\": 0,\n" +
                    "      \"warnThreshold\": 0\n" +
                    "    }\n" +
                    "  },\n" +
                    "  \"default\": false\n" +
                    "}" +
                    MARKDOWN_CODE_BLOCK_END +
                    "Remove 'id', from the request body example (below) to create new Tenant Profile entity." +
                    SYSTEM_AUTHORITY_PARAGRAPH)
    @PreAuthorize("hasAuthority('SYS_ADMIN')")
    @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST)
    @ResponseBody
    public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.")
                                           @RequestBody TenantProfile tenantProfile) throws ThingsboardException {
        TenantProfile oldProfile;
        if (tenantProfile.getId() == null) {
            accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE);
            oldProfile = null;
        } else {
            oldProfile = checkTenantProfileId(tenantProfile.getId(), Operation.WRITE);
        }

        return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile);
    }
@Test
    public void testSaveTenantProfile() throws Exception {
        loginSysAdmin();

        Mockito.reset(tbClusterService);

        TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
        TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
        Assert.assertNotNull(savedTenantProfile);
        Assert.assertNotNull(savedTenantProfile.getId());
        Assert.assertTrue(savedTenantProfile.getCreatedTime() > 0);
        Assert.assertEquals(tenantProfile.getName(), savedTenantProfile.getName());
        Assert.assertEquals(tenantProfile.getDescription(), savedTenantProfile.getDescription());
        Assert.assertEquals(tenantProfile.getProfileData(), savedTenantProfile.getProfileData());
        Assert.assertEquals(tenantProfile.isDefault(), savedTenantProfile.isDefault());
        Assert.assertEquals(tenantProfile.isIsolatedTbRuleEngine(), savedTenantProfile.isIsolatedTbRuleEngine());

        testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.CREATED, 1);

        savedTenantProfile.setName("New tenant profile");
        doPost("/api/tenantProfile", savedTenantProfile, TenantProfile.class);
        TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString(), TenantProfile.class);
        Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName());

        testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.UPDATED, 1);
    }