package com.pandoli365.bibimbap.controller.api;

import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.GameReviewData;
import com.pandoli365.bibimbap.data.ReviewAxisRow;
import com.pandoli365.bibimbap.mapper.GameReviewAxesMapper;
import com.pandoli365.bibimbap.mapper.GameReviewStatsMapper;
import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
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.util.LinkedHashMap;
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.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
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 GameReviewControllerTest {

    @Mock
    private GameReviewsMapper gameReviewsMapper;

    @Mock
    private GameReviewAxesMapper gameReviewAxesMapper;

    @Mock
    private GameReviewStatsMapper gameReviewStatsMapper;

    @Mock
    private GamesMapper gamesMapper;

    @Mock
    private PermissionGate permissionGate;

    // 6축 유효 입력(immersion,creativity,controls,completeness,sound,visual).
    private static final String[] AXES_OK = {"4", "5", "3", "4", "2", "5"};

    // ---- AC-4: 수정 시 edited 토글 (작성 직후 false → 수정 후 true) ----

    @Test
    void createReviewReturnsEditedFalse() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.getActiveReviewByGameAndUser(1L, 7L)).thenReturn(null);
        when(gameReviewsMapper.addGameReview(any(GameReviewData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameReviewData.class).setId(50L);
            return 1;
        });
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, null, "정말 재미있는 게임입니다", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("reviewId", 50L);
        assertThat(response.getBody()).containsEntry("edited", false);
        verify(gameReviewsMapper).addGameReview(any(GameReviewData.class));
    }

    @Test
    void updateReviewReturnsEditedTrue() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gameReviewsMapper.getGameReview(50L))
                .thenReturn(review(50L, 1L, 7L, 4, false))
                .thenReturn(review(50L, 1L, 7L, 5, true));

        ResponseEntity<Map<String, Object>> response =
                updateReview(controller, 1L, 50L, "5", "수정 후 훨씬 더 좋아졌습니다", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("edited", true);
        verify(gameReviewsMapper).editGameReview(any(GameReviewData.class));
    }

    // ---- AC-3: 게임당 1회 409 ----

    @Test
    void createSecondReviewReturnsConflict() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.getActiveReviewByGameAndUser(1L, 7L))
                .thenReturn(review(50L, 1L, 7L, 4, false));

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, "5", "같은 게임에 또 작성 시도합니다", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
        verify(gameReviewsMapper, never()).addGameReview(any());
    }

    // ---- AC-6: overall rating 0/6 거부, 1/5 경계 ----

    @Test
    void createReviewRejectsRatingZero() {
        assertCreateRatingRejected("0");
    }

    @Test
    void createReviewRejectsRatingSix() {
        assertCreateRatingRejected("6");
    }

    @Test
    void createReviewAcceptsRatingOneBoundary() {
        assertCreateRatingAccepted("1");
    }

    @Test
    void createReviewAcceptsRatingFiveBoundary() {
        assertCreateRatingAccepted("5");
    }

    @Test
    void updateReviewRejectsRatingSix() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));

        ResponseEntity<Map<String, Object>> response =
                updateReview(controller, 1L, 50L, "6", "범위초과", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(gameReviewsMapper, never()).editGameReview(any());
    }

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

    @Test
    void updateReviewByNonAuthorIsForbidden() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(8L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));

        ResponseEntity<Map<String, Object>> response =
                updateReview(controller, 1L, 50L, "3", "강제수정", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(gameReviewsMapper, never()).editGameReview(any());
    }

    @Test
    void deleteReviewByOperatorSucceeds() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(99L, "ADMIN");
        MockHttpServletRequest request = csrfPost(session);
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));
        // 작성자 타인(99 != 7) → 모더레이션 게이트 경유. ADMIN 모더레이터 통과.
        when(permissionGate.canModerate(any())).thenReturn(true);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameReviewsMapper).softDeleteGameReview(50L);
    }

    @Test
    void deleteReviewBySubadminModeratorSucceeds() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(50L, "SUBADMIN");
        MockHttpServletRequest request = csrfPost(session);
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));
        // 작성자 타인(50 != 7) → 게이트 계약으로 SUBADMIN 모더레이터 통과.
        when(permissionGate.canModerate(any())).thenReturn(true);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameReviewsMapper).softDeleteGameReview(50L);
    }

    // ---- AC-7: CSRF 부재 403 ----

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

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, "4", "내용입니다열자", AXES_OK, request, session);

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

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

        ResponseEntity<Map<String, Object>> response =
                updateReview(controller, 1L, 50L, "4", "내용입니다열자", AXES_OK, request, session);

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

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

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

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

    // ---- AC-7(다축): 6축 누락 시 400 ----

    @Test
    void createReviewRejectsMissingAxis() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));

        // immersion 빈값 → 6축 미충족 → 400.
        String[] missing = {"", "5", "3", "4", "2", "5"};
        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, "4", "내용입니다열자", missing, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(gameReviewsMapper, never()).addGameReview(any());
    }

    // ---- AC-8: overall 자동평균 / 수동 ----

    @Test
    void createReviewAutoAveragesOverallWhenRatingOmitted() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.getActiveReviewByGameAndUser(1L, 7L)).thenReturn(null);
        when(gameReviewsMapper.addGameReview(any(GameReviewData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameReviewData.class).setId(50L);
            return 1;
        });
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));

        // 6축 [4,5,3,4,2,5] 합 23 / 6 = 3.83 → round 4, ratingManual=false.
        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, null, "별점 자동평균 계산 검증 본문", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        ArgumentCaptor<GameReviewData> captor = ArgumentCaptor.forClass(GameReviewData.class);
        verify(gameReviewsMapper).addGameReview(captor.capture());
        assertThat(captor.getValue().getRating()).isEqualTo(4);
        assertThat(captor.getValue().getRatingManual()).isFalse();
    }

    @Test
    void createReviewUsesManualOverallWhenRatingProvided() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.getActiveReviewByGameAndUser(1L, 7L)).thenReturn(null);
        when(gameReviewsMapper.addGameReview(any(GameReviewData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameReviewData.class).setId(50L);
            return 1;
        });
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 2, false));

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, "2", "수동 별점 사용 검증 본문입니다", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        ArgumentCaptor<GameReviewData> captor = ArgumentCaptor.forClass(GameReviewData.class);
        verify(gameReviewsMapper).addGameReview(captor.capture());
        assertThat(captor.getValue().getRating()).isEqualTo(2);
        assertThat(captor.getValue().getRatingManual()).isTrue();
    }

    // ---- 다축 저장: addReviewAxes 6행 ----

    @Test
    void createReviewPersistsSixAxes() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.getActiveReviewByGameAndUser(1L, 7L)).thenReturn(null);
        when(gameReviewsMapper.addGameReview(any(GameReviewData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameReviewData.class).setId(50L);
            return 1;
        });
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, null, "여섯 축 저장 검증용 본문입니다", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        @SuppressWarnings("unchecked")
        ArgumentCaptor<List<ReviewAxisRow>> captor = ArgumentCaptor.forClass(List.class);
        verify(gameReviewAxesMapper).addReviewAxes(eq(50L), captor.capture());
        List<ReviewAxisRow> axes = captor.getValue();
        assertThat(axes).hasSize(6);
        assertThat(axes).extracting(ReviewAxisRow::getAxisKey)
                .containsExactlyInAnyOrder(
                        "immersion", "creativity", "controls", "completeness", "sound", "visual");
    }

    // ---- AC-9: B2 본문 최소 10자 ----

    @Test
    void createReviewRejectsBodyUnderTenChars() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));

        // trim 후 9자 → 400.
        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, "4", "가".repeat(9), AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(gameReviewsMapper, never()).addGameReview(any());
    }

    @Test
    void createReviewAcceptsBodyTenCharsBoundary() {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.getActiveReviewByGameAndUser(1L, 7L)).thenReturn(null);
        when(gameReviewsMapper.addGameReview(any(GameReviewData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameReviewData.class).setId(50L);
            return 1;
        });
        when(gameReviewsMapper.getGameReview(50L)).thenReturn(review(50L, 1L, 7L, 4, false));

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, "4", "가".repeat(10), AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameReviewsMapper).addGameReview(any(GameReviewData.class));
    }

    // ---- B1: listReviews 미허용 sort 관대 처리(200) ----

    @Test
    void listReviewsToleratesUnknownSort() {
        GameReviewController controller = controller();
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.listGameReviews(eq(1L), anyString(), anyInt(), anyInt()))
                .thenReturn(List.of(review(50L, 1L, 7L, 4, false)));
        lenient().when(gameReviewAxesMapper.listAxesByReviewIds(anyList())).thenReturn(List.of());
        lenient().when(gameReviewStatsMapper.getStats(1L)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response = controller.listReviews(1L, 0, "garbage");

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("hasMore");
    }

    // ---- helpers ----

    private void assertCreateRatingRejected(String rating) {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, rating, "별점 범위 검증용 본문입니다", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(gameReviewsMapper, never()).addGameReview(any());
    }

    private void assertCreateRatingAccepted(String rating) {
        GameReviewController controller = controller();
        MockHttpSession session = loginSession(7L, "USER");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameReviewsMapper.getActiveReviewByGameAndUser(1L, 7L)).thenReturn(null);
        when(gameReviewsMapper.addGameReview(any(GameReviewData.class))).thenAnswer(inv -> {
            inv.getArgument(0, GameReviewData.class).setId(50L);
            return 1;
        });
        when(gameReviewsMapper.getGameReview(50L))
                .thenReturn(review(50L, 1L, 7L, Integer.parseInt(rating), false));

        ResponseEntity<Map<String, Object>> response =
                createReview(controller, 1L, rating, "별점 경계값 통과 검증 본문입니다", AXES_OK, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(gameReviewsMapper).addGameReview(any(GameReviewData.class));
    }

    // createReview positional 호출 래퍼. axes = [immersion,creativity,controls,completeness,sound,visual].
    private ResponseEntity<Map<String, Object>> createReview(
            GameReviewController controller, long id, String rating, String body,
            String[] axes, MockHttpServletRequest request, MockHttpSession session) {
        return controller.createReview(id, rating, body,
                axes[0], axes[1], axes[2], axes[3], axes[4], axes[5], request, session);
    }

    private ResponseEntity<Map<String, Object>> updateReview(
            GameReviewController controller, long id, long reviewId, String rating, String body,
            String[] axes, MockHttpServletRequest request, MockHttpSession session) {
        return controller.updateReview(id, reviewId, rating, body,
                axes[0], axes[1], axes[2], axes[3], axes[4], axes[5], request, session);
    }

    private GameReviewController controller() {
        return new GameReviewController(
                gameReviewsMapper, gameReviewAxesMapper, gameReviewStatsMapper, gamesMapper, permissionGate);
    }

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

    private GameReviewData review(long id, long gameId, long userId, int rating, boolean edited) {
        GameReviewData r = new GameReviewData();
        r.setId(id);
        r.setGameId(gameId);
        r.setUserId(userId);
        r.setRating(rating);
        r.setBody("내용");
        r.setAuthorName("작성자");
        r.setEdited(edited);
        r.setRatingManual(false);
        Map<String, Integer> axes = new LinkedHashMap<>();
        axes.put("immersion", 4);
        axes.put("creativity", 5);
        axes.put("controls", 3);
        axes.put("completeness", 4);
        axes.put("sound", 2);
        axes.put("visual", 5);
        r.setAxes(axes);
        return r;
    }

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