package app.mealsmadeeasy.api.image;

import app.mealsmadeeasy.api.PostgresTestsExtension;
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService;
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import static app.mealsmadeeasy.api.image.ContainsImagesMatcher.containsImages;
import static app.mealsmadeeasy.api.user.ContainsUsersMatcher.containsUsers;
import static app.mealsmadeeasy.api.user.IsUserMatcher.isUser;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

@SpringBootTest
@ExtendWith(PostgresTestsExtension.class)
public class S3ImageServiceTests {

    private static InputStream getHal9000InputStream() {
        return S3ImageServiceTests.class.getResourceAsStream("HAL9000.svg");
    }

    private record ImageResource(InputStream inputStream, long size) {}

    private static ImageResource getAsResource(@Nullable InputStream inputStream) {
        if (inputStream == null) {
            throw new RuntimeException("Input stream is null");
        }
        try {
            final byte[] bytes = inputStream.readAllBytes();
            final long size = bytes.length;
            return new ImageResource(new ByteArrayInputStream(bytes), size);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static ImageResource getWebpResource() {
        return getAsResource(S3ImageServiceTests.class.getResourceAsStream("/KingArthurSourdough.webp"));
    }

    private static ImageResource getJpgResource() {
        return getAsResource(S3ImageServiceTests.class.getResourceAsStream("/Obazda.jpg"));
    }

    private static ImageResource getPngResource() {
        return getAsResource(S3ImageServiceTests.class.getResourceAsStream("/veganfood.png"));
    }

    private static ImageResource getSvgResource() {
        return getAsResource(getHal9000InputStream());
    }

    @Autowired
    private UserService userService;

    @Autowired
    private ImageService imageService;

    private static final String TEST_PASSWORD = "test";
    private static final long HAL_LENGTH = 27881L;

    private User seedUser() {
        final String uuid = UUID.randomUUID().toString();
        try {
            return this.userService.createUser(uuid, uuid + "@test.com", TEST_PASSWORD);
        } catch (UserCreateException e) {
            throw new RuntimeException(e);
        }
    }

    private static String makeUserFilename() {
        return UUID.randomUUID() + ".svg";
    }

    private Image seedImage(User owner, ImageCreateSpec spec) {
        try (final InputStream hal9000 = getHal9000InputStream()) {
            return this.imageService.create(
                    owner,
                    makeUserFilename(),
                    hal9000,
                    HAL_LENGTH,
                    spec
            );
        } catch (ImageException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Image seedImage(User owner) {
        return this.seedImage(owner, ImageCreateSpec.builder().build());
    }

    private Image makePublic(Image image, User modifier) {
        final ImageUpdateSpec spec = ImageUpdateSpec.builder()
                .isPublic(true)
                .build();
        return this.imageService.update(image, modifier, spec);
    }

    @Test
    public void smokeScreen() {}

    @Test
    public void simpleCreate() {
        final User owner = this.seedUser();
        final Image image = this.seedImage(owner);
        assertThat(image.getOwner(), isUser(owner));
        assertThat(image.getCreated(), is(notNullValue()));
        assertThat(image.getModified(), is(nullValue()));
        assertThat(image.getUserFilename(), is(notNullValue()));
        assertThat(image.getMimeType(), is("image/svg+xml"));
        assertThat(image.getAlt(), is(nullValue()));
        assertThat(image.getCaption(), is(nullValue()));
        assertThat(image.getIsPublic(), is(false));
        assertThat(image.getViewers(), is(empty()));
    }

    @Test
    public void properlyLoadsContent() throws IOException {
        final User owner = this.seedUser();
        final Image image = this.seedImage(owner);
        final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(image, owner));
        final byte[] contentBytes = content.readAllBytes();
        assertThat(contentBytes.length, is((int) HAL_LENGTH));
        content.close();
    }

    @Test
    public void loadImageWithOwnerAsViewer() throws IOException {
        final User owner = this.seedUser();
        final Image image = this.seedImage(owner);
        final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(image, owner));
        content.close();
    }

    @Test
    public void loadPublicImage() throws IOException {
        final User owner = this.seedUser();
        final Image seedImage = this.seedImage(owner);
        final Image publicImage = this.makePublic(seedImage, owner);
        final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(publicImage, null));
        content.close();
    }

    @Test
    public void loadImageWithViewer() throws IOException {
        final User owner = this.seedUser();
        final User viewer = this.seedUser();
        Image seedImage = this.seedImage(owner);
        final ImageUpdateSpec spec = ImageUpdateSpec.builder()
                .viewersToAdd(Set.of(viewer))
                .build();
        final Image imageWithViewer = this.imageService.update(seedImage, owner, spec);
        final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(imageWithViewer, viewer));
        content.close();
    }

    @Test
    public void getImagesOwnedBy() {
        final User owner = this.seedUser();
        final User otherOwner = this.seedUser();
        final Image image0 = this.seedImage(owner);
        final Image image1 = this.seedImage(owner);
        final Image image2 = this.seedImage(otherOwner);

        final List<Image> ownedImages = this.imageService.getImagesOwnedBy(owner);
        assertThat(ownedImages.size(), is(2));
        assertThat(ownedImages, containsImages(image0, image1));

        final List<Image> otherOwnedImages = this.imageService.getImagesOwnedBy(otherOwner);
        assertThat(otherOwnedImages.size(), is(1));
        assertThat(otherOwnedImages, containsImages(image2));
    }

    @Test
    public void updateAlt() {
        final User owner = this.seedUser();
        Image image = this.seedImage(owner);
        final ImageUpdateSpec spec = ImageUpdateSpec.builder()
                .alt("HAL 9000")
                .build();
        image = this.imageService.update(image, owner, spec);
        assertThat(image.getAlt(), is("HAL 9000"));
    }

    @Test
    public void updateCaption() {
        final User owner = this.seedUser();
        Image image = this.seedImage(owner);
        final ImageUpdateSpec spec = ImageUpdateSpec.builder()
                .caption("HAL 9000 from 2001: A Space Odyssey")
                .build();
        image = this.imageService.update(image, owner, spec);
        assertThat(image.getCaption(), is("HAL 9000 from 2001: A Space Odyssey"));
    }

    @Test
    public void updateIsPublic() {
        final User owner = this.seedUser();
        Image image = this.seedImage(owner);
        final ImageUpdateSpec spec = ImageUpdateSpec.builder()
                .isPublic(true)
                .build();
        image = this.imageService.update(image, owner, spec);
        assertThat(image.getIsPublic(), is(true));
    }

    private Image addViewer(Image image, User owner, User viewer) {
        final ImageUpdateSpec spec0 = ImageUpdateSpec.builder()
                .viewersToAdd(Set.of(viewer))
                .build();
        return this.imageService.update(image, owner, spec0);
    }

    @Test
    public void addViewers() {
        final User owner = this.seedUser();
        final User viewer = this.seedUser();
        Image image = this.seedImage(owner);
        image = this.addViewer(image, owner, viewer);
        assertThat(image.getViewers(), containsUsers(viewer));
    }

    @Test
    public void removeViewers() {
        final User owner = this.seedUser();
        final User viewer = this.seedUser();
        Image image = this.seedImage(owner);
        image = this.addViewer(image, owner, viewer);
        assertThat(image.getViewers(), containsUsers(viewer));

        final ImageUpdateSpec spec1 = ImageUpdateSpec.builder()
                .viewersToRemove(Set.of(viewer))
                .build();
        image = this.imageService.update(image, owner, spec1);
        assertThat(image.getViewers(), empty());
    }

    @Test
    public void clearAllViewers() {
        final User owner = this.seedUser();
        final User viewer = this.seedUser();
        Image image = this.seedImage(owner);
        image = this.addViewer(image, owner, viewer);
        assertThat(image.getViewers(), containsUsers(viewer));

        final ImageUpdateSpec spec1 = ImageUpdateSpec.builder()
                .clearAllViewers(true)
                .build();
        image = this.imageService.update(image, owner, spec1);
        assertThat(image.getViewers(), empty());
    }

    @Test
    public void deleteImage() throws Exception {
        final User owner = this.seedUser();
        final Image image = this.seedImage(owner);
        this.imageService.deleteImage(image, owner);
        assertThrows(NoSuchEntityWithIdException.class, () -> this.imageService.getById(image.getId(), owner));
    }

    @TestFactory
    public List<DynamicTest> supportedImageTypesTests() {
        return Map.of(
                "jpeg", getJpgResource(),
                "png", getPngResource(),
                "webp", getWebpResource(),
                "svg", getSvgResource()
        )
                .entrySet()
                .stream()
                .map(entry -> DynamicTest.dynamicTest(
                        String.format("%s_supported", entry.getKey()),
                        () -> {
                            final User owner = this.seedUser();
                            final Image image = this.imageService.create(
                                    owner,
                                    UUID.randomUUID() + "." + entry.getKey(),
                                    entry.getValue().inputStream(),
                                    entry.getValue().size(),
                                    ImageCreateSpec.builder().build()
                            );
                            assertThat(image, is(notNullValue()));
                        }
                        )
                )
                .toList();
    }

}
