package alexp.blog.controller;

import alexp.blog.AbstractIntegrationTest;
import alexp.blog.model.Comment;
import alexp.blog.model.Post;
import alexp.blog.model.PostEditDto;
import alexp.blog.utils.HsqldbSequenceResetter;
import com.github.springtestdbunit.annotation.*;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.http.MediaType;

import static alexp.blog.utils.SecurityUtils.userAdmin;
import static alexp.blog.utils.SecurityUtils.userAlice;
import static alexp.blog.utils.SecurityUtils.userBob;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@DatabaseSetup("data.xml")
@DbUnitConfiguration(databaseOperationLookup = HsqldbSequenceResetter.class)
public class PostsControllerIT extends AbstractIntegrationTest {

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldShowPostsPage() throws Exception {
        mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(2L))));
    }

    @Test
    @DatabaseSetup("data.xml")
    public void shouldGetPostsList() throws Exception {
        mockMvc.perform(get("/posts").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].id", is("2")))
                .andExpect(jsonPath("$[0].title", is("meow title")))
                .andExpect(jsonPath("$[1].id", is("1")))
                .andExpect(jsonPath("$[1].title", is("hello title")));
    }

    @Test
    @ExpectedDatabase("data-posts-voted.xml")
    @DatabaseSetup("data-posts-voted.xml")
    @DatabaseTearDown(value = "data.xml", type = DatabaseOperation.TRUNCATE_TABLE) // to reset id sequence, otherwise other tests that insert and check id will fail on ExpectedDatabase
    public void shouldGetTopPostsList() throws Exception {
        mockMvc.perform(get("/posts/top").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].id", is("1")))
                .andExpect(jsonPath("$[0].title", is("hello title")))
                .andExpect(jsonPath("$[1].id", is("2")))
                .andExpect(jsonPath("$[1].title", is("meow title")));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldShowPostPage() throws Exception {
        mockMvc.perform(get("/posts/1"))
                .andExpect(status().isOk())
                .andExpect(view().name("post"))
                .andExpect(model().attributeDoesNotExist("comment"));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldShowCommentFormWhenAuthorized() throws Exception {
        mockMvc.perform(get("/posts/1").with(userBob()))
                .andExpect(status().isOk())
                .andExpect(view().name("post"))
                .andExpect(model().attribute("comment", instanceOf(Comment.class)));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldReturn404WhenPostNotExists() throws Exception {
        mockMvc.perform(get("/posts/999"))
                .andExpect(status().isNotFound());
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldShowCreatePostPageIfAdmin() throws Exception {
        mockMvc.perform(get("/posts/create").with(userAdmin()))
                .andExpect(status().isOk())
                .andExpect(view().name("editpost"))
                .andExpect(model().attribute("post", instanceOf(PostEditDto.class)))
                .andExpect(model().attribute("edit", is(equalTo(false))));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldDenyCreatePostIfNotAdmin() throws Exception {
        mockMvc.perform(get("/posts/create"))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/login"));

        mockMvc.perform(get("/posts/create").with(userBob()))
                .andExpect(status().isForbidden());

        mockMvc.perform(post("/posts/create").with(userBob()).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldReturnAddPostFormWithErrorsWhenSubmittedInvalidPost() throws Exception {
        mockMvc.perform(post("/posts/create").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isOk())
                .andExpect(model().attributeHasFieldErrors("post", "title"))
                .andExpect(model().attributeHasFieldErrors("post", "text"))
                .andExpect(model().attribute("edit", is(equalTo(false))));

        String title = "post title";
        String text = "too short";

        mockMvc.perform(post("/posts/create").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("title", title)
                .param("text", text))
                .andExpect(status().isOk())
                .andExpect(model().attributeHasFieldErrors("post", "text"))
                .andExpect(model().attribute("post", hasProperty("title", Matchers.is(equalTo(title)))))
                .andExpect(model().attribute("post", hasProperty("text", Matchers.is(equalTo(text)))))
                .andExpect(model().attribute("edit", is(equalTo(false))));
    }

    @Test
    @ExpectedDatabase(value = "data-posts-added.xml", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)
    @DatabaseTearDown(value = "data.xml", type = DatabaseOperation.TRUNCATE_TABLE) // to reset id sequence, otherwise other tests that insert tags will fail on ExpectedDatabase
    public void shouldAddPosts() throws Exception {
        String title = "new post title";
        String text = "new post short text===cut===new post full text Lorem ipsum";
        String tags = "c++, java, hello world";

        mockMvc.perform(post("/posts/create").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("title", title)
                .param("text", text)
                .param("tags", tags))
                .andExpect(status().isFound())
                .andExpect(model().hasNoErrors())
                .andExpect(view().name("redirect:/posts"));

        String text2 = "new post 2 text Lorem ipsum dolor sit amet, consectetur adipiscing elit";
        String tags2 = "java";

        mockMvc.perform(post("/posts/create").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("title", title)
                .param("text", text2)
                .param("tags", tags2))
                .andExpect(status().isFound())
                .andExpect(model().hasNoErrors())
                .andExpect(view().name("redirect:/posts"));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldShowEditPostPageIfAdmin() throws Exception {
        mockMvc.perform(get("/posts/1/edit").with(userAdmin()))
                .andExpect(status().isOk())
                .andExpect(view().name("editpost"))
                .andExpect(model().attribute("post", hasProperty("id", is(Matchers.equalTo(1L)))))
                .andExpect(model().attribute("edit", is(equalTo(true))));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldDenyEditPostIfNotAdmin() throws Exception {
        mockMvc.perform(get("/posts/1/edit"))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/login"));

        mockMvc.perform(get("/posts/1/edit").with(userBob()))
                .andExpect(status().isForbidden());

        mockMvc.perform(post("/posts/1/edit").with(userBob()).with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldReturnEditPostFormWithErrorsWhenSubmittedInvalidPost() throws Exception {
        mockMvc.perform(post("/posts/1/edit").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isOk())
                .andExpect(model().attributeHasFieldErrors("post", "title"))
                .andExpect(model().attributeHasFieldErrors("post", "text"))
                .andExpect(model().attribute("post", hasProperty("id", is(Matchers.equalTo(1L)))))
                .andExpect(model().attribute("edit", is(equalTo(true))));

        String title = "post title";
        String text = "too short";

        mockMvc.perform(post("/posts/1/edit").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("title", title)
                .param("text", text))
                .andExpect(status().isOk())
                .andExpect(model().attributeHasFieldErrors("post", "text"))
                .andExpect(model().attribute("post", hasProperty("title", Matchers.is(equalTo(title)))))
                .andExpect(model().attribute("post", hasProperty("text", Matchers.is(equalTo(text)))))
                .andExpect(model().attribute("post", hasProperty("id", is(Matchers.equalTo(1L)))))
                .andExpect(model().attribute("edit", is(equalTo(true))));
    }

    @Test
    @ExpectedDatabase(value = "data-post-edited.xml", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)
    @DatabaseTearDown(value = "data.xml", type = DatabaseOperation.TRUNCATE_TABLE) // to reset id sequence, otherwise other tests that insert tags will fail on ExpectedDatabase
    public void shouldEditPosts() throws Exception {
        String title = "edited title";
        String text = "edited Lorem ipsum dolor sit amet, consectetur adipiscing elit";
        String tags = "c, c++, c#";

        mockMvc.perform(post("/posts/1/edit").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("title", title)
                .param("text", text)
                .param("tags", tags))
                .andExpect(status().isFound())
                .andExpect(model().hasNoErrors())
                .andExpect(view().name("redirect:/posts/1"));

        String text2 = "edited short===cut===edited Lorem ipsum dolor sit amet, consectetur adipiscing elit";
        String tags2 = "c++, meow";

        mockMvc.perform(post("/posts/2/edit").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("title", title)
                .param("text", text2)
                .param("tags", tags2))
                .andExpect(status().isFound())
                .andExpect(model().hasNoErrors())
                .andExpect(view().name("redirect:/posts/2"));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldDenyHidePostIfNotAdmin() throws Exception {
        mockMvc.perform(post("/posts/1/hide").with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/login"));

        mockMvc.perform(post("/posts/1/hide").with(userBob()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isForbidden());
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldDenyUnhidePostIfNotAdmin() throws Exception {
        mockMvc.perform(post("/posts/1/unhide").with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/login"));

        mockMvc.perform(post("/posts/1/unhide").with(userBob()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isForbidden());
    }

    @Test
    @ExpectedDatabase("data-post-hidden.xml")
    public void shouldHidePost() throws Exception {
        mockMvc.perform(post("/posts/1/hide").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));

        mockMvc.perform(get("/posts/1"))
                .andExpect(status().isNotFound());

        mockMvc.perform(get("/posts/1").with(userBob()))
                .andExpect(status().isNotFound());

        mockMvc.perform(get("/posts/1").with(userAdmin()))
                .andExpect(status().isOk());
    }

    @Test
    @ExpectedDatabase("data.xml")
    @DatabaseSetup("data-post-hidden.xml")
    public void shouldUnhidePost() throws Exception {
        mockMvc.perform(post("/posts/1/unhide").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }

    @Test
    @ExpectedDatabase("data-post-hidden.xml")
    @DatabaseSetup("data-post-hidden.xml")
    public void shouldNotShowHiddenPostIfNotAdmin() throws Exception {
        mockMvc.perform(get("/").with(userBob()))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(1L))));

        mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(1L))));
    }

    @Test
    @ExpectedDatabase("data-post-hidden.xml")
    @DatabaseSetup("data-post-hidden.xml")
    public void shouldShowHiddenPostIfAdmin() throws Exception {
        mockMvc.perform(get("/").with(userAdmin()))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(2L))));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldDenyDeletePostIfNotAdmin() throws Exception {
        mockMvc.perform(post("/posts/1/delete").with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/login"));

        mockMvc.perform(post("/posts/1/delete").with(userBob()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isForbidden());
    }

    @Test
    @ExpectedDatabase("data-post-deleted.xml")
    public void shouldDeletePost() throws Exception {
        mockMvc.perform(post("/posts/1/delete").with(userAdmin()).with(csrf())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldShowPostsByTag() throws Exception {
        mockMvc.perform(get("/posts?tagged=c++"))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(2L))));

        mockMvc.perform(get("/posts?tagged=meow"))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(1L))))
                .andExpect(model().attribute("postsPage", hasItems(hasProperty("id", equalTo(2L)))));

        mockMvc.perform(get("/posts?tagged=c++, hello world"))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(1L))))
                .andExpect(model().attribute("postsPage", hasItems(hasProperty("id", equalTo(1L)))));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldShowNoPostsWhenTagNotExists() throws Exception {
        mockMvc.perform(get("/posts?tagged=not exists"))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(0L))));
    }

    @Test
    @ExpectedDatabase("data-post-hidden.xml")
    @DatabaseSetup("data-post-hidden.xml")
    public void shouldNotShowHiddenPostsByTagIfNotAdmin() throws Exception {
        mockMvc.perform(get("/posts?tagged=c++"))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(1L))));
    }

    @Test
    @ExpectedDatabase("data-post-hidden.xml")
    @DatabaseSetup("data-post-hidden.xml")
    public void shouldShowHiddenPostsByTagIfAdmin() throws Exception {
        mockMvc.perform(get("/posts?tagged=c++").with(userAdmin()))
                .andExpect(status().isOk())
                .andExpect(view().name("posts"))
                .andExpect(model().attribute("postsPage", hasProperty("totalElements", equalTo(2L))));
    }

    @Test
    @ExpectedDatabase("data-posts-voted.xml")
    public void shouldVote() throws Exception {
        mockMvc.perform(post("/posts/1/like").with(userBob()).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));

        mockMvc.perform(post("/posts/2/dislike").with(userBob()).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }

    @Test
    @ExpectedDatabase("data.xml")
    public void shouldDenyVoteMoreThanOnce() throws Exception {
        mockMvc.perform(post("/posts/1/like").with(userAlice()).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(content().string("already_voted"));

        mockMvc.perform(post("/posts/1/dislike").with(userAlice()).with(csrf()))
                .andExpect(status().isOk())
                .andExpect(content().string("already_voted"));
    }
}