package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.PostCategoryData;
import com.pandoli365.bibimbap.data.PostData;
import com.pandoli365.bibimbap.mapper.PostCategoriesMapper;
import com.pandoli365.bibimbap.mapper.PostsMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.security.PermissionGate;
import com.pandoli365.bibimbap.security.PermissionKeys;
import com.pandoli365.bibimbap.service.OgPreviewService;
import com.pandoli365.bibimbap.service.PostMarkdownService;
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.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

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.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
 * PostController 단위 테스트 (plain Mockito, MockMvc 미사용 — 동일 패키지 컨벤션).
 *
 * <p>create/update/delete 의 게이트 순서(CSRF→인증→POST_WRITE 인가→검증)와
 * keyset 페이징 hasNext 처리를 검증한다. 차단 경로마다 mapper 쓰기(insert/softDelete)
 * 가 호출되지 않음을 verify(never) 로 확인해 "쓰기 차단"(VP-5) 을 grounding 한다.
 *
 * <p>설계 매핑: VP-1(POST_WRITE 게이트), VP-5(상태변경 CSRF + 미인가 시 쓰기 차단),
 * VP-6(keyset 페이징 hasNext).
 */
@ExtendWith(MockitoExtension.class)
class PostControllerTest {

    private static final long CATEGORY_ID = 3L;
    private static final long USER_ID = 5L;
    private static final long POST_ID = 1L;
    private static final int PAGE_SIZE = 20;
    private static final String POST_WRITE = PermissionKeys.POST_WRITE.name();

    @Mock
    private PostsMapper postsMapper;

    @Mock
    private PostCategoriesMapper postCategoriesMapper;

    @Mock
    private PermissionGate gate;

    @Mock
    private PostMarkdownService postMarkdownService;

    @Mock
    private OgPreviewService ogPreviewService;

    // ==== create (POST /posts): 게이트 + 검증 ====

    /** VP-5: CSRF 누락 → 403, 인가 게이트/쓰기 매퍼 진입 전 차단. */
    @Test
    void create_withoutCsrf_returns403() {
        PostController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = noCsrfPost(session);

        ResponseEntity<Map<String, Object>> response = controller.create(
                CATEGORY_ID, "제목", "본문", null, null, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        assertThat(response.getBody()).containsEntry("status", 403);
        verifyNoInteractions(gate);
        verify(postsMapper, never()).insert(any());
    }

    /** 미인증(userId 미설정) → 401, 인가 게이트/쓰기 매퍼 미진입. */
    @Test
    void create_unauthenticated_returns401() {
        PostController controller = controller();
        MockHttpSession session = anonymousSession();
        MockHttpServletRequest request = csrfPost(session);

        ResponseEntity<Map<String, Object>> response = controller.create(
                CATEGORY_ID, "제목", "본문", null, null, request, session);

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

    /** VP-1/VP-5: POST_WRITE 미보유 → 403, 검증/쓰기 매퍼 미진입. */
    @Test
    void create_withoutPostWrite_returns403() {
        PostController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(gate.has(any(), eq(POST_WRITE))).thenReturn(false);

        ResponseEntity<Map<String, Object>> response = controller.create(
                CATEGORY_ID, "제목", "본문", null, null, request, session);

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

    /** VP-1: 권한 보유 + 유효 입력 → 200 {postId, location}, insert 1회. */
    @Test
    void create_withPostWrite_returns200() {
        PostController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(gate.has(any(), eq(POST_WRITE))).thenReturn(true);
        when(postCategoriesMapper.getActive(CATEGORY_ID)).thenReturn(activeCategory());
        when(postMarkdownService.render(any())).thenReturn("<p>본문</p>");
        when(postsMapper.insert(any())).thenAnswer(inv -> {
            ((PostData) inv.getArgument(0)).setId(POST_ID);
            return 1;
        });

        ResponseEntity<Map<String, Object>> response = controller.create(
                CATEGORY_ID, "제목", "본문", null, "PUBLISHED", request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsEntry("postId", POST_ID);
        assertThat(response.getBody()).containsEntry("location", "/posts/" + POST_ID);
        verify(postsMapper).insert(any());
    }

    /** 검증 실패(title 201자) → 400, 쓰기 매퍼/카테고리 조회 미진입. */
    @Test
    void create_titleTooLong_returns4xx() {
        PostController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(gate.has(any(), eq(POST_WRITE))).thenReturn(true);

        String longTitle = "x".repeat(201);
        ResponseEntity<Map<String, Object>> response = controller.create(
                CATEGORY_ID, longTitle, "본문", null, null, request, session);

        assertThat(response.getStatusCode().is4xxClientError()).isTrue();
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        verify(postsMapper, never()).insert(any());
        // title 검증이 categoryId 조회보다 먼저라 getActive 미호출.
        verify(postCategoriesMapper, never()).getActive(anyLong());
    }

    /** 카테고리 없음(getActive→null) → 404, 쓰기 매퍼 미진입. */
    @Test
    void create_unknownCategory_returns404() {
        PostController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(gate.has(any(), eq(POST_WRITE))).thenReturn(true);
        when(postCategoriesMapper.getActive(CATEGORY_ID)).thenReturn(null);

        ResponseEntity<Map<String, Object>> response = controller.create(
                CATEGORY_ID, "제목", "본문", null, null, request, session);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        verify(postsMapper, never()).insert(any());
    }

    // ==== delete (POST /posts/{id}/delete): 게이트 ====

    /** VP-1/VP-5: delete 도 POST_WRITE 게이트 — 미보유 → 403, softDelete 미수행. */
    @Test
    void delete_withoutPostWrite_returns403() {
        PostController controller = controller();
        MockHttpSession session = userSession(USER_ID);
        MockHttpServletRequest request = csrfPost(session);
        when(gate.has(any(), eq(POST_WRITE))).thenReturn(false);

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

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        verify(postsMapper, never()).softDelete(anyLong());
    }

    // ==== list (GET /posts): keyset 페이징 ====

    /** VP-6: listPublishedKeyset 가 PAGE_SIZE+1(21)건 → hasNext=true, 표시 20건. */
    @Test
    void list_keysetPaging() {
        PostController controller = controller();
        when(postsMapper.listPublishedKeyset(any(), any(), any(), eq(PAGE_SIZE + 1)))
                .thenReturn(rows(PAGE_SIZE + 1));
        lenient().when(postCategoriesMapper.listActive()).thenReturn(List.of());
        Model model = new ExtendedModelMap();

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

        assertThat(view).isEqualTo("posts-list");
        assertThat(model.getAttribute("hasNext")).isEqualTo(true);
        @SuppressWarnings("unchecked")
        List<PostData> shown = (List<PostData>) model.getAttribute("posts");
        assertThat(shown).hasSize(PAGE_SIZE);
        assertThat(model.getAttribute("nextCursorId")).isNotNull();
    }

    /** VP-6: PAGE_SIZE 이하(20)면 hasNext=false, 커서 미노출. */
    @Test
    void list_lastPage_hasNextFalse() {
        PostController controller = controller();
        when(postsMapper.listPublishedKeyset(any(), any(), any(), eq(PAGE_SIZE + 1)))
                .thenReturn(rows(PAGE_SIZE));
        lenient().when(postCategoriesMapper.listActive()).thenReturn(List.of());
        Model model = new ExtendedModelMap();

        controller.list(null, null, null, model);

        assertThat(model.getAttribute("hasNext")).isEqualTo(false);
        @SuppressWarnings("unchecked")
        List<PostData> shown = (List<PostData>) model.getAttribute("posts");
        assertThat(shown).hasSize(PAGE_SIZE);
        assertThat(model.getAttribute("nextCursorId")).isNull();
    }

    // ==== helpers ====

    private PostController controller() {
        return new PostController(postsMapper, postCategoriesMapper, gate,
                postMarkdownService, ogPreviewService);
    }

    private PostCategoryData activeCategory() {
        PostCategoryData category = new PostCategoryData();
        category.setId(CATEGORY_ID);
        category.setName("공지");
        category.setSlug("notice");
        category.setIsActive(true);
        return category;
    }

    private List<PostData> rows(int count) {
        List<PostData> rows = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            PostData post = new PostData();
            post.setId((long) (i + 1));
            rows.add(post);
        }
        return rows;
    }

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