package org.iri_research.renkan.test.repositories;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;

import org.iri_research.renkan.models.Edge;
import org.iri_research.renkan.models.EdgeStyle;
import org.iri_research.renkan.models.Node;
import org.iri_research.renkan.models.NodeStyle;
import org.iri_research.renkan.models.Project;
import org.iri_research.renkan.models.ProjectRevision;
import org.iri_research.renkan.models.Space;
import org.iri_research.renkan.models.User;
import org.iri_research.renkan.repositories.EdgesRepository;
import org.iri_research.renkan.repositories.NodesRepository;
import org.iri_research.renkan.repositories.ProjectRevisionsRepository;
import org.iri_research.renkan.repositories.ProjectsRepository;
import org.iri_research.renkan.repositories.SpacesRepository;
import org.iri_research.renkan.repositories.UsersRepository;
import org.iri_research.renkan.utils.ColorGenerator;
import org.joda.time.DateTime;
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.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.geo.Point;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.DBRef;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("repositories-context.xml")
public class ProjectsRepositoryTest {

    private final static int SPACE_NB = 3;

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

    @Autowired
    private ProjectsRepository projectsRepository;

    @Autowired
    private SpacesRepository spacesRepository;

    @Autowired
    private NodesRepository nodesRepository;

    @Autowired
    private EdgesRepository edgesRepository;

    @Autowired
    private UsersRepository usersRepository;

    @Autowired
    private ProjectRevisionsRepository projectRevisionsRepository;

    @Autowired
    private MongoTemplate mongoTemplate;

    private ArrayList<Project> testProjects = new ArrayList<Project>();
    private List<Node> testNodes = new ArrayList<>();
    private List<Edge> testEdges = new ArrayList<>();
    private List<User> testUsers = new ArrayList<>();

    private DateTime creationDate = new DateTime();

    private List<String> spaceIds = new ArrayList<>();

    private Project copyProject;

    public ProjectsRepositoryTest() {
    }

    @Before
    public void setup() {

        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
        logger.debug("Setup");

        // CREATE USERS
        for (int i = 0; i < SPACE_NB + 1; i++) {
            this.testUsers.add(new User(UUID.randomUUID().toString(),
                    "test_user" + (i + 1), "Test user " + (i + 1),
                    "http://www.iri.centrepompidou.fr/users/test_user"
                            + (i + 1), "#" + ColorGenerator.randomColorHex()));
        }
        usersRepository.save(this.testUsers);

        ArrayList<Project> pl = new ArrayList<Project>();
        Space testSpace = null;
        for (int i = 0; i < SPACE_NB; i++) {
            spaceIds.add(UUID.randomUUID().toString());
            testSpace = new Space(this.spaceIds.get(i), "test space " + i,
                    "Test space " + i, null, null, null, "test_user", null,
                    this.creationDate);
            testSpace = spacesRepository.save(testSpace);
            for (int j = 0; j < SPACE_NB - 1 - i; j++) {
                final Project project = new Project(testSpace.getId(), UUID
                        .randomUUID().toString(), "test project "
                        + ((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),
                        this.creationDate);
                pl.add(project);
            }
        }

        for (int i = 0; i < testUsers.size() - 1; i++) {
            User u = this.testUsers.get(i);
            for (int j = 0; j < pl.size(); j++) {
                Project p = pl.get(j);
                if (j < pl.size() - i) {
                    p.addUser(u);
                } else {
                    p.addUser(null, null);
                }

            }
        }

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

        copyProject = this.testProjects.get(0);

        for (int i = 0; i < 3; i++) {
            Node node = new Node("Node" + i, "Node" + i, "Node " + i,
                    "http://renkan.org/nodes/node" + i, new NodeStyle("#ffff0" + i, false, 1),
                    "test_user", new Point(0, i),
                    "http://renkan.org/images/node" + i, i, "circle", "Node",
                    false, copyProject.getId());
            node = this.nodesRepository.save(node);
            copyProject.getNodes().add(node);
            this.testNodes.add(node);
        }
        for (int i = 0; i < 3; i++) {
            Edge edge = new Edge("Node" + i, "Node" + i, "Node " + i,
                    "http://renkan.org/edges/edge" + i, new EdgeStyle("#ffff0" + i, false, 1, true),
                    this.testNodes.get((i + 2) % 3), this.testNodes.get(i),
                    "test_user", copyProject.getId());
            edge = this.edgesRepository.save(edge);
            copyProject.getEdges().add(edge);
            this.testEdges.add(edge);
        }

        this.projectsRepository.save(copyProject);

    }

    @After
    public void teardown() {
        logger.debug("Teardown");
        edgesRepository.deleteAll();
        nodesRepository.deleteAll();
        projectRevisionsRepository.deleteAll();
        projectsRepository.deleteAll();
        spacesRepository.deleteAll();
        usersRepository.deleteAll();
    }

    @Test
    public void testInitialRevCounter() {

        Project p = projectsRepository
                .findOne(this.testProjects.get(0).getId());

        Assert.assertEquals("Initial rev counter should be 1", 1,
                p.getRevCounter());
    }

    @Test
    public void testIncrementRevCounter() {

        int revCounter = projectsRepository.getRevCounter(this.testProjects
                .get(0).getId());
        Assert.assertEquals("After 1 call rev counter should be 1", 1,
                revCounter);
        revCounter = projectsRepository.getRevCounter(this.testProjects.get(0)
                .getId());
        Assert.assertEquals("After 2 calls rev counter should be 2", 2,
                revCounter);

        for (int i = 0; i < 10; i++) {
            revCounter = projectsRepository.getRevCounter(this.testProjects
                    .get(0).getId());
        }

        Assert.assertEquals("After 10 more calls rev counter should be 12", 12,
                revCounter);

        Project p = projectsRepository
                .findOne(this.testProjects.get(0).getId());

        Assert.assertEquals("next rev counter should be 13", 13,
                p.getRevCounter());

        p = projectsRepository.findOne(this.testProjects.get(1).getId());

        Assert.assertEquals("other project next rev counter should be 1", 1,
                p.getRevCounter());

    }

    @Test
    public void testIncrementNonExisting() {

        int revCounter = projectsRepository.getRevCounter("aaaa");
        Assert.assertEquals("Rev counter non existing == -1", -1, revCounter);

    }

    @Test
    public void testIncrementNull() {

        int revCounter = projectsRepository.getRevCounter(null);
        Assert.assertEquals("Rev counter null == -1", -1, revCounter);

    }

    @Test
    public void testIncrementEmpty() {

        int revCounter = projectsRepository.getRevCounter("");
        Assert.assertEquals("Rev counter empty == -1", -1, revCounter);

    }

    @Test
    public void testDateCreation() {

        for (Project p : projectsRepository.findAll()) {
            Assert.assertEquals(this.creationDate, p.getCreated());
        }
    }

    @Test
    public void testGetCountBySpace() {

        Map<String, Integer> groupRes = projectsRepository.getCountBySpace();

        Assert.assertNotNull("GroupRes not null", groupRes);
        Assert.assertEquals("Group res size", SPACE_NB - 1, groupRes.size());

        for (int i = 0; i < (SPACE_NB - 1); i++) {
            Integer count = groupRes.get(this.spaceIds.get(i));
            Assert.assertNotNull("count not null", count);
            Assert.assertEquals("Nb of project/space", 2 - i, count.intValue());
        }

        Assert.assertNull("Last space id has no project i.e count is null",
                groupRes.get(this.spaceIds.get(SPACE_NB - 1)));
    }

    @Test
    public void testGetCountBySpaceFilter() {

        List<String> spacesIdsFilter = Arrays.asList(this.spaceIds.get(0));

        Map<String, Integer> groupRes = projectsRepository
                .getCountBySpace(spacesIdsFilter);

        Assert.assertNotNull("GroupRes not null", groupRes);
        Assert.assertEquals("Group res size", 1, groupRes.size());

        Integer count = groupRes.get(this.spaceIds.get(0));
        Assert.assertNotNull("count not null", count);
        Assert.assertEquals("Nb of project/space", 2, count.intValue());

        for (int i = 1; i < SPACE_NB; i++) {
            Assert.assertNull(
                    "other space id has no project i.e count is null",
                    groupRes.get(this.spaceIds.get(i)));
        }
    }

    @Test
    public void testGetCountByUser() {

        Map<String, Integer> groupRes = projectsRepository.getCountByUser();
        int nu = this.testUsers.size();

        Assert.assertNotNull("GroupRes not null", groupRes);
        Assert.assertEquals("Group res size", nu, groupRes.size());

        for (int i = 0; i < nu - 1; i++) {
            Integer count = groupRes.get(this.testUsers.get(i).getId());
            Assert.assertNotNull("count not null", count);
            Assert.assertEquals("Nb of user/project", testProjects.size() - i,
                    count.intValue());
        }
        Assert.assertNull("Last user id has no project i.e count is null",
                groupRes.get(this.testUsers.get(nu - 1).getId()));

        Integer count = groupRes.get(null);
        Assert.assertNotNull("count not null", count);
        Assert.assertEquals("Nb of anonymous user/project", (nu - 1) * (nu - 2)
                / 2, count.intValue());
    }

    @Test
    public void testGetCountByUserFilter() {

        List<String> userIdsFilter = Arrays.asList(this.testUsers.get(0)
                .getId());

        Map<String, Integer> groupRes = projectsRepository
                .getCountByUser(userIdsFilter);

        Assert.assertNotNull("GroupRes not null", groupRes);
        Assert.assertEquals("Group res size", 1, groupRes.size());

        Integer count = groupRes.get(this.testUsers.get(0).getId());
        Assert.assertNotNull("count not null", count);
        Assert.assertEquals("Nb of user/project", testProjects.size(),
                count.intValue());

    }

    @Test
    public void testGetCountByUsername() {

        Map<String, Integer> groupRes = projectsRepository.getCountByUsername();
        int nu = this.testUsers.size() - 1;
        int nb_username = this.testUsers.size() + (SPACE_NB - 1) * SPACE_NB / 2
                - 2;

        Assert.assertNotNull("GroupRes not null", groupRes);
        Assert.assertEquals("Group res size", nb_username, groupRes.size());

        for (int i = 0; i < nu - 1; i++) {
            Integer count = groupRes.get(this.testUsers.get(i).getUsername());
            Assert.assertNotNull("count not null", count);
            Assert.assertEquals("Nb of user/project", testProjects.size() - i,
                    count.intValue());
        }
        Assert.assertNull("Last user id has no project i.e count is null",
                groupRes.get(this.testUsers.get(nu).getUsername()));

        for (int i = 1; i < nu; i++) {
            String anonymous_username = String.format("Anonymous-%d", i);
            Integer count = groupRes.get(anonymous_username);
            Assert.assertEquals("Nb of anonymous user/project", nu - i,
                    count.intValue());
        }

    }

    @Test
    public void testGetCountByUsernameFilter() {

        List<String> usernamesFilter = Arrays.asList(this.testUsers.get(0)
                .getUsername());

        Map<String, Integer> groupRes = projectsRepository
                .getCountByUsername(usernamesFilter);

        Assert.assertNotNull("GroupRes not null", groupRes);
        Assert.assertEquals("Group res size", 1, groupRes.size());

        Integer count = groupRes.get(this.testUsers.get(0).getUsername());
        Assert.assertNotNull("count not null", count);
        Assert.assertEquals("Nb of user/project", testProjects.size(),
                count.intValue());

    }

    @Test
    public void testCopyProjectCreation() {
        DBCollection coll = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(Project.class));

        DBObject filter = new BasicDBObject();
        filter.put("_id", copyProject.getId());

        DBCursor resFind = coll.find(filter);

        Assert.assertEquals("The project must be found", 1, resFind.count());

        for (DBObject obj : coll.find(filter)) {
            Assert.assertEquals("id must be equal", copyProject.getId(),
                    obj.get("_id"));
        }
    }

    @Test
    public void testCopyProjectCopy() {

        Project newProject = this.projectsRepository.copy(this.copyProject,
                "copy_project");

        DBCollection coll = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(Project.class));

        DBObject filter = new BasicDBObject();
        filter.put("_id", newProject.getId());

        DBCursor resFind = coll.find(filter);

        Assert.assertEquals("The project must be found", 1, resFind.count());

        for (DBObject obj : resFind) {
            Assert.assertEquals("id must be equal", newProject.getId(),
                    obj.get("_id"));
            Assert.assertEquals("title must be must be copy_project",
                    "copy_project", obj.get("title"));
            Assert.assertEquals(
                    "space_id must be must be same than copyProject",
                    this.copyProject.getSpaceId(), obj.get("space_id"));
        }

        filter = new BasicDBObject();
        filter.put("project_id", newProject.getId());

        DBCollection nodesColl = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(Node.class));
        resFind = nodesColl.find(filter);

        Assert.assertEquals("Must have 3 nodes", this.copyProject.getNodes()
                .size(), resFind.count());

        for (DBObject obj : resFind) {
            // find node with same title in copy project
            Node originalNode = null;
            for (Node n : this.copyProject.getNodes()) {
                if (n.getTitle().equals(obj.get("title"))) {
                    originalNode = n;
                    break;
                }
            }

            Assert.assertNotNull("Must fincd original Node", originalNode);

            // color, desc, id diff, uri, created_by, image, position, diff
            // projectif, size
            Assert.assertNotSame("id must be differents", originalNode.getId(),
                    obj.get("_id"));
            Assert.assertNotSame("project id must be differents",
                    originalNode.getProjectId(), obj.get("project_id"));

            DBObject style = (BasicDBObject)obj.get("style");

            Assert.assertEquals("same color", originalNode.getStyle().getColor(),
                    style.get("color"));
            Assert.assertEquals("same desc", originalNode.getDescription(),
                    obj.get("description"));
            Assert.assertEquals("same uri", originalNode.getUri(),
                    obj.get("uri"));
            Assert.assertEquals("same image", originalNode.getImage(),
                    obj.get("image"));
            Assert.assertEquals("same position", originalNode.getPosition()
                    .getX(), ((DBObject) obj.get("position")).get("x"));
            Assert.assertEquals("same position", originalNode.getPosition()
                    .getY(), ((DBObject) obj.get("position")).get("y"));
            Assert.assertEquals("same size", originalNode.getSize(),
                    obj.get("size"));

        }

        DBCollection edgesColl = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(Edge.class));
        resFind = edgesColl.find(filter);

        Assert.assertEquals("Must have 3 edges", this.copyProject.getEdges()
                .size(), resFind.count());

        for (DBObject obj : resFind) {
            // find node with same title in copy project
            Edge originalEdge = null;
            for (Edge e : this.copyProject.getEdges()) {
                if (e.getTitle().equals(obj.get("title"))) {
                    originalEdge = e;
                    break;
                }
            }

            Assert.assertNotNull("Must find original Node", originalEdge);

            // color, desc, id diff, uri, created_by, image, position, diff
            // projectif, size
            Assert.assertFalse("id must be differents", originalEdge.getId()
                    .equals(obj.get("_id")));
            Assert.assertFalse("project id must be differents", originalEdge
                    .getProjectId().equals(obj.get("project_id")));

            DBObject style = (BasicDBObject)obj.get("style");
            Assert.assertEquals("same color", originalEdge.getStyle().getColor(),
                    style.get("color"));
            Assert.assertEquals("same desc", originalEdge.getDescription(),
                    obj.get("description"));
            Assert.assertEquals("same uri", originalEdge.getUri(),
                    obj.get("uri"));

            // get FromNode
            DBObject fromNode = nodesColl.findOne(((DBRef) obj.get("from"))
                    .getId());
            Assert.assertNotNull("fromNode must exits ", fromNode);
            Assert.assertFalse("must be different from node", originalEdge
                    .getFrom().equals(fromNode.get("_id")));
            Assert.assertEquals("same from title", originalEdge.getFromNode()
                    .getTitle(), fromNode.get("title"));

            DBObject toNode = nodesColl
                    .findOne(((DBRef) obj.get("to")).getId());
            Assert.assertNotNull("toNode must exits", toNode);
            Assert.assertFalse("must be different to node", originalEdge
                    .getTo().equals(toNode.get("_id")));
            Assert.assertEquals("same to title", originalEdge.getToNode()
                    .getTitle(), toNode.get("title"));

        }
    }

    @Test
    public void testDeleteRecursive() {

        List<Node> nodes = copyProject.getNodes();
        List<Edge> edges = copyProject.getEdges();

        this.projectsRepository.deleteRecursive(copyProject);

        DBObject filter = new BasicDBObject();
        filter.put("_id", copyProject.getId());

        DBCollection coll = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(Project.class));
        DBCursor resFind = coll.find(filter);

        Assert.assertEquals("The project must not be found", 0, resFind.count());

        filter = new BasicDBObject();
        filter.put("project_id", copyProject.getId());

        DBCollection nodesColl = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(Node.class));
        resFind = nodesColl.find(filter);

        Assert.assertEquals("no nodes left", 0, resFind.count());

        for (Node n : nodes) {
            DBObject nobj = nodesColl.findOne(n.getId());
            Assert.assertNull("node should be deleted", nobj);
        }

        DBCollection edgesColl = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(Edge.class));
        resFind = edgesColl.find(filter);

        Assert.assertEquals("no edges left", 0, resFind.count());

        for (Edge e : edges) {
            DBObject eobj = edgesColl.findOne(e.getId());
            Assert.assertNull("edge should be deleted", eobj);
        }

    }

    @SuppressWarnings("unchecked")
    @Test
    public void testDeleteRecursiveProjectRevision() {

        List<Node> nodes = copyProject.getNodes();
        List<Edge> edges = copyProject.getEdges();

        int revCounter = copyProject.getRevCounter();

        this.projectsRepository.deleteRecursive(copyProject);

        DBCollection coll = mongoTemplate.getCollection(mongoTemplate
                .getCollectionName(ProjectRevision.class));

        DBObject filter = new BasicDBObject();
        filter.put("project", new DBRef(mongoTemplate.getDb(), "projects",
                copyProject.getId()));
        filter.put("revision", new Integer(revCounter));

        DBObject revision = coll.findOne(filter);

        Assert.assertNotNull("Must find revision", revision);

        for (DBObject nobj : ((Iterable<DBObject>) revision.get("nodes"))) {
            Node node = null;
            for (Node n : nodes) {
                if (n.getId().equals(nobj.get("_id"))) {
                    node = n;
                    break;
                }
            }
            Assert.assertNotNull("Node must be in the original list", node);
            Assert.assertEquals("Nodes must have same title", node.getTitle(),
                    nobj.get("title"));
        }

        for (DBObject eobj : ((Iterable<DBObject>) revision.get("edges"))) {
            Edge edge = null;
            for (Edge e : edges) {
                if (e.getId().equals(eobj.get("_id"))) {
                    edge = e;
                    break;
                }
            }
            Assert.assertNotNull("Edge must be in the original list", edge);
            Assert.assertEquals("Edges must have same title", edge.getTitle(),
                    eobj.get("title"));
        }

    }

    @Test
    public void testFindBySpaceIdAndTitleRegex() {

        List<Project> res = this.projectsRepository.findBySpaceIdAndTitleRegex(
                this.spaceIds.get(0), ".*Project.*");

        Assert.assertEquals("res must have length " + (SPACE_NB - 1),
                SPACE_NB - 1, res.size());
        for (Project p : res) {
            Assert.assertEquals("project must belong to the same space",
                    this.spaceIds.get(0), p.getSpaceId());
            Assert.assertNotNull("project title must not be null", p.getTitle());
            Assert.assertTrue("project tilte must contains title", p.getTitle()
                    .matches(".*project.*"));
        }
    }

    @Test
    public void testFindBySpaceIdAndTitleRegexBad() {

        List<Project> res = this.projectsRepository.findBySpaceIdAndTitleRegex(
                this.spaceIds.get(0), ".*foo.*");

        Assert.assertEquals("res must have zero length", 0, res.size());
    }

    @Test
    public void testFindBySpaceIdAndTitleRegexPageable() {

        PageRequest pr = new PageRequest(0, 1);

        Page<Project> res = this.projectsRepository.findBySpaceIdAndTitleRegex(
                this.spaceIds.get(0), ".*Project.*", pr);

        Assert.assertEquals("res must have length 1", 1,
                res.getNumberOfElements());
        for (Project p : res) {
            Assert.assertEquals("project must belong to the same space",
                    this.spaceIds.get(0), p.getSpaceId());
            Assert.assertNotNull("project title must not be null", p.getTitle());
            Assert.assertTrue("project tilte must contains title", p.getTitle()
                    .matches(".*project.*"));
        }
    }

    @Test
    public void testFindBySpaceIdAndTitleRegexPageableBadSize() {

        PageRequest pr = new PageRequest(0, 3);

        Page<Project> res = this.projectsRepository.findBySpaceIdAndTitleRegex(
                this.spaceIds.get(0), ".*Project.*", pr);

        Assert.assertEquals("res must have length 2", 2,
                res.getNumberOfElements());
        for (Project p : res) {
            Assert.assertEquals("project must belong to the same space",
                    this.spaceIds.get(0), p.getSpaceId());
            Assert.assertNotNull("project title must not be null", p.getTitle());
            Assert.assertTrue("project tilte must contains title", p.getTitle()
                    .matches(".*project.*"));
        }
    }

    @Test
    public void testFindBySpaceIdAndTitleRegexPageableBadPage() {

        PageRequest pr = new PageRequest(1, 3);

        Page<Project> res = this.projectsRepository.findBySpaceIdAndTitleRegex(
                this.spaceIds.get(0), ".*Project.*", pr);

        Assert.assertEquals("res must have length 0", 0,
                res.getNumberOfElements());
    }
}
