package com.xyw.console.auth;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xyw.console.asset.entity.SystemUserEntity;
import com.xyw.console.asset.mapper.SystemUserMapper;
import com.xyw.console.auth.dto.SystemUserCreateRequest;
import com.xyw.console.auth.dto.SystemUserUpdateRequest;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;

/** File purpose (plain language): verifies that account authorization changes revoke old login versions while existing role boundaries remain enforced. */
class SystemUserAdminServiceTest {
  private SystemUserMapper users;
  private SystemUserAdminService service;

  /** Plain purpose: build a developer-authenticated service fixture before each account-security test. Related files: SystemUserAdminService.java, AuthTokenFilter.java. Flow: developer principal -> SecurityContext -> service role boundary -> mocked user update. */
  @BeforeEach void setUp() {
    users = mock(SystemUserMapper.class);
    service = new SystemUserAdminService(users, new PagePermissionService(new ObjectMapper()), mock(PasswordEncoder.class));
    authenticate("DEVELOPER");
  }

  /** Plain purpose: remove the test principal so it cannot influence the next security test. Related files: AuthTokenFilter.java, SystemUserAdminService.java. Flow: test completion -> SecurityContext clear -> isolated next request. */
  @AfterEach void tearDown() { SecurityContextHolder.clearContext(); }

  /** Plain purpose: prove that promotion changes the account version used to reject its old token. Related files: SystemUserAdminService.java, AuthTokenFilter.java. Flow: role edit -> authVersion increment -> next old-token request rejected. */
  @Test void promotionIncrementsAuthVersion() {
    SystemUserEntity user = activeUser("FINANCE", 4, Map.of("phone-assets", "READ"));
    when(users.selectOne(any())).thenReturn(user);
    service.updateUser(9L, new SystemUserUpdateRequest("SUPER_ADMIN", "ACTIVE", Map.of("phone-assets", "READ")));
    assertEquals(5, user.getAuthVersion());
    verify(users).updateById(user);
  }

  /** Plain purpose: prove that disabling an account revokes its prior session even when the role stays the same. Related files: SystemUserAdminService.java, AuthTokenFilter.java. Flow: status edit -> authVersion increment -> disabled account cannot keep old session. */
  @Test void statusChangeIncrementsAuthVersion() {
    SystemUserEntity user = activeUser("FINANCE", 4, Map.of());
    when(users.selectOne(any())).thenReturn(user);
    service.updateUser(9L, new SystemUserUpdateRequest("FINANCE", "DISABLED", Map.of()));
    assertEquals(5, user.getAuthVersion());
  }

  /** Plain purpose: prove that changed page permissions invalidate a token that still carries the previous authorization state. Related files: PagePermissionService.java, SystemUserAdminService.java. Flow: permission edit -> normalized comparison -> authVersion increment. */
  @Test void permissionChangeIncrementsAuthVersion() {
    SystemUserEntity user = activeUser("FINANCE", 4, Map.of("phone-assets", "READ"));
    when(users.selectOne(any())).thenReturn(user);
    service.updateUser(9L, new SystemUserUpdateRequest("FINANCE", "ACTIVE", Map.of("phone-assets", "EDIT")));
    assertEquals(5, user.getAuthVersion());
  }

  /** Plain purpose: prove that saving unchanged authorization facts does not unnecessarily sign the account out. Related files: SystemUserAdminService.java, PagePermissionService.java. Flow: no-op edit -> equal normalized values -> retained authVersion. */
  @Test void noOpUpdateRetainsAuthVersion() {
    SystemUserEntity user = activeUser("FINANCE", 4, Map.of("phone-assets", "READ"));
    when(users.selectOne(any())).thenReturn(user);
    service.updateUser(9L, new SystemUserUpdateRequest("FINANCE", "ACTIVE", Map.of("phone-assets", "READ")));
    assertEquals(4, user.getAuthVersion());
  }

  /** Plain purpose: prove that a fresh developer principal can still create an allowed non-developer account. Related files: SystemUserAdminService.java, SystemUserCreateRequest.java. Flow: developer principal -> create boundary -> mapper insert -> safe response. */
  @Test void developerCanCreateSuperAdministrator() {
    when(users.selectCount(any())).thenReturn(0L);
    PasswordEncoder encoder = mock(PasswordEncoder.class);
    when(encoder.encode(any())).thenReturn("hash");
    service = new SystemUserAdminService(users, new PagePermissionService(new ObjectMapper()), encoder);
    service.createUser(new SystemUserCreateRequest("admin_two", "SUPER_ADMIN", "strong-password-123", Map.of()));
    verify(users).insert(any(SystemUserEntity.class));
  }

  /** Plain purpose: allow the user-approved username-containing password policy while retaining the existing password length boundary. Related files: SystemUserAdminService.java, SystemUserCreateRequest.java. Flow: developer create request -> password validation -> BCrypt encoder -> account insert. */
  @Test void developerCanCreateAccountWhenPasswordEqualsUsernameAndContainsJeddy() {
    when(users.selectCount(any())).thenReturn(0L);
    PasswordEncoder encoder = mock(PasswordEncoder.class);
    when(encoder.encode(any())).thenReturn("hash");
    service = new SystemUserAdminService(users, new PagePermissionService(new ObjectMapper()), encoder);
    service.createUser(new SystemUserCreateRequest("Jeddy2026User", "OPERATIONS", "Jeddy2026User", Map.of()));
    verify(users).insert(any(SystemUserEntity.class));
  }

  /** Plain purpose: prove that an ordinary role cannot manage accounts even if it calls the service directly. Related files: SystemUserAdminService.java, SystemUserAdminController.java. Flow: finance principal -> role boundary -> access denial -> no mapper write. */
  @Test void nonAdministratorCannotCreateAccounts() {
    authenticate("FINANCE");
    assertThrows(AccessDeniedException.class, () -> service.createUser(new SystemUserCreateRequest("admin_two", "SUPER_ADMIN", "strong-password-123", Map.of())));
    verifyNoInteractions(users);
  }

  /** Plain purpose: install a minimal signed-token equivalent principal for one service test. Related files: AuthPrincipal.java, AuthTokenFilter.java. Flow: role code -> SecurityContext principal -> SystemUserAdminService authorization branch. */
  private void authenticate(String roleCode) { SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(new AuthPrincipal(1L, "tester", roleCode, 1), null)); }

  /** Plain purpose: create a persisted account shape with a validated permission map and active credentials. Related files: SystemUserEntity.java, PagePermissionService.java. Flow: test fixture -> mapper select result -> authorization-state comparison. */
  private SystemUserEntity activeUser(String roleCode, int authVersion, Map<String, String> permissions) {
    SystemUserEntity user = new SystemUserEntity();
    user.setId(9L); user.setUsername("managed_user"); user.setRoleCode(roleCode); user.setStatus("ACTIVE"); user.setPasswordHash("hash"); user.setDeleteTime(0L); user.setAuthVersion(authVersion);
    user.setPagePermissions(new PagePermissionService(new ObjectMapper()).validatePermissions(permissions));
    return user;
  }
}
