package com.pandoli365.bibimbap.security;

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

import com.pandoli365.bibimbap.data.UserData;
import com.pandoli365.bibimbap.mapper.UserPermissionsMapper;
import com.pandoli365.bibimbap.mapper.UsersMapper;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpSession;

class PermissionGateTest {

    private final UsersMapper usersMapper = mock(UsersMapper.class);
    private final UserPermissionsMapper userPermissionsMapper = mock(UserPermissionsMapper.class);
    private final PermissionGate gate = new PermissionGate(usersMapper, userPermissionsMapper);

    @Test
    void hasReturnsFalseWhenUnauthenticated() {
        // given: userId 없는 빈 세션
        MockHttpSession session = session(null, null, null, null);

        // when / then
        assertThat(gate.has(session, PermissionKeys.POST_WRITE.name())).isFalse();
        // 미인증이면 epoch 대조조차 하지 않는다 (mapper 미호출)
        verify(usersMapper, never()).getPermissionsEpoch(anyLong());
    }

    @Test
    void adminPassesAnyPermission() {
        // given: ADMIN, epoch 정합 → 재로딩 없음
        MockHttpSession session = session(1L, Roles.ADMIN, 0L, Set.of());
        when(usersMapper.getPermissionsEpoch(1L)).thenReturn(0L);

        // when / then: 전권
        assertThat(gate.has(session, PermissionKeys.POST_WRITE.name())).isTrue();
        assertThat(gate.canModerate(session)).isTrue();
        assertThat(gate.isAdmin(session)).isTrue();
        verify(userPermissionsMapper, never()).listKeys(anyLong());
    }

    @Test
    void subadminPassesOnlyHeldKey() {
        // given: SUBADMIN, POST_WRITE 보유, epoch 정합
        MockHttpSession session =
                session(2L, Roles.SUBADMIN, 5L, Set.of(PermissionKeys.POST_WRITE.name()));
        when(usersMapper.getPermissionsEpoch(2L)).thenReturn(5L);

        // when / then
        assertThat(gate.has(session, PermissionKeys.POST_WRITE.name())).isTrue();
        assertThat(gate.has(session, PermissionKeys.GAME_JAM_MANAGE.name())).isFalse();
        assertThat(gate.isAdmin(session)).isFalse();
        verify(userPermissionsMapper, never()).listKeys(anyLong());
    }

    @Test
    void userPassesNothing() {
        // given: 일반 USER, epoch 정합
        MockHttpSession session = session(3L, Roles.USER, 0L, Set.of());
        when(usersMapper.getPermissionsEpoch(3L)).thenReturn(0L);

        // when / then
        assertThat(gate.has(session, PermissionKeys.POST_WRITE.name())).isFalse();
        assertThat(gate.isAdmin(session)).isFalse();
    }

    @Test
    void epochMismatchReloadsPermissions() {
        // given (AC-2 회수 즉시성): 세션 캐시엔 POST_WRITE 보유(epoch=1),
        // 그러나 DB 에서 회수됨 → epoch bump(2), listKeys 는 빈 목록
        MockHttpSession session =
                session(4L, Roles.SUBADMIN, 1L, Set.of(PermissionKeys.POST_WRITE.name()));
        when(usersMapper.getPermissionsEpoch(4L)).thenReturn(2L);
        UserData reloaded = new UserData();
        reloaded.setId(4L);
        reloaded.setRole(Roles.SUBADMIN);
        reloaded.setPermissionsEpoch(2L);
        when(usersMapper.getUser(4L)).thenReturn(reloaded);
        when(userPermissionsMapper.listKeys(4L)).thenReturn(List.of());

        // when / then: 재로딩으로 회수 반영 → 캐시에 있던 키도 통과 못 함
        assertThat(gate.has(session, PermissionKeys.POST_WRITE.name())).isFalse();
        // 재로딩이 실제로 발생했는지
        verify(userPermissionsMapper).listKeys(4L);
        // 세션 epoch 가 DB 값으로 갱신됐는지
        assertThat(session.getAttribute("permsEpoch")).isEqualTo(2L);
        // 권한 캐시가 빈 목록으로 갱신됐는지
        assertThat(session.getAttribute("permissions"))
                .isInstanceOf(Set.class)
                .satisfies(p -> assertThat((Set<?>) p).isEmpty());
    }

    @Test
    void epochMismatchGrantsNewKey() {
        // given (AC-1 대칭): 세션 캐시엔 미보유(epoch=1),
        // DB 에서 부여됨 → epoch bump(2), listKeys 는 POST_WRITE 포함
        MockHttpSession session = session(5L, Roles.SUBADMIN, 1L, Set.of());
        when(usersMapper.getPermissionsEpoch(5L)).thenReturn(2L);
        UserData reloaded = new UserData();
        reloaded.setId(5L);
        reloaded.setRole(Roles.SUBADMIN);
        reloaded.setPermissionsEpoch(2L);
        when(usersMapper.getUser(5L)).thenReturn(reloaded);
        when(userPermissionsMapper.listKeys(5L))
                .thenReturn(List.of(PermissionKeys.POST_WRITE.name()));

        // when / then: 재로딩으로 부여 반영
        assertThat(gate.has(session, PermissionKeys.POST_WRITE.name())).isTrue();
        verify(userPermissionsMapper).listKeys(5L);
        assertThat(session.getAttribute("permsEpoch")).isEqualTo(2L);
    }

    @Test
    void isAuthenticatedReflectsSession() {
        // given
        MockHttpSession authed = session(6L, Roles.USER, 0L, Set.of());
        MockHttpSession anonymous = session(null, null, null, null);

        // when / then
        assertThat(gate.isAuthenticated(authed)).isTrue();
        assertThat(gate.isAuthenticated(anonymous)).isFalse();
    }

    /**
     * 세션 빌더. null 인 attr 은 setAttribute 하지 않는다(특히 userId null 이면 미인증 세션).
     */
    private MockHttpSession session(Long userId, String role, Long epoch, Set<String> perms) {
        MockHttpSession session = new MockHttpSession();
        if (userId != null) {
            session.setAttribute("userId", userId);
        }
        if (role != null) {
            session.setAttribute("role", role);
        }
        if (epoch != null) {
            session.setAttribute("permsEpoch", epoch);
        }
        if (perms != null) {
            session.setAttribute("permissions", perms);
        }
        return session;
    }
}
