/**
 * 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.google.common.hash.Hashing;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.resource.TbResourceRepository;

import java.util.Base64;
import java.util.List;

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

@DaoSqlTest
public class ImageControllerTest extends AbstractControllerTest {

    private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("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");
    private static final byte[] JPEG_IMAGE = Base64.getDecoder().decode(" ");
    private static final byte[] SVG_IMAGE = Base64.getDecoder().decode("PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiMyNzg2MjJ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+Cg==");

    @Autowired
    private TbResourceRepository resourceRepository;

    @Before
    public void beforeEach() throws Exception {
        loginTenantAdmin();
    }

    @After
    public void afterEach() {
        resourceRepository.deleteAll();
    }

    @Test
    public void testUploadPngImage() throws Exception {
        String filename = "my_png_image.png";
        TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);

        assertThat(imageInfo.getTitle()).isEqualTo(filename);
        assertThat(imageInfo.getResourceType()).isEqualTo(ResourceType.IMAGE);
        assertThat(imageInfo.getResourceKey()).isEqualTo(filename);
        assertThat(imageInfo.getFileName()).isEqualTo(filename);
        assertThat(imageInfo.getEtag()).isEqualTo(Hashing.sha256().hashBytes(PNG_IMAGE).toString());

        checkPngImageDescriptor(imageInfo.getDescriptor(ImageDescriptor.class));

        assertThat(downloadImage("tenant", filename)).containsExactly(PNG_IMAGE);
        assertThat(downloadImagePreview("tenant", filename)).containsExactly(PNG_IMAGE);

        assertThat(getImages(null, false, 10)).contains(imageInfo);
    }

    @Test
    public void testUploadJpegImage() throws Exception {
        String filename = "my_jpeg_image.jpg";
        TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE);

        ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
        checkJpegImageDescriptor(imageDescriptor);

        assertThat(downloadImage("tenant", filename)).containsExactly(JPEG_IMAGE);
        assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize());
    }

    @Test
    public void testUploadSvgImage() throws Exception {
        String filename = "my_svg_image.svg";
        TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE);

        ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
        checkSvgImageDescriptor(imageDescriptor);

        assertThat(downloadImage("tenant", filename)).containsExactly(SVG_IMAGE);
        assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize());
    }

    @Test
    public void testUploadImageWithSameFilename() throws Exception {
        String filename = "my_jpeg_image.jpg";
        TbResourceInfo imageInfo1 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE);
        assertThat(imageInfo1.getTitle()).isEqualTo(filename);
        assertThat(imageInfo1.getFileName()).isEqualTo(filename);
        assertThat(imageInfo1.getResourceKey()).isEqualTo(filename);

        TbResourceInfo imageInfo2 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE);
        assertThat(imageInfo2.getTitle()).isEqualTo(filename);
        assertThat(imageInfo2.getFileName()).isEqualTo(filename);
        assertThat(imageInfo2.getResourceKey()).isEqualTo("my_jpeg_image_(1).jpg");

        TbResourceInfo imageInfo3 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE);
        assertThat(imageInfo3.getTitle()).isEqualTo(filename);
        assertThat(imageInfo3.getFileName()).isEqualTo(filename);
        assertThat(imageInfo3.getResourceKey()).isEqualTo("my_jpeg_image_(2).jpg");
    }

    @Test
    public void testUpdateImage() throws Exception {
        String filename = "my_png_image.png";
        TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);
        checkPngImageDescriptor(imageInfo.getDescriptor(ImageDescriptor.class));

        String newFilename = "my_jpeg_image.png";
        imageInfo = uploadImage(HttpMethod.PUT, "/api/images/tenant/" + filename, newFilename, "image/jpeg", JPEG_IMAGE);

        assertThat(imageInfo.getTitle()).isEqualTo(filename);
        assertThat(imageInfo.getResourceKey()).isEqualTo(filename);
        assertThat(imageInfo.getFileName()).isEqualTo(newFilename);

        ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
        checkJpegImageDescriptor(imageDescriptor);

        assertThat(downloadImage("tenant", filename)).containsExactly(JPEG_IMAGE);
        assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize());
    }

    @Test
    public void testUpdateImageInfo() throws Exception {
        String filename = "my_png_image.png";
        TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);
        ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);

        assertThat(imageInfo.getTitle()).isEqualTo(filename);
        assertThat(imageInfo.getResourceKey()).isEqualTo(filename);
        assertThat(imageInfo.getFileName()).isEqualTo(filename);

        String newTitle = "My PNG image";
        imageInfo.setTitle(newTitle);
        imageInfo.setDescriptor(JacksonUtil.newObjectNode());
        imageInfo = doPut("/api/images/tenant/" + filename + "/info", imageInfo, TbResourceInfo.class);

        assertThat(imageInfo.getTitle()).isEqualTo(newTitle);
        assertThat(imageInfo.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor);
    }

    @Test
    public void testExportImportImage() throws Exception {
        String filename = "my_png_image.png";
        uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);

        ImageExportData exportData = doGet("/api/images/tenant/" + filename + "/export", ImageExportData.class);
        assertThat(exportData.getMediaType()).isEqualTo("image/png");
        assertThat(exportData.getFileName()).isEqualTo(filename);
        assertThat(exportData.getTitle()).isEqualTo(filename);
        assertThat(exportData.getResourceKey()).isEqualTo(filename);
        assertThat(exportData.getData()).isEqualTo(Base64.getEncoder().encodeToString(PNG_IMAGE));
        assertThat(exportData.isPublic()).isTrue();
        assertThat(exportData.getPublicResourceKey()).isNotEmpty();

        doDelete("/api/images/tenant/" + filename).andExpect(status().isOk());

        TbResourceInfo importedImageInfo = doPut("/api/image/import", exportData, TbResourceInfo.class);
        assertThat(importedImageInfo.getTitle()).isEqualTo(filename);
        assertThat(importedImageInfo.getResourceKey()).isEqualTo(filename);
        assertThat(importedImageInfo.getFileName()).isEqualTo(filename);
        assertThat(importedImageInfo.isPublic()).isTrue();
        assertThat(importedImageInfo.getPublicResourceKey()).isEqualTo(exportData.getPublicResourceKey());
        checkPngImageDescriptor(importedImageInfo.getDescriptor(ImageDescriptor.class));
        assertThat(downloadImage("tenant", filename)).containsExactly(PNG_IMAGE);
    }

    @Test
    public void testGetImages() throws Exception {
        loginSysAdmin();
        String systemImageName = "my_system_png_image.png";
        TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE);

        loginTenantAdmin();
        String tenantImageName = "my_jpeg_image.jpg";
        TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE);

        List<TbResourceInfo> tenantImages = getImages(null, false, 10);
        assertThat(tenantImages).containsOnly(tenantImage);

        List<TbResourceInfo> allImages = getImages(null, true, 10);
        assertThat(allImages).containsOnly(tenantImage, systemImage);

        assertThat(getImages("png", true, 10))
                .containsOnly(systemImage);
        assertThat(getImages("jpg", true, 10))
                .containsOnly(tenantImage);
    }

    @Test
    public void testUploadPublicImage() throws Exception {
        String filename = "my_public_image.png";
        TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);

        assertThat(imageInfo.isPublic()).isTrue();
        assertThat(imageInfo.getPublicResourceKey()).hasSize(32);
        assertThat(imageInfo.getPublicLink()).isEqualTo("/api/images/public/" + imageInfo.getPublicResourceKey());

        assertThat(downloadImage("tenant", filename)).containsExactly(PNG_IMAGE);
        resetTokens();
        assertThat(downloadPublicImage(imageInfo.getPublicResourceKey())).containsExactly(PNG_IMAGE);
    }

    @Test
    public void testMakeImagePublic() throws Exception {
        String filename = "my_public_image.png";
        TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);
        String publicKey = imageInfo.getPublicResourceKey();
        assertThat(publicKey).hasSize(32);

        updateImagePublicStatus(filename, false);
        doGet("/api/images/public/" + publicKey).andExpect(status().isNotFound());

        updateImagePublicStatus(filename, true);
        resetTokens();
        assertThat(downloadPublicImage(publicKey)).containsExactly(PNG_IMAGE);

        loginTenantAdmin();
        updateImagePublicStatus(filename, false);
        doGet("/api/images/public/" + publicKey).andExpect(status().isNotFound());
    }

    @Test
    public void testGetImageUploadSpecs() throws Exception {
        SystemParams systemParams = doGet("/api/system/params", SystemParams.class);
        assertThat(systemParams.getMaxResourceSize()).isZero();

        loginSysAdmin();
        updateDefaultTenantProfileConfig(tenantProfileConfig -> {
            tenantProfileConfig.setMaxResourceSize(100);
        });
        loginTenantAdmin();
        systemParams = doGet("/api/system/params", SystemParams.class);
        assertThat(systemParams.getMaxResourceSize()).isEqualTo(100);

        loginSysAdmin();
        updateDefaultTenantProfileConfig(tenantProfileConfig -> {
            tenantProfileConfig.setMaxResourceSize(0);
        });
        loginTenantAdmin();
        systemParams = doGet("/api/system/params", SystemParams.class);
        assertThat(systemParams.getMaxResourceSize()).isEqualTo(0);
    }

    private TbResourceInfo updateImagePublicStatus(String filename, boolean isPublic) throws Exception {
        return doPut("/api/images/tenant/" + filename + "/public/" + isPublic, "", TbResourceInfo.class);
    }

    private void checkPngImageDescriptor(ImageDescriptor imageDescriptor) {
        assertThat(imageDescriptor.getMediaType()).isEqualTo("image/png");
        assertThat(imageDescriptor.getWidth()).isEqualTo(200);
        assertThat(imageDescriptor.getHeight()).isEqualTo(160);
        assertThat(imageDescriptor.getSize()).isEqualTo(PNG_IMAGE.length);
        assertThat(imageDescriptor.getEtag()).isEqualTo(Hashing.sha256().hashBytes(PNG_IMAGE).toString());

        ImageDescriptor previewDescriptor = imageDescriptor.getPreviewDescriptor();
        assertThat(previewDescriptor.getMediaType()).isEqualTo("image/png");
        assertThat(previewDescriptor.getWidth()).isEqualTo(200);
        assertThat(previewDescriptor.getHeight()).isEqualTo(160);
        assertThat(previewDescriptor.getSize()).isEqualTo(PNG_IMAGE.length);
        assertThat(previewDescriptor.getEtag()).isEqualTo(imageDescriptor.getEtag());
    }

    private void checkJpegImageDescriptor(ImageDescriptor imageDescriptor) {
        assertThat(imageDescriptor.getMediaType()).isEqualTo("image/jpeg");
        assertThat(imageDescriptor.getWidth()).isEqualTo(225);
        assertThat(imageDescriptor.getHeight()).isEqualTo(225);
        assertThat(imageDescriptor.getSize()).isEqualTo(JPEG_IMAGE.length);
        assertThat(imageDescriptor.getEtag()).isEqualTo(Hashing.sha256().hashBytes(JPEG_IMAGE).toString());

        ImageDescriptor previewDescriptor = imageDescriptor.getPreviewDescriptor();
        assertThat(previewDescriptor.getMediaType()).isEqualTo("image/png");
        assertThat(previewDescriptor.getWidth()).isEqualTo(225);
        assertThat(previewDescriptor.getHeight()).isEqualTo(225);
        assertThat(previewDescriptor.getSize()).isEqualTo(53498);
        assertThat(previewDescriptor.getEtag()).isEqualTo("c909e20ba942f95f4ed1ddfcf4307ce846b4a689195c629cd85f2517f46e84f9");
    }

    private void checkSvgImageDescriptor(ImageDescriptor imageDescriptor) {
        assertThat(imageDescriptor.getMediaType()).isEqualTo("image/svg+xml");
        assertThat(imageDescriptor.getWidth()).isEqualTo(150);
        assertThat(imageDescriptor.getHeight()).isEqualTo(150);
        assertThat(imageDescriptor.getSize()).isEqualTo(SVG_IMAGE.length);
        assertThat(imageDescriptor.getEtag()).isEqualTo(Hashing.sha256().hashBytes(SVG_IMAGE).toString());
    }

    private List<TbResourceInfo> getImages(String searchText, boolean includeSystemImages, int limit) throws Exception {
        PageData<TbResourceInfo> images = doGetTypedWithPageLink("/api/images?includeSystemImages=" + includeSystemImages + "&", new TypeReference<>() {}, new PageLink(limit, 0, searchText));
        return images.getData();
    }

    private byte[] downloadImage(String type, String key) throws Exception {
        return doGet("/api/images/" + type + "/" + key).andExpect(status().isOk())
                .andReturn().getResponse().getContentAsByteArray();
    }

    private byte[] downloadImagePreview(String type, String key) throws Exception {
        return doGet("/api/images/" + type + "/" + key + "/preview").andExpect(status().isOk())
                .andReturn().getResponse().getContentAsByteArray();
    }

    private byte[] downloadPublicImage(String publicKey) throws Exception {
        return doGet("/api/images/public/" + publicKey).andExpect(status().isOk())
                .andReturn().getResponse().getContentAsByteArray();
    }

    private <R> TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception {
        MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content);
        var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file);
        setJwtToken(request);
        return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class);
    }

}
