package org.iri_research.renkan.test.controller;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;

import org.apache.commons.codec.binary.Hex;
import org.iri_research.renkan.models.Group;
import org.iri_research.renkan.models.Project;
import org.iri_research.renkan.models.Space;
import org.iri_research.renkan.models.User;
import org.iri_research.renkan.repositories.GroupsRepository;
import org.iri_research.renkan.repositories.ProjectsRepository;
import org.iri_research.renkan.repositories.SpacesRepository;
import org.iri_research.renkan.repositories.UsersRepository;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.util.NestedServletException;


@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = { "controller-context.xml",
        "file:src/main/webapp/WEB-INF/spring-servlet.xml" })
public class UsersAdminControllerTest {

    private final static int SPACE_NB = 3;
    private final static int USER_NB = 3;
    private final static int GROUP_NB = 4;

    private Logger logger = LoggerFactory.getLogger(UsersAdminControllerTest.class);

    @Autowired
    private SpacesRepository spacesRepository;
    @Autowired
    private ProjectsRepository projectsRepository;
    @Autowired
    private UsersRepository usersRepository;
    @Autowired
    private GroupsRepository groupsRepository;


    @Autowired
    private PasswordEncoder renkanPasswordEncoder;

    private Map<String, Space> spacesList = new HashMap<String, Space>(SPACE_NB);
    private List<String> spacesUuids = new ArrayList<>(SPACE_NB);

    private ArrayList<Project> testProjects = new ArrayList<Project>();
    
    private Map<String, User> usersList = new HashMap<String, User>(USER_NB);
    private List<String> usersUuids = new ArrayList<>(USER_NB);
    
    private Map<String, Group> groupsList = new HashMap<String, Group>(GROUP_NB);
    private List<String> groupsUuids = new ArrayList<>(GROUP_NB);

    @Autowired
    private WebApplicationContext context;
    private MockMvc mvc;

    @Before
    public void setup() {

        logger.debug("Setup");
        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
        spacesRepository.deleteAll();
        projectsRepository.deleteAll();
        usersRepository.deleteAll();

        for(int i=0; i < USER_NB; i++) {
            String uuid = UUID.randomUUID().toString();
            User user = new User(uuid, "user" + i, "User nb " + i, "http://www.iri.centrepompidou.fr", "#ababab");
            user.setLocked(false);
            user.setEnabled(true);
            user.setAvatar("A pretty picture");
            user.setExpirationDate(new LocalDate());
            user.setCredentialsExpirationDate(new LocalDate());
            user.setEmail(String.format("user%d@mail.com", i));
            user = usersRepository.save(user);
            this.usersUuids.add(uuid);
            this.usersList.put(uuid, user);
        }
        
        for(int i=0; i < GROUP_NB; i++) {
            String uuid = UUID.randomUUID().toString();
            Group group = new Group(uuid, "group" + i, "Group nb " + i, "http://www.iri.centrepompidou.fr/group/"+uuid, "#ababab");
            group.setAvatar("A pretty group picture " + i);
            group = groupsRepository.save(group);
            this.groupsUuids.add(uuid);
            this.groupsList.put(uuid, group);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        
        ArrayList<Project> pl = new ArrayList<Project>();
        for (int i = 0; i < SPACE_NB; i++) {
            DateTime creationDate = new DateTime();
            String uuid = UUID.randomUUID().toString();
            spacesUuids.add(uuid);
            Space testSpace = new Space(uuid, "test " + i, "Test space " + 1,
                    "{}", "http://ldt.iri.centrepompidou.fr", "#ababab",
                    "test_user", "http://ldt.iri.centrepompidou.fr",
                    creationDate);
            testSpace = spacesRepository.save(testSpace);
            this.spacesList.put(uuid, testSpace);
            for (int j = 0; j < SPACE_NB - 1 - i; j++) {
                Project p = new Project(testSpace.getId(), UUID.randomUUID()
                        .toString(), "test" + ((SPACE_NB - 1) * i + j + 1),
                        "desc" + ((SPACE_NB - 1) * i + j + 1),
                        "http://localhost:8080/rest/projects/id"
                                + ((SPACE_NB - 1) * i + j + 1), creationDate);
                p.addUser(this.usersList.get(this.usersUuids.get(0)));
                pl.add(p);
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        for (Project p : projectsRepository.save(pl)) {
            this.testProjects.add(p);
        }
        
        

        this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
    }

    @After
    public void teardown() {
        spacesRepository.deleteAll();
        projectsRepository.deleteAll();
        usersRepository.deleteAll();
    }

    @Test
    public void testUserPostUpdate() throws Exception {

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders
                .post("/admin/users/save");
        post = post.param("id", this.usersUuids.get(0));
        post = post.param("title", "New name");
        post = post.param("description", "New description");
        post = post.param("uri", "http://ldt.iri.centrepompidou.fr/new/uri");
        post = post.param("color", "#ffffff");

        this.mvc.perform(post)
                .andExpect(MockMvcResultMatchers.status().isSeeOther())
                .andExpect(MockMvcResultMatchers.redirectedUrl("/admin/users?version=NULL"));

        User user = this.usersRepository.findOne(this.usersUuids.get(0));

        Assert.assertNotNull("Should find user", user);
        Assert.assertEquals("Title equals", "New name", user.getTitle());
        Assert.assertEquals("Description equals", "New description",
                user.getDescription());
        Assert.assertEquals("Uri equals",
                "http://ldt.iri.centrepompidou.fr/new/uri", user.getUri());
        Assert.assertEquals("Color equals", "#ffffff", user.getColor());
    }

    @Test
    public void testUserPostCreate() throws Exception {

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders
                .post("/admin/users/save")
                    .param("title", "New name")
                    .param("description", "New description")
                    .param("uri", "http://ldt.iri.centrepompidou.fr/new/uri")
                    .param("color", "#ffffff")
                    .param("expirationDate","2007-11-24")
                    .param("credentialsExpirationDate","2009-11-29")
                    .param("password", "test")
                    .param("passwordConfirm", "test");

        this.mvc.perform(post)
                .andExpect(MockMvcResultMatchers.status().isSeeOther())
                .andExpect(MockMvcResultMatchers.redirectedUrl("/admin/users?version=NULL"));

        Assert.assertEquals("Must have one more space", USER_NB + 1,
                this.usersRepository.count());

        for (User user : this.usersRepository.findAll()) {
            if (this.usersList.containsKey(user.getId())) {
                continue;
            }
            else {
                Assert.assertNotNull("Should find user", user);
                Assert.assertEquals("Title equals", "New name", user.getTitle());
                Assert.assertEquals("Description equals", "New description",
                        user.getDescription());
                Assert.assertEquals("Uri equals",
                        "http://ldt.iri.centrepompidou.fr/new/uri", user.getUri());
                Assert.assertEquals("Color equals", "#ffffff", user.getColor());
                Assert.assertTrue(
                        "id sould match uuid regex",
                        user.getId()
                                .matches(
                                        "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"));
                Assert.assertTrue("password must match \"test\"", renkanPasswordEncoder.matches("test", user.getPassword()));
                Assert.assertEquals(new LocalDate(2007, 11, 24), user.getExpirationDate());
            }
        }
    }

    @Test
    public void testUserPostUpdateEmptyTitle() throws Exception {

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders
                .post("/admin/users/save")
                .param("id", this.usersUuids.get(0))
                .param("title", "")
                .param("description", "New description")
                .param("uri", "http://ldt.iri.centrepompidou.fr/new/uri")
                .param("color", "#ffffff")
                .param("expirationDate","2007-11-24")
                .param("credentialsExpirationDate","2009-11-29")
                .param("password", "test")
                .param("passwordConfirm", "test");


        this.mvc.perform(post)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.view().name("admin/userEdit"))
                .andExpect(MockMvcResultMatchers.model().hasErrors())
                .andExpect(MockMvcResultMatchers.model().errorCount(1))
                .andExpect(MockMvcResultMatchers.model().attributeHasErrors("user"))
                .andExpect(MockMvcResultMatchers.model().attributeHasFieldErrors("user", "title"));

        User user = this.usersRepository.findOne(this.usersUuids.get(0));

        Assert.assertNotNull("Should find user", user);
        Assert.assertEquals("name equals", "user0", user.getTitle());
        
        Assert.assertEquals("name equals", "User nb 0", user.getDescription());

    }

    @Test
    public void testUserPostCreateEmptyName() throws Exception {

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders
                .post("/admin/users/save")
                .param("title", "")
                .param("description", "New description")
                .param("uri", "http://ldt.iri.centrepompidou.fr/new/uri")
                .param("color", "#ffffff")
                .param("expirationDate","2007-11-24")
                .param("credentialsExpirationDate","2009-11-29")
                .param("password", "test")
                .param("passwordConfirm", "test");

        
        this.mvc.perform(post)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.view().name("admin/userEdit"))
                .andExpect(MockMvcResultMatchers.model().hasErrors())
                .andExpect(MockMvcResultMatchers.model().errorCount(1))
                .andExpect(MockMvcResultMatchers.model().attributeHasErrors("user"))
                .andExpect(MockMvcResultMatchers.model().attributeHasFieldErrors("user", "title"));

        Assert.assertEquals("Must not have one more user", USER_NB,this.usersRepository.count());

    }

    @Test
    public void testUserPostCreateBadPassword() throws Exception {

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders
                .post("/admin/users/save")
                .param("title", "user")
                .param("description", "New description")
                .param("uri", "http://ldt.iri.centrepompidou.fr/new/uri")
                .param("color", "#ffffff")
                .param("expirationDate","2007-11-24")
                .param("credentialsExpirationDate","2009-11-29")
                .param("password", "test")
                .param("passwordConfirm", "test2");

        this.mvc.perform(post)
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.view().name("admin/userEdit"))
            .andExpect(MockMvcResultMatchers.model().hasErrors())
            .andExpect(MockMvcResultMatchers.model().errorCount(1))
            .andExpect(MockMvcResultMatchers.model().attributeHasErrors("user"))
            .andExpect(MockMvcResultMatchers.model().attributeHasFieldErrors("user", "password"));
        
        Assert.assertEquals("Must not have one more user", USER_NB,this.usersRepository.count());

    }

    @Test
    public void testDeleteUser() throws Exception {

        MockHttpServletRequestBuilder get = MockMvcRequestBuilders
                .get("/admin/users/delete/"
                        + this.usersUuids.get(USER_NB - 1));

        MvcResult res = this.mvc
                .perform(get)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(
                        MockMvcResultMatchers.view().name(
                                "admin/userDeleteConfirm"))
                .andExpect(
                        MockMvcResultMatchers.model().attributeExists(
                                "userObj", "key", "salt")).andReturn();

        Map<String, Object> model = res.getModelAndView().getModel();

        User user = (User) model.get("userObj");
        Assert.assertNotNull("User is not null", user);
        Assert.assertEquals("Must be first user id",
                this.usersUuids.get(USER_NB - 1), user.getId());

        String key = (String) model.get("key");
        Assert.assertNotNull("key is not null", key);

        String salt = (String) model.get("salt");
        Assert.assertNotNull("salt is not null", salt);

        Assert.assertTrue("Key must be checked", user.checkKey(key, salt));

    }

    @Test
    public void testDeleteFakeUser() throws Exception {

        MockHttpServletRequestBuilder get = MockMvcRequestBuilders
                .get("/admin/users/delete/" + UUID.randomUUID().toString());

        try {
            this.mvc.perform(get).andExpect(
                    MockMvcResultMatchers.status().isNotFound());
        } catch (NestedServletException e) {
            Assert.assertNotNull("Nested exception must not be null",
                    e.getCause());
            Assert.assertEquals(
                    "Inner exception must be a HttpClientErrorException",
                    HttpClientErrorException.class, e.getCause().getClass());
            Assert.assertEquals("Exception error status must be not found",
                    HttpStatus.NOT_FOUND,
                    ((HttpClientErrorException) e.getCause()).getStatusCode());
        }

    }

    @Test
    public void testDeleteUserProject() throws Exception {

        MockHttpServletRequestBuilder get = MockMvcRequestBuilders
                .get("/admin/users/delete/" + this.usersUuids.get(0));

        try {
            this.mvc.perform(get).andExpect(
                    MockMvcResultMatchers.status().isBadRequest());
        } catch (NestedServletException e) {
            Assert.assertNotNull("Nested exception must not be null",
                    e.getCause());
            Assert.assertEquals(
                    "Inner exception must be a HttpClientErrorException",
                    HttpClientErrorException.class, e.getCause().getClass());
            Assert.assertEquals("Exception error status must be not found",
                    HttpStatus.BAD_REQUEST,
                    ((HttpClientErrorException) e.getCause()).getStatusCode());
        }

    }

    @Test
    public void testDoDeleteUserNoKey() throws Exception {
        MockHttpServletRequestBuilder post = MockMvcRequestBuilders
                .post("/admin/users/delete/"
                        + this.usersUuids.get(USER_NB - 1));

        try {
            this.mvc.perform(post).andExpect(
                    MockMvcResultMatchers.status().isBadRequest());
        } catch (NestedServletException e) {
            Assert.assertNotNull("Nested exception must not be null",
                    e.getCause());
            Assert.assertEquals(
                    "Inner exception must be a HttpClientErrorException",
                    HttpClientErrorException.class, e.getCause().getClass());
            Assert.assertEquals("Exception error status must be not found",
                    HttpStatus.BAD_REQUEST,
                    ((HttpClientErrorException) e.getCause()).getStatusCode());
        }

        Assert.assertEquals("Must have same nb of user", USER_NB,
                this.usersRepository.count());

    }

    @Test
    public void testDoDeleteUser() throws Exception {

        User user = this.usersList.get(this.usersUuids.get(USER_NB - 1));

        SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
        rand.setSeed(System.currentTimeMillis());
        byte[] rawSalt = new byte[50];
        rand.nextBytes(rawSalt);
        String salt = Hex.encodeHexString(rawSalt);
        String key = user.getKey(salt);

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String
                .format("/admin/users/delete/%s?key=%s&salt=%s",
                        this.usersUuids.get(USER_NB - 1), key, salt));

        this.mvc.perform(post)
                .andExpect(MockMvcResultMatchers.status().isSeeOther())
                .andExpect(MockMvcResultMatchers.redirectedUrl("/admin/users?version=NULL"));

        Assert.assertEquals("Must have one less user", USER_NB - 1,
                this.usersRepository.count());

        user = this.usersRepository.findOne(this.usersUuids
                .get(USER_NB - 1));

        Assert.assertNull("User " + this.usersUuids.get(USER_NB - 1)
                + " deleted", user);

    }

    @Test
    public void testDoDeleteSpaceFake() throws Exception {

        User user = this.usersList.get(this.usersUuids.get(USER_NB - 1));

        SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
        rand.setSeed(System.currentTimeMillis());
        byte[] rawSalt = new byte[50];
        rand.nextBytes(rawSalt);
        String salt = Hex.encodeHexString(rawSalt);
        String key = user.getKey(salt);

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String
                .format("/admin/users/delete/%s?key=%s&salt=%s",
                        UUID.randomUUID(), key, salt));

        this.mvc.perform(post)
                .andExpect(MockMvcResultMatchers.status().isSeeOther())
                .andExpect(MockMvcResultMatchers.redirectedUrl("/admin/users?version=NULL"));

        Assert.assertEquals("Must have the same nb of user", USER_NB,
                this.usersRepository.count());

    }

    @Test
    public void testDoDeleteUserProject() throws Exception {

        User user = this.usersList.get(this.usersUuids.get(0));

        SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
        rand.setSeed(System.currentTimeMillis());
        byte[] rawSalt = new byte[50];
        rand.nextBytes(rawSalt);
        String salt = Hex.encodeHexString(rawSalt);
        String key = user.getKey(salt);

        MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String
                .format("/admin/users/delete/%s?key=%s&salt=%s",
                        this.usersUuids.get(0), key, salt));

        try {
            this.mvc.perform(post).andExpect(
                    MockMvcResultMatchers.status().isBadRequest());
        } catch (NestedServletException e) {
            Assert.assertNotNull("Nested exception must not be null",
                    e.getCause());
            Assert.assertEquals(
                    "Inner exception must be a HttpClientErrorException",
                    HttpClientErrorException.class, e.getCause().getClass());
            Assert.assertEquals("Exception error status must be not found",
                    HttpStatus.BAD_REQUEST,
                    ((HttpClientErrorException) e.getCause()).getStatusCode());
        }

        Assert.assertEquals("Must have the same nb of user", USER_NB,
                this.usersRepository.count());

    }
    
    @Test
    public void testUpdateUserGroups() throws Exception {
        
        MockHttpServletRequestBuilder post = MockMvcRequestBuilders
                .post("/admin/users/save");
        post = post.param("id", this.usersUuids.get(0));
        post = post.param("title", "New name");
        post = post.param("description", "New description");
        post = post.param("uri", "http://ldt.iri.centrepompidou.fr/new/uri");
        post = post.param("color", "#ffffff");
        post = post.param("groups", this.groupsUuids.toArray(new String[UsersAdminControllerTest.GROUP_NB]));

        this.mvc.perform(post)
                .andExpect(MockMvcResultMatchers.status().isSeeOther())
                .andExpect(MockMvcResultMatchers.redirectedUrl("/admin/users?version=NULL"));

        User user = this.usersRepository.findOne(this.usersUuids.get(0));

        Assert.assertNotNull("Should find user", user);
        Assert.assertEquals("Title equals", "New name", user.getTitle());
        Assert.assertEquals("Description equals", "New description",
                user.getDescription());
        Assert.assertEquals("Uri equals",
                "http://ldt.iri.centrepompidou.fr/new/uri", user.getUri());
        Assert.assertEquals("Color equals", "#ffffff", user.getColor());

        Assert.assertNotNull("Groups must be set", user.getGroups());
        Assert.assertEquals("all users must be set", GROUP_NB, user.getGroups().size());
        Assert.assertTrue("users list must contains all users", this.groupsUuids.containsAll(user.getGroups()));

    }

}
