package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.JamCriterionData;
import com.pandoli365.bibimbap.data.JamData;
import com.pandoli365.bibimbap.mapper.JamCriteriaMapper;
import com.pandoli365.bibimbap.mapper.JamEntriesMapper;
import com.pandoli365.bibimbap.mapper.JamScoreStatsMapper;
import com.pandoli365.bibimbap.mapper.JamScoresMapper;
import com.pandoli365.bibimbap.mapper.JamsMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.security.JamRoleGate;
import com.pandoli365.bibimbap.security.PermissionGate;
import com.pandoli365.bibimbap.security.PermissionKeys;
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.math.BigDecimal;
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.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
 * W2-4 심사위원 평가 컨트롤러 단위 테스트 (plain Mockito, MockMvc 미사용).
 *
 * <p>submitScores 게이트 순서(난제1)를 첫 실패 지점으로 전수 검증:
 * CSRF(403) → 인증(401) → 심사자격(403) → 잼존재(404) → 평가기간(422) →
 * 출품작존재(404) → 자기출품작(422) → body 검증(422). upsertScore 는 전 게이트
 * 통과 + 전수 검증 통과 후에만 호출(부분 저장 금지).
 *
 * <p>AC-T5 5분류(401/403/404/422 + 200) 전수 커버, VP-1~VP-6 매핑은 메서드 주석 참조.
 */
@ExtendWith(MockitoExtension.class)
class JamScoringControllerTest {

    private static final String GAME_JAM_MANAGE = PermissionKeys.GAME_JAM_MANAGE.name();
    private static final long JUDGE_ID = 99L;
    private static final long JAM_ID = 42L;
    private static final long GAME_ID = 7L;

    @Mock
    private JamsMapper jamsMapper;

    @Mock
    private JamEntriesMapper jamEntriesMapper;

    @Mock
    private JamCriteriaMapper jamCriteriaMapper;

    @Mock
    private JamScoresMapper jamScoresMapper;

    @Mock
    private JamScoreStatsMapper jamScoreStatsMapper;

    @Mock
    private JamRoleGate jamRoleGate;

    @Mock
    private PermissionGate permissionGate;

    // ==== submitScores: 게이트 순서 전수 검증 ====

    /** VP-5: CSRF 누락 → 403, 매퍼 접근 전 차단. */
    @Test
    void submitRejectsMissingCsrf() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        assertThat(response.getBody()).containsEntry("status", 403);
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
        verifyNoInteractions(jamsMapper);
        verifyNoInteractions(jamCriteriaMapper);
    }

    /** 인증 미통과 → 401 (CSRF 토큰은 세션에 있으나 userId 미설정). */
    @Test
    void submitReturns401WhenUnauthenticated() {
        JamScoringController controller = controller();
        MockHttpSession session = anonymousSession();
        MockHttpServletRequest request = csrfPost(session);

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        assertThat(response.getBody()).containsEntry("status", HttpStatus.UNAUTHORIZED.value());
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** VP-1: 심사 자격 없음 → 403. */
    @Test
    void submitReturns403WhenNotJudge() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        assertThat(response.getBody()).containsEntry("message", "심사 권한이 없습니다.");
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** 잼 없음 → 404. */
    @Test
    void submitReturns404WhenJamMissing() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(true);
        when(jamsMapper.getById(JAM_ID)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** 평가 기간 아님(RECRUIT 상태) → 422. */
    @Test
    void submitReturns422WhenEvalClosed() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(true);
        when(jamsMapper.getById(JAM_ID)).thenReturn(recruitJam());

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        assertThat(response.getBody()).containsEntry("message", "평가 기간이 아닙니다.");
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** 출품작 없음 → 404. */
    @Test
    void submitReturns404WhenEntryMissing() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(true);
        when(jamsMapper.getById(JAM_ID)).thenReturn(evalJam());
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** 자기 출품작 → 422. */
    @Test
    void submitReturns422WhenOwnEntry() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(true);
        when(jamsMapper.getById(JAM_ID)).thenReturn(evalJam());
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(true);
        when(jamRoleGate.isOwnEntry(JAM_ID, GAME_ID, JUDGE_ID)).thenReturn(true);

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        assertThat(response.getBody()).containsEntry("message", "자기 출품작은 심사할 수 없습니다.");
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** VP-6: 화이트리스트 밖 criterion → 422, 전건 미저장. */
    @Test
    void submitReturns422WhenUnknownCriterion() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        passAllGates(session);
        when(jamCriteriaMapper.listByJam(JAM_ID)).thenReturn(List.of(criterion("fun", 1)));

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("unknown", 4), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        assertThat(response.getBody()).containsEntry("message", "등록되지 않은 평가 기준입니다.");
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** 점수 범위 밖(9) → 422, 전건 미저장. */
    @Test
    void submitReturns422WhenScoreOutOfRange() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        passAllGates(session);
        when(jamCriteriaMapper.listByJam(JAM_ID)).thenReturn(List.of(criterion("fun", 1)));

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 9), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        assertThat(response.getBody()).containsEntry("message", "점수는 1~5 사이여야 합니다.");
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** 점수 배열 비어 있음 → 422. */
    @Test
    void submitReturns422WhenScoresEmpty() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        passAllGates(session);

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, Map.of("scores", List.of()), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        assertThat(response.getBody()).containsEntry("message", "입력된 점수가 없습니다.");
        verify(jamScoresMapper, never()).upsertScore(anyLong(), anyLong(), anyLong(), any(), anyInt());
    }

    /** VP-1 성공: 전 게이트 + 전수 검증 통과 → 200, savedCount 2, upsert 2회. */
    @Test
    void submitUpsertsWhenAllGatesPass() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        passAllGates(session);
        when(jamCriteriaMapper.listByJam(JAM_ID))
                .thenReturn(List.of(criterion("fun", 1), criterion("art", 2)));

        Map<String, Object> body = Map.of("scores", List.of(
                Map.of("criterionKey", "fun", "score", 4),
                Map.of("criterionKey", "art", "score", 5)
        ));

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, body, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("status", 200);
        assertThat(response.getBody()).containsEntry("savedCount", 2);
        verify(jamScoresMapper).upsertScore(JAM_ID, GAME_ID, JUDGE_ID, "fun", 4);
        verify(jamScoresMapper).upsertScore(JAM_ID, GAME_ID, JUDGE_ID, "art", 5);
    }

    /** VP-2 멱등: 동일 키 재제출도 200 + upsert 호출(멱등은 SQL ON CONFLICT 책임). */
    @Test
    void submitReturns200OnIdempotentResubmit() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        MockHttpServletRequest request = csrfPost(session);
        passAllGates(session);
        when(jamCriteriaMapper.listByJam(JAM_ID)).thenReturn(List.of(criterion("fun", 1)));

        ResponseEntity<Map<String, Object>> response =
                controller.submitScores(JAM_ID, GAME_ID, scoresBody("fun", 3), request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("savedCount", 1);
        verify(jamScoresMapper, times(1)).upsertScore(JAM_ID, GAME_ID, JUDGE_ID, "fun", 3);
    }

    // ==== myScores ====

    @Test
    void myScoresReturns401WhenUnauthenticated() {
        JamScoringController controller = controller();
        MockHttpSession session = anonymousSession();

        ResponseEntity<Map<String, Object>> response =
                controller.myScores(JAM_ID, GAME_ID, session);

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

    @Test
    void myScoresReturns403WhenNotJudge() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(false);

        ResponseEntity<Map<String, Object>> response =
                controller.myScores(JAM_ID, GAME_ID, session);

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

    @Test
    void myScoresReturnsScoresAndCriteria() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(true);
        when(jamScoresMapper.listByJudge(JAM_ID, GAME_ID, JUDGE_ID)).thenReturn(List.of());
        when(jamCriteriaMapper.listByJam(JAM_ID)).thenReturn(List.of(criterion("fun", 1)));

        ResponseEntity<Map<String, Object>> response =
                controller.myScores(JAM_ID, GAME_ID, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("scores");
        assertThat(response.getBody()).containsKey("criteria");
        verify(jamScoresMapper).listByJudge(JAM_ID, GAME_ID, JUDGE_ID);
    }

    // ==== summary: 노출 게이트 ====

    @Test
    void summaryReturns404WhenJamMissing() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        when(jamsMapper.getById(JAM_ID)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response = controller.summary(JAM_ID, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        verify(jamScoreStatsMapper, never()).listStatsByJam(anyLong());
    }

    @Test
    void summaryReturns403WhenEvalInProgressAndNotManager() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        when(jamsMapper.getById(JAM_ID)).thenReturn(evalJam());
        when(permissionGate.has(session, GAME_JAM_MANAGE)).thenReturn(false);

        ResponseEntity<Map<String, Object>> response = controller.summary(JAM_ID, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(jamScoreStatsMapper, never()).listStatsByJam(anyLong());
    }

    @Test
    void summaryReturnsStatsWhenClosed() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        when(jamsMapper.getById(JAM_ID)).thenReturn(closedJam());
        when(jamScoreStatsMapper.listStatsByJam(JAM_ID)).thenReturn(List.of());

        ResponseEntity<Map<String, Object>> response = controller.summary(JAM_ID, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("stats");
        verify(jamScoreStatsMapper).listStatsByJam(JAM_ID);
    }

    @Test
    void summaryReturnsStatsWhenManagerDuringEval() {
        JamScoringController controller = controller();
        MockHttpSession session = judgeSession(JUDGE_ID);
        when(jamsMapper.getById(JAM_ID)).thenReturn(evalJam());
        when(permissionGate.has(session, GAME_JAM_MANAGE)).thenReturn(true);
        when(jamScoreStatsMapper.listStatsByJam(JAM_ID)).thenReturn(List.of());

        ResponseEntity<Map<String, Object>> response = controller.summary(JAM_ID, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("stats");
        verify(jamScoreStatsMapper).listStatsByJam(JAM_ID);
    }

    // ==== helpers ====

    private JamScoringController controller() {
        return new JamScoringController(
                jamsMapper,
                jamEntriesMapper,
                jamCriteriaMapper,
                jamScoresMapper,
                jamScoreStatsMapper,
                jamRoleGate,
                permissionGate
        );
    }

    /** submitScores 게이트 3~7(자기출품작 제외)을 통과시킨다. */
    private void passAllGates(MockHttpSession session) {
        when(jamRoleGate.isJudge(session, JAM_ID)).thenReturn(true);
        when(jamsMapper.getById(JAM_ID)).thenReturn(evalJam());
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(true);
        when(jamRoleGate.isOwnEntry(JAM_ID, GAME_ID, JUDGE_ID)).thenReturn(false);
    }

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

    private JamCriterionData criterion(String key, int sortOrder) {
        JamCriterionData data = new JamCriterionData();
        data.setCriterionKey(key);
        data.setDisplayName(key);
        data.setSortOrder(sortOrder);
        data.setWeight(BigDecimal.ONE);
        return data;
    }

    private Map<String, Object> scoresBody(String criterionKey, int score) {
        return Map.of("scores", List.of(Map.of("criterionKey", criterionKey, "score", score)));
    }

    private MockHttpSession judgeSession(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;
    }
}
