package io.cameleer.server.app.security;

import io.cameleer.server.app.license.LicenseCapExceededException;
import io.cameleer.server.app.license.LicenseEnforcer;
import io.cameleer.server.core.security.OidcConfig;
import io.cameleer.server.core.security.OidcConfigRepository;
import io.cameleer.server.core.security.UserInfo;
import io.cameleer.server.core.security.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.jwt.Jwt;

import java.time.Instant;
import java.util.List;
import java.util.Optional;

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

class OidcAccountSyncServiceTest {

    private UserRepository userRepository;
    private OidcConfigRepository oidcConfigRepository;
    private LicenseEnforcer licenseEnforcer;
    private OidcAccountSyncService service;

    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        oidcConfigRepository = mock(OidcConfigRepository.class);
        licenseEnforcer = mock(LicenseEnforcer.class);
        service = new OidcAccountSyncService(userRepository, oidcConfigRepository, licenseEnforcer);
    }

    @Test
    void existingUser_returnsExistingId_skipsUpsert() {
        when(userRepository.findById("oidc:abc")).thenReturn(Optional.of(existingUser("oidc:abc")));

        Optional<String> result = service.ensureProvisioned(jwt("abc", "https://logto.example", "x@y.com", "Display"));

        assertThat(result).contains("oidc:abc");
        verify(userRepository, never()).upsert(any());
        verify(licenseEnforcer, never()).assertWithinCap(any(), anyLong(), anyLong());
    }

    @Test
    void newUser_autoSignupTrue_upsertsBareUserId() {
        when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
        when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(true)));
        when(userRepository.count()).thenReturn(0L);

        Optional<String> result = service.ensureProvisioned(
                jwt("abc", "https://logto.example", "x@y.com", "Display"));

        assertThat(result).contains("oidc:abc");
        verify(licenseEnforcer).assertWithinCap("max_users", 0L, 1L);
        verify(userRepository).upsert(argThat(u ->
                "oidc:abc".equals(u.userId())
                        && "oidc:logto.example".equals(u.provider())
                        && "x@y.com".equals(u.email())
                        && "Display".equals(u.displayName())
        ));
    }

    @Test
    void newUser_oidcConfigAbsent_defaultsToAutoSignupTrue() {
        when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
        when(oidcConfigRepository.find()).thenReturn(Optional.empty());
        when(userRepository.count()).thenReturn(0L);

        Optional<String> result = service.ensureProvisioned(
                jwt("abc", "https://logto.example", null, null));

        assertThat(result).contains("oidc:abc");
        verify(userRepository).upsert(argThat(u ->
                "oidc:abc".equals(u.userId()) && u.email().isEmpty() && u.displayName().isEmpty()));
    }

    @Test
    void newUser_autoSignupFalse_returnsEmpty_noUpsert() {
        when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
        when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(false)));

        Optional<String> result = service.ensureProvisioned(
                jwt("abc", "https://logto.example", "x@y.com", "Display"));

        assertThat(result).isEmpty();
        verify(userRepository, never()).upsert(any());
        verify(licenseEnforcer, never()).assertWithinCap(any(), anyLong(), anyLong());
    }

    @Test
    void newUser_capExceeded_returnsEmpty_noUpsert() {
        when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
        when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(true)));
        when(userRepository.count()).thenReturn(10L);
        doThrow(new LicenseCapExceededException("max_users", 10, 5))
                .when(licenseEnforcer).assertWithinCap(eq("max_users"), eq(10L), eq(1L));

        Optional<String> result = service.ensureProvisioned(
                jwt("abc", "https://logto.example", "x@y.com", "Display"));

        assertThat(result).isEmpty();
        verify(userRepository, never()).upsert(any());
    }

    @Test
    void newUser_displayNameFallback_prefersPreferredUsernameOverEmail() {
        when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
        when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(true)));
        when(userRepository.count()).thenReturn(0L);

        Jwt jwt = Jwt.withTokenValue("t")
                .header("alg", "ES384")
                .subject("abc")
                .issuer("https://logto.example")
                .claim("preferred_username", "alice42")
                .claim("email", "alice@example.com")
                .build();

        service.ensureProvisioned(jwt);

        verify(userRepository).upsert(argThat(u ->
                "alice42".equals(u.displayName()) && "alice@example.com".equals(u.email())));
    }

    private OidcConfig oidcConfig(boolean autoSignup) {
        return new OidcConfig(
                true, "https://logto.example", "client", "secret",
                "roles", List.of("VIEWER"), autoSignup,
                "name", "sub", null, null);
    }

    private UserInfo existingUser(String userId) {
        return new UserInfo(userId, "oidc:logto.example", "", "", Instant.now());
    }

    private Jwt jwt(String sub, String issuer, String email, String name) {
        Jwt.Builder b = Jwt.withTokenValue("t")
                .header("alg", "ES384")
                .subject(sub)
                .issuer(issuer);
        if (email != null) b.claim("email", email);
        if (name != null) b.claim("name", name);
        return b.build();
    }
}
