package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.OperatorView;
import com.pandoli365.bibimbap.data.UserData;
import com.pandoli365.bibimbap.mapper.PermissionsMapper;
import com.pandoli365.bibimbap.mapper.RbacAuditMapper;
import com.pandoli365.bibimbap.mapper.UserPermissionsMapper;
import com.pandoli365.bibimbap.mapper.UsersMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.security.PermissionKeys;
import com.pandoli365.bibimbap.security.Roles;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;

import java.util.List;
import java.util.Map;

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

@ExtendWith(MockitoExtension.class)
class AdminConsoleControllerTest {

    private static final long ACTOR_ID = 99L;
    private static final long TARGET_ID = 42L;
    private static final String POST_WRITE = PermissionKeys.POST_WRITE.name();

    @Mock
    private UsersMapper usersMapper;

    @Mock
    private UserPermissionsMapper userPermissionsMapper;

    @Mock
    private RbacAuditMapper auditMapper;

    @Mock
    private PermissionsMapper permissionsMapper;

    // ---- appoint ----

    @Test
    void appointPromotesUserToSubadmin() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(TARGET_ID)).thenReturn(user(TARGET_ID, Roles.USER));

        ResponseEntity<Map<String, Object>> response = controller.appoint(TARGET_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("role", Roles.SUBADMIN);
        verify(usersMapper).updateRole(TARGET_ID, Roles.SUBADMIN);
        verify(usersMapper).bumpPermissionsEpoch(TARGET_ID);
        verify(auditMapper).insert(eq(ACTOR_ID), eq(TARGET_ID), eq("APPOINT"), isNull());
    }

    @Test
    void appointRejectsMissingCsrf() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response = controller.appoint(TARGET_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(usersMapper, never()).updateRole(anyLong(), any());
    }

    @Test
    void appointReturns404WhenTargetMissing() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(99L)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response = controller.appoint(99L, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        verify(usersMapper, never()).updateRole(anyLong(), any());
    }

    @Test
    void appointReturns409WhenAlreadyOperator() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(TARGET_ID)).thenReturn(user(TARGET_ID, Roles.SUBADMIN));

        ResponseEntity<Map<String, Object>> response = controller.appoint(TARGET_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
        verify(usersMapper, never()).updateRole(anyLong(), any());
    }

    // ---- togglePermission ----

    @Test
    void toggleGrantsWhenAbsent() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(TARGET_ID)).thenReturn(user(TARGET_ID, Roles.SUBADMIN));
        when(userPermissionsMapper.exists(TARGET_ID, POST_WRITE)).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.togglePermission(TARGET_ID, POST_WRITE, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("granted", true);
        verify(userPermissionsMapper).insert(eq(TARGET_ID), eq(POST_WRITE), anyLong());
        verify(usersMapper).bumpPermissionsEpoch(TARGET_ID);
        verify(auditMapper).insert(eq(ACTOR_ID), eq(TARGET_ID), eq("GRANT"), eq(POST_WRITE));
    }

    @Test
    void toggleRevokesWhenPresent() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(TARGET_ID)).thenReturn(user(TARGET_ID, Roles.SUBADMIN));
        when(userPermissionsMapper.exists(TARGET_ID, POST_WRITE)).thenReturn(true);

        ResponseEntity<Map<String, Object>> response =
                controller.togglePermission(TARGET_ID, POST_WRITE, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("granted", false);
        verify(userPermissionsMapper).delete(TARGET_ID, POST_WRITE);
        verify(usersMapper).bumpPermissionsEpoch(TARGET_ID);
        verify(auditMapper).insert(eq(ACTOR_ID), eq(TARGET_ID), eq("REVOKE"), eq(POST_WRITE));
    }

    @Test
    void toggleRejectsMissingCsrf() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.togglePermission(TARGET_ID, POST_WRITE, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(userPermissionsMapper, never()).insert(anyLong(), any(), anyLong());
        verify(userPermissionsMapper, never()).delete(anyLong(), any());
    }

    @Test
    void toggleReturns404WhenKeyInvalid() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.togglePermission(TARGET_ID, "NOPE", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

    @Test
    void toggleReturns422WhenTargetNotSubadmin() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(TARGET_ID)).thenReturn(user(TARGET_ID, Roles.USER));

        ResponseEntity<Map<String, Object>> response =
                controller.togglePermission(TARGET_ID, POST_WRITE, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        verify(userPermissionsMapper, never()).insert(anyLong(), any(), anyLong());
        verify(userPermissionsMapper, never()).delete(anyLong(), any());
    }

    // ---- demote ----

    @Test
    void demoteRevokesAllAndSetsUser() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(TARGET_ID)).thenReturn(user(TARGET_ID, Roles.SUBADMIN));
        when(userPermissionsMapper.deleteAllByUser(TARGET_ID)).thenReturn(3);

        ResponseEntity<Map<String, Object>> response = controller.demote(TARGET_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("role", Roles.USER);
        assertThat(response.getBody()).containsEntry("revokedCount", 3);
        verify(userPermissionsMapper).deleteAllByUser(TARGET_ID);
        verify(usersMapper).updateRole(TARGET_ID, Roles.USER);
        verify(usersMapper).bumpPermissionsEpoch(TARGET_ID);
        verify(auditMapper).insert(eq(ACTOR_ID), eq(TARGET_ID), eq("DEMOTE"), isNull());
    }

    @Test
    void demoteRejectsMissingCsrf() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response = controller.demote(TARGET_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(userPermissionsMapper, never()).deleteAllByUser(anyLong());
    }

    @Test
    void demoteReturns422WhenNotSubadmin() {
        AdminConsoleController controller = controller();
        MockHttpSession session = adminSession(ACTOR_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(usersMapper.getUser(TARGET_ID)).thenReturn(user(TARGET_ID, Roles.USER));

        ResponseEntity<Map<String, Object>> response = controller.demote(TARGET_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        verify(userPermissionsMapper, never()).deleteAllByUser(anyLong());
    }

    // ---- operators ----

    @Test
    void operatorsReturnsListWithPermissions() {
        AdminConsoleController controller = controller();
        OperatorView op = new OperatorView();
        op.setUserId(TARGET_ID);
        op.setRole(Roles.SUBADMIN);
        op.setDisplayName("운영");
        when(usersMapper.listOperators()).thenReturn(List.of(op));
        when(userPermissionsMapper.listKeys(TARGET_ID)).thenReturn(List.of(POST_WRITE));

        ResponseEntity<Map<String, Object>> response = controller.operators();

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> operators = (List<Map<String, Object>>) response.getBody().get("operators");
        assertThat(operators).hasSize(1);
        assertThat(operators.get(0)).containsEntry("userId", TARGET_ID);
        assertThat(operators.get(0)).containsEntry("role", Roles.SUBADMIN);
        assertThat(operators.get(0)).containsEntry("displayName", "운영");
        assertThat(operators.get(0)).containsEntry("permissions", List.of(POST_WRITE));
    }

    // ---- helpers ----

    private AdminConsoleController controller() {
        return new AdminConsoleController(usersMapper, userPermissionsMapper, auditMapper, permissionsMapper);
    }

    private MockHttpSession adminSession(long actorId) {
        MockHttpSession session = new MockHttpSession();
        session.setAttribute("userId", actorId);
        session.setAttribute("role", Roles.ADMIN);
        CsrfTokens.getOrCreate(session);
        return session;
    }

    private MockHttpServletRequest csrfPost(MockHttpSession session) {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setSession(session);
        request.addHeader(CsrfTokens.HEADER_NAME, (String) session.getAttribute(CsrfTokens.SESSION_ATTRIBUTE));
        return request;
    }

    private MockHttpServletRequest noCsrfPost(MockHttpSession session) {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setSession(session);
        return request;
    }

    private UserData user(long id, String role) {
        UserData data = new UserData();
        data.setId(id);
        data.setRole(role);
        data.setDisplayName("대상");
        return data;
    }
}
