package com.atlassian.stash.internal.task;

import com.atlassian.bitbucket.annotation.Dao;
import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.bitbucket.hamcrest.PageMatchers;
import com.atlassian.bitbucket.task.TaskCount;
import com.atlassian.bitbucket.task.TaskState;
import com.atlassian.stash.internal.AbstractDaoTest;
import com.atlassian.stash.internal.DataSets;
import com.atlassian.stash.internal.comment.CommentDao;
import com.atlassian.stash.internal.comment.HibernateCommentDao;
import com.atlassian.stash.internal.comment.InternalComment;
import com.atlassian.stash.internal.pull.HibernatePullRequestDao;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.pull.PullRequestDao;
import com.atlassian.stash.internal.user.HibernateStashUserDao;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import com.atlassian.stash.internal.user.StashUserDao;
import com.atlassian.bitbucket.task.TaskAnchorType;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequestImpl;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import liquibase.util.StringUtils;
import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;
import org.junit.Test;

import javax.annotation.Nonnull;
import java.util.*;

import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.*;

@DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.PULL_REQUESTS,
          DataSets.COMMENTS, DataSets.TASKS})
public class HibernateTaskDaoTest extends AbstractDaoTest {

    private static final long DATE_WITH_MS = 209606400123L;
    private static final long DATE_WITHOUT_MS = 209606400000L;

    @Dao(HibernateCommentDao.class)
    private CommentDao commentDao;
    @Dao(HibernatePullRequestDao.class)
    private PullRequestDao pullRequestDao;
    @Dao(HibernateStashUserDao.class)
    private StashUserDao userDao;

    @Dao
    private HibernateTaskDao dao;

    @Test
    public void testGetByIdShouldReturnNewlyCreatedTask() {
        InternalApplicationUser author = userDao.loadById(1);
        final InternalComment comment = commentDao.loadById(1L);
        final InternalPullRequest pullRequest = pullRequestDao.loadById(1L);

        String text = "Task with log to text: " + StringUtils.repeat(DESCRIPTION_CHARS, 100);
        final Date date = new Date();
        final TaskState state = TaskState.OPEN;
        InternalTask task = new InternalTask.Builder()
                .anchor(comment)
                .author(author)
                .context(pullRequest)
                .createdDate(date)
                .text(text)
                .state(state)
                .build();
        InternalTask created = dao.create(task);
        assertNotNull(created.getId());
        assertEquals(created.getId(), task.getId());

        flush(); //The session _must_ be flushed before evict or the insert _may_ never happen
        evict(created, created.getId());

        InternalTask loaded = dao.getById(created.getId());
        assertNotNull(loaded);
        assertTask(author, text, date, state, loaded);
        assertEquals("Anchor did not round-trip correctly", comment.getId(), loaded.getAnchor().getId());
        assertEquals("Context did not round-trip correctly", pullRequest.getId(), loaded.getContext().getId());
        dao.delete(loaded);
    }

    @Test
    public void testCreateDateIsStrippedOfMilliseconds() {
        InternalTask task = new InternalTask.Builder()
                .anchor(commentDao.loadById(1L))
                .author(userDao.loadById(1))
                .context(pullRequestDao.loadById(1L))
                .createdDate(new Date(DATE_WITH_MS))
                .text("text")
                .state(TaskState.OPEN)
                .build();
        InternalTask created = dao.create(task);
        long taskId = created.getId();

        flush(); //The session _must_ be flushed before evict or the insert _may_ never happen
        evict(created, taskId);

        InternalTask loaded = dao.getById(taskId);

        assertEquals("Created date includes milliseconds", DATE_WITHOUT_MS, loaded.getCreatedDate().getTime());
    }

    @Test
    public void testDeleteShouldRemoveTaskFromDatabase() {
        InternalTask comment = dao.loadById(2L);
        assertNotNull(comment);
        dao.delete(comment);

        flush();

        assertNull(dao.getById(2L));
    }

    @Test
    public void testGetByIdShouldReturnNewStateAfterUpdate() throws Exception {
        InternalTask initial = dao.getById(2L);

        final String taskText = "Task to increase code coverage";
        final TaskState state = TaskState.RESOLVED;
        InternalTask modified = new InternalTask.Builder(initial)
                .text(taskText)
                .state(state)
                .build();
        dao.update(modified);

        flush();
        evict(modified, modified.getId());

        InternalTask updated = dao.getById(2L);
        assertTask(initial.getAuthor(), taskText, initial.getCreatedDate(), state, updated);
    }

    @Test
    public void testFindAllShouldOrderByCreatedDate() throws Exception {
        final Page<InternalTask> page = dao.findAll(new PageRequestImpl(0, 4));
        assertTrue(page.getIsLastPage());
        assertThat(page, PageMatchers.isPageOf(task(1L), task(2L), task(4L), task(3L)));
    }

    @Test
    public void testFindTasksByContextShouldOrderByStateThenCreateDate() {
        final InternalPullRequest pullRequest = pullRequestDao.loadById(1L);
        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequest)
                .build();
        final Page<InternalTask> page = dao.search(searchCriteria, PageUtils.newRequest(0, 99));
        assertThat(page, PageMatchers.isPageOf(task(2L), task(3L), task(1L)));
    }

    @Test
    public void testFindTasksByContextAndAnchor() {
        final InternalPullRequest pullRequest = pullRequestDao.loadById(1L);
        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequest)
                .anchors(asList(1L), TaskAnchorType.COMMENT)
                .build();
        final Page<InternalTask> page = dao.search(searchCriteria, PageUtils.newRequest(0, 99));
        assertThat(page, PageMatchers.isPageOf(task(2L), task(1L)));
    }

    @Test
    public void testFindTasksByContextAndMultipleAnchors() {
        final InternalPullRequest pullRequest = pullRequestDao.loadById(1L);
        final List<Long> comments = new ArrayList<>();
        comments.add(1L); // comment with 2 tasks
        comments.add(2L); // comment with 0 tasks
        comments.add(3L); // comment with 1 task

        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequest)
                .anchors(comments, TaskAnchorType.COMMENT)
                .build();
        final Page<InternalTask> page = dao.search(searchCriteria, PageUtils.newRequest(0, 99));

        assertThat(page, PageMatchers.isPageOf(task(2L), task(3L), task(1L)));
    }

    @Test
    public void testCountTasksByContext() {
        final InternalPullRequest pullRequest = pullRequestDao.loadById(1L);
        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequest)
                .build();
        final TaskCount count = dao.count(searchCriteria);
        assertEquals(1, count.getCount(TaskState.RESOLVED));
        assertEquals(2, count.getCount(TaskState.OPEN));
    }

    @Test
    public void testCountPullRequestWithoutTasksShouldReturnCount() throws Exception {
        final InternalPullRequest pullRequest = pullRequestDao.loadById(4L);
        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequest)
                .build();
        final TaskCount count = dao.count(searchCriteria);
        assertEquals(0, count.getCount(TaskState.RESOLVED));
        assertEquals(0, count.getCount(TaskState.OPEN));
    }

    @Test
    public void testCountForWithEmptyCollectionReturnsEmptyMap() throws Exception {
        dao.countForContexts(Lists.<InternalTaskContext>newArrayList()).isEmpty();
    }

    @Test
    public void testCountForContextReturnsTaskCounts() throws Exception {
        final InternalPullRequest pullRequest1 = pullRequestDao.loadById(1L);
        final InternalPullRequest pullRequest2 = pullRequestDao.loadById(4L);
        Map<InternalPullRequest, TaskCount> counts = dao.countForContexts(Lists.newArrayList(pullRequest2, pullRequest1));
        assertThat(counts.keySet(), hasSize(2));
        assertEquals(1, counts.get(pullRequest1).getCount(TaskState.RESOLVED));
        assertEquals(2, counts.get(pullRequest1).getCount(TaskState.OPEN));
        assertEquals(0, counts.get(pullRequest2).getCount(TaskState.RESOLVED));
        assertEquals(0, counts.get(pullRequest2).getCount(TaskState.OPEN));
    }

    @Test
    public void testCountForContextWorksWithBatches() throws Exception {
        final InternalPullRequest pullRequest1 = pullRequestDao.loadById(1L);
        final InternalPullRequest pullRequest2 = pullRequestDao.loadById(2L);
        final InternalPullRequest pullRequest3 = pullRequestDao.loadById(3L);
        final InternalPullRequest pullRequest4 = pullRequestDao.loadById(4L);
        Map<InternalPullRequest, TaskCount> counts =
                dao.countForContexts(Lists.newArrayList(pullRequest2, pullRequest3, pullRequest4, pullRequest1));
        assertThat(counts.keySet(), hasSize(4));
        assertEquals(1, counts.get(pullRequest1).getCount(TaskState.RESOLVED));
        assertEquals(2, counts.get(pullRequest1).getCount(TaskState.OPEN));
        assertEquals(0, counts.get(pullRequest2).getCount(TaskState.RESOLVED));
        assertEquals(1, counts.get(pullRequest2).getCount(TaskState.OPEN));
        assertEquals(0, counts.get(pullRequest3).getCount(TaskState.RESOLVED));
        assertEquals(0, counts.get(pullRequest3).getCount(TaskState.OPEN));
        assertEquals(0, counts.get(pullRequest4).getCount(TaskState.RESOLVED));
        assertEquals(0, counts.get(pullRequest4).getCount(TaskState.OPEN));
    }

    @Test
    public void testDeleteForShouldDeleteExpectedTasks() throws Exception {
        dao.deleteForContexts(ImmutableList.of(pullRequestDao.loadById(1L)));

        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequestDao.loadById(1L))
                .build();
        final TaskCount count = dao.count(searchCriteria);
        assertEquals(0, count.getCount(TaskState.RESOLVED));
        assertEquals(0, count.getCount(TaskState.OPEN));
        assertNotNull(dao.getById(4L));
    }

    @Test
    public void testDeleteShouldPaginateTaskContexts() throws Exception {
        dao.deleteForContexts(ImmutableList.of(pullRequestDao.loadById(4L), pullRequestDao.loadById(3L),
                pullRequestDao.loadById(2L), pullRequestDao.loadById(1L)));

        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequestDao.loadById(1L))
                .build();
        final TaskCount count = dao.count(searchCriteria);
        assertEquals(0, count.getCount(TaskState.RESOLVED));
        assertEquals(0, count.getCount(TaskState.OPEN));
    }

    @Test
    public void testDeleteShouldPaginateTaskContexts2() throws Exception {
        dao.deleteForContexts(ImmutableList.of(pullRequestDao.loadById(1L), pullRequestDao.loadById(2L),
                pullRequestDao.loadById(3L), pullRequestDao.loadById(4L)));

        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequestDao.loadById(1L))
                .build();
        final TaskCount count = dao.count(searchCriteria);
        assertEquals(0, count.getCount(TaskState.RESOLVED));
        assertEquals(0, count.getCount(TaskState.OPEN));
    }

    @Test
    public void testDeleteForShouldIgnorePullRequestsWithNoTasks() throws Exception {
        dao.deleteForContexts(ImmutableList.of(pullRequestDao.loadById(4L)));

        // check other tasks are left alone
        final InternalTaskSearchRequest searchCriteria = new InternalTaskSearchRequest.Builder(pullRequestDao.loadById(1L))
                .build();
        final TaskCount count = dao.count(searchCriteria);
        assertEquals(1, count.getCount(TaskState.RESOLVED));
        assertEquals(2, count.getCount(TaskState.OPEN));
    }

    @Test
    public void testHasTasks() throws Exception {
        List<InternalComment> comments = commentDao.getByIds(Arrays.asList(1L, 2L, 3L));
        assertTrue(dao.hasTasks(comments.get(0)));
        assertFalse(dao.hasTasks(comments.get(1)));
        assertTrue(dao.hasTasks(comments.get(2)));
    }

    private Matcher<InternalTask> task(long expectedId) {
        return new FeatureMatcher<InternalTask, Long>(is(expectedId), "id that", "id") {
            @Override
            protected Long featureValueOf(@Nonnull InternalTask actual) {
                return actual.getId();
            }
        };
    }

    private void assertTask(InternalApplicationUser author, String text, Date createDate, TaskState state, InternalTask loaded) {
        assertEquals("Description did not round-trip correctly", text, loaded.getText());
        assertEquals("State did not round-trip correctly", state, loaded.getState());
        assertEquals("Author did not round-trip correctly", author, loaded.getAuthor());
        long createDateDifferenceInMs = Math.abs(createDate.getTime() - loaded.getCreatedDate().getTime());
        assertThat("Create date did not round-trip correctly", createDateDifferenceInMs, is(lessThan(1000L)));
    }

}