/**
 * Copyright © 2016-2023 The Thingsboard 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 org.thingsboard.server.controller;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;

import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@DaoSqlTest
public class TbResourceControllerTest extends AbstractControllerTest {

    private IdComparator<TbResourceInfo> idComparator = new IdComparator<>();

    private static final String DEFAULT_FILE_NAME = "test.jks";
    private static final String DEFAULT_FILE_NAME_2 = "test2.jks";
    private static final String JS_TEST_FILE_NAME = "test.js";
    private static final String TEST_DATA = " ";


    private Tenant savedTenant;
    private User tenantAdmin;

    @Before
    public void beforeTest() throws Exception {
        loginSysAdmin();

        Tenant tenant = new Tenant();
        tenant.setTitle("My tenant");
        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
        Assert.assertNotNull(savedTenant);

        tenantAdmin = new User();
        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
        tenantAdmin.setTenantId(savedTenant.getId());
        tenantAdmin.setEmail("tenant2@thingsboard.org");
        tenantAdmin.setFirstName("Joe");
        tenantAdmin.setLastName("Downs");

        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
    }

    @After
    public void afterTest() throws Exception {
        loginSysAdmin();

        doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
                .andExpect(status().isOk());
    }

    @Test
    public void testSaveTbResource() throws Exception {

        Mockito.reset(tbClusterService, auditLogService);

        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JKS);
        resource.setTitle("My first resource");
        resource.setFileName(DEFAULT_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(),
                savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
                ActionType.ADDED, ActionType.ADDED);

        Assert.assertNotNull(savedResource);
        Assert.assertNotNull(savedResource.getId());
        Assert.assertTrue(savedResource.getCreatedTime() > 0);
        Assert.assertEquals(savedTenant.getId(), savedResource.getTenantId());
        Assert.assertEquals(resource.getTitle(), savedResource.getTitle());
        Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getFileName());
        Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey());
        Assert.assertArrayEquals(resource.getData(), download(savedResource.getId()));

        savedResource.setTitle("My new resource");
        savedResource.setData(null);

        save(savedResource);

        TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class);
        Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle());

        testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(foundResource, foundResource.getId(), foundResource.getId(),
                savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
                ActionType.UPDATED, ActionType.UPDATED);
    }

    @Test
    public void saveResourceInfoWithViolationOfLengthValidation() throws Exception {
        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JKS);
        resource.setTitle(StringUtils.randomAlphabetic(300));
        resource.setFileName(DEFAULT_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        Mockito.reset(tbClusterService, auditLogService);

        String msgError = msgErrorFieldLength("title");
        doPost("/api/resource", resource)
                .andExpect(status().isBadRequest())
                .andExpect(statusReason(containsString(msgError)));

        testNotifyEntityEqualsOneTimeServiceNeverError(resource, savedTenant.getId(),
                tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
    }

    @Test
    public void testUpdateTbResourceFromDifferentTenant() throws Exception {
        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JKS);
        resource.setTitle("My first resource");
        resource.setFileName(DEFAULT_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        loginDifferentTenant();

        Mockito.reset(tbClusterService, auditLogService);

        doPost("/api/resource", savedResource)
                .andExpect(status().isForbidden())
                .andExpect(statusReason(containsString(msgErrorPermission)));

        testNotifyEntityNever(savedResource.getId(), savedResource);

        doDelete("/api/resource/" + savedResource.getId().getId().toString())
                .andExpect(status().isForbidden())
                .andExpect(statusReason(containsString(msgErrorPermission)));

        testNotifyEntityNever(savedResource.getId(), savedResource);

        deleteDifferentTenant();
    }

    @Test
    public void testFindTbResourceById() throws Exception {
        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JKS);
        resource.setTitle("My first resource");
        resource.setFileName(DEFAULT_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        TbResource foundResource = doGet("/api/resource/" + savedResource.getUuidId(), TbResource.class);
        Assert.assertNotNull(foundResource);
        Assert.assertEquals(savedResource.getId(), foundResource.getId());
        Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName());
    }

    @Test
    public void testDeleteTbResource() throws Exception {
        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JKS);
        resource.setTitle("My first resource");
        resource.setFileName(DEFAULT_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        Mockito.reset(tbClusterService, auditLogService);
        String resourceIdStr = savedResource.getId().getId().toString();
        doDelete("/api/resource/" + resourceIdStr)
                .andExpect(status().isOk());


        testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(),
                savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
                ActionType.DELETED, ActionType.DELETED, resourceIdStr);

        doGet("/api/resource/" + savedResource.getUuidId())
                .andExpect(status().isNotFound())
                .andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr))));
    }

    @Test
    public void testShoudNotDeleteTbResourceIfAssignedToWidgetType() throws Exception {
        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JKS);
        resource.setTitle("My first resource");
        resource.setFileName(DEFAULT_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        Mockito.reset(tbClusterService, auditLogService);
        String resourceIdStr = savedResource.getId().getId().toString();

        //create widget type
        WidgetTypeDetails widgetType = new WidgetTypeDetails();
        widgetType.setName("Widget Type");
        widgetType.setDescriptor(JacksonUtil.fromString(String.format("{ \"resources\": [{\"url\":{\"entityType\":\"TB_RESOURCE\",\"id\":\"%s\"},\"isModule\":true}]}", savedResource.getId()), JsonNode.class));
        doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);

        doDelete("/api/resource/" + resourceIdStr)
                .andExpect(status().isBadRequest())
                .andExpect(statusReason(containsString("Following widget types uses current resource: ["
                        + widgetType.getName() + "]")));
    }

    @Test
    public void testFindTenantTbResources() throws Exception {

        Mockito.reset(tbClusterService, auditLogService);

        List<TbResourceInfo> resources = new ArrayList<>();
        int cntEntity = 173;
        for (int i = 0; i < cntEntity; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("Resource" + i);
            resource.setResourceType(ResourceType.JKS);
            resource.setFileName(i + DEFAULT_FILE_NAME);
            resource.setEncodedData(TEST_DATA);
            resources.add(new TbResourceInfo(save(resource)));
        }
        List<TbResourceInfo> loadedResources = new ArrayList<>();
        PageLink pageLink = new PageLink(24);
        PageData<TbResourceInfo> pageData;
        do {
            pageData = doGetTypedWithPageLink("/api/resource?",
                    new TypeReference<>() {
                    }, pageLink);
            loadedResources.addAll(pageData.getData());
            if (pageData.hasNext()) {
                pageLink = pageLink.nextPageLink();
            }
        } while (pageData.hasNext());

        testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new TbResource(), new TbResource(),
                savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
                ActionType.ADDED, cntEntity, cntEntity, cntEntity);

        Collections.sort(resources, idComparator);
        Collections.sort(loadedResources, idComparator);

        Assert.assertEquals(resources, loadedResources);
    }

    @Test
    public void testFindTenantTbResourcesByType() throws Exception {
        Mockito.reset(tbClusterService, auditLogService);

        List<TbResourceInfo> resources = new ArrayList<>();
        int jksCntEntity = 17;
        for (int i = 0; i < jksCntEntity; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("JKS Resource" + i);
            resource.setResourceType(ResourceType.JKS);
            resource.setFileName(i + DEFAULT_FILE_NAME);
            resource.setEncodedData(TEST_DATA);
            resources.add(new TbResourceInfo(save(resource)));
        }

        int lwm2mCntEntity = 19;
        for (int i = 0; i < lwm2mCntEntity; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("LWM2M Resource" + i);
            resource.setResourceType(ResourceType.PKCS_12);
            resource.setFileName(i + DEFAULT_FILE_NAME_2);
            resource.setEncodedData(TEST_DATA);
            save(resource);
        }

        List<TbResourceInfo> loadedResources = new ArrayList<>();
        PageLink pageLink = new PageLink(5);
        PageData<TbResourceInfo> pageData;
        do {
            pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS.name() + "&",
                    new TypeReference<>() {
                    }, pageLink);
            loadedResources.addAll(pageData.getData());
            if (pageData.hasNext()) {
                pageLink = pageLink.nextPageLink();
            }
        } while (pageData.hasNext());

        testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new TbResource(), new TbResource(),
                savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED,
                jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity);

        Collections.sort(resources, idComparator);
        Collections.sort(loadedResources, idComparator);

        Assert.assertEquals(resources, loadedResources);
    }

    @Test
    public void testFindSystemTbResources() throws Exception {
        loginSysAdmin();

        List<TbResourceInfo> resources = new ArrayList<>();
        for (int i = 0; i < 173; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("Resource" + i);
            resource.setResourceType(ResourceType.JKS);
            resource.setFileName(i + DEFAULT_FILE_NAME);
            resource.setEncodedData(TEST_DATA);
            resources.add(new TbResourceInfo(save(resource)));
        }
        List<TbResourceInfo> loadedResources = new ArrayList<>();
        PageLink pageLink = new PageLink(24);
        PageData<TbResourceInfo> pageData;
        do {
            pageData = doGetTypedWithPageLink("/api/resource?",
                    new TypeReference<>() {
                    }, pageLink);
            loadedResources.addAll(pageData.getData());
            if (pageData.hasNext()) {
                pageLink = pageLink.nextPageLink();
            }
        } while (pageData.hasNext());

        Collections.sort(resources, idComparator);
        Collections.sort(loadedResources, idComparator);

        Assert.assertEquals(resources, loadedResources);

        Mockito.reset(tbClusterService, auditLogService);

        int cntEntity = resources.size();
        for (TbResourceInfo resource : resources) {
            doDelete("/api/resource/" + resource.getId().getId().toString())
                    .andExpect(status().isOk());
        }

        testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new TbResource(), new TbResource(),
                resources.get(0).getTenantId(), null, null, SYS_ADMIN_EMAIL,
                ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity, 1);

        pageLink = new PageLink(27);
        loadedResources.clear();
        do {
            pageData = doGetTypedWithPageLink("/api/resource?",
                    new TypeReference<>() {
                    }, pageLink);
            loadedResources.addAll(pageData.getData());
            if (pageData.hasNext()) {
                pageLink = pageLink.nextPageLink();
            }
        } while (pageData.hasNext());

        Assert.assertTrue(loadedResources.isEmpty());
    }

    @Test
    public void testFindSystemTbResourcesByType() throws Exception {
        loginSysAdmin();

        List<TbResourceInfo> jksResources = new ArrayList<>();
        List<TbResourceInfo> lwm2mesources = new ArrayList<>();
        int jksCntEntity = 17;
        for (int i = 0; i < jksCntEntity; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("JKS Resource" + i);
            resource.setResourceType(ResourceType.JKS);
            resource.setFileName(i + DEFAULT_FILE_NAME);
            resource.setEncodedData(TEST_DATA);
            TbResourceInfo saved = new TbResourceInfo(save(resource));
            jksResources.add(saved);
        }

        int lwm2mCntEntity = 19;
        for (int i = 0; i < lwm2mCntEntity; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("LWM2M Resource" + i);
            resource.setResourceType(ResourceType.PKCS_12);
            resource.setFileName(i + DEFAULT_FILE_NAME_2);
            resource.setEncodedData(TEST_DATA);
            TbResource saved = save(resource);
            lwm2mesources.add(saved);
        }

        List<TbResourceInfo> loadedResources = new ArrayList<>();
        PageLink pageLink = new PageLink(30);
        PageData<TbResourceInfo> pageData;
        do {
            pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS + "&",
                    new TypeReference<>() {
                    }, pageLink);
            loadedResources.addAll(pageData.getData());
            if (pageData.hasNext()) {
                pageLink = pageLink.nextPageLink();
            }
        } while (pageData.hasNext());

        Collections.sort(jksResources, idComparator);
        Collections.sort(loadedResources, idComparator);

        Assert.assertEquals(jksResources, loadedResources);

        Mockito.reset(tbClusterService, auditLogService);

        int cntEntity = jksResources.size();
        for (TbResourceInfo resource : jksResources) {
            doDelete("/api/resource/" + resource.getId().getId().toString())
                    .andExpect(status().isOk());
        }

        testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new TbResource(), new TbResource(),
                jksResources.get(0).getTenantId(), null, null, SYS_ADMIN_EMAIL,
                ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity, 1);

        pageLink = new PageLink(27);
        loadedResources.clear();
        do {
            pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS + "&",
                    new TypeReference<>() {
                    }, pageLink);
            loadedResources.addAll(pageData.getData());
            if (pageData.hasNext()) {
                pageLink = pageLink.nextPageLink();
            }
        } while (pageData.hasNext());

        Assert.assertTrue(loadedResources.isEmpty());

        loginSysAdmin();

        for (TbResourceInfo resource : lwm2mesources) {
            doDelete("/api/resource/" + resource.getId().getId().toString())
                    .andExpect(status().isOk());
        }
    }

    @Test
    public void testFindSystemAndTenantTbResources() throws Exception {
        List<TbResourceInfo> systemResources = new ArrayList<>();
        List<TbResourceInfo> expectedResources = new ArrayList<>();
        for (int i = 0; i < 73; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("Resource" + i);
            resource.setResourceType(ResourceType.JKS);
            resource.setFileName(i + DEFAULT_FILE_NAME);
            resource.setEncodedData(TEST_DATA);
            expectedResources.add(new TbResourceInfo(save(resource)));
        }

        loginSysAdmin();

        for (int i = 0; i < 173; i++) {
            TbResource resource = new TbResource();
            resource.setTitle("Resource" + i);
            resource.setResourceType(ResourceType.JKS);
            resource.setFileName(i + DEFAULT_FILE_NAME);
            resource.setEncodedData(TEST_DATA);
            TbResourceInfo savedResource = new TbResourceInfo(save(resource));
            systemResources.add(savedResource);
            if (i >= 73) {
                expectedResources.add(savedResource);
            }
        }

        login(tenantAdmin.getEmail(), "testPassword1");

        List<TbResourceInfo> loadedResources = new ArrayList<>();
        PageLink pageLink = new PageLink(24);
        PageData<TbResourceInfo> pageData;
        do {
            pageData = doGetTypedWithPageLink("/api/resource?",
                    new TypeReference<PageData<TbResourceInfo>>() {
                    }, pageLink);
            loadedResources.addAll(pageData.getData());
            if (pageData.hasNext()) {
                pageLink = pageLink.nextPageLink();
            }
        } while (pageData.hasNext());

        Collections.sort(expectedResources, idComparator);
        Collections.sort(loadedResources, idComparator);

        Assert.assertEquals(expectedResources, loadedResources);

        loginSysAdmin();

        for (TbResourceInfo resource : systemResources) {
            doDelete("/api/resource/" + resource.getId().getId().toString())
                    .andExpect(status().isOk());
        }
    }

    @Test
    public void testDownloadTbResourceIfChanged() throws Exception {
        Mockito.reset(tbClusterService, auditLogService);

        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JS_MODULE);
        resource.setTitle("Js resource");
        resource.setFileName(JS_TEST_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(),
                savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
                ActionType.ADDED, ActionType.ADDED);

        ResultActions resultActions = doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download")
                .andExpect(status().isOk());
        MockHttpServletResponse response = resultActions.andReturn().getResponse();
        String eTag = response.getHeader("ETag");
        Assert.assertNotNull(eTag);
        Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);

        //download with if-none-match header
        HttpHeaders headers = new HttpHeaders();
        headers.setIfNoneMatch(eTag);
        doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download", headers)
                .andExpect(status().isNotModified());
    }

    @Test
    public void testDownloadTbResourceIfChangedAsPublicCustomer() throws Exception {
        loginTenantAdmin();
        Mockito.reset(tbClusterService, auditLogService);

        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JS_MODULE);
        resource.setTitle("Js resource");
        resource.setFileName(JS_TEST_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        //download as public customer
        Device device = new Device();
        device.setName("Test Public Device");
        device.setLabel("Label");
        device.setCustomerId(customerId);
        device = doPost("/api/device", device, Device.class);
        device = doPost("/api/customer/public/device/" + device.getUuidId(), Device.class);

        String publicId = device.getCustomerId().toString();

        Mockito.reset(tbClusterService, auditLogService);
        resetTokens();

        JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}");
        JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class);
        this.token = tokens.get("token").asText();

        ResultActions resultActions = doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download")
                .andExpect(status().isOk());
        MockHttpServletResponse response = resultActions.andReturn().getResponse();
        String eTag = response.getHeader("ETag");
        Assert.assertNotNull(eTag);
        Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);

        //download with if-none-match header
        HttpHeaders headers = new HttpHeaders();
        headers.setIfNoneMatch(eTag);
        doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download", headers)
                .andExpect(status().isNotModified());
    }

    @Test
    public void testDownloadTbResourceIfChangedAsCustomerOfDifferentTenant() throws Exception {
        loginTenantAdmin();
        Mockito.reset(tbClusterService, auditLogService);

        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JS_MODULE);
        resource.setTitle("Js resource");
        resource.setFileName(JS_TEST_FILE_NAME);
        resource.setEncodedData(TEST_DATA);

        TbResource savedResource = save(resource);

        loginDifferentTenant();
        loginDifferentTenantCustomer();
        doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download")
                .andExpect(status().isForbidden());
    }

    @Test
    public void testUpdateResourceData_nonUpdatableResourceType() throws Exception {
        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.PKCS_12);
        resource.setTitle("My resource");
        resource.setFileName("3.pks");
        resource.setEncodedData(TEST_DATA);
        TbResource savedResource = save(resource);
        resource.setEtag(savedResource.getEtag());

        savedResource.setEncodedData(TEST_DATA);
        doPost("/api/resource", savedResource)
                .andExpect(status().isBadRequest())
                .andExpect(statusReason(containsString("can't be updated")));

        savedResource.setData(null);
        savedResource.setTitle("Updated resource");
        savedResource = doPost("/api/resource", savedResource, TbResource.class);
        assertThat(savedResource.getTitle()).isEqualTo("Updated resource");
        assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName());
        assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag());
        assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA);
    }

    @Test
    public void testUpdateResourceData_updatableResourceType() throws Exception {
        TbResource resource = new TbResource();
        resource.setResourceType(ResourceType.JS_MODULE);
        resource.setTitle("My resource");
        resource.setFileName("module.js");
        resource.setEncodedData(TEST_DATA);
        TbResource savedResource = save(resource);
        resource.setEtag(savedResource.getEtag());

        String newData = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3});
        savedResource.setEncodedData(newData);
        savedResource.setFileName("new-module.js");
        savedResource.setTitle("Updated title");
        savedResource = save(savedResource);

        assertThat(savedResource.getTitle()).isEqualTo("Updated title");
        assertThat(savedResource.getFileName()).isEqualTo("new-module.js");
        assertThat(savedResource.getEtag()).isNotEqualTo(resource.getEtag());
        assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(newData);
    }

    @Test
    public void testGetLwm2mListObjectsPage() throws Exception {
        loginTenantAdmin();

        List<TbResource> resources = loadLwm2mResources();

        List<LwM2mObject> objects =
                doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>() {});
        Assert.assertNotNull(objects);
        Assert.assertEquals(resources.size(), objects.size());

        removeLoadResources(resources);
    }

    @Test
    public void testGetLwm2mListObjects() throws Exception {
        loginTenantAdmin();

        List<TbResource> resources = loadLwm2mResources();

        List<LwM2mObject> objects =
                doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>() {});
        Assert.assertNotNull(objects);
        Assert.assertEquals(3, objects.size());

        removeLoadResources(resources);
    }

    private TbResource save(TbResource tbResource) throws Exception {
        return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
        });
    }

    private byte[] download(TbResourceId resourceId) throws Exception {
        return doGet("/api/resource/" + resourceId + "/download")
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsByteArray();
    }

    private String getImageLink(TbResourceInfo resourceInfo) {
        return "/api/images/" + (resourceInfo.getTenantId().isSysTenantId() ? "system/" : "") + resourceInfo.getResourceKey();
    }


    private List<TbResource> loadLwm2mResources() throws Exception {
        var models = List.of("1", "2", "3", "5", "6", "9", "19", "3303");

        List<TbResource> resources = new ArrayList<>(models.size());

        for (String model : models) {
            String fileName = model + ".xml";
            byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName));

            TbResource resource = new TbResource();
            resource.setResourceType(ResourceType.LWM2M_MODEL);
            resource.setFileName(fileName);
            resource.setData(bytes);

            resources.add(save(resource));
        }
        return resources;
    }

    private void removeLoadResources(List<TbResource> resources) throws Exception {
        for (TbResourceInfo resource : resources) {
            doDelete("/api/resource/" + resource.getId().getId().toString())
                    .andExpect(status().isOk());
        }
    }

}
