package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.JamData;
import com.pandoli365.bibimbap.data.JamEntryData;
import com.pandoli365.bibimbap.data.JamTeamData;
import com.pandoli365.bibimbap.mapper.GamesMapper;
import com.pandoli365.bibimbap.mapper.JamAwardsMapper;
import com.pandoli365.bibimbap.mapper.JamEntriesMapper;
import com.pandoli365.bibimbap.mapper.JamTeamMembersMapper;
import com.pandoli365.bibimbap.mapper.JamTeamsMapper;
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 org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;

import java.time.OffsetDateTime;
import java.util.ArrayList;
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.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class JamControllerTest {

    private static final long USER_ID = 7L;
    private static final long GAME_ID = 55L;
    private static final long JAM_ID = 3L;
    private static final long TEAM_ID = 11L;
    private static final String SLUG = "spring-jam";

    @Mock
    private JamsMapper jamsMapper;

    @Mock
    private JamEntriesMapper jamEntriesMapper;

    @Mock
    private JamTeamsMapper jamTeamsMapper;

    @Mock
    private JamTeamMembersMapper jamTeamMembersMapper;

    @Mock
    private GamesMapper gamesMapper;

    @Mock
    private JamAwardsMapper jamAwardsMapper;

    // ---- list (VP-4 keyset) ----

    @Test
    void listFirstPageWithoutNextCursorWhenUnderPageSize() {
        JamController controller = controller();
        Model model = new ExtendedModelMap();
        when(jamsMapper.listVisibleKeyset(isNull(), isNull(), eq(21))).thenReturn(jams(20));

        String view = controller.list(null, model);

        assertThat(view).isEqualTo("jam-list");
        assertThat(model.getAttribute("nextCursor")).isNull();
        @SuppressWarnings("unchecked")
        List<JamData> jams = (List<JamData>) model.getAttribute("jams");
        assertThat(jams).hasSize(20);
    }

    @Test
    void listTrimsToPageSizeAndEmitsNextCursorWhenMoreExist() {
        JamController controller = controller();
        Model model = new ExtendedModelMap();
        when(jamsMapper.listVisibleKeyset(isNull(), isNull(), eq(21))).thenReturn(jams(21));

        String view = controller.list(null, model);

        assertThat(view).isEqualTo("jam-list");
        @SuppressWarnings("unchecked")
        List<JamData> jams = (List<JamData>) model.getAttribute("jams");
        assertThat(jams).hasSize(20);
        JamData last = jams.get(19);
        String expectedCursor = last.getCreatedAt().toString() + "_" + last.getId();
        assertThat(model.getAttribute("nextCursor")).isEqualTo(expectedCursor);
    }

    // ---- detail ----

    @Test
    void detailRedirectsWhenJamMissing() {
        JamController controller = controller();
        when(jamsMapper.getBySlug(SLUG)).thenReturn(null);

        String view = controller.detail(SLUG, new ExtendedModelMap(), new MockHttpServletRequest());

        assertThat(view).isEqualTo("redirect:/jams");
    }

    @Test
    void detailRedirectsWhenJamNotVisible() {
        JamController controller = controller();
        JamData jam = jam(JAM_ID, "RECRUIT", false);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam);

        String view = controller.detail(SLUG, new ExtendedModelMap(), new MockHttpServletRequest());

        assertThat(view).isEqualTo("redirect:/jams");
    }

    @Test
    void detailRendersVisibleJam() {
        JamController controller = controller();
        JamData jam = jam(JAM_ID, "RECRUIT", true);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam);
        when(jamEntriesMapper.listByJam(JAM_ID)).thenReturn(List.of());
        when(jamTeamsMapper.listByJam(JAM_ID)).thenReturn(List.of());
        Model model = new ExtendedModelMap();
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setSession(new MockHttpSession());

        String view = controller.detail(SLUG, model, request);

        assertThat(view).isEqualTo("jam-detail");
        assertThat(model.getAttribute("jam")).isSameAs(jam);
        assertThat(model.getAttribute("entries")).isNotNull();
        assertThat(model.getAttribute("teams")).isNotNull();
        assertThat(model.getAttribute("csrfToken")).isNotNull();
    }

    // ---- submitEntry (VP-3 출품 무결성 + VP-5 CSRF) ----

    @Test
    void submitEntryRejectsMissingCsrf() {
        JamController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = noCsrfPost(session);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(jamEntriesMapper, never()).insert(any());
    }

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

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        verify(jamEntriesMapper, never()).insert(any());
    }

    @Test
    void submitEntrySucceedsForOwnedSoloGame() {
        JamController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam(JAM_ID, "RECRUIT", true));
        when(gamesMapper.getGame(GAME_ID)).thenReturn(game(GAME_ID, USER_ID));
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(false);
        doAnswer(invocation -> {
            JamEntryData entry = invocation.getArgument(0);
            entry.setId(900L);
            return 1;
        }).when(jamEntriesMapper).insert(any(JamEntryData.class));

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("entryId", 900L);
        verify(jamEntriesMapper).insert(any(JamEntryData.class));
    }

    @Test
    void submitEntryRejectsNonOwnedSoloGame() {
        JamController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam(JAM_ID, "RECRUIT", true));
        when(gamesMapper.getGame(GAME_ID)).thenReturn(game(GAME_ID, 99L));
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(false);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        verify(jamEntriesMapper, never()).insert(any());
    }

    @Test
    void submitEntryRejectsDuplicateEntry() {
        JamController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam(JAM_ID, "RECRUIT", true));
        when(gamesMapper.getGame(GAME_ID)).thenReturn(game(GAME_ID, USER_ID));
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(true);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
        verify(jamEntriesMapper, never()).insert(any());
    }

    @Test
    void submitEntryRejectsNonTeamMember() {
        JamController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam(JAM_ID, "RECRUIT", true));
        when(gamesMapper.getGame(GAME_ID)).thenReturn(game(GAME_ID, USER_ID));
        when(jamEntriesMapper.exists(JAM_ID, GAME_ID)).thenReturn(false);
        when(jamTeamMembersMapper.exists(TEAM_ID, USER_ID)).thenReturn(false);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        verify(jamEntriesMapper, never()).insert(any());
    }

    @Test
    void submitEntryRejectsWhenJamNotInSubmittableState() {
        JamController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam(JAM_ID, "EVAL", true));

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        verify(gamesMapper, never()).getGame(anyLong());
        verify(jamEntriesMapper, never()).insert(any());
    }

    // ---- submitTeam ----

    @Test
    void submitTeamSucceeds() {
        JamController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam(JAM_ID, "RECRUIT", true));
        doAnswer(invocation -> {
            JamTeamData team = invocation.getArgument(0);
            team.setId(700L);
            return 1;
        }).when(jamTeamsMapper).insert(any(JamTeamData.class));

        ResponseEntity<Map<String, Object>> response =
                controller.submitTeam(SLUG, "드림팀", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("jamTeamId", 700L);
        verify(jamTeamsMapper).insert(any(JamTeamData.class));
    }

    // ---- addMember ----

    @Test
    void addMemberRejectsNonOwner() {
        JamController controller = controller();
        MockHttpSession session = userSession(2L);
        MockHttpServletRequest request = csrfPost(session);
        when(jamsMapper.getBySlug(SLUG)).thenReturn(jam(JAM_ID, "RECRUIT", true));
        when(jamTeamsMapper.getById(TEAM_ID)).thenReturn(team(TEAM_ID, 1L));

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        verify(jamTeamMembersMapper, never()).insert(anyLong(), anyLong());
    }

    // ---- helpers ----

    private JamController controller() {
        return new JamController(jamsMapper, jamEntriesMapper, jamTeamsMapper, jamTeamMembersMapper, gamesMapper, jamAwardsMapper);
    }

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

    private JamData jam(long id, String status, boolean visible) {
        JamData jam = new JamData();
        jam.setId(id);
        jam.setSlug(SLUG);
        jam.setStatus(status);
        jam.setIsVisible(visible);
        jam.setCreatedAt(OffsetDateTime.parse("2026-06-01T00:00:00Z"));
        return jam;
    }

    private GameData game(long id, long userId) {
        GameData game = new GameData();
        game.setId(id);
        game.setUserId(userId);
        return game;
    }

    private JamTeamData team(long id, long ownerUserId) {
        JamTeamData team = new JamTeamData();
        team.setId(id);
        team.setOwnerUserId(ownerUserId);
        return team;
    }

    private List<JamData> jams(int count) {
        List<JamData> jams = new ArrayList<>(count);
        OffsetDateTime base = OffsetDateTime.parse("2026-06-01T00:00:00Z");
        for (int i = 0; i < count; i++) {
            JamData jam = new JamData();
            jam.setId((long) (count - i));
            jam.setSlug("jam-" + i);
            jam.setStatus("RECRUIT");
            jam.setIsVisible(true);
            jam.setCreatedAt(base.minusDays(i));
            jams.add(jam);
        }
        return jams;
    }
}
