package com.codebyte.api.auth.service;

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.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import com.codebyte.api.auth.api.dto.ActivateRequest;
import com.codebyte.api.auth.api.dto.RegisterRequest;
import com.codebyte.api.auth.domain.TokenType;
import com.codebyte.api.auth.domain.User;
import com.codebyte.api.auth.repository.UserRepository;
import com.codebyte.api.config.AppProperties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;

@ExtendWith(MockitoExtension.class)
class AuthServiceTest {

    @Mock private UserRepository userRepository;
    @Mock private PasswordEncoder passwordEncoder;
    @Mock private TokenService tokenService;
    @Mock private com.codebyte.api.auth.service.RefreshTokenService refreshTokenService;
    @Mock private com.codebyte.api.auth.security.JwtService jwtService;
    @Mock private MailService mailService;

    private AuthService authService;

    private final AppProperties properties = com.codebyte.api.support.TestAppProperties.defaults();

    private AuthService service() {
        return new AuthService(
                userRepository,
                passwordEncoder,
                tokenService,
                refreshTokenService,
                jwtService,
                mailService,
                properties);
    }

    @Test
    void should_create_inactive_user_and_send_activation_when_email_is_new() {
        authService = service();
        RegisterRequest request = new RegisterRequest("New@Example.com", "correct horse battery");
        when(userRepository.existsByEmailIgnoreCase("new@example.com")).thenReturn(false);
        when(passwordEncoder.encode("correct horse battery")).thenReturn("{argon2}hash");
        when(tokenService.issue(any(User.class), eq(TokenType.ACCOUNT_ACTIVATION), any()))
                .thenReturn("raw-token");

        authService.register(request);

        ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
        verify(userRepository).save(userCaptor.capture());
        User saved = userCaptor.getValue();
        assertThat(saved.getEmail()).isEqualTo("new@example.com"); // normalized
        assertThat(saved.getPassword()).isEqualTo("{argon2}hash");
        assertThat(saved.isActivatedAccount()).isFalse();
        assertThat(saved.getRoles()).containsExactly("ROLE_USER");
        verify(mailService).sendActivationEmail("new@example.com", "raw-token");
    }

    @Test
    void should_do_nothing_observable_when_email_already_registered() {
        authService = service();
        RegisterRequest request = new RegisterRequest("taken@example.com", "correct horse battery");
        when(userRepository.existsByEmailIgnoreCase("taken@example.com")).thenReturn(true);

        authService.register(request);

        verify(userRepository, never()).save(any());
        verifyNoInteractions(passwordEncoder, tokenService, mailService);
    }

    @Test
    void should_activate_user_for_valid_token() {
        authService = service();
        User user = new User("user@example.com", "hash");
        when(tokenService.consume("tok", TokenType.ACCOUNT_ACTIVATION)).thenReturn(user);

        authService.activate(new ActivateRequest("tok"));

        assertThat(user.isActivatedAccount()).isTrue();
        assertThat(user.getActivatedAt()).isNotNull();
    }

    @Test
    void should_propagate_invalid_token_on_activation() {
        authService = service();
        when(tokenService.consume(anyString(), eq(TokenType.ACCOUNT_ACTIVATION)))
                .thenThrow(new InvalidTokenException(TokenType.ACCOUNT_ACTIVATION));

        assertThatThrownBy(() -> authService.activate(new ActivateRequest("bad")))
                .isInstanceOf(InvalidTokenException.class);
    }

    // --- login ---

    private static User activatedUser() {
        User user = new User("user@example.com", "storedHash");
        user.setId(java.util.UUID.randomUUID());
        user.activate(java.time.Instant.now());
        return user;
    }

    @Test
    void should_issue_tokens_on_successful_login() {
        authService = service();
        User user = activatedUser();
        when(userRepository.findByEmailIgnoreCase("user@example.com"))
                .thenReturn(java.util.Optional.of(user));
        when(passwordEncoder.matches("secretpassword", "storedHash")).thenReturn(true);
        when(jwtService.issueAccessToken(user)).thenReturn("access-jwt");
        when(jwtService.accessTokenTtlSeconds()).thenReturn(900L);
        when(refreshTokenService.issue(user, "agent", "1.2.3.4")).thenReturn("refresh-raw");

        var response =
                authService.login(
                        new com.codebyte.api.auth.api.dto.LoginRequest(
                                "user@example.com", "secretpassword"),
                        "agent",
                        "1.2.3.4");

        assertThat(response.accessToken()).isEqualTo("access-jwt");
        assertThat(response.refreshToken()).isEqualTo("refresh-raw");
        assertThat(response.expiresIn()).isEqualTo(900L);
        assertThat(user.getFailedLoginAttempts()).isZero();
    }

    @Test
    void should_reject_login_for_unknown_email() {
        authService = service();
        when(userRepository.findByEmailIgnoreCase("ghost@example.com"))
                .thenReturn(java.util.Optional.empty());

        assertThatThrownBy(
                        () ->
                                authService.login(
                                        new com.codebyte.api.auth.api.dto.LoginRequest(
                                                "ghost@example.com", "whatever12345"),
                                        "agent",
                                        "ip"))
                .isInstanceOf(InvalidCredentialsException.class);
        verifyNoInteractions(jwtService, refreshTokenService);
    }

    @Test
    void should_reject_login_for_locked_account() {
        authService = service();
        User user = activatedUser();
        user.setLockedUntil(java.time.Instant.now().plusSeconds(600));
        when(userRepository.findByEmailIgnoreCase("user@example.com"))
                .thenReturn(java.util.Optional.of(user));

        assertThatThrownBy(
                        () ->
                                authService.login(
                                        new com.codebyte.api.auth.api.dto.LoginRequest(
                                                "user@example.com", "secretpassword"),
                                        "a",
                                        "ip"))
                .isInstanceOf(AccountLockedException.class);
    }

    @Test
    void should_increment_and_lock_on_repeated_wrong_password() {
        authService = service();
        User user = activatedUser();
        user.setFailedLoginAttempts(4); // one away from the max of 5
        when(userRepository.findByEmailIgnoreCase("user@example.com"))
                .thenReturn(java.util.Optional.of(user));
        when(passwordEncoder.matches("wrongpassword", "storedHash")).thenReturn(false);

        assertThatThrownBy(
                        () ->
                                authService.login(
                                        new com.codebyte.api.auth.api.dto.LoginRequest(
                                                "user@example.com", "wrongpassword"),
                                        "a",
                                        "ip"))
                .isInstanceOf(InvalidCredentialsException.class);
        assertThat(user.getFailedLoginAttempts()).isEqualTo(5);
        assertThat(user.getLockedUntil()).isNotNull();
    }

    @Test
    void should_reject_login_for_unactivated_account() {
        authService = service();
        User user = new User("user@example.com", "storedHash");
        when(userRepository.findByEmailIgnoreCase("user@example.com"))
                .thenReturn(java.util.Optional.of(user));
        when(passwordEncoder.matches("secretpassword", "storedHash")).thenReturn(true);

        assertThatThrownBy(
                        () ->
                                authService.login(
                                        new com.codebyte.api.auth.api.dto.LoginRequest(
                                                "user@example.com", "secretpassword"),
                                        "a",
                                        "ip"))
                .isInstanceOf(AccountNotActivatedException.class);
    }

    // --- refresh / logout ---

    @Test
    void should_rotate_refresh_token() {
        authService = service();
        User user = activatedUser();
        when(refreshTokenService.rotate("old-refresh", "agent", "ip"))
                .thenReturn(new RefreshTokenService.Rotation(user, "new-refresh"));
        when(jwtService.issueAccessToken(user)).thenReturn("access-jwt");
        when(jwtService.accessTokenTtlSeconds()).thenReturn(900L);

        var response =
                authService.refresh(
                        new com.codebyte.api.auth.api.dto.RefreshRequest("old-refresh"),
                        "agent",
                        "ip");

        assertThat(response.refreshToken()).isEqualTo("new-refresh");
        assertThat(response.accessToken()).isEqualTo("access-jwt");
    }

    @Test
    void should_revoke_token_on_logout() {
        authService = service();

        authService.logout(new com.codebyte.api.auth.api.dto.LogoutRequest("some-refresh"));

        verify(refreshTokenService).revoke("some-refresh");
    }

    // --- resend activation ---

    @Test
    void should_resend_activation_for_unactivated_account() {
        authService = service();
        User user = new User("user@example.com", "hash");
        when(userRepository.findByEmailIgnoreCase("user@example.com"))
                .thenReturn(java.util.Optional.of(user));
        when(tokenService.issue(eq(user), eq(TokenType.ACCOUNT_ACTIVATION), any()))
                .thenReturn("new-token");

        authService.resendActivation(
                new com.codebyte.api.auth.api.dto.ResendActivationRequest("User@Example.com"));

        verify(tokenService).invalidateActive(user, TokenType.ACCOUNT_ACTIVATION);
        verify(mailService).sendActivationEmail("user@example.com", "new-token");
    }

    @Test
    void should_not_resend_activation_for_already_activated_account() {
        authService = service();
        User user = new User("user@example.com", "hash");
        user.activate(java.time.Instant.now());
        when(userRepository.findByEmailIgnoreCase("user@example.com"))
                .thenReturn(java.util.Optional.of(user));

        authService.resendActivation(
                new com.codebyte.api.auth.api.dto.ResendActivationRequest("user@example.com"));

        verifyNoInteractions(mailService);
    }

    @Test
    void should_not_resend_activation_for_unknown_email() {
        authService = service();
        when(userRepository.findByEmailIgnoreCase("ghost@example.com"))
                .thenReturn(java.util.Optional.empty());

        authService.resendActivation(
                new com.codebyte.api.auth.api.dto.ResendActivationRequest("ghost@example.com"));

        verifyNoInteractions(tokenService, mailService);
    }
}
