package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.JamData;
import com.pandoli365.bibimbap.mapper.JamEntriesMapper;
import com.pandoli365.bibimbap.mapper.JamVotesMapper;
import com.pandoli365.bibimbap.mapper.JamsMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
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.time.OffsetDateTime;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
 * W2-5 인기투표 컨트롤러 단위 테스트 (plain Mockito, MockMvc 미사용).
 *
 * <p>vote/cancelVote 의 게이트 순서(CSRF→인증→잼존재→평가기간→출품작존재)와 토글
 * 시맨틱(신규/멱등 no-op/교체)을 검증하고, results 노출 게이트(밴드왜건 회피)를
 * 진행 중 은닉 / 종료 후 노출로 전수 검증한다.
 *
 * <p>VP-1~VP-8, VP-11 매핑은 메서드 주석 참조. AC-T1(상태변경 2핸들러 CSRF 차단),
 * AC-T3(results 진행 중 출품작별 집계 은닉, 종료 후 노출) 커버.
 */
@ExtendWith(MockitoExtension.class)
class JamVoteControllerTest {

    private static final long JAM_ID = 42L;
    private static final long GAME_ID = 7L;
    private static final long OTHER_GAME_ID = 8L;
    private static final long USER_ID = 99L;
    private static final String SLUG = "my-jam";

    @Mock
    private JamsMapper jamsMapper;

    @Mock
    private JamEntriesMapper jamEntriesMapper;

    @Mock
    private JamVotesMapper jamVotesMapper;

    // ==== vote (POST): 게이트 + 토글 ====

    /** VP-5/AC-T1: CSRF 누락 → 403, 매퍼 접근 전 차단. */
    @Test
    void voteRejectsMissingCsrf() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        assertThat(response.getBody()).containsEntry("status", 403);
        verifyNoInteractions(jamsMapper);
        verify(jamVotesMapper, never()).castVote(anyLong(), anyLong(), anyLong());
        verify(jamVotesMapper, never()).updateVote(anyLong(), anyLong(), anyLong());
    }

    /** VP-3: 미인증(userId 미설정) → 401, 표 매퍼 미상호작용. */
    @Test
    void voteReturns401WhenUnauthenticated() {
        JamVoteController controller = controller();
        MockHttpSession session = anonymousSession();
        MockHttpServletRequest request = csrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

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

    /** 잼 없음 → 404. */
    @Test
    void voteReturns404WhenJamMissing() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

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

    /** VP-2/AC-4: 평가 기간 아님(RECRUIT) → 422, 투표 미반영. */
    @Test
    void voteReturns422WhenNotEvalPeriod() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(recruitJam());

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        assertThat(response.getBody()).containsEntry("message", "투표 기간이 아닙니다.");
        verifyNoInteractions(jamVotesMapper);
    }

    /** 출품작 없음 → 404. */
    @Test
    void voteReturns404WhenEntryMissing() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

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

    /** VP-1/VP-8: 첫 투표 → castVote, 200 changed=false votedGameId=GAME_ID. */
    @Test
    void voteCastsWhenFirstVote() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(true);
        when(jamVotesMapper.findVotedGameId(JAM_ID, USER_ID)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("changed", false);
        assertThat(response.getBody()).containsEntry("votedGameId", GAME_ID);
        verify(jamVotesMapper).castVote(JAM_ID, GAME_ID, USER_ID);
        verify(jamVotesMapper, never()).updateVote(anyLong(), anyLong(), anyLong());
    }

    /** VP-8 멱등: 이미 같은 대상 → no-op, 200 changed=false. */
    @Test
    void voteIsNoOpWhenSameGame() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(true);
        when(jamVotesMapper.findVotedGameId(JAM_ID, USER_ID)).thenReturn(GAME_ID);

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("changed", false);
        verify(jamVotesMapper, never()).castVote(anyLong(), anyLong(), anyLong());
        verify(jamVotesMapper, never()).updateVote(anyLong(), anyLong(), anyLong());
    }

    /** VP-1/VP-8 변경: 다른 대상으로 → updateVote, 200 changed=true. */
    @Test
    void voteUpdatesWhenDifferentGame() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(true);
        when(jamVotesMapper.findVotedGameId(JAM_ID, USER_ID)).thenReturn(OTHER_GAME_ID);

        ResponseEntity<Map<String, Object>> response =
                controller.vote(SLUG, GAME_ID, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("changed", true);
        assertThat(response.getBody()).containsEntry("votedGameId", GAME_ID);
        verify(jamVotesMapper).updateVote(JAM_ID, USER_ID, GAME_ID);
        verify(jamVotesMapper, never()).castVote(anyLong(), anyLong(), anyLong());
    }

    // ==== cancelVote (DELETE) ====

    /** AC-T1: CSRF 누락 → 403, 삭제 미수행. */
    @Test
    void cancelRejectsMissingCsrf() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = noCsrfPost(session);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        assertThat(response.getBody()).containsEntry("status", 403);
        verifyNoInteractions(jamsMapper);
        verify(jamVotesMapper, never()).deleteVote(anyLong(), anyLong());
    }

    /** VP-3: 미인증 → 401. */
    @Test
    void cancelReturns401WhenUnauthenticated() {
        JamVoteController controller = controller();
        MockHttpSession session = anonymousSession();
        MockHttpServletRequest request = csrfPost(session);

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

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

    /** 잼 없음 → 404. */
    @Test
    void cancelReturns404WhenJamMissing() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(null);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        verify(jamVotesMapper, never()).deleteVote(anyLong(), anyLong());
    }

    /** AC-4: 평가 기간 아님 → 422, 삭제 미수행. */
    @Test
    void cancelReturns422WhenNotEvalPeriod() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(recruitJam());

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        verify(jamVotesMapper, never()).deleteVote(anyLong(), anyLong());
    }

    /** 취소할 표 없음(affected 0) → 404. */
    @Test
    void cancelReturns404WhenNoVote() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamVotesMapper.deleteVote(JAM_ID, USER_ID)).thenReturn(0);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        assertThat(response.getBody()).containsEntry("message", "취소할 표가 없습니다.");
    }

    /** VP-8: 취소 성공 → 200, votedGameId null. */
    @Test
    void cancelSucceeds() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamVotesMapper.deleteVote(JAM_ID, USER_ID)).thenReturn(1);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("votedGameId");
        assertThat(response.getBody().get("votedGameId")).isNull();
    }

    // ==== myVote (GET /mine) ====

    /** VP-3: 미인증 → 401, 잼 조회 전 차단. */
    @Test
    void myVoteReturns401WhenUnauthenticated() {
        JamVoteController controller = controller();
        MockHttpSession session = anonymousSession();

        ResponseEntity<Map<String, Object>> response = controller.myVote(SLUG, session);

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

    /** 잼 없음 → 404. */
    @Test
    void myVoteReturns404WhenJamMissing() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response = controller.myVote(SLUG, session);

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

    /** VP-11/AC-11: 진행 중에도 본인 표 노출 → votedGameId=GAME_ID. */
    @Test
    void myVoteReturnsVotedGameId() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamVotesMapper.findVotedGameId(JAM_ID, USER_ID)).thenReturn(GAME_ID);

        ResponseEntity<Map<String, Object>> response = controller.myVote(SLUG, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("votedGameId", GAME_ID);
    }

    /** 본인 표 없음 → 200, votedGameId null. */
    @Test
    void myVoteReturnsNullWhenNotVoted() {
        JamVoteController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamVotesMapper.findVotedGameId(JAM_ID, USER_ID)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response = controller.myVote(SLUG, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("votedGameId");
        assertThat(response.getBody().get("votedGameId")).isNull();
    }

    // ==== results (GET /results): 노출 게이트(AC-T3) ====

    /** 잼 없음 → 404. */
    @Test
    void resultsReturns404WhenJamMissing() {
        JamVoteController controller = controller();
        when(jamsMapper.getBySlug(SLUG)).thenReturn(null);

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

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

    /** VP-4/AC-6/AC-T3: 진행 중에는 출품작별 집계 은닉 — open/total 만 노출. */
    @Test
    void resultsHidesCountsDuringEval() {
        JamVoteController controller = controller();
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalJam());
        when(jamVotesMapper.countByJam(JAM_ID)).thenReturn(5L);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("open", true);
        assertThat(response.getBody()).containsEntry("total", 5L);
        assertThat(response.getBody().get("results")).isNull();
        verify(jamVotesMapper).countByJam(JAM_ID);
        verify(jamVotesMapper, never()).listCountsByJam(anyLong());
    }

    /** VP-4/AC-6: 종료(CLOSED)면 출품작별 집계 노출, total 합산. */
    @Test
    void resultsRevealsCountsWhenClosed() {
        JamVoteController controller = controller();
        when(jamsMapper.getBySlug(SLUG)).thenReturn(closedJam());
        when(jamVotesMapper.listCountsByJam(JAM_ID)).thenReturn(List.of(
                Map.of("gameId", 7L, "voteCount", 3L),
                Map.of("gameId", 8L, "voteCount", 1L)
        ));

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("results");
        assertThat(response.getBody().get("results")).isNotNull();
        assertThat(response.getBody()).containsEntry("total", 4L);
        verify(jamVotesMapper).listCountsByJam(JAM_ID);
        verify(jamVotesMapper, never()).countByJam(anyLong());
    }

    /** VP-4: 평가 종료(now > evalEnd)면 CLOSED 아니어도 집계 노출. */
    @Test
    void resultsRevealsCountsWhenEvalEnded() {
        JamVoteController controller = controller();
        when(jamsMapper.getBySlug(SLUG)).thenReturn(evalEndedJam());
        when(jamVotesMapper.listCountsByJam(JAM_ID)).thenReturn(List.of(
                Map.of("gameId", 7L, "voteCount", 2L),
                Map.of("gameId", 8L, "voteCount", 2L)
        ));

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().get("results")).isNotNull();
        assertThat(response.getBody()).containsEntry("total", 4L);
        verify(jamVotesMapper).listCountsByJam(JAM_ID);
    }

    /** VP-3: results 는 세션 인자 없음 — 미로그인도 잼 존재 시 200. */
    @Test
    void resultsAccessibleWhenAnonymous() {
        JamVoteController controller = controller();
        when(jamsMapper.getBySlug(SLUG)).thenReturn(closedJam());
        when(jamVotesMapper.listCountsByJam(JAM_ID)).thenReturn(List.of());

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("total", 0L);
    }

    // ==== helpers ====

    private JamVoteController controller() {
        return new JamVoteController(jamsMapper, jamEntriesMapper, jamVotesMapper);
    }

    private JamData evalJam() {
        JamData jam = new JamData();
        jam.setId(JAM_ID);
        jam.setStatus("EVAL");
        jam.setEvalStartAt(OffsetDateTime.now().minusHours(1));
        jam.setEvalEndAt(OffsetDateTime.now().plusHours(1));
        return jam;
    }

    private JamData recruitJam() {
        JamData jam = new JamData();
        jam.setId(JAM_ID);
        jam.setStatus("RECRUIT");
        return jam;
    }

    private JamData closedJam() {
        JamData jam = new JamData();
        jam.setId(JAM_ID);
        jam.setStatus("CLOSED");
        return jam;
    }

    /** 평가 종료 시각이 과거 — results 노출 게이트(now > evalEnd) 검증용. */
    private JamData evalEndedJam() {
        JamData jam = new JamData();
        jam.setId(JAM_ID);
        jam.setStatus("EVAL");
        jam.setEvalStartAt(OffsetDateTime.now().minusHours(2));
        jam.setEvalEndAt(OffsetDateTime.now().minusHours(1));
        return jam;
    }

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

    /** userId 미설정 — CSRF 통과를 위해 토큰만 보유. */
    private MockHttpSession anonymousSession() {
        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;
    }
}
