package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.JamData;
import com.pandoli365.bibimbap.mapper.GamesMapper;
import com.pandoli365.bibimbap.mapper.JamsMapper;
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.web.servlet.ModelAndView;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;

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.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class WebMvcControllerTest {

    private static final int HUB_PAGE_SIZE = 24;
    private static final int FETCH_LIMIT = HUB_PAGE_SIZE + 1;

    @Mock
    private GamesMapper gamesMapper;

    @Mock
    private JamsMapper jamsMapper;

    @Test
    void firstPage_returnsPageSizeAndNextCursor() {
        when(jamsMapper.countActive()).thenReturn(0);
        when(gamesMapper.listVisibleKeyset(isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
                .thenReturn(games(FETCH_LIMIT));

        ModelAndView mv = controller().indexView(null, null);

        assertThat(mv.getViewName()).isEqualTo("index");
        List<?> games = (List<?>) mv.getModel().get("games");
        assertThat(games).hasSize(HUB_PAGE_SIZE);

        // nextCursor 는 잘린 24건 중 마지막(index 23) 행 기준이어야 한다.
        GameData boundary = (GameData) games.get(HUB_PAGE_SIZE - 1);
        String expected = boundary.getSortOrder() + "_"
                + boundary.getCreatedAt().toInstant().toEpochMilli() + "_"
                + boundary.getId();
        assertThat(mv.getModel().get("nextCursor")).isEqualTo(expected);
    }

    @Test
    void firstPage_noNextCursor_whenResultsAtOrBelowPageSize() {
        when(jamsMapper.countActive()).thenReturn(0);
        when(gamesMapper.listVisibleKeyset(isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
                .thenReturn(games(HUB_PAGE_SIZE));

        ModelAndView mv = controller().indexView(null, null);

        List<?> games = (List<?>) mv.getModel().get("games");
        assertThat(games).hasSize(HUB_PAGE_SIZE);
        assertThat(mv.getModel().get("nextCursor")).isNull();
    }

    @Test
    void secondPage_parsesCursorAndPassesToMapper() {
        when(jamsMapper.countActive()).thenReturn(0);
        when(gamesMapper.listVisibleKeyset(any(), any(), any(), eq(FETCH_LIMIT)))
                .thenReturn(new ArrayList<>());

        controller().indexView(null, "10_1718000000000_57");

        ArgumentCaptor<Integer> sortCaptor = ArgumentCaptor.forClass(Integer.class);
        ArgumentCaptor<OffsetDateTime> createdCaptor = ArgumentCaptor.forClass(OffsetDateTime.class);
        ArgumentCaptor<Long> idCaptor = ArgumentCaptor.forClass(Long.class);
        ArgumentCaptor<Integer> limitCaptor = ArgumentCaptor.forClass(Integer.class);
        verify(gamesMapper).listVisibleKeyset(
                sortCaptor.capture(), createdCaptor.capture(), idCaptor.capture(), limitCaptor.capture());

        assertThat(sortCaptor.getValue()).isEqualTo(10);
        assertThat(idCaptor.getValue()).isEqualTo(57L);
        assertThat(createdCaptor.getValue().toInstant().toEpochMilli()).isEqualTo(1718000000000L);
        assertThat(limitCaptor.getValue()).isEqualTo(FETCH_LIMIT);
    }

    @Test
    void malformedCursor_fallsBackToFirstPage() {
        when(jamsMapper.countActive()).thenReturn(0);
        when(gamesMapper.listVisibleKeyset(isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
                .thenReturn(new ArrayList<>());

        WebMvcController controller = controller();
        // 각각: 구분자 없음 / 컴포넌트 3 미만 / 숫자 파싱 실패 — 모두 첫 페이지 폴백, 예외 없음.
        controller.indexView(null, "garbage");
        controller.indexView(null, "1_2");
        controller.indexView(null, "x_y_z");

        verify(gamesMapper, never()).listVisibleKeyset(
                any(Integer.class), any(OffsetDateTime.class), any(Long.class), anyInt());
    }

    @Test
    void searchQuery_usesSearchKeysetAndPreservesQuery() {
        when(jamsMapper.countActive()).thenReturn(0);
        when(gamesMapper.searchVisibleKeyset(eq("플랫폼"), isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
                .thenReturn(games(FETCH_LIMIT));

        ModelAndView mv = controller().indexView("플랫폼", null);

        assertThat(mv.getModel().get("searchQuery")).isEqualTo("플랫폼");
        assertThat(mv.getModel().get("nextCursor")).isNotNull();
        verify(gamesMapper).searchVisibleKeyset("플랫폼", null, null, null, FETCH_LIMIT);
        verify(gamesMapper, never()).listVisibleKeyset(any(), any(), any(), anyInt());
    }

    @Test
    void searchQuery_withCursor_passesBoth() {
        when(jamsMapper.countActive()).thenReturn(0);
        when(gamesMapper.searchVisibleKeyset(eq("플랫폼"), any(), any(), any(), eq(FETCH_LIMIT)))
                .thenReturn(new ArrayList<>());

        controller().indexView("플랫폼", "10_1718000000000_57");

        ArgumentCaptor<String> queryCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<Integer> sortCaptor = ArgumentCaptor.forClass(Integer.class);
        ArgumentCaptor<OffsetDateTime> createdCaptor = ArgumentCaptor.forClass(OffsetDateTime.class);
        ArgumentCaptor<Long> idCaptor = ArgumentCaptor.forClass(Long.class);
        ArgumentCaptor<Integer> limitCaptor = ArgumentCaptor.forClass(Integer.class);
        verify(gamesMapper).searchVisibleKeyset(
                queryCaptor.capture(), sortCaptor.capture(), createdCaptor.capture(),
                idCaptor.capture(), limitCaptor.capture());

        assertThat(queryCaptor.getValue()).isEqualTo("플랫폼");
        assertThat(sortCaptor.getValue()).isEqualTo(10);
        assertThat(idCaptor.getValue()).isEqualTo(57L);
        assertThat(limitCaptor.getValue()).isEqualTo(FETCH_LIMIT);
    }

    @Test
    void banner_renderedWhenActiveJamExists() {
        when(jamsMapper.countActive()).thenReturn(3);
        when(jamsMapper.listActive(1)).thenReturn(List.of(jam("summer-jam", "여름 게임잼")));
        when(gamesMapper.listVisibleKeyset(isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
                .thenReturn(new ArrayList<>());

        ModelAndView mv = controller().indexView(null, null);

        assertThat(mv.getModel().get("activeJam")).isNotNull();
        assertThat(mv.getModel().get("activeJamCount")).isEqualTo(3);
    }

    @Test
    void banner_notRendered_whenNoActiveJam() {
        when(jamsMapper.countActive()).thenReturn(0);
        when(gamesMapper.listVisibleKeyset(isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
                .thenReturn(new ArrayList<>());

        ModelAndView mv = controller().indexView(null, null);

        assertThat(mv.getModel().get("activeJam")).isNull();
        assertThat(mv.getModel().get("activeJamCount")).isNull();
        verify(jamsMapper, never()).listActive(anyInt());
    }

    // ---- helpers ----

    private WebMvcController controller() {
        return new WebMvcController(gamesMapper, jamsMapper);
    }

    private List<GameData> games(int count) {
        List<GameData> rows = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            // id/sortOrder/createdAt 모두 유일한 값으로 채워 nextCursor 인코딩이 행마다 구분되게 한다.
            rows.add(game((long) (i + 1), i + 1, 1_700_000_000_000L + i));
        }
        return rows;
    }

    private GameData game(long id, int sortOrder, long createdAtMillis) {
        GameData g = new GameData();
        g.setId(id);
        g.setSortOrder(sortOrder);
        g.setCreatedAt(OffsetDateTime.ofInstant(Instant.ofEpochMilli(createdAtMillis), ZoneOffset.UTC));
        g.setName("game-" + id);
        return g;
    }

    private JamData jam(String slug, String title) {
        JamData j = new JamData();
        j.setSlug(slug);
        j.setTitle(title);
        j.setStatus("RECRUIT");
        j.setCreatedAt(OffsetDateTime.ofInstant(Instant.ofEpochMilli(1_700_000_000_000L), ZoneOffset.UTC));
        return j;
    }
}
