package org.raddatz.familienarchiv.auth;

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.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.UserService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;

import java.util.Set;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class AuthServiceTest {

    @Mock AuthenticationManager authenticationManager;
    @Mock UserService userService;
    @Mock AuditService auditService;
    @Mock LoginRateLimiter loginRateLimiter;
    @Mock SessionRevocationPort sessionRevocationPort;
    @InjectMocks AuthService authService;

    private static final String IP = "127.0.0.1";
    private static final String UA = "Mozilla/5.0 (Test)";

    @Test
    void login_returns_user_on_valid_credentials() {
        UUID userId = UUID.randomUUID();
        AppUser user = AppUser.builder().id(userId).email("user@test.de").build();
        Authentication auth = new UsernamePasswordAuthenticationToken("user@test.de", null, Set.of());
        when(authenticationManager.authenticate(any())).thenReturn(auth);
        when(userService.findByEmail("user@test.de")).thenReturn(user);

        AuthService.LoginResult result = authService.login("user@test.de", "pass123", IP, UA);

        assertThat(result.user()).isEqualTo(user);
        assertThat(result.authentication()).isEqualTo(auth);
    }

    @Test
    void login_fires_LOGIN_SUCCESS_audit_on_valid_credentials() {
        UUID userId = UUID.randomUUID();
        AppUser user = AppUser.builder().id(userId).email("user@test.de").build();
        Authentication auth = new UsernamePasswordAuthenticationToken("user@test.de", null, Set.of());
        when(authenticationManager.authenticate(any())).thenReturn(auth);
        when(userService.findByEmail("user@test.de")).thenReturn(user);

        authService.login("user@test.de", "pass123", IP, UA);

        verify(auditService).log(
            eq(AuditKind.LOGIN_SUCCESS),
            eq(userId),
            isNull(),
            argThat(payload -> userId.toString().equals(payload.get("userId").toString())
                && IP.equals(payload.get("ip"))
                && !payload.containsKey("password"))
        );
    }

    @Test
    void login_throws_INVALID_CREDENTIALS_on_bad_password() {
        when(authenticationManager.authenticate(any())).thenThrow(new BadCredentialsException("bad"));

        assertThatThrownBy(() -> authService.login("user@test.de", "wrong", IP, UA))
            .isInstanceOf(DomainException.class)
            .satisfies(ex -> assertThat(((DomainException) ex).getCode())
                .isEqualTo(ErrorCode.INVALID_CREDENTIALS));
    }

    @Test
    void login_fires_LOGIN_FAILED_audit_on_bad_credentials_without_password_in_payload() {
        when(authenticationManager.authenticate(any())).thenThrow(new BadCredentialsException("bad"));

        assertThatThrownBy(() -> authService.login("user@test.de", "wrong", IP, UA))
            .isInstanceOf(DomainException.class);

        verify(auditService).log(
            eq(AuditKind.LOGIN_FAILED),
            isNull(),
            isNull(),
            argThat(payload -> "user@test.de".equals(payload.get("email"))
                && IP.equals(payload.get("ip"))
                && !payload.containsKey("password")
                && !payload.containsKey("pwd")
                && !payload.containsKey("passwordAttempt"))
        );
    }

    @Test
    void login_treats_unknown_user_identically_to_bad_password() {
        when(authenticationManager.authenticate(any()))
            .thenThrow(new BadCredentialsException("unknown user hidden as bad creds"));

        assertThatThrownBy(() -> authService.login("unknown@test.de", "any", IP, UA))
            .isInstanceOf(DomainException.class)
            .satisfies(ex -> assertThat(((DomainException) ex).getCode())
                .isEqualTo(ErrorCode.INVALID_CREDENTIALS));

        verify(auditService).log(eq(AuditKind.LOGIN_FAILED), isNull(), isNull(), anyMap());
    }

    @Test
    void logout_fires_LOGOUT_audit() {
        UUID userId = UUID.randomUUID();
        AppUser user = AppUser.builder().id(userId).email("user@test.de").build();
        when(userService.findByEmail("user@test.de")).thenReturn(user);

        authService.logout("user@test.de", IP, UA);

        verify(auditService).log(
            eq(AuditKind.LOGOUT),
            eq(userId),
            isNull(),
            argThat(payload -> userId.toString().equals(payload.get("userId").toString())
                && IP.equals(payload.get("ip"))
                && !payload.containsKey("password"))
        );
    }

    @Test
    void login_checks_rate_limit_before_authenticating() {
        doThrow(DomainException.tooManyRequests(ErrorCode.TOO_MANY_LOGIN_ATTEMPTS, "rate limited"))
            .when(loginRateLimiter).checkAndConsume(IP, "user@test.de");

        assertThatThrownBy(() -> authService.login("user@test.de", "pass", IP, UA))
            .isInstanceOf(DomainException.class)
            .satisfies(ex -> assertThat(((DomainException) ex).getCode())
                .isEqualTo(ErrorCode.TOO_MANY_LOGIN_ATTEMPTS));

        verify(authenticationManager, never()).authenticate(any());
    }

    @Test
    void login_fires_LOGIN_RATE_LIMITED_audit_when_rate_limited() {
        doThrow(DomainException.tooManyRequests(ErrorCode.TOO_MANY_LOGIN_ATTEMPTS, "rate limited"))
            .when(loginRateLimiter).checkAndConsume(IP, "user@test.de");

        assertThatThrownBy(() -> authService.login("user@test.de", "pass", IP, UA))
            .isInstanceOf(DomainException.class);

        verify(auditService).log(eq(AuditKind.LOGIN_RATE_LIMITED), isNull(), isNull(),
            argThat(payload -> IP.equals(payload.get("ip")) && "user@test.de".equals(payload.get("email"))));
    }

    @Test
    void login_invalidates_rate_limit_on_success() {
        UUID userId = UUID.randomUUID();
        AppUser user = AppUser.builder().id(userId).email("user@test.de").build();
        Authentication auth = new UsernamePasswordAuthenticationToken("user@test.de", null, Set.of());
        when(authenticationManager.authenticate(any())).thenReturn(auth);
        when(userService.findByEmail("user@test.de")).thenReturn(user);

        authService.login("user@test.de", "pass123", IP, UA);

        verify(loginRateLimiter).invalidateOnSuccess(IP, "user@test.de");
    }

    @Test
    void revokeOtherSessions_delegates_to_port() {
        when(sessionRevocationPort.revokeOtherSessions("session-keep", "user@test.de")).thenReturn(2);

        int count = authService.revokeOtherSessions("session-keep", "user@test.de");

        assertThat(count).isEqualTo(2);
        verify(sessionRevocationPort).revokeOtherSessions("session-keep", "user@test.de");
    }

    @Test
    void revokeAllSessions_delegates_to_port() {
        when(sessionRevocationPort.revokeAllSessions("user@test.de")).thenReturn(3);

        int count = authService.revokeAllSessions("user@test.de");

        assertThat(count).isEqualTo(3);
        verify(sessionRevocationPort).revokeAllSessions("user@test.de");
    }
}
