package com.pingcap.community.blogserver.controller;

import com.pingcap.community.blogserver.support.sso.SSOClient;
import com.pingcap.community.blogserver.support.sso.SSOConfiguration;
import com.pingcap.community.blogserver.testutils.PopulateDataExtension;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.Cookie;
import java.net.URI;

import static com.pingcap.community.blogserver.testutils.RequestPostProcessors.editor1User;
import static com.pingcap.community.blogserver.testutils.RequestPostProcessors.reader1User;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(PopulateDataExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class UserControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private SSOClient ssoClient;

  @Autowired
  private SSOConfiguration ssoConfiguration;

  @Test
  @Order(1)
  void getUserDetail() throws Exception {
    mockMvc
        .perform(get("/api/users/1015"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id", equalTo(1015)))
        .andExpect(jsonPath("$.name").doesNotExist())
        .andExpect(jsonPath("$.username", equalTo("reader1")))
        .andExpect(jsonPath("$.posts", equalTo(2)))
        .andExpect(jsonPath("$.favorites", equalTo(7)))
        .andExpect(jsonPath("$.likes", equalTo(7)))
        .andExpect(jsonPath("$.comments", equalTo(7)))
        .andDo(print());
  }

  @Test
  @Order(1)
  void getOwnUserDetail() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015")
                .cookie(new Cookie("dev_sid", "0"))
                .with(reader1User())
        )
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id", equalTo(1015)))
        .andExpect(jsonPath("$.name").doesNotExist())
        .andExpect(jsonPath("$.username", equalTo("reader1")))
        .andExpect(jsonPath("$.posts", equalTo(8)))
        .andExpect(jsonPath("$.favorites", equalTo(7)))
        .andExpect(jsonPath("$.likes", equalTo(7)))
        .andExpect(jsonPath("$.comments", equalTo(7)))
        .andDo(print());
  }

  @Test
  @Order(1)
  void editorGetUserDetail() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015")
                .cookie(new Cookie("dev_sid", "0"))
                .with(editor1User())
        )
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id", equalTo(1015)))
        .andExpect(jsonPath("$.name").doesNotExist())
        .andExpect(jsonPath("$.username", equalTo("reader1")))
        .andExpect(jsonPath("$.posts", equalTo(8)))
        .andExpect(jsonPath("$.favorites", equalTo(7)))
        .andExpect(jsonPath("$.likes", equalTo(7)))
        .andExpect(jsonPath("$.comments", equalTo(7)))
        .andDo(print());
  }

  @Test
  @Order(1)
  void getUserDetailByUsername() throws Exception {
    mockMvc
        .perform(get("/api/users/username/reader1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id", equalTo(1015)))
        .andExpect(jsonPath("$.name").doesNotExist())
        .andExpect(jsonPath("$.username", equalTo("reader1")))
        .andExpect(jsonPath("$.posts", equalTo(2)))
        .andExpect(jsonPath("$.favorites", equalTo(7)))
        .andExpect(jsonPath("$.likes", equalTo(7)))
        .andExpect(jsonPath("$.comments", equalTo(7)));
  }

  @Test
  @Order(2)
  void nonLoginUserGetUserPosts() throws Exception {
    mockMvc
        .perform(get("/api/users/1015/posts").queryParam("page", "1").queryParam("size", "5"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1008, 1007)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  @Test
  @Order(2)
  void getUserPostsByUsername() throws Exception {
    mockMvc
        .perform(
            get("/api/users/username/reader1/posts")
                .queryParam("page", "1")
                .queryParam("size", "5"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1008, 1007)))
        .andExpect(
            jsonPath("$.content[*].summary", hasItems(equalTo("Heading1"), equalTo("Heading1"))))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  @Test
  @Order(2)
  void getUserPostsByNoExistedUser() throws Exception {
    String apiURL =
        String.format("%s/api/admin/users/%s", ssoConfiguration.getApiHost(), "not_existed");

    RestTemplate ssoRestClient = ssoClient.getRestTemplate();
    ClientHttpRequestFactory originalRequestFactory = ssoRestClient.getRequestFactory();
    MockRestServiceServer mockServer = MockRestServiceServer.bindTo(ssoRestClient).build();
    mockServer
        .expect(ExpectedCount.once(), requestTo(new URI(apiURL)))
        .andExpect(method(HttpMethod.GET))
        .andRespond(withStatus(HttpStatus.NOT_FOUND));

    mockMvc
        .perform(
            get("/api/users/username/not_existed/posts")
                .queryParam("page", "1")
                .queryParam("size", "5"))
        .andExpect(status().isBadRequest());

    mockServer.reset();
    ssoRestClient.setRequestFactory(originalRequestFactory);
  }

  @Test
  @Order(2)
  void nonPostAuthorUserGetUserPosts() throws Exception {
    mockMvc
        .perform(get("/api/users/1015/posts").queryParam("page", "1").queryParam("size", "5"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1008, 1007)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  @Test
  @Order(2)
  void postAuthorUserGetUserPosts() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/posts")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .cookie(new Cookie("dev_sid", "0"))
                .with(reader1User()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1008, 1007, 1006, 1005, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(8)));
  }

  @Test
  @Order(2)
  void editorUserGetUserPosts() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/posts")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .cookie(new Cookie("dev_sid", "0"))
                .with(editor1User()))
        .andExpect(status().isOk())
        .andExpect(
            jsonPath("$.content[*].id", containsInRelativeOrder(1008, 1007, 1006, 1005, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(8)));
  }

  /**
   * Notice: When the post is in the DRAFT state, the post may not have a category,
   * and the category obtained by the query should be null.
   */
  @Test
  @Order(2)
  void postAuthorUserGetUserDraftPosts() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/posts")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("status", "DRAFT")
                .cookie(new Cookie("dev_sid", "0"))
                .with(reader1User()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1002, 1001)))
        .andExpect(jsonPath("$.content[*].category", containsInRelativeOrder(
            notNullValue(),
            nullValue()
        )))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  /**
   * Notice: When trying to access a user's non-published posts, if the current user
   * is not the author and does not have sufficient permissions to access all status posts,
   * only the published posts will be returned.
   */
  @Test
  @Order(2)
  void nonLoginUserGetUserPendingPosts() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/posts")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("status", "PENDING"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1008, 1007)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  /**
   * Notice: When trying to access a user's non-published posts, if the current user
   * is not the author and does not have sufficient permissions to access all status posts,
   * only the published posts will be returned.
   */
  @Test
  @Order(2)
  void nonPostAuthorUserGetUserPendingPosts() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/posts")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("status", "PENDING"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1008, 1007)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  @Test
  @Order(2)
  void postAuthorUserGetUserPendingPosts() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/posts")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("status", "PENDING")
                .cookie(new Cookie("dev_sid", "0"))
                .with(reader1User()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1004, 1003)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  @Test
  @Order(2)
  void editorUserGetUserPendingPosts() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/posts")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("sort", "publishedAt,desc")
                .queryParam("status", "PENDING")
                .cookie(new Cookie("dev_sid", "0"))
                .with(editor1User()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(1003, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(2)));
  }

  @Test
  @Order(3)
  void getUserComments() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/comments")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("sort", "createdAt,desc"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(10, 9, 8, 7, 6)))
        .andExpect(
            jsonPath("$.content[*].post.id", containsInRelativeOrder(1012, 1008, 1006, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(7)));
  }

  @Test
  @Order(3)
  void getUserCommentsByUsername() throws Exception {
    mockMvc
        .perform(
            get("/api/users/username/reader1/comments")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("sort", "createdAt,desc"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content[*].id", containsInRelativeOrder(10, 9, 8, 7, 6)))
        .andExpect(
            jsonPath("$.content[*].post.id", containsInRelativeOrder(1012, 1008, 1006, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(7)));
  }

  @Test
  @Order(4)
  void getUserLikes() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/likes")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("sort", "likedAt,desc"))
        .andExpect(status().isOk())
        .andExpect(
            jsonPath("$.content[*].post.id", containsInRelativeOrder(1012, 1008, 1006, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(7)));
  }

  @Test
  @Order(4)
  void getUserLikesByUsername() throws Exception {
    mockMvc
        .perform(
            get("/api/users/username/reader1/likes")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("sort", "likedAt,desc"))
        .andExpect(status().isOk())
        .andExpect(
            jsonPath("$.content[*].post.id", containsInRelativeOrder(1012, 1008, 1006, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(7)));
  }

  @Test
  @Order(5)
  void getUserFavorites() throws Exception {
    mockMvc
        .perform(
            get("/api/users/1015/favorites")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("sort", "favoritedAt,desc"))
        .andExpect(status().isOk())
        .andExpect(
            jsonPath("$.content[*].post.id", containsInRelativeOrder(1012, 1008, 1006, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(7)));
  }

  @Test
  @Order(5)
  void getUserFavoritesByUsername() throws Exception {
    mockMvc
        .perform(
            get("/api/users/username/reader1/favorites")
                .queryParam("page", "1")
                .queryParam("size", "5")
                .queryParam("sort", "favoritedAt,desc"))
        .andExpect(status().isOk())
        .andExpect(
            jsonPath("$.content[*].post.id", containsInRelativeOrder(1012, 1008, 1006, 1004)))
        .andExpect(jsonPath("$.page.number", equalTo(1)))
        .andExpect(jsonPath("$.page.size", equalTo(5)))
        .andExpect(jsonPath("$.page.totalElements", equalTo(7)));
  }
}
