package org.raddatz.familienarchiv.user;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.doThrow;

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.user.ResetPasswordRequest;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.PasswordResetToken;
import org.raddatz.familienarchiv.user.PasswordResetTokenRepository;
import org.springframework.mail.MailSendException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.raddatz.familienarchiv.auth.AuthService;
import org.springframework.test.util.ReflectionTestUtils;

@ExtendWith(MockitoExtension.class)
class PasswordResetServiceTest {

    @Mock UserService userService;
    @Mock PasswordResetTokenRepository tokenRepository;
    @Mock PasswordEncoder passwordEncoder;
    @Mock JavaMailSender mailSender;
    @Mock AuthService authService;
    @InjectMocks PasswordResetService service;


    private AppUser makeUser(String email) {
        return AppUser.builder()
                .id(UUID.randomUUID())
                .email("testuser@example.com")
                .email(email)
                .password("hashed")
                .build();
    }

    // ─── requestReset ─────────────────────────────────────────────────────────

    @Test
    void requestReset_savesTokenForKnownEmail() {
        AppUser user = makeUser("user@example.com");
        when(userService.findByEmailOptional("user@example.com")).thenReturn(Optional.of(user));

        service.requestReset("user@example.com", "http://localhost:3000");

        verify(tokenRepository).save(argThat(t ->
                t.getUser().equals(user)
                        && t.getToken().length() == 64
                        && !t.isUsed()));
    }

    @Test
    void requestReset_doesNothingForUnknownEmail() {
        when(userService.findByEmailOptional("ghost@example.com")).thenReturn(Optional.empty());

        service.requestReset("ghost@example.com", "http://localhost:3000");

        verify(tokenRepository, never()).save(any());
    }

    // ─── resetPassword ────────────────────────────────────────────────────────

    @Test
    void resetPassword_updatesPasswordForValidToken() {
        AppUser user = makeUser("user@example.com");
        PasswordResetToken token = PasswordResetToken.builder()
                .id(UUID.randomUUID())
                .token("validtoken123")
                .user(user)
                .expiresAt(LocalDateTime.now().plusHours(1))
                .used(false)
                .build();
        when(tokenRepository.findByToken("validtoken123")).thenReturn(Optional.of(token));
        when(passwordEncoder.encode("newpass")).thenReturn("hashed-newpass");

        ResetPasswordRequest req = new ResetPasswordRequest();
        req.setToken("validtoken123");
        req.setNewPassword("newpass");
        service.resetPassword(req);

        verify(passwordEncoder).encode("newpass");
        verify(userService).save(argThat(u -> u.getPassword().equals("hashed-newpass")));
        assertThat(token.isUsed()).isTrue();
    }

    @Test
    void resetPassword_throwsForExpiredToken() {
        AppUser user = makeUser("user@example.com");
        PasswordResetToken token = PasswordResetToken.builder()
                .token("expiredtoken")
                .user(user)
                .expiresAt(LocalDateTime.now().minusMinutes(1))
                .used(false)
                .build();
        when(tokenRepository.findByToken("expiredtoken")).thenReturn(Optional.of(token));

        ResetPasswordRequest req = new ResetPasswordRequest();
        req.setToken("expiredtoken");
        req.setNewPassword("newpass");

        assertThatThrownBy(() -> service.resetPassword(req))
                .isInstanceOf(DomainException.class);
    }

    @Test
    void resetPassword_throwsForUnknownToken() {
        when(tokenRepository.findByToken("nosuchtoken")).thenReturn(Optional.empty());

        ResetPasswordRequest req = new ResetPasswordRequest();
        req.setToken("nosuchtoken");
        req.setNewPassword("newpass");

        assertThatThrownBy(() -> service.resetPassword(req))
                .isInstanceOf(DomainException.class);
    }

    @Test
    void resetPassword_throwsForAlreadyUsedToken() {
        AppUser user = makeUser("user@example.com");
        PasswordResetToken token = PasswordResetToken.builder()
                .token("usedtoken")
                .user(user)
                .expiresAt(LocalDateTime.now().plusHours(1))
                .used(true) // already used
                .build();
        when(tokenRepository.findByToken("usedtoken")).thenReturn(Optional.of(token));

        ResetPasswordRequest req = new ResetPasswordRequest();
        req.setToken("usedtoken");
        req.setNewPassword("newpass");

        assertThatThrownBy(() -> service.resetPassword(req))
                .isInstanceOf(DomainException.class);
    }

    // ─── requestReset — mail sending branches ─────────────────────────────────

    @Test
    void requestReset_skipsEmail_whenMailSenderIsNull() {
        ReflectionTestUtils.setField(service, "mailSender", null);
        AppUser user = makeUser("user@example.com");
        when(userService.findByEmailOptional("user@example.com")).thenReturn(Optional.of(user));

        // Must not throw even without mail sender
        service.requestReset("user@example.com", "http://localhost:3000");

        verify(tokenRepository).save(any());
        verify(mailSender, never()).send(any(SimpleMailMessage.class));
    }

    @Test
    void requestReset_logsError_whenMailExceptionThrown() {
        // mailSender is @Autowired(required=false) — not in constructor, so needs explicit injection
        ReflectionTestUtils.setField(service, "mailSender", mailSender);
        AppUser user = makeUser("user@example.com");
        when(userService.findByEmailOptional("user@example.com")).thenReturn(Optional.of(user));
        doThrow(new MailSendException("SMTP error")).when(mailSender).send(any(SimpleMailMessage.class));

        // Must not propagate the MailException
        service.requestReset("user@example.com", "http://localhost:3000");

        verify(tokenRepository).save(any());
        verify(mailSender).send(any(SimpleMailMessage.class));
    }

    @Test
    void resetPassword_revokes_all_sessions_after_password_reset() {
        AppUser user = makeUser("user@example.com");
        PasswordResetToken token = PasswordResetToken.builder()
                .id(UUID.randomUUID())
                .token("validtoken123")
                .user(user)
                .expiresAt(LocalDateTime.now().plusHours(1))
                .used(false)
                .build();
        when(tokenRepository.findByToken("validtoken123")).thenReturn(Optional.of(token));
        when(passwordEncoder.encode(any())).thenReturn("hashed");

        ResetPasswordRequest req = new ResetPasswordRequest();
        req.setToken("validtoken123");
        req.setNewPassword("newpass");
        service.resetPassword(req);

        verify(authService).revokeAllSessions("user@example.com");
    }

    // ─── cleanupExpiredTokens ─────────────────────────────────────────────────

    @Test
    void cleanupExpiredTokens_delegatesToRepository() {
        service.cleanupExpiredTokens();

        verify(tokenRepository).deleteExpiredAndUsed(any(LocalDateTime.class));
    }
}
