package com.nonononoki.alovoa.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nonononoki.alovoa.Tools;
import com.nonononoki.alovoa.component.TextEncryptorConverter;
import com.nonononoki.alovoa.entity.Captcha;
import com.nonononoki.alovoa.entity.User;
import com.nonononoki.alovoa.entity.user.Gender;
import com.nonononoki.alovoa.entity.user.UserDeleteToken;
import com.nonononoki.alovoa.entity.user.UserInterest;
import com.nonononoki.alovoa.entity.user.UserMiscInfo;
import com.nonononoki.alovoa.model.*;
import com.nonononoki.alovoa.repo.ConversationRepository;
import com.nonononoki.alovoa.repo.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.annotation.Transactional;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;

@SpringBootTest
@ActiveProfiles("test")
@Transactional
class UserServiceTest {

    @Autowired
    private RegisterService registerService;
    @Autowired
    private CaptchaService captchaService;
    @Autowired
    private UserService userService;
    @Autowired
    private UserRepository userRepo;
    @Autowired
    private ConversationRepository conversationRepo;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private TextEncryptorConverter textEncryptor;
    @Value("${app.age.min}")
    private int minAge;
    @Value("${app.age.max}")
    private int maxAge;
    @Value("${app.message.size}")
    private int maxMessageSize;
    @Value("${app.conversation.messages-max}")
    private int maxConvoMessages;
    @Value("${app.first-name.length-max}")
    private int firstNameLengthMax;
    @Value("${app.first-name.length-min}")
    private int firstNameLengthMin;
    @Value("${app.interest.autocomplete.max}")
    private int interestAutocompleteMax;
    @MockitoBean
    private AuthService authService;
    @MockitoBean
    private MailService mailService;
    private List<User> testUsers;
    @Autowired
    private MessageService messageService;

    @BeforeEach
    void before() throws Exception {
        Mockito.when(mailService.sendMail(Mockito.any(String.class), any(String.class), any(String.class),
                any(String.class))).thenReturn(true);
        testUsers = RegisterServiceTest.getTestUsers(captchaService, registerService, firstNameLengthMax,
                firstNameLengthMin);
    }

    @AfterEach
    void after() throws Exception {
        RegisterServiceTest.deleteAllUsers(userService, authService, captchaService, conversationRepo, userRepo);
    }

    @Test
    void test() throws Exception {

        User user1 = testUsers.get(0);
        User user2 = testUsers.get(1);
        User user3 = testUsers.get(2);

        // set location manually since no extra service is needed
        user1.setLocationLatitude(0.0);
        user1.setLocationLongitude(0.0);

        user2.setLocationLatitude(0.0);
        user2.setLocationLongitude(0.0);

        user3.setLocationLatitude(0.0);
        user3.setLocationLongitude(0.0);

        userRepo.saveAndFlush(user1);
        userRepo.saveAndFlush(user2);
        userRepo.saveAndFlush(user3);

        assertEquals(4, userRepo.count());

        String imgMimePng = "png";
        // setup settings
        Mockito.when(authService.getCurrentUser()).thenReturn(user1);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user1);
        byte[] img1 = Tools.resourceToBytes("img/profile1.png");
        userService.updateProfilePicture(img1, imgMimePng);
        String INTEREST = "interest";
        userService.addInterest(INTEREST);
        userService.updateDescription("description1");
        Long INTENTION_TEST = 1L;
        userService.updateIntention(INTENTION_TEST);
        userService.updateMaxAge(100);
        userService.updateMinAge(16);
        userService.updatePreferedGender(2, true);

        // check for urls
        {
            assertThrows(Exception.class, () -> {
                userService.updateDescription("hidden url example.com");
            });

            assertThrows(Exception.class, () -> {
                userService.updateDescription("hidden url test.example.com");
            });

            assertThrows(Exception.class, () -> {
                userService.updateDescription("hidden url test.bit.ly/fdsfasdgadrsfgafgfdsaf13");
            });

            assertThrows(Exception.class, () -> {
                userService.updateDescription("hidden email test.test@test.com");
            });

            assertThrows(Exception.class, () -> {
                userService.updateDescription("hidden email test.test+1234@test.net");
            });
        }

        //MISC INFO
        userService.updateUserMiscInfo(UserMiscInfo.DRUGS_ALCOHOL_NO, true);
        userService.updateUserMiscInfo(UserMiscInfo.FAMILY_WANT, true);
        userService.updateUserMiscInfo(UserMiscInfo.DRUGS_CANNABIS_YES, true);
        userService.updateUserMiscInfo(UserMiscInfo.DRUGS_TOBACCO_SOMETIMES, true);
        userService.updateUserMiscInfo(UserMiscInfo.GENDER_IDENTITY_CIS, true);
        userService.updateUserMiscInfo(UserMiscInfo.RELIGION_NO, true);
        userService.updateUserMiscInfo(UserMiscInfo.RELATIONSHIP_OTHER, true);
        userService.updateUserMiscInfo(UserMiscInfo.RELATIONSHIP_TYPE_POLYAMOROUS, true);
        user1 = userRepo.findById(user1.getId()).get();
        assertEquals(7, user1.getMiscInfos().size());
        userService.updateUserMiscInfo(UserMiscInfo.RELATIONSHIP_TYPE_MONOGAMOUS, false);
        user1 = userRepo.findById(user1.getId()).get();
        assertEquals(6, user1.getMiscInfos().size());

        Mockito.when(authService.getCurrentUser()).thenReturn(user2);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user2);
        byte[] img2 = Tools.resourceToBytes("img/profile2.png");
        userService.updateProfilePicture(img2, imgMimePng);
        userService.addInterest(INTEREST);
        userService.updateDescription("description2");
        userService.updateIntention(INTENTION_TEST);
        userService.updateMaxAge(100);
        userService.updateMinAge(16);
        userService.updatePreferedGender(1, true);

        Mockito.when(authService.getCurrentUser()).thenReturn(user3);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user3);
        byte[] img3 = Tools.resourceToBytes("img/profile3.png");
        userService.updateProfilePicture(img3, imgMimePng);
        assertNotNull(user3.getProfilePicture());
        userService.addInterest(INTEREST);
        assertEquals(1, user3.getInterests().size());
        String description = "description3";
        userService.updateDescription(description);
        assertEquals(description, user3.getDescription());
        userService.updateIntention(INTENTION_TEST);
        assertEquals(INTENTION_TEST, user3.getIntention().getId());
        userService.updateMaxAge(maxAge);
        assertEquals(maxAge, user3.getPreferedMaxAge());

        userService.updateMinAge(minAge);
        assertEquals(minAge, user3.getPreferedMinAge());
        userService.updatePreferedGender(1, true);
        assertEquals(3, user3.getPreferedGenders().size());
        userService.updatePreferedGender(2, true);
        assertEquals(3, user3.getPreferedGenders().size());
        userService.updatePreferedGender(2, false);
        assertEquals(2, user3.getPreferedGenders().size());

        userService.deleteInterest(authService.getCurrentUser().getInterests().get(0).getText());
        assertEquals(0, authService.getCurrentUser().getInterests().size());
        userService.addInterest(INTEREST);
        userService.addImage(img3, imgMimePng);
        assertEquals(1, authService.getCurrentUser().getImages().size());
        userService.deleteImage(authService.getCurrentUser().getImages().get(0).getId());
        assertEquals(0, authService.getCurrentUser().getImages().size());
        userService.updateProfilePicture(img3, imgMimePng);
        assertNotNull(authService.getCurrentUser().getProfilePicture());
        userService.updateAudio(Tools.resourceToBytes("audio/file_example_MP3_700KB.mp3"), "mpeg");
        assertNotNull(user3.getAudio());
        userService.deleteAudio();
        assertNull(user3.getAudio());

        User finalUser = user1;
        assertThrows(Exception.class, () -> {
            deleteTest(finalUser);
        });

        //PROFILE PICTURE
        byte[] imgLong = Tools.resourceToBytes("img/long.jpeg");
        userService.updateProfilePicture(imgLong, "jpeg");
        byte[] imgWide = Tools.resourceToBytes("img/wide.webp");
        userService.updateProfilePicture(imgWide, "webp");

        // USERDATA
        Mockito.when(authService.getCurrentUser()).thenReturn(user1);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user1);
        UUID uuid = user1.getUuid();
        ResponseEntity<Resource> userData = userService.getUserdata(uuid);
        InputStream inputStream = Objects.requireNonNull(userData.getBody()).getInputStream();
        String userDataString = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
                .collect(Collectors.joining("\n"));
        UserGdpr gdpr = objectMapper.readValue(userDataString, UserGdpr.class);
        assertEquals(authService.getCurrentUser().getDescription(), gdpr.getDescription());
        assertEquals(authService.getCurrentUser().getEmail(), gdpr.getEmail());
        assertEquals(authService.getCurrentUser().getFirstName(), gdpr.getFirstName());
        assertEquals(objectMapper.writeValueAsString(authService.getCurrentUser().getDates()),
                objectMapper.writeValueAsString(gdpr.getDates()));
        assertEquals(objectMapper.writeValueAsString(authService.getCurrentUser().getDonations()),
                objectMapper.writeValueAsString(gdpr.getDonations()));
        assertEquals(objectMapper.writeValueAsString(authService.getCurrentUser().getGender()),
                objectMapper.writeValueAsString(gdpr.getGender()));
        assertEquals(objectMapper.writeValueAsString(authService.getCurrentUser().getIntention()),
                objectMapper.writeValueAsString(gdpr.getIntention()));
		assertEquals(authService.getCurrentUser().getPreferedGenders(), gdpr.getPreferedGenders());
        assertIterableEquals(authService.getCurrentUser().getInterests(),
				gdpr.getInterests());
        assertIterableEquals(authService.getCurrentUser().getMessageSent(),
				gdpr.getMessageSent());
        assertEquals(authService.getCurrentUser().getMiscInfos(), gdpr.getMiscInfo());
        assertEquals(authService.getCurrentUser().getLocationLatitude(), gdpr.getLocationLatitude());
        assertEquals(authService.getCurrentUser().getLocationLongitude(), gdpr.getLocationLongitude());
        assertEquals(authService.getCurrentUser().getPreferedMaxAge(), gdpr.getPreferedMaxAge());
        assertEquals(authService.getCurrentUser().getTotalDonations(), gdpr.getTotalDonations(), 0.001);
    }

    @Test
    void testGetInterestAutocomplete() throws AlovoaException {

        removeAllInterests(testUsers);

        User user1 = testUsers.get(0);
        User user2 = testUsers.get(1);
        User user3 = testUsers.get(2);

        String query = "test";

        String INTEREST1 = "test1";
        String INTEREST2 = "test2";
        String INTEREST3 = "test3";
        String INTEREST4 = "test4";
        String INTEREST5 = "test5";

        Mockito.when(authService.getCurrentUser()).thenReturn(user1);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user1);
        userService.addInterest(INTEREST1);
        userService.addInterest(INTEREST2);
        userService.addInterest(INTEREST3);
        userService.addInterest(INTEREST4);
        userService.addInterest(INTEREST5);

        Mockito.when(authService.getCurrentUser()).thenReturn(user2);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user2);
        userService.addInterest(INTEREST1);
        userService.addInterest(INTEREST2);
        userService.addInterest(INTEREST3);
        userService.addInterest(INTEREST4);

        Mockito.when(authService.getCurrentUser()).thenReturn(user3);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user3);
        List<UserInterestDto> iterests = userService.getInterestAutocomplete(query);
        List<String> interestNames = iterests.stream().map(UserInterestDto::getName).toList();

        assertEquals(interestAutocompleteMax, iterests.size());
        assertEquals(2, iterests.get(0).getCount());
        assertEquals(2, iterests.get(1).getCount());
        assertEquals(2, iterests.get(2).getCount());
        assertEquals(2, iterests.get(3).getCount());
        assertEquals(1, iterests.get(4).getCount());
        assertTrue(interestNames.contains(INTEREST1));
        assertTrue(interestNames.contains(INTEREST2));
        assertTrue(interestNames.contains(INTEREST3));
        assertTrue(interestNames.contains(INTEREST4));
        assertTrue(interestNames.contains(INTEREST5));

        Mockito.when(authService.getCurrentUser()).thenReturn(user2);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user2);
        iterests = userService.getInterestAutocomplete(query);
        assertEquals(1, iterests.size());
        assertEquals(1, iterests.get(0).getCount());
        assertEquals(INTEREST5, iterests.get(0).getName());

        removeAllInterests(testUsers);
    }

    @Test
    void testVerificationProcess() throws AlovoaException, NoSuchAlgorithmException, IOException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, NoSuchPaddingException,
            BadPaddingException, InvalidKeyException {

        User user1 = testUsers.get(0);
        User user2 = testUsers.get(1);
        User user3 = testUsers.get(2);

        UUID uuid1 = user1.getUuid();

        Mockito.when(authService.getCurrentUser()).thenReturn(user1);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user1);

        String imgMimePng = "png";
        byte[] img1 = Tools.resourceToBytes("img/profile1.png");
        userService.updateProfilePicture(img1, imgMimePng);
        user1 = userRepo.findById(user1.getId()).get();
        userService.updateVerificationPicture(img1, imgMimePng);
        user1 = userRepo.findById(user1.getId()).get();

        Mockito.when(authService.getCurrentUser()).thenReturn(user2);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user2);
        userService.upvoteVerificationPicture(uuid1);

        Assertions.assertThrows(AlovoaException.class, () -> {
            userService.downvoteVerificationPicture(uuid1);
        });

        Assertions.assertThrows(AlovoaException.class, () -> {
            userService.upvoteVerificationPicture(uuid1);
        });

        Mockito.when(authService.getCurrentUser()).thenReturn(user3);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user3);
        userService.downvoteVerificationPicture(uuid1);

        Assertions.assertThrows(AlovoaException.class, () -> {
            userService.upvoteVerificationPicture(uuid1);
        });

        Assertions.assertThrows(AlovoaException.class, () -> {
            userService.downvoteVerificationPicture(uuid1);
        });

        user1 = userRepo.findById(user1.getId()).get();
        assertEquals(1, user1.getVerificationPicture().getUserYes().size());
        assertEquals(1, user1.getVerificationPicture().getUserNo().size());

        Mockito.when(authService.getCurrentUser()).thenReturn(user1);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user1);
        userService.updateProfilePicture(img1, imgMimePng);
        user1 = userRepo.findById(user1.getId()).get();
        assertNull(user1.getVerificationPicture());
    }

    @Test
    void testGenerateVerificationCode() throws AlovoaException {
        User user1 = testUsers.get(0);
        Mockito.when(authService.getCurrentUser()).thenReturn(user1);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user1);
        String code = userService.getVerificationCode();
        user1 = userRepo.findById(user1.getId()).get();

        assertNotNull(code);
        assertEquals(code, user1.getVerificationCode());
    }

    @Test
    void testOnboarding() throws Exception {
        User user1 = testUsers.get(0);
        Mockito.when(authService.getCurrentUser()).thenReturn(user1);
        Mockito.when(authService.getCurrentUser(true)).thenReturn(user1);

        String imgMimePng = "png";
        byte[] img1 = Tools.resourceToBytes("img/profile1.png");

        String description = "description";
        Long intentionId = 1L;
        Set<String> interests = Set.of("int1", "int2");
        List<Long> preferredGenders = List.of(1L, 2L);

        ProfileOnboardingDto model = new ProfileOnboardingDto();
        model.setProfilePictureMime(imgMimePng);
        model.setDescription(description);
        model.setIntention(intentionId);
        model.setInterests(interests);
        model.setPreferredGenders(preferredGenders);
        model.setNotificationLike(false);
        model.setNotificationChat(true);

        user1 = userService.onboarding(img1, model);

        assertEquals(description, user1.getDescription());
        assertEquals(intentionId, user1.getIntention().getId());
        assertTrue(interests.containsAll(user1.getInterests().stream().map(UserInterest::getText).collect(Collectors.toSet())));
        assertTrue(preferredGenders.containsAll(user1.getPreferedGenders().stream().map(Gender::getId).toList()));
        assertNotNull(user1.getProfilePicture());
        assertFalse(user1.getUserSettings().isEmailLike());
        assertTrue(user1.getUserSettings().isEmailChat());
    }

    private void removeAllInterests(List<User> ul) {
        ul.forEach(u -> {
            u.getInterests().clear();
            userRepo.saveAndFlush(u);
        });
    }

    private void deleteTest(User user) throws Exception {
        UserDeleteToken token = userService.deleteAccountRequest();
        UserDeleteAccountDto dto = new UserDeleteAccountDto();
        Captcha captcha = captchaService.generate();
        dto.setCaptchaId(captcha.getId());
        dto.setCaptchaText(captcha.getText());
        dto.setConfirm(true);
        dto.setEmail(user.getEmail());
        dto.setTokenString(token.getContent());
        userService.deleteAccountConfirm(dto);
    }
}
