package com.pandoli365.bibimbap.controller.api;

import com.pandoli365.bibimbap.data.GameCommentData;
import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
import com.pandoli365.bibimbap.mapper.GamesMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.security.PermissionGate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
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.time.OffsetDateTime;
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.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
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 GameCommentControllerTest {

    @Mock
    private GameCommentsMapper gameCommentsMapper;

    @Mock
    private GamesMapper gamesMapper;

    @Mock
    private PermissionGate permissionGate;

    // ---- AC-1: 댓글 CRUD ----

    @Test
    void createCommentPersistsAndReturnsView() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameCommentsMapper.addGameComment(any(GameCommentData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameCommentData.class).setId(100L);
            return 1;
        });
        when(gameCommentsMapper.getGameComment(100L)).thenReturn(persistedComment(100L, 1L, 7L, "작성자", false));

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, "좋은 게임", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("commentId", 100L);
        assertThat(response.getBody()).containsEntry("authorName", "작성자");
        assertThat(response.getBody()).containsEntry("userId", 7L);
        verify(gameCommentsMapper).addGameComment(any(GameCommentData.class));
    }

    @Test
    void listCommentsReturnsViewArray() {
        GameCommentController controller = controller();
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        GameCommentData c = persistedComment(5L, 1L, 7L, "작성자", false);
        when(gameCommentsMapper.listGameComments(eq(1L), anyString(), anyInt(), anyInt()))
                .thenReturn(List.of(c));

        ResponseEntity<Map<String, Object>> response = controller.listComments(1L, 0, null);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("hasMore");
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> comments = (List<Map<String, Object>>) response.getBody().get("comments");
        assertThat(comments).hasSize(1);
        assertThat(comments.get(0)).containsEntry("commentId", 5L);
        assertThat(comments.get(0)).containsEntry("authorName", "작성자");
    }

    @Test
    void updateCommentByAuthorSucceeds() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gameCommentsMapper.getGameComment(5L))
                .thenReturn(comment(5L, 1L, 7L))
                .thenReturn(persistedComment(5L, 1L, 7L, "작성자", true));

        ResponseEntity<Map<String, Object>> response =
                controller.updateComment(1L, 5L, "수정된 내용", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameCommentsMapper).editGameComment(any(GameCommentData.class));
    }

    @Test
    void deleteCommentByAuthorSucceeds() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gameCommentsMapper.getGameComment(5L)).thenReturn(comment(5L, 1L, 7L));

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameCommentsMapper).softDeleteGameComment(5L);
    }

    // ---- AC-2: 비작성자 불가 + 운영자 예외 ----

    @Test
    void updateCommentByNonAuthorIsForbidden() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(8L, "USER", "다른사용자");
        MockHttpServletRequest request = csrfPost(session);
        when(gameCommentsMapper.getGameComment(5L)).thenReturn(comment(5L, 1L, 7L));

        ResponseEntity<Map<String, Object>> response =
                controller.updateComment(1L, 5L, "강제수정", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(gameCommentsMapper, never()).editGameComment(any());
    }

    @Test
    void deleteCommentByOperatorSucceeds() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(99L, "ADMIN", "운영자");
        MockHttpServletRequest request = csrfPost(session);
        when(gameCommentsMapper.getGameComment(5L)).thenReturn(comment(5L, 1L, 7L));
        when(permissionGate.canModerate(any())).thenReturn(true);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameCommentsMapper).softDeleteGameComment(5L);
    }

    @Test
    void deleteCommentBySubadminModeratorSucceeds() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(50L, "SUBADMIN", "부운영자");
        MockHttpServletRequest request = csrfPost(session);
        when(gameCommentsMapper.getGameComment(5L)).thenReturn(comment(5L, 1L, 7L));
        when(permissionGate.canModerate(any())).thenReturn(true);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameCommentsMapper).softDeleteGameComment(5L);
    }

    // ---- AC-5: 댓글 201자 거부 / 200자 경계 ----

    @Test
    void createCommentRejects201Chars() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, "가".repeat(201), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(gameCommentsMapper, never()).addGameComment(any());
    }

    @Test
    void createCommentAccepts200CharsBoundary() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameCommentsMapper.addGameComment(any(GameCommentData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameCommentData.class).setId(101L);
            return 1;
        });
        when(gameCommentsMapper.getGameComment(101L)).thenReturn(persistedComment(101L, 1L, 7L, "작성자", false));

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, "가".repeat(200), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameCommentsMapper).addGameComment(any(GameCommentData.class));
    }

    // ---- AC-7: CSRF 부재 403 (mutation 전) ----

    @Test
    void createCommentRejectsMissingCsrfBeforeMapperAccess() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, "내용", request, session);

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

    @Test
    void updateCommentRejectsMissingCsrfBeforeMapperAccess() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.updateComment(1L, 5L, "내용", request, session);

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

    @Test
    void deleteCommentRejectsMissingCsrfBeforeMapperAccess() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = noCsrfPost(session);

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

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

    // ---- 401 (비로그인) ----

    @Test
    void createCommentRequiresLogin() {
        GameCommentController controller = controller();
        MockHttpSession session = new MockHttpSession();
        CsrfTokens.getOrCreate(session);
        MockHttpServletRequest request = csrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, "내용", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        verifyNoInteractions(gameCommentsMapper);
    }

    // ---- AC-11 (A1): POST/PUT 응답 = commentView 전체 8키 ----

    @Test
    void createCommentReturnsFullCommentView() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameCommentsMapper.addGameComment(any(GameCommentData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameCommentData.class).setId(100L);
            return 1;
        });
        OffsetDateTime created = OffsetDateTime.now();
        GameCommentData persisted = persistedComment(100L, 1L, 7L, "작성자", false);
        persisted.setCreatedAt(created);
        persisted.setUpdatedAt(created);
        when(gameCommentsMapper.getGameComment(100L)).thenReturn(persisted);

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, "좋은 게임", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        Map<String, Object> body = response.getBody();
        assertThat(body).containsKeys(
                "commentId", "gameId", "authorName", "userId",
                "content", "createdAt", "edited", "updatedAt");
        assertThat(body).containsEntry("commentId", 100L);
        assertThat(body).containsEntry("edited", false);
        assertThat(body.get("createdAt")).isEqualTo(created);
        assertThat(body.get("updatedAt")).isEqualTo(created);
    }

    @Test
    void updateCommentReturnsFullCommentView() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        OffsetDateTime created = OffsetDateTime.now().minusMinutes(10);
        OffsetDateTime updated = OffsetDateTime.now();
        GameCommentData persisted = persistedComment(5L, 1L, 7L, "작성자", true);
        persisted.setCreatedAt(created);
        persisted.setUpdatedAt(updated);
        when(gameCommentsMapper.getGameComment(5L))
                .thenReturn(comment(5L, 1L, 7L))
                .thenReturn(persisted);

        ResponseEntity<Map<String, Object>> response =
                controller.updateComment(1L, 5L, "수정된 내용", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        Map<String, Object> body = response.getBody();
        assertThat(body).containsKeys(
                "commentId", "gameId", "authorName", "userId",
                "content", "createdAt", "edited", "updatedAt");
        assertThat(body).containsEntry("edited", true);
        assertThat(body.get("updatedAt")).isEqualTo(updated);
    }

    // ---- AC-12 (A2): 탈퇴 사용자 마스킹 (authorName 우선 노출) ----

    @Test
    void createCommentExposesMaskedAuthorNameFromReFetch() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameCommentsMapper.addGameComment(any(GameCommentData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameCommentData.class).setId(100L);
            return 1;
        });
        // 재조회 SQL 의 CASE 결과 authorName 이 마스킹 값이면 컨트롤러는 getAuthorName 우선 노출
        when(gameCommentsMapper.getGameComment(100L))
                .thenReturn(persistedComment(100L, 1L, 7L, "(탈퇴한 사용자)", false));

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, "좋은 게임", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("authorName", "(탈퇴한 사용자)");
    }

    // ---- AC-13 (A3): 수정 시 editGameComment 호출 + updatedAt 노출 ----

    @Test
    void updateCommentInvokesEditAndExposesUpdatedAt() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        OffsetDateTime updated = OffsetDateTime.now();
        GameCommentData persisted = persistedComment(5L, 1L, 7L, "작성자", true);
        persisted.setUpdatedAt(updated);
        when(gameCommentsMapper.getGameComment(5L))
                .thenReturn(comment(5L, 1L, 7L))
                .thenReturn(persisted);

        ResponseEntity<Map<String, Object>> response =
                controller.updateComment(1L, 5L, "수정", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameCommentsMapper).editGameComment(any(GameCommentData.class));
        assertThat(response.getBody()).containsEntry("edited", true);
        assertThat(response.getBody().get("updatedAt")).isEqualTo(updated);
    }

    // ---- B3: 제어문자(NUL) 정규화 — TextNormalizer 가 NUL 제거, 일반 공백은 보존 ----

    @Test
    void createCommentStripsControlCharsBeforePersist() {
        GameCommentController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "작성자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameCommentsMapper.addGameComment(any(GameCommentData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameCommentData.class).setId(100L);
            return 1;
        });
        when(gameCommentsMapper.getGameComment(100L)).thenReturn(persistedComment(100L, 1L, 7L, "작성자", false));

        String nul = "\u0000";
        String withControlChars = "좋은" + nul + " " + nul + "게임";

        ResponseEntity<Map<String, Object>> response =
                controller.createComment(1L, withControlChars, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        ArgumentCaptor<GameCommentData> captor = ArgumentCaptor.forClass(GameCommentData.class);
        verify(gameCommentsMapper).addGameComment(captor.capture());
        String stored = captor.getValue().getContent();
        assertThat(stored).doesNotContain(nul);
        assertThat(stored).isEqualTo("좋은 게임");
    }

    // ---- helpers ----

    private GameCommentController controller() {
        return new GameCommentController(gameCommentsMapper, gamesMapper, permissionGate);
    }

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

    private GameCommentData comment(long id, long gameId, long userId) {
        GameCommentData c = new GameCommentData();
        c.setId(id);
        c.setGameId(gameId);
        c.setUserId(userId);
        c.setNickname("작성자");
        c.setContent("기존 내용");
        return c;
    }

    // 재조회분: SQL alias 로 authorName/edited/createdAt/updatedAt 가 채워진 행
    private GameCommentData persistedComment(long id, long gameId, long userId, String authorName, boolean edited) {
        GameCommentData c = new GameCommentData();
        c.setId(id);
        c.setGameId(gameId);
        c.setUserId(userId);
        c.setNickname("작성자");
        c.setAuthorName(authorName);
        c.setContent("내용");
        c.setEdited(edited);
        c.setCreatedAt(OffsetDateTime.now());
        c.setUpdatedAt(OffsetDateTime.now());
        return c;
    }

    private MockHttpSession loginSession(long userId, String role, String displayName) {
        MockHttpSession session = new MockHttpSession();
        session.setAttribute("userId", userId);
        session.setAttribute("role", role);
        session.setAttribute("displayName", displayName);
        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;
    }
}
