package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.TagData;
import com.pandoli365.bibimbap.mapper.GameTagsMapper;
import com.pandoli365.bibimbap.mapper.GamesMapper;
import com.pandoli365.bibimbap.mapper.TagsMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.security.PermissionKeys;
import com.pandoli365.bibimbap.security.PermissionGate;
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.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class TagControllerTest {

    @Mock
    private TagsMapper tagsMapper;

    @Mock
    private GameTagsMapper gameTagsMapper;

    @Mock
    private GamesMapper gamesMapper;

    @Mock
    private PermissionGate permissionGate;

    private static final String MODERATE = PermissionKeys.CONTENT_MODERATE.name();

    // ---- §12.3 보안 VP-1: CSRF 부재 403 (상태변경 엔드포인트는 mapper 접근 전 차단) ----

    @Test
    void createUserTagRejectsMissingCsrf() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.createUserTag("rpg", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verifyNoInteractions(tagsMapper);
    }

    @Test
    void createAdminTagRejectsMissingCsrf() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.createAdminTag("rpg", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verifyNoInteractions(tagsMapper);
        verifyNoInteractions(permissionGate);
    }

    @Test
    void approveTagRejectsMissingCsrf() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = noCsrfPost(session);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verifyNoInteractions(tagsMapper);
        verifyNoInteractions(permissionGate);
    }

    @Test
    void deactivateTagRejectsMissingCsrf() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = noCsrfPost(session);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verifyNoInteractions(tagsMapper);
        verifyNoInteractions(permissionGate);
    }

    @Test
    void attachGameTagsRejectsMissingCsrf() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.attachGameTags(1L, List.of(1L, 2L), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verifyNoInteractions(gameTagsMapper);
        verify(gamesMapper, never()).getGame(anyLong());
    }

    // ---- VP-2: 권한 게이트 미통과 403 ----

    @Test
    void createAdminTagRejectsWithoutModeratePermission() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(permissionGate.has(any(), eq(MODERATE))).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.createAdminTag("rpg", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(tagsMapper, never()).insertTag(any());
    }

    @Test
    void approveTagRejectsWithoutModeratePermission() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(permissionGate.has(any(), eq(MODERATE))).thenReturn(false);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(tagsMapper, never()).approve(anyLong());
    }

    // ---- createUserTag 입력 검증 (XSS 무력화 / tagType / 로그인) ----

    @Test
    void createUserTagRejectsXssLikeNameAfterSlugSanitize() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);

        // "!" → toSlug "" → isAllowed false → 400. 특수문자(XSS 페이로드 포함)는 slug 단계에서 제거된다.
        ResponseEntity<Map<String, Object>> response =
                controller.createUserTag("!", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(tagsMapper, never()).insertTag(any());
    }

    @Test
    void createUserTagRejectsInvalidTagType() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.createUserTag("rpg", "INVALID", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(tagsMapper, never()).insertTag(any());
    }

    @Test
    void createUserTagRequiresLogin() {
        TagController controller = controller();
        MockHttpSession session = anonSession();
        MockHttpServletRequest request = csrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.createUserTag("rpg", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        verify(tagsMapper, never()).insertTag(any());
    }

    // ---- createUserTag 성공/중복 ----

    @Test
    void createUserTagCreatesPendingTag() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(tagsMapper.findBySlug("rpg")).thenReturn(null);
        when(tagsMapper.insertTag(any(TagData.class))).thenAnswer(inv -> {
            inv.getArgument(0, TagData.class).setId(10L);
            return 1;
        });

        ResponseEntity<Map<String, Object>> response =
                controller.createUserTag("rpg", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("id", 10L);
        assertThat(response.getBody()).containsEntry("isActive", false);
        verify(tagsMapper).insertTag(any(TagData.class));
    }

    @Test
    void createUserTagReturnsExistingOnDuplicate() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(tagsMapper.findBySlug("rpg")).thenReturn(tagData(5L, "RPG", "rpg", true));

        ResponseEntity<Map<String, Object>> response =
                controller.createUserTag("rpg", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("id", 5L);
        verify(tagsMapper, never()).insertTag(any());
    }

    // ---- createAdminTag 성공: 즉시 활성 ----

    @Test
    void createAdminTagCreatesActiveTag() {
        TagController controller = controller();
        MockHttpSession session = loginSession(99L, "ADMIN");
        MockHttpServletRequest request = csrfPost(session);
        when(permissionGate.has(any(), eq(MODERATE))).thenReturn(true);
        when(tagsMapper.findBySlug("rpg")).thenReturn(null);
        when(tagsMapper.insertTag(any(TagData.class))).thenAnswer(inv -> {
            inv.getArgument(0, TagData.class).setId(11L);
            return 1;
        });

        ResponseEntity<Map<String, Object>> response =
                controller.createAdminTag("rpg", "GAME", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("isActive", true);
        verify(tagsMapper).insertTag(any(TagData.class));
    }

    // ---- approve / deactivate 성공 ----

    @Test
    void approveTagSucceeds() {
        TagController controller = controller();
        MockHttpSession session = loginSession(99L, "ADMIN");
        MockHttpServletRequest request = csrfPost(session);
        when(permissionGate.has(any(), eq(MODERATE))).thenReturn(true);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("isActive", true);
        verify(tagsMapper).approve(5L);
    }

    @Test
    void deactivateTagSucceeds() {
        TagController controller = controller();
        MockHttpSession session = loginSession(99L, "ADMIN");
        MockHttpServletRequest request = csrfPost(session);
        when(permissionGate.has(any(), eq(MODERATE))).thenReturn(true);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("isActive", false);
        verify(tagsMapper).deactivate(5L);
    }

    // ---- attachGameTags: 소유자 / 비소유자+모더레이터 / 비소유자+비모더레이터 ----

    @Test
    void attachGameTagsByOwnerSucceeds() {
        TagController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L, 7L));
        // 소유자라 모더레이터 평가가 단축될 수 있어 lenient 로 둔다.
        lenient().when(permissionGate.has(any(), eq(MODERATE))).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.attachGameTags(1L, List.of(1L, 2L), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameTagsMapper).attachTags(1L, List.of(1L, 2L));
    }

    @Test
    void attachGameTagsByNonOwnerNonModeratorIsForbidden() {
        TagController controller = controller();
        MockHttpSession session = loginSession(8L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L, 7L));
        when(permissionGate.has(any(), eq(MODERATE))).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.attachGameTags(1L, List.of(1L, 2L), request, session);

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

    @Test
    void attachGameTagsByNonOwnerModeratorSucceeds() {
        TagController controller = controller();
        MockHttpSession session = loginSession(99L, "ADMIN");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L, 7L));
        when(permissionGate.has(any(), eq(MODERATE))).thenReturn(true);

        ResponseEntity<Map<String, Object>> response =
                controller.attachGameTags(1L, List.of(1L, 2L), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameTagsMapper).attachTags(1L, List.of(1L, 2L));
    }

    // ---- listTags 매핑 (GET, ResponseBody → List<Map> 직접 반환) ----

    @Test
    void listTagsMapsTagFields() {
        TagController controller = controller();
        when(tagsMapper.listActiveByType("GAME", null))
                .thenReturn(List.of(tagData(1L, "RPG", "rpg", true)));

        List<Map<String, Object>> result = controller.listTags("GAME", null);

        assertThat(result).hasSize(1);
        assertThat(result.get(0)).containsEntry("id", 1L);
        assertThat(result.get(0)).containsEntry("name", "RPG");
        assertThat(result.get(0)).containsEntry("slug", "rpg");
    }

    // ---- helpers ----

    private TagController controller() {
        return new TagController(tagsMapper, gameTagsMapper, gamesMapper, permissionGate);
    }

    private TagData tagData(long id, String name, String slug, boolean isActive) {
        TagData t = new TagData();
        t.setId(id);
        t.setName(name);
        t.setSlug(slug);
        t.setIsActive(isActive);
        return t;
    }

    private GameData game(long id, long userId) {
        GameData g = new GameData();
        g.setId(id);
        g.setUserId(userId);
        return g;
    }

    private MockHttpSession loginSession(long userId, String role) {
        MockHttpSession session = new MockHttpSession();
        session.setAttribute("userId", userId);
        session.setAttribute("role", role);
        CsrfTokens.getOrCreate(session);
        return session;
    }

    private MockHttpSession anonSession() {
        MockHttpSession session = new MockHttpSession();
        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;
    }
}
