package com.pandoli365.bibimbap.controller.api;

import com.pandoli365.bibimbap.badge.ReputationService;
import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.GameLikeData;
import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
import com.pandoli365.bibimbap.mapper.GameLikesMapper;
import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
import com.pandoli365.bibimbap.mapper.GameViewsMapper;
import com.pandoli365.bibimbap.mapper.GamesMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.service.GameAssetCleanupService;
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 org.springframework.ui.ExtendedModelMap;
import org.springframework.web.server.ResponseStatusException;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowableOfType;
import static org.mockito.ArgumentMatchers.any;
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 GameLikeControllerTest {

    @Mock
    private GamesMapper gamesMapper;

    @Mock
    private GameCommentsMapper gameCommentsMapper;

    @Mock
    private GameReviewsMapper gameReviewsMapper;

    @Mock
    private GameViewsMapper gameViewsMapper;

    @Mock
    private GameLikesMapper gameLikesMapper;

    @Mock
    private GameAssetCleanupService assetCleanupService;

    @Mock
    private ReputationService reputationService;

    // ---- AC-1: 미좋아요 → 좋아요 추가, incrementLikeCount 호출 + 응답 likeCount 반영 ----

    @Test
    void toggleLikeAddsLikeAndIncrementsCount() {
        GameController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "사용자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameLikesMapper.findByGameAndUser(1L, "7")).thenReturn(null);
        when(gamesMapper.getLikeCount(1L)).thenReturn(6);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("liked", true);
        assertThat(response.getBody()).containsEntry("likeCount", 6);
        // 회귀 가드: 컬럼 +1 = incrementLikeCount 호출 + row 추가
        verify(gameLikesMapper).addGameLike(any());
        verify(gamesMapper).incrementLikeCount(1L);
    }

    // ---- AC-2: 이미 좋아요 → 토글 취소, decrementLikeCount 호출, increment 미호출 ----

    @Test
    void toggleLikeRemovesLikeAndDecrementsCount() {
        GameController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "사용자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameLikesMapper.findByGameAndUser(1L, "7")).thenReturn(existingLike(1L, "7"));
        when(gamesMapper.getLikeCount(1L)).thenReturn(5);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("liked", false);
        assertThat(response.getBody()).containsEntry("likeCount", 5);
        verify(gameLikesMapper).deleteByGameAndUser(eq(1L), anyString());
        verify(gamesMapper).decrementLikeCount(1L);
        verify(gamesMapper, never()).incrementLikeCount(anyLong());
    }

    // ---- AC-3: 추가 경로에서 game_likes row 생성 + userKey == String.valueOf(userId) ----
    // created_at 은 DB default 이므로 단위테스트 범위 밖 (mapper insert 시점 자동 채움).

    @Test
    void toggleLikePersistsRowWithUserKeyEqualToUserIdString() {
        GameController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "사용자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(game(1L));
        when(gameLikesMapper.findByGameAndUser(1L, "7")).thenReturn(null);
        when(gamesMapper.getLikeCount(1L)).thenReturn(1);

        controller.toggleLike(1L, request, session);

        ArgumentCaptor<GameLikeData> captor = ArgumentCaptor.forClass(GameLikeData.class);
        verify(gameLikesMapper).addGameLike(captor.capture());
        assertThat(captor.getValue().getGameId()).isEqualTo(1L);
        assertThat(captor.getValue().getUserKey()).isEqualTo(String.valueOf(7L));
    }

    // ---- AC-4: CSRF 누락 → 403, mutation 전 차단 ----

    @Test
    void toggleLikeRejectsMissingCsrfBeforeMutation() {
        GameController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "사용자");
        MockHttpServletRequest request = noCsrfPost(session);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        assertThat(response.getBody()).containsEntry("status", 403);
        verifyNoInteractions(gameLikesMapper);
        verify(gamesMapper, never()).getGame(anyLong());
        verify(gamesMapper, never()).incrementLikeCount(anyLong());
    }

    // ---- AC-5: 미로그인 → 401 ----

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

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        assertThat(response.getBody()).containsEntry("message", "로그인이 필요합니다.");
        verifyNoInteractions(gameLikesMapper);
        verify(gamesMapper, never()).getGame(anyLong());
        verify(gamesMapper, never()).incrementLikeCount(anyLong());
        verify(gamesMapper, never()).decrementLikeCount(anyLong());
    }

    // ---- AC-6a: 게이트 순서 — CSRF 무효 + 미로그인 동시 → 403 우선(401 아님) ----

    @Test
    void toggleLikePrefersCsrfFailureOverLoginFailure() {
        GameController controller = controller();
        MockHttpSession session = new MockHttpSession();
        CsrfTokens.getOrCreate(session);
        MockHttpServletRequest request = noCsrfPost(session);

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

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

    // ---- AC-6b: 게임 존재 안 함 → 404 ----

    @Test
    void toggleLikeReturnsNotFoundWhenGameMissing() {
        GameController controller = controller();
        MockHttpSession session = loginSession(7L, "USER", "사용자");
        MockHttpServletRequest request = csrfPost(session);
        when(gamesMapper.getGame(1L)).thenReturn(null);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        assertThat(response.getBody()).containsEntry("message", "게임을 찾을 수 없습니다.");
        verifyNoInteractions(gameLikesMapper);
    }

    // ---- B2 회귀 가드: 없는 게임 ID 상세 접근 → 404 (GameCatalog fallback 제거 후) ----

    @Test
    void gameDetailThrowsNotFoundWhenGameMissing() {
        GameController controller = controller();
        when(gamesMapper.getGame(99999L)).thenReturn(null);

        ResponseStatusException ex = catchThrowableOfType(
                () -> controller.gameDetail(99999L, new ExtendedModelMap(), new MockHttpSession()),
                ResponseStatusException.class);

        assertThat(ex).isNotNull();
        assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

    // ---- helpers ----

    private GameController controller() {
        return new GameController(
                gamesMapper,
                gameCommentsMapper,
                gameReviewsMapper,
                gameViewsMapper,
                gameLikesMapper,
                assetCleanupService,
                reputationService);
    }

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

    private GameLikeData existingLike(long gameId, String userKey) {
        GameLikeData like = new GameLikeData();
        like.setId(99L);
        like.setGameId(gameId);
        like.setUserKey(userKey);
        return like;
    }

    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;
    }
}
