text:
package org.innovateuk.ifs.project.spendprofile.transactional;

import org.innovateuk.ifs.BaseServiceUnitTest;
import org.innovateuk.ifs.application.domain.Application;
import org.innovateuk.ifs.commons.error.CommonFailureKeys;
import org.innovateuk.ifs.commons.error.Error;
import org.innovateuk.ifs.commons.error.ValidationMessages;
import org.innovateuk.ifs.commons.service.ServiceResult;
import org.innovateuk.ifs.competition.domain.Competition;
import org.innovateuk.ifs.competition.resource.FundingRules;
import org.innovateuk.ifs.form.domain.Question;
import org.innovateuk.ifs.notifications.resource.*;
import org.innovateuk.ifs.notifications.service.NotificationService;
import org.innovateuk.ifs.organisation.domain.Organisation;
import org.innovateuk.ifs.organisation.domain.OrganisationType;
import org.innovateuk.ifs.organisation.repository.OrganisationRepository;
import org.innovateuk.ifs.organisation.resource.OrganisationTypeEnum;
import org.innovateuk.ifs.project.core.ProjectParticipantRole;
import org.innovateuk.ifs.project.core.builder.ProjectBuilder;
import org.innovateuk.ifs.project.core.domain.PartnerOrganisation;
import org.innovateuk.ifs.project.core.domain.Project;
import org.innovateuk.ifs.project.core.domain.ProjectUser;
import org.innovateuk.ifs.project.core.repository.ProjectRepository;
import org.innovateuk.ifs.project.core.transactional.PartnerOrganisationService;
import org.innovateuk.ifs.project.core.util.ProjectUsersHelper;
import org.innovateuk.ifs.project.finance.resource.EligibilityState;
import org.innovateuk.ifs.project.finance.resource.FundingRulesState;
import org.innovateuk.ifs.project.finance.resource.TimeUnit;
import org.innovateuk.ifs.project.finance.resource.ViabilityState;
import org.innovateuk.ifs.project.financechecks.domain.*;
import org.innovateuk.ifs.project.financechecks.repository.CostCategoryRepository;
import org.innovateuk.ifs.project.financechecks.repository.CostCategoryTypeRepository;
import org.innovateuk.ifs.project.financechecks.workflow.financechecks.configuration.EligibilityWorkflowHandler;
import org.innovateuk.ifs.project.financechecks.workflow.financechecks.configuration.FundingRulesWorkflowHandler;
import org.innovateuk.ifs.project.financechecks.workflow.financechecks.configuration.ViabilityWorkflowHandler;
import org.innovateuk.ifs.project.internal.ProjectSetupStage;
import org.innovateuk.ifs.project.monitoring.domain.MonitoringOfficer;
import org.innovateuk.ifs.project.monitoring.repository.MonitoringOfficerRepository;
import org.innovateuk.ifs.project.resource.ApprovalType;
import org.innovateuk.ifs.project.resource.PartnerOrganisationResource;
import org.innovateuk.ifs.project.resource.ProjectOrganisationCompositeId;
import org.innovateuk.ifs.project.spendprofile.configuration.workflow.SpendProfileWorkflowHandler;
import org.innovateuk.ifs.project.spendprofile.domain.SpendProfile;
import org.innovateuk.ifs.project.spendprofile.domain.SpendProfileNotifications;
import org.innovateuk.ifs.project.spendprofile.domain.SpendProfileProcess;
import org.innovateuk.ifs.project.spendprofile.repository.SpendProfileRepository;
import org.innovateuk.ifs.project.spendprofile.resource.SpendProfileCSVResource;
import org.innovateuk.ifs.project.spendprofile.resource.SpendProfileResource;
import org.innovateuk.ifs.project.spendprofile.resource.SpendProfileState;
import org.innovateuk.ifs.project.spendprofile.resource.SpendProfileTableResource;
import org.innovateuk.ifs.project.spendprofile.validator.SpendProfileValidationUtil;
import org.innovateuk.ifs.user.domain.User;
import org.innovateuk.ifs.user.mapper.UserMapper;
import org.innovateuk.ifs.user.repository.UserRepository;
import org.innovateuk.ifs.user.resource.Role;
import org.innovateuk.ifs.user.resource.UserResource;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.springframework.test.util.ReflectionTestUtils;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.*;

import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsNull.nullValue;
import static org.hibernate.validator.internal.util.CollectionHelper.asSet;
import static org.innovateuk.ifs.LambdaMatcher.createLambdaMatcher;
import static org.innovateuk.ifs.application.builder.ApplicationBuilder.newApplication;
import static org.innovateuk.ifs.commons.error.CommonFailureKeys.*;
import static org.innovateuk.ifs.commons.service.ServiceResult.serviceFailure;
import static org.innovateuk.ifs.commons.service.ServiceResult.serviceSuccess;
import static org.innovateuk.ifs.competition.builder.CompetitionBuilder.newCompetition;
import static org.innovateuk.ifs.finance.resource.cost.FinanceRowType.LABOUR;
import static org.innovateuk.ifs.finance.resource.cost.FinanceRowType.MATERIALS;
import static org.innovateuk.ifs.form.builder.QuestionBuilder.newQuestion;
import static org.innovateuk.ifs.notifications.resource.NotificationMedium.EMAIL;
import static org.innovateuk.ifs.organisation.builder.OrganisationBuilder.newOrganisation;
import static org.innovateuk.ifs.organisation.builder.OrganisationTypeBuilder.newOrganisationType;
import static org.innovateuk.ifs.organisation.resource.OrganisationTypeEnum.BUSINESS;
import static org.innovateuk.ifs.project.builder.PartnerOrganisationResourceBuilder.newPartnerOrganisationResource;
import static org.innovateuk.ifs.project.core.builder.PartnerOrganisationBuilder.newPartnerOrganisation;
import static org.innovateuk.ifs.project.core.builder.ProjectBuilder.newProject;
import static org.innovateuk.ifs.project.core.builder.ProjectStageBuilder.newProjectStages;
import static org.innovateuk.ifs.project.core.builder.ProjectUserBuilder.newProjectUser;
import static org.innovateuk.ifs.project.finance.resource.TimeUnit.MONTH;
import static org.innovateuk.ifs.project.financecheck.builder.CostCategoryBuilder.newCostCategory;
import static org.innovateuk.ifs.project.financecheck.builder.CostCategoryGroupBuilder.newCostCategoryGroup;
import static org.innovateuk.ifs.project.financecheck.builder.CostCategoryTypeBuilder.newCostCategoryType;
import static org.innovateuk.ifs.project.monitoring.builder.MonitoringOfficerBuilder.newMonitoringOfficer;
import static org.innovateuk.ifs.project.spendprofile.builder.SpendProfileBuilder.newSpendProfile;
import static org.innovateuk.ifs.question.resource.QuestionSetupType.SUBSIDY_BASIS;
import static org.innovateuk.ifs.user.builder.UserBuilder.newUser;
import static org.innovateuk.ifs.user.builder.UserResourceBuilder.newUserResource;
import static org.innovateuk.ifs.util.CollectionFunctions.simpleFindFirst;
import static org.innovateuk.ifs.util.MapFunctions.asMap;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class SpendProfileServiceImplTest extends BaseServiceUnitTest<SpendProfileServiceImpl> {

    private static final String webBaseUrl = "https://ifs-local-dev/dashboard";
    private Long projectId = 123L;
    private Long organisationId = 456L;

    @Mock
    private SpendProfileCostCategorySummaryStrategy spendProfileCostCategorySummaryStrategy;
    @Mock
    private NotificationService notificationService;
    @Mock
    private SpendProfileValidationUtil validationUtil;
    @Mock
    private Error mockedError;
    @Mock
    private ViabilityWorkflowHandler viabilityWorkflowHandler;
    @Mock
    private EligibilityWorkflowHandler eligibilityWorkflowHandler;
    @Mock
    private FundingRulesWorkflowHandler fundingRulesWorkflowHandler;
    @Mock
    private SpendProfileWorkflowHandler spendProfileWorkflowHandler;
    @Mock
    private SpendProfileRepository spendProfileRepository;
    @Mock
    private ProjectUsersHelper projectUsersHelper;
    @Mock
    private PartnerOrganisationService partnerOrganisationService;
    @Mock
    private ProjectRepository projectRepository;
    @Mock
    private OrganisationRepository organisationRepository;
    @Mock
    private CostCategoryRepository costCategoryRepository;
    @Mock
    private CostCategoryTypeRepository costCategoryTypeRepository;
    @Mock
    private UserRepository userRepository;
    @Mock
    private SystemNotificationSource systemNotificationSource;
    @Mock
    private UserMapper userMapper;
    @InjectMocks
    @Spy
    private DefaultSpendProfileFigureDistributer defaultSpendProfileFigureDistributer;
    @Mock
    private MonitoringOfficerRepository monitoringOfficerRepository;

    @Test
    public void generateSpendProfile() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();
        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);
        CostCategoryType costCategoryType1 = generateSpendProfileData.getCostCategoryType1();
        CostCategoryType costCategoryType2 = generateSpendProfileData.getCostCategoryType2();
        CostCategory type1Cat1 = generateSpendProfileData.type1Cat1;
        CostCategory type1Cat2 = generateSpendProfileData.type1Cat2;
        CostCategory type2Cat1 = generateSpendProfileData.type2Cat1;

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.APPROVED);
        when(spendProfileWorkflowHandler.isAlreadyGenerated(project)).thenReturn(false);
        when(spendProfileWorkflowHandler.projectHasNoPendingPartners(project)).thenReturn(true);
        when(spendProfileWorkflowHandler.spendProfileGenerated(eq(project), any())).thenReturn(true);

        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation1.getId())).thenReturn(Optional.empty());
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation2.getId())).thenReturn(Optional.empty());

        User generatedBy = generateSpendProfileData.getUser();

        List<Cost> expectedOrganisation1EligibleCosts = asList(
                new Cost("100").withCategory(type1Cat1),
                new Cost("200").withCategory(type1Cat2));

        List<Cost> expectedOrganisation1SpendProfileFigures = asList(
                new Cost("34").withCategory(type1Cat1).withTimePeriod(0, MONTH, 1, MONTH),
                new Cost("33").withCategory(type1Cat1).withTimePeriod(1, MONTH, 1, MONTH),
                new Cost("33").withCategory(type1Cat1).withTimePeriod(2, MONTH, 1, MONTH),
                new Cost("68").withCategory(type1Cat2).withTimePeriod(0, MONTH, 1, MONTH),
                new Cost("66").withCategory(type1Cat2).withTimePeriod(1, MONTH, 1, MONTH),
                new Cost("66").withCategory(type1Cat2).withTimePeriod(2, MONTH, 1, MONTH));

        Calendar generatedDate = Calendar.getInstance();

        SpendProfile expectedOrganisation1Profile = new SpendProfile(organisation1, project, costCategoryType1,
                expectedOrganisation1EligibleCosts, expectedOrganisation1SpendProfileFigures, generatedBy, generatedDate, false);

        List<Cost> expectedOrganisation2EligibleCosts = singletonList(
                new Cost("301").withCategory(type2Cat1));

        List<Cost> expectedOrganisation2SpendProfileFigures = asList(
                new Cost("101").withCategory(type2Cat1).withTimePeriod(0, MONTH, 1, MONTH),
                new Cost("100").withCategory(type2Cat1).withTimePeriod(1, MONTH, 1, MONTH),
                new Cost("100").withCategory(type2Cat1).withTimePeriod(2, MONTH, 1, MONTH));

        SpendProfile expectedOrganisation2Profile = new SpendProfile(organisation2, project, costCategoryType2,
                expectedOrganisation2EligibleCosts, expectedOrganisation2SpendProfileFigures, generatedBy, generatedDate, false);

        when(spendProfileRepository.save(spendProfileExpectations(expectedOrganisation1Profile))).thenReturn(null);
        when(spendProfileRepository.save(spendProfileExpectations(expectedOrganisation2Profile))).thenReturn(null);

        User financeContactUser1 = newUser().withEmailAddress("z@abc.com").withFirstName("A").withLastName("Z").build();
        ProjectUser financeContact1 = newProjectUser().withUser(financeContactUser1).build();
        User financeContactUser2 = newUser().withEmailAddress("a@abc.com").withFirstName("A").withLastName("A").build();
        ProjectUser financeContact2 = newProjectUser().withUser(financeContactUser2).build();
        when(projectUsersHelper.getFinanceContact(project.getId(), organisation1.getId())).thenReturn(Optional.of(financeContact1));
        when(projectUsersHelper.getFinanceContact(project.getId(), organisation2.getId())).thenReturn(Optional.of(financeContact2));

        Map<String, Object> expectedNotificationArguments = asMap(
                "dashboardUrl", "https://ifs-local-dev/dashboard",
                "applicationId", project.getApplication().getId(),
                "competitionName", "Competition 1"
        );

        NotificationTarget to1 = new UserNotificationTarget("A Z", "z@abc.com");
        NotificationTarget to2 = new UserNotificationTarget("A A", "a@abc.com");

        Notification notification1 = new Notification(systemNotificationSource, new NotificationMessage(to1), SpendProfileNotifications.FINANCE_CONTACT_SPEND_PROFILE_AVAILABLE, expectedNotificationArguments);
        Notification notification2 = new Notification(systemNotificationSource, new NotificationMessage(to2), SpendProfileNotifications.FINANCE_CONTACT_SPEND_PROFILE_AVAILABLE, expectedNotificationArguments);

        when(notificationService.sendNotificationWithFlush(notification1, EMAIL)).thenReturn(serviceSuccess());
        when(notificationService.sendNotificationWithFlush(notification2, EMAIL)).thenReturn(serviceSuccess());

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isSuccess());

        verify(spendProfileRepository).save(spendProfileExpectations(expectedOrganisation1Profile));
        verify(spendProfileRepository).save(spendProfileExpectations(expectedOrganisation2Profile));

        verify(notificationService).sendNotificationWithFlush(notification1, EMAIL);
        verify(notificationService).sendNotificationWithFlush(notification2, EMAIL);
    }

    @Test
    public void generateSpendProfileButNotAllViabilityApproved() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();
        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.REVIEW);

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(SPEND_PROFILE_CANNOT_BE_GENERATED_UNTIL_ALL_VIABILITY_APPROVED));

        verify(spendProfileRepository, never()).save(isA(SpendProfile.class));
        verifyNoMoreInteractions(spendProfileRepository);
    }

    @Test
    public void generateSpendProfileButWithPendingPartner() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();

        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.NOT_APPLICABLE);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.APPROVED);
        when(spendProfileWorkflowHandler.isAlreadyGenerated(project)).thenReturn(false);
        when(spendProfileWorkflowHandler.projectHasNoPendingPartners(project)).thenReturn(false);

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(SPEND_PROFILE_CANNOT_BE_GENERATED_UNTIL_ALL_PARTNERS_ARE_NO_LONGER_PENDING));

        verify(spendProfileRepository, never()).save(isA(SpendProfile.class));
        verifyNoMoreInteractions(spendProfileRepository);
    }

    @Test
    public void generateSpendProfileWhenNotAllEligibilityApproved() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();
        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.REVIEW);

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(SPEND_PROFILE_CANNOT_BE_GENERATED_UNTIL_ALL_ELIGIBILITY_APPROVED));

        verify(spendProfileRepository, never()).save(isA(SpendProfile.class));
        verifyNoMoreInteractions(spendProfileRepository);
    }

    @Test
    public void generateSpendProfileWhenNotAllFundingRulesApproved() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();
        project.getApplication().getCompetition().setFundingRules(FundingRules.SUBSIDY_CONTROL);
        Question question = newQuestion().withQuestionSetupType(SUBSIDY_BASIS).build();
        project.getApplication().getCompetition().getQuestions().add(question);

        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.APPROVED);
        when(fundingRulesWorkflowHandler.getState(partnerOrganisation1)).thenReturn(FundingRulesState.APPROVED);
        when(fundingRulesWorkflowHandler.getState(partnerOrganisation2)).thenReturn(FundingRulesState.REVIEW);

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(SPEND_PROFILE_CANNOT_BE_GENERATED_UNTIL_ALL_FUNDING_RULES_APPROVED));

        verify(spendProfileRepository, never()).save(isA(SpendProfile.class));
        verifyNoMoreInteractions(spendProfileRepository);
    }

    @Test
    public void generateSpendProfileWhenSpendProfileAlreadyGenerated() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();
        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.APPROVED);
        when(spendProfileWorkflowHandler.isAlreadyGenerated(project)).thenReturn(true);

        SpendProfile spendProfileForOrganisation1 = new SpendProfile();
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation1.getId())).thenReturn(Optional.of(spendProfileForOrganisation1));
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation2.getId())).thenReturn(Optional.empty());

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(SPEND_PROFILE_HAS_ALREADY_BEEN_GENERATED));

        verify(spendProfileRepository, never()).save(isA(SpendProfile.class));
    }

    @Test
    public void generateSpendProfileWhenSendingEmailFails() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();
        Project project = generateSpendProfileData.getProject();
        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.NOT_APPLICABLE);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.APPROVED);
        when(spendProfileWorkflowHandler.isAlreadyGenerated(project)).thenReturn(false);
        when(spendProfileWorkflowHandler.projectHasNoPendingPartners(project)).thenReturn(true);
        when(spendProfileWorkflowHandler.spendProfileGenerated(eq(project), any())).thenReturn(true);

        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation1.getId())).thenReturn(Optional.empty());
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation2.getId())).thenReturn(Optional.empty());

        User financeContactUser1 = newUser().withEmailAddress("z@abc.com").withFirstName("A").withLastName("Z").build();
        ProjectUser financeContact1 = newProjectUser().withUser(financeContactUser1).build();
        User financeContactUser2 = newUser().withEmailAddress("a@abc.com").withFirstName("A").withLastName("A").build();
        ProjectUser financeContact2 = newProjectUser().withUser(financeContactUser2).build();
        when(projectUsersHelper.getFinanceContact(project.getId(), organisation1.getId())).thenReturn(Optional.of(financeContact1));
        when(projectUsersHelper.getFinanceContact(project.getId(), organisation2.getId())).thenReturn(Optional.of(financeContact2));

        Map<String, Object> expectedNotificationArguments = asMap(
                "dashboardUrl", "https://ifs-local-dev/dashboard",
                "competitionName", "Competition 1",
                "applicationId", project.getApplication().getId()
        );

        NotificationTarget to1 = new UserNotificationTarget("A Z", "z@abc.com");
        NotificationTarget to2 = new UserNotificationTarget("A A", "a@abc.com");

        Notification notification1 = new Notification(systemNotificationSource, to1, SpendProfileNotifications.FINANCE_CONTACT_SPEND_PROFILE_AVAILABLE, expectedNotificationArguments);
        Notification notification2 = new Notification(systemNotificationSource, to2, SpendProfileNotifications.FINANCE_CONTACT_SPEND_PROFILE_AVAILABLE, expectedNotificationArguments);

        when(notificationService.sendNotificationWithFlush(notification1, EMAIL)).thenReturn(serviceSuccess());
        when(notificationService.sendNotificationWithFlush(notification2, EMAIL)).thenReturn(serviceFailure(CommonFailureKeys.NOTIFICATIONS_UNABLE_TO_SEND_SINGLE));

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(CommonFailureKeys.NOTIFICATIONS_UNABLE_TO_SEND_SINGLE));

        verify(spendProfileRepository, times(2)).save(isA(SpendProfile.class));

        verify(notificationService).sendNotificationWithFlush(notification1, EMAIL);
        verify(notificationService).sendNotificationWithFlush(notification2, EMAIL);
    }

    @Test
    public void generateSpendProfileSendEmailFailsDueToNoFinanceContact() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();
        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.NOT_APPLICABLE);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.APPROVED);
        when(spendProfileWorkflowHandler.isAlreadyGenerated(project)).thenReturn(false);
        when(spendProfileWorkflowHandler.projectHasNoPendingPartners(project)).thenReturn(true);
        when(spendProfileWorkflowHandler.spendProfileGenerated(eq(project), any())).thenReturn(true);

        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation1.getId())).thenReturn(Optional.empty());
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(project.getId(),
                organisation2.getId())).thenReturn(Optional.empty());

        ProjectUser financeContact2 = newProjectUser().withUser((User[]) null).build();
        when(projectUsersHelper.getFinanceContact(project.getId(), organisation1.getId())).thenReturn(Optional.empty());
        when(projectUsersHelper.getFinanceContact(project.getId(), organisation2.getId())).thenReturn(Optional.of(financeContact2));

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(CommonFailureKeys.SPEND_PROFILE_FINANCE_CONTACT_NOT_PRESENT, CommonFailureKeys.SPEND_PROFILE_FINANCE_CONTACT_NOT_PRESENT));

        verify(spendProfileRepository, times(2)).save(isA(SpendProfile.class));
    }

    @Test
    public void generateSpendProfileNotReadyToGenerate() {

        GenerateSpendProfileData generateSpendProfileData = new GenerateSpendProfileData().build();

        Project project = generateSpendProfileData.getProject();
        Organisation organisation1 = generateSpendProfileData.getOrganisation1();
        Organisation organisation2 = generateSpendProfileData.getOrganisation2();
        PartnerOrganisation partnerOrganisation1 = project.getPartnerOrganisations().get(0);
        PartnerOrganisation partnerOrganisation2 = project.getPartnerOrganisations().get(1);

        setupGenerateSpendProfilesExpectations(generateSpendProfileData, project, organisation1, organisation2);

        when(viabilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(ViabilityState.APPROVED);
        when(viabilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(ViabilityState.NOT_APPLICABLE);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation1)).thenReturn(EligibilityState.APPROVED);
        when(eligibilityWorkflowHandler.getState(partnerOrganisation2)).thenReturn(EligibilityState.APPROVED);
        when(spendProfileWorkflowHandler.isAlreadyGenerated(project)).thenReturn(true);

        ServiceResult<Void> generateResult = service.generateSpendProfile(projectId);
        assertTrue(generateResult.isFailure());
        assertTrue(generateResult.getFailure().is(CommonFailureKeys.SPEND_PROFILE_HAS_ALREADY_BEEN_GENERATED));
    }

    private void setupGenerateSpendProfilesExpectations(GenerateSpendProfileData generateSpendProfileData, Project project, Organisation organisation1, Organisation organisation2) {
        CostCategoryType costCategoryType1 = generateSpendProfileData.getCostCategoryType1();
        CostCategoryType costCategoryType2 = generateSpendProfileData.getCostCategoryType2();
        CostCategory type1Cat1 = generateSpendProfileData.type1Cat1;
        CostCategory type1Cat2 = generateSpendProfileData.type1Cat2;
        CostCategory type2Cat1 = generateSpendProfileData.type2Cat1;

        List<PartnerOrganisationResource> partnerOrganisationResources =
                newPartnerOrganisationResource().withOrganisation(organisation1.getId()).build(1);
        if (organisation2 != null) {
            partnerOrganisationResources.add(newPartnerOrganisationResource().withOrganisation(organisation2.getId()).build());
        }
        when(partnerOrganisationService.getProjectPartnerOrganisations(projectId)).thenReturn(serviceSuccess(partnerOrganisationResources));

        // setup expectations for finding finance figures per Cost Category from which to generate the spend profile
        when(spendProfileCostCategorySummaryStrategy.getCostCategorySummaries(project.getId(), organisation1.getId())).thenReturn(serviceSuccess(
                new SpendProfileCostCategorySummaries(
                        asList(
                                new SpendProfileCostCategorySummary(type1Cat1, new BigDecimal("100.00"), project.getDurationInMonths()),
                                new SpendProfileCostCategorySummary(type1Cat2, new BigDecimal("200.00"), project.getDurationInMonths())),
                        costCategoryType1)));
        if (organisation2 != null) {
            when(spendProfileCostCategorySummaryStrategy.getCostCategorySummaries(project.getId(), organisation2.getId())).thenReturn(serviceSuccess(
                    new SpendProfileCostCategorySummaries(
                            singletonList(new SpendProfileCostCategorySummary(type2Cat1, new BigDecimal("300.66"), project.getDurationInMonths())),
                            costCategoryType2)));
        }

    }

    @Test
    public void generateSpendProfileCSV() {
        Project project = newProject()
                .withApplication(newApplication().withCompetition(newCompetition().withIncludeJesForm(true).build()).build())
                .withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).build();
        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, organisationId);
        SpendProfile spendProfileInDB = createSpendProfile(project,
                // eligible costs
                asMap(
                        1L, new BigDecimal("100"),
                        2L, new BigDecimal("180"),
                        3L, new BigDecimal("55")),

                // Spend Profile costs
                asMap(
                        1L, asList(new BigDecimal("30"), new BigDecimal("30"), new BigDecimal("50")),
                        2L, asList(new BigDecimal("70"), new BigDecimal("50"), new BigDecimal("60")),
                        3L, asList(new BigDecimal("50"), new BigDecimal("5"), new BigDecimal("0")))
        );
        CostCategory testCostCategory = new CostCategory();
        testCostCategory.setId(1L);
        testCostCategory.setName("Category One");
        testCostCategory.setLabel("Group Name");

        OrganisationType organisationType = newOrganisationType().withOrganisationType(BUSINESS).build();
        Organisation organisation1 = newOrganisation().withId(organisationId).withOrganisationType(organisationType).withName("TEST").build();
        when(organisationRepository.findById(organisation1.getId())).thenReturn(Optional.of(organisation1));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(projectId, organisationId)).thenReturn(Optional.of(spendProfileInDB));
        when(costCategoryRepository.findById(anyLong())).thenReturn(Optional.of(testCostCategory));
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        ServiceResult<SpendProfileCSVResource> serviceResult = service.getSpendProfileCSV(projectOrganisationCompositeId);

        assertTrue(serviceResult.getSuccess().getFileName().startsWith("TEST_Spend_Profile_" + dateFormat.format(date)));
        assertTrue(serviceResult.getSuccess().getCsvData().contains("Category One"));
        assertEquals(6, Arrays.stream(serviceResult.getSuccess().getCsvData().split("\n")).filter(s -> s.contains("Category One")
                && !s.contains("Month") && !s.contains("TOTAL")).count());
    }

    @Test
    public void generateSpendProfileCSVWithCategoryGroupLabelEmpty() {
        Project project = newProject()
                .withApplication(newApplication().withCompetition(newCompetition().withIncludeJesForm(true).build()).build())
                .withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).build();
        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, organisationId);
        SpendProfile spendProfileInDB = createSpendProfile(project,
                // eligible costs
                asMap(
                        1L, new BigDecimal("100"),
                        2L, new BigDecimal("180"),
                        3L, new BigDecimal("55")),

                // Spend Profile costs
                asMap(
                        1L, asList(new BigDecimal("30"), new BigDecimal("30"), new BigDecimal("50")),
                        2L, asList(new BigDecimal("70"), new BigDecimal("50"), new BigDecimal("60")),
                        3L, asList(new BigDecimal("50"), new BigDecimal("5"), new BigDecimal("0")))
        );
        CostCategory testCostCategory = new CostCategory();
        testCostCategory.setId(1L);
        testCostCategory.setName("One");

        OrganisationType organisationType = newOrganisationType().withOrganisationType(BUSINESS).build();
        Organisation organisation1 = newOrganisation().withId(organisationId).withOrganisationType(organisationType).withName("TEST").build();
        when(organisationRepository.findById(organisation1.getId())).thenReturn(Optional.of(organisation1));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(projectId, organisationId)).thenReturn(Optional.of(spendProfileInDB));
        when(costCategoryRepository.findById(anyLong())).thenReturn(Optional.of(testCostCategory));
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        ServiceResult<SpendProfileCSVResource> serviceResult = service.getSpendProfileCSV(projectOrganisationCompositeId);

        assertTrue(serviceResult.getSuccess().getFileName().startsWith("TEST_Spend_Profile_" + dateFormat.format(date)));
        assertFalse(serviceResult.getSuccess().getCsvData().contains("Group Name"));
        assertEquals(Arrays.stream(serviceResult.getSuccess().getCsvData().split("\n")).filter(s -> s.contains("Group Name")
                && !s.contains("Month") && !s.contains("TOTAL")).count(), 0);
    }

    @Test
    public void getSpendProfileStatusByProjectIdApproved() {
        Project project = newProject().build();
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileWorkflowHandler.getApproval(project)).thenReturn(ApprovalType.APPROVED);

        ServiceResult<ApprovalType> result = service.getSpendProfileStatusByProjectId(projectId);
        assertTrue(result.isSuccess());
        assertEquals(ApprovalType.APPROVED, result.getSuccess());
    }

    @Test
    public void getSpendProfileStatusByProjectIdRejected() {
        Project project = newProject().build();
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileWorkflowHandler.getApproval(project)).thenReturn(ApprovalType.REJECTED);

        ServiceResult<ApprovalType> result = service.getSpendProfileStatusByProjectId(projectId);
        assertTrue(result.isSuccess());
        assertEquals(ApprovalType.REJECTED, result.getSuccess());
    }

    @Test
    public void getSpendProfileStatusByProjectIdUnset() {
        List<SpendProfile> spendProfileList = newSpendProfile().build(3);
        when(spendProfileRepository.findByProjectId(projectId)).thenReturn(spendProfileList);

        ServiceResult<ApprovalType> result = service.getSpendProfileStatusByProjectId(projectId);
        assertTrue(result.isSuccess());
        assertEquals(ApprovalType.UNSET, result.getSuccess());
    }

    @Test
    public void approveSpendProfile() {
        List<SpendProfile> spendProfileList = getSpendProfilesAndSetWhenSpendProfileRepositoryMock(projectId);
        Project project = newProject().withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileWorkflowHandler.isReadyToApprove(project)).thenReturn(true);

        Long userId = 1234L;
        User user = newUser().withId(userId).build();
        UserResource loggedInUser = newUserResource().withId(user.getId()).build();
        setLoggedInUser(loggedInUser);
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(spendProfileWorkflowHandler.spendProfileApproved(project, user)).thenReturn(true);

        ServiceResult<Void> result = service.approveOrRejectSpendProfile(projectId, ApprovalType.APPROVED);

        assertTrue(result.isSuccess());
        verify(spendProfileRepository).saveAll(spendProfileList);
        verify(spendProfileWorkflowHandler).spendProfileApproved(project, user);
    }

    @Test
    public void rejectSpendProfile() {
        Long projectId = 4234L;
        List<SpendProfile> spendProfileList = getSpendProfilesAndSetWhenSpendProfileRepositoryMock(projectId);
        Project project = newProject().withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(spendProfileWorkflowHandler.isReadyToApprove(project)).thenReturn(true);

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));

        Long userId = 1234L;
        UserResource loggedInUser = newUserResource().withId(userId).build();
        User user = newUser().withId(loggedInUser.getId()).build();
        setLoggedInUser(loggedInUser);
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(spendProfileWorkflowHandler.spendProfileRejected(project, user)).thenReturn(true);

        ServiceResult<Void> resultNew = service.approveOrRejectSpendProfile(projectId, ApprovalType.REJECTED);

        assertTrue(resultNew.isSuccess());
        assertNull(project.getSpendProfileSubmittedDate());

        verify(spendProfileRepository).saveAll(spendProfileList);
        verify(spendProfileWorkflowHandler).spendProfileRejected(project, user);
    }

    @Test
    public void deleteSpendProfile() {
        Long userId = 1234L;
        UserResource loggedInUser = newUserResource().withId(userId).build();
        User user = newUser().withId(loggedInUser.getId()).build();
        setLoggedInUser(loggedInUser);
        Long projectId = 4234L;
        List<SpendProfile> spendProfileList = getSpendProfilesAndSetWhenSpendProfileRepositoryMock(projectId);
        Project project = newProject().withId(projectId).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(spendProfileRepository.findByProjectId(projectId)).thenReturn(spendProfileList);
        when(spendProfileWorkflowHandler.spendProfileDeleted(project, user)).thenReturn(true);

        ServiceResult<Void> result = service.deleteSpendProfile(projectId);

        assertTrue(result.isSuccess());
        assertNull(project.getSpendProfileSubmittedDate());
    }

    @Test
    public void approveSpendProfileProcessNotApproved() {
        List<SpendProfile> spendProfileList = getSpendProfilesAndSetWhenSpendProfileRepositoryMock(projectId);
        Project project = newProject().withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileWorkflowHandler.isReadyToApprove(project)).thenReturn(true);
        Long userId = 1234L;
        User user = newUser().withId(userId).build();
        UserResource loggedInUser = newUserResource().withId(user.getId()).build();
        setLoggedInUser(loggedInUser);
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(spendProfileWorkflowHandler.spendProfileApproved(project, user)).thenReturn(false);

        ServiceResult<Void> result = service.approveOrRejectSpendProfile(projectId, ApprovalType.APPROVED);

        assertFalse(result.isSuccess());
        assertEquals(SPEND_PROFILE_CANNOT_BE_APPROVED.getErrorKey(), result.getFailure().getErrors().get(0).getErrorKey());

        verify(spendProfileRepository).saveAll(spendProfileList);
        verify(spendProfileWorkflowHandler).spendProfileApproved(project, user);
    }

    @Test
    public void rejectSpendProfileNotReadyToApprove() {
        Project project = newProject().withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileWorkflowHandler.isReadyToApprove(project)).thenReturn(false);

        ServiceResult<Void> result = service.approveOrRejectSpendProfile(projectId, ApprovalType.REJECTED);

        assertFalse(result.isSuccess());
        assertEquals(SPEND_PROFILE_NOT_READY_TO_APPROVE.getErrorKey(), result.getFailure().getErrors().get(0).getErrorKey());
    }

    @Test
    public void approveSpendProfileInvalidApprovalType() {
        Project project = newProject().withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileWorkflowHandler.isReadyToApprove(project)).thenReturn(true);

        ServiceResult<Void> result = service.approveOrRejectSpendProfile(projectId, ApprovalType.UNSET);

        assertFalse(result.isSuccess());
        assertEquals(SPEND_PROFILE_NOT_READY_TO_APPROVE.getErrorKey(), result.getFailure().getErrors().get(0).getErrorKey());
    }

    @Test
    public void approveSpendProfileNotReadyToApprove() {
        Project project = newProject().withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(spendProfileWorkflowHandler.isReadyToApprove(project)).thenReturn(false);

        ServiceResult<Void> result = service.approveOrRejectSpendProfile(projectId, ApprovalType.APPROVED);

        assertFalse(result.isSuccess());
        assertEquals(SPEND_PROFILE_NOT_READY_TO_APPROVE.getErrorKey(), result.getFailure().getErrors().get(0).getErrorKey());
    }

    @Test
    public void rejectSpendProfileFails() {
        Long projectId = 4234L;
        Project project = newProject().withId(projectId).withDuration(3L).withTargetStartDate(LocalDate.of(2018, 3, 1)).withSpendProfileSubmittedDate(ZonedDateTime.now()).build();

        when(spendProfileWorkflowHandler.isReadyToApprove(project)).thenReturn(true);

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));

        Long userId = 1234L;
        UserResource loggedInUser = newUserResource().withId(userId).build();
        User user = newUser().withId(loggedInUser.getId()).build();
        setLoggedInUser(loggedInUser);
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(spendProfileWorkflowHandler.spendProfileRejected(project, user)).thenReturn(false);

        ServiceResult<Void> result = service.approveOrRejectSpendProfile(projectId, ApprovalType.REJECTED);

        assertFalse(result.isSuccess());
        assertEquals(SPEND_PROFILE_CANNOT_BE_REJECTED.getErrorKey(), result.getFailure().getErrors().get(0).getErrorKey());
    }

    @Test
    public void approveSpendProfileNoProject() {
        when(projectRepository.findById(projectId)).thenReturn(Optional.empty());

        ServiceResult<Void> result = service.approveOrRejectSpendProfile(projectId, ApprovalType.APPROVED);

        assertFalse(result.isSuccess());
        assertEquals(SPEND_PROFILE_NOT_READY_TO_APPROVE.getErrorKey(), result.getFailure().getErrors().get(0).getErrorKey());
    }

    @Test
    public void saveSpendProfileWhenValidationFails() {

        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, organisationId);

        SpendProfileTableResource table = new SpendProfileTableResource();

        // the validation is tested in the validator related unit tests
        table.setMonthlyCostsPerCategoryMap(Collections.emptyMap());

        ValidationMessages validationMessages = new ValidationMessages();
        validationMessages.setErrors(Collections.singletonList(mockedError));

        when(validationUtil.validateSpendProfileTableResource(eq(table))).thenReturn(Optional.of(validationMessages));

        ServiceResult<Void> result = service.saveSpendProfile(projectOrganisationCompositeId, table);

        assertFalse(result.isSuccess());
    }

    @Test
    public void saveSpendProfileEnsureSpendProfileDomainIsCorrectlyUpdated() {

        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, organisationId);

        SpendProfileTableResource table = new SpendProfileTableResource();

        table.setEligibleCostPerCategoryMap(asMap(
                173L, new BigDecimal("100"),
                174L, new BigDecimal("180"),
                175L, new BigDecimal("55")));

        table.setMonthlyCostsPerCategoryMap(asMap(
                173L, asList(new BigDecimal("30"), new BigDecimal("30"), new BigDecimal("40")),
                174L, asList(new BigDecimal("70"), new BigDecimal("50"), new BigDecimal("60")),
                175L, asList(new BigDecimal("50"), new BigDecimal("5"), new BigDecimal("0"))));
        List<Cost> spendProfileFigures = buildCostsForCategories(Arrays.asList(173L, 174L, 175L), 3);
        User generatedBy = newUser().build();
        Calendar generatedDate = Calendar.getInstance();

        SpendProfile spendProfileInDB = new SpendProfile(null, newProject().build(), null, Collections.emptyList(), spendProfileFigures, generatedBy, generatedDate, false);

        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(projectId, organisationId)).thenReturn(Optional.of(spendProfileInDB));
        when(validationUtil.validateSpendProfileTableResource(eq(table))).thenReturn(Optional.empty());

        // Before the call (ie before the SpendProfile is updated), ensure that the values are set to 1
        assertCostForCategoryForGivenMonth(spendProfileInDB, 173L, 0, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 173L, 1, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 173L, 2, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 174L, 0, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 174L, 1, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 174L, 2, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 175L, 0, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 175L, 1, BigDecimal.ONE);
        assertCostForCategoryForGivenMonth(spendProfileInDB, 175L, 2, BigDecimal.ONE);

        ServiceResult<Void> result = service.saveSpendProfile(projectOrganisationCompositeId, table);

        assertTrue(result.isSuccess());

        // Assert that the SpendProfile domain is correctly updated
        assertCostForCategoryForGivenMonth(spendProfileInDB, 173L, 0, new BigDecimal("30"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 173L, 1, new BigDecimal("30"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 173L, 2, new BigDecimal("40"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 174L, 0, new BigDecimal("70"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 174L, 1, new BigDecimal("50"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 174L, 2, new BigDecimal("60"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 175L, 0, new BigDecimal("50"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 175L, 1, new BigDecimal("5"));
        assertCostForCategoryForGivenMonth(spendProfileInDB, 175L, 2, new BigDecimal("0"));

        verify(spendProfileRepository).save(spendProfileInDB);
    }

    @Test
    public void markSpendProfileIncomplete() {

        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, organisationId);

        Project projectInDB = ProjectBuilder.newProject()
                .withApplication(newApplication().withCompetition(newCompetition().withIncludeJesForm(true).build()).build())
                .withDuration(3L)
                .withTargetStartDate(LocalDate.of(2018, 3, 1))
                .withId(projectId)
                .build();

        SpendProfile spendProfileInDB = createSpendProfile(projectInDB,
                // eligible costs
                asMap(
                        1L, new BigDecimal("100"),
                        2L, new BigDecimal("180"),
                        3L, new BigDecimal("55")),

                // Spend Profile costs
                asMap(
                        1L, asList(new BigDecimal("30"), new BigDecimal("30"), new BigDecimal("50")),
                        2L, asList(new BigDecimal("70"), new BigDecimal("50"), new BigDecimal("60")),
                        3L, asList(new BigDecimal("50"), new BigDecimal("5"), new BigDecimal("0")))
        );

        spendProfileInDB.setMarkedAsComplete(true);

        OrganisationType organisationType = newOrganisationType().withOrganisationType(BUSINESS).build();
        Organisation organisation1 = newOrganisation().withId(organisationId).withOrganisationType(organisationType).withName("TEST").build();
        when(organisationRepository.findById(organisation1.getId())).thenReturn(Optional.of(organisation1));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDB));

        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(projectId, organisationId)).thenReturn(Optional.of(spendProfileInDB));

        ServiceResult<Void> result = service.markSpendProfileIncomplete(projectOrganisationCompositeId);

        assertTrue(result.isSuccess());
        assertFalse(spendProfileInDB.isMarkedAsComplete());
    }

    @Test
    public void markSpendProfileWhenActualTotalsGreaterThanEligibleCosts() {

        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, organisationId);

        Project projectInDB = ProjectBuilder.newProject()
                .withApplication(newApplication().withCompetition(newCompetition().withIncludeJesForm(true).build()).build())
                .withDuration(3L)
                .withTargetStartDate(LocalDate.of(2018, 3, 1))
                .build();

        SpendProfile spendProfileInDB = createSpendProfile(projectInDB,
                // eligible costs
                asMap(
                        1L, new BigDecimal("100"),
                        2L, new BigDecimal("180"),
                        3L, new BigDecimal("55")),

                // Spend Profile costs
                asMap(
                        1L, asList(new BigDecimal("30"), new BigDecimal("30"), new BigDecimal("50")),
                        2L, asList(new BigDecimal("70"), new BigDecimal("50"), new BigDecimal("60")),
                        3L, asList(new BigDecimal("50"), new BigDecimal("5"), new BigDecimal("0")))
        );

        OrganisationType organisationType = newOrganisationType().withOrganisationType(BUSINESS).build();
        Organisation organisation1 = newOrganisation().withId(organisationId).withOrganisationType(organisationType).withName("TEST").build();
        when(organisationRepository.findById(organisation1.getId())).thenReturn(Optional.of(organisation1));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDB));

        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(projectId, organisationId)).thenReturn(Optional.of(spendProfileInDB));

        ServiceResult<Void> result = service.markSpendProfileComplete(projectOrganisationCompositeId);

        assertTrue(result.isFailure());
        assertTrue(result.getFailure().is(SPEND_PROFILE_CANNOT_MARK_AS_COMPLETE_BECAUSE_SPEND_HIGHER_THAN_ELIGIBLE));
    }

    @Test
    public void markSpendProfileSuccessWhenActualTotalsAreLessThanEligibleCosts() {

        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, organisationId);

        Project projectInDB = ProjectBuilder.newProject()
                .withApplication(newApplication().withCompetition(newCompetition().withIncludeJesForm(true).build()).build())
                .withDuration(3L)
                .withTargetStartDate(LocalDate.of(2018, 3, 1))
                .build();

        SpendProfile spendProfileInDB = createSpendProfile(projectInDB,
                // eligible costs
                asMap(
                        1L, new BigDecimal("100"),
                        2L, new BigDecimal("180"),
                        3L, new BigDecimal("55")),

                // Spend Profile costs
                asMap(
                        1L, asList(new BigDecimal("30"), new BigDecimal("30"), new BigDecimal("40")),
                        2L, asList(new BigDecimal("70"), new BigDecimal("10"), new BigDecimal("60")),
                        3L, asList(new BigDecimal("50"), new BigDecimal("5"), new BigDecimal("0")))
        );

        OrganisationType organisationType = newOrganisationType().withOrganisationType(BUSINESS).build();
        Organisation organisation1 = newOrganisation().withId(organisationId).withOrganisationType(organisationType).withName("TEST").build();
        when(organisationRepository.findById(organisation1.getId())).thenReturn(Optional.of(organisation1));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDB));

        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(projectId, organisationId)).thenReturn(Optional.of(spendProfileInDB));

        ServiceResult<Void> result = service.markSpendProfileComplete(projectOrganisationCompositeId);

        assertTrue(result.isSuccess());
    }

    @Test
    public void completeSpendProfilesReviewSuccess() {
        Project projectInDb = new Project();
        projectInDb.setSpendProfileSubmittedDate(null);
        SpendProfile spendProfileInDb = new SpendProfile();
        spendProfileInDb.setMarkedAsComplete(true);
        projectInDb.setSpendProfiles(singletonList(spendProfileInDb));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDb));
        assertThat(projectInDb.getSpendProfileSubmittedDate(), nullValue());
        when(spendProfileWorkflowHandler.submit(projectInDb)).thenReturn(true);

        ServiceResult<Void> result = service.completeSpendProfilesReview(projectId, true);

        assertTrue(result.isSuccess());
        assertThat(projectInDb.getSpendProfileSubmittedDate(), notNullValue());
    }

    @Test
    public void completeSpendProfilesReviewSuccessWithoutEmailNotification() {
        Project projectInDb = new Project();
        projectInDb.setSpendProfileSubmittedDate(null);
        SpendProfile spendProfileInDb = new SpendProfile();
        spendProfileInDb.setMarkedAsComplete(true);
        projectInDb.setSpendProfiles(singletonList(spendProfileInDb));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDb));
        assertThat(projectInDb.getSpendProfileSubmittedDate(), nullValue());
        when(spendProfileWorkflowHandler.submit(projectInDb)).thenReturn(true);

        ServiceResult<Void> result = service.completeSpendProfilesReview(projectId, false);

        assertTrue(result.isSuccess());
        assertThat(projectInDb.getSpendProfileSubmittedDate(), notNullValue());
    }

    @Test
    public void completeSpendProfilesReviewFailureWhenSpendProfileIncomplete() {
        Project projectInDb = new Project();
        projectInDb.setSpendProfileSubmittedDate(null);
        SpendProfile spendProfileInDb = new SpendProfile();
        spendProfileInDb.setMarkedAsComplete(false);
        projectInDb.setSpendProfiles(singletonList(spendProfileInDb));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDb));
        assertThat(projectInDb.getSpendProfileSubmittedDate(), nullValue());

        ServiceResult<Void> result = service.completeSpendProfilesReview(projectId, true);

        assertTrue(result.isFailure());
    }

    @Test
    public void completeSpendProfilesReviewFailureWhenAlreadySubmitted() {
        Project projectInDb = new Project();
        projectInDb.setSpendProfileSubmittedDate(ZonedDateTime.now());
        SpendProfile spendProfileInDb = new SpendProfile();
        spendProfileInDb.setMarkedAsComplete(true);
        projectInDb.setSpendProfiles(singletonList(spendProfileInDb));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDb));

        ServiceResult<Void> result = service.completeSpendProfilesReview(projectId, true);

        assertTrue(result.isFailure());
    }

    @Test
    public void getSpendProfile() {
        Long leadOrganisationId = 1L;
        Long monitoringOfficerId = 3L;
        ZonedDateTime now = ZonedDateTime.now();

        User monitoringOfficer = newUser()
                .withId(monitoringOfficerId)
                .withRoles(asSet(Role.MONITORING_OFFICER))
                .build();
        UserResource monitoringOfficerResource = newUserResource()
                .withId(monitoringOfficer.getId())
                .withRoleGlobal(Role.MONITORING_OFFICER)
                .build();

        Project project = newProject()
                .withId(projectId)
                .withDuration(3L)
                .withTargetStartDate(LocalDate.of(2018, 3, 1))
                .withSpendProfileSubmittedDate(ZonedDateTime.now())
                .build();

        Organisation leadOrganisation = newOrganisation()
                .withId(leadOrganisationId)
                .build();
        SpendProfile spendProfile = newSpendProfile()
                .withOrganisation(leadOrganisation)
                .withProject(project)
                .build();
        when(spendProfileRepository.findOneByProjectIdAndOrganisationId(projectId, leadOrganisationId)).thenReturn(Optional.of(spendProfile));

        SpendProfileProcess spendProfileProcess = new SpendProfileProcess(monitoringOfficer, project, SpendProfileState.APPROVED);
        spendProfileProcess.setLastModified(now);
        when(spendProfileWorkflowHandler.getReviewOutcome(project)).thenReturn(spendProfileProcess);

        when(userMapper.mapToResource(any(User.class))).thenReturn(monitoringOfficerResource);

        ProjectOrganisationCompositeId projectOrganisationCompositeId = new ProjectOrganisationCompositeId(projectId, leadOrganisationId);

        ServiceResult<SpendProfileResource> result = service.getSpendProfile(projectOrganisationCompositeId);

        assertTrue(result.isSuccess());

        SpendProfileResource spendProfileResource = result.getSuccess();

        assertNotNull(spendProfileResource.getReviewedBy());
        assertEquals(monitoringOfficerId, spendProfileResource.getReviewedBy().getId());
        assertTrue(spendProfileResource.getReviewedBy().getRoles().contains(Role.MONITORING_OFFICER));
        assertEquals(now, spendProfileResource.getReviewedOn());

        verify(spendProfileRepository).findOneByProjectIdAndOrganisationId(projectId, leadOrganisationId);
        verify(spendProfileWorkflowHandler).getReviewOutcome(project);
        verify(userMapper).mapToResource(any(User.class));
    }

    @Test
    public void completeSpendProfilesReviewSuccessAndSendNotificationToMO() {
        ReflectionTestUtils.setField(service, "isMOSpendProfileUpdateEnabled", true);
        Project projectInDb = ProjectBuilder.newProject()
                .withId(projectId)
                .withName("Test Project")
                .withApplication(newApplication().withCompetition(newCompetition().withIncludeJesForm(true).build()).build())
                .withDuration(3L)
                .withTargetStartDate(LocalDate.of(2018, 3, 1))
                .build();

        User user = newUser().withRoles(singleton(Role.MONITORING_OFFICER)).build();
        MonitoringOfficer moUser = newMonitoringOfficer().withUser(user).build();
        projectInDb.setProjectMonitoringOfficer(moUser);

        projectInDb.setSpendProfileSubmittedDate(null);
        SpendProfile spendProfileInDb = new SpendProfile();
        spendProfileInDb.setMarkedAsComplete(true);
        projectInDb.setSpendProfiles(singletonList(spendProfileInDb));
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectInDb));
        when(monitoringOfficerRepository.findOneByProjectIdAndRole(projectId, ProjectParticipantRole.MONITORING_OFFICER))
                .thenReturn(Optional.of(moUser));
        when(projectUsersHelper.getMOByProjectId(projectId)).thenReturn(Optional.of(moUser));
        when(spendProfileWorkflowHandler.submit(projectInDb)).thenReturn(true);
        ServiceResult<Void> result = service.completeSpendProfilesReview(projectId, true);
        assertTrue(result.isSuccess());
        assertThat(projectInDb.getSpendProfileSubmittedDate(), notNullValue());
    }

    private SpendProfile createSpendProfile(Project projectInDB, Map<Long, BigDecimal> eligibleCostsMap, Map<Long, List<BigDecimal>> spendProfileCostsMap) {
        CostCategoryType costCategoryType = createCostCategoryType();

        List<Cost> eligibleCosts = buildEligibleCostsForCategories(eligibleCostsMap, costCategoryType.getCostCategoryGroup());

        List<Cost> spendProfileFigures = buildCostsForCategoriesWithGivenValues(spendProfileCostsMap, 3, costCategoryType.getCostCategoryGroup());

        User generatedBy = newUser().build();
        Calendar generatedDate = Calendar.getInstance();

        return new SpendProfile(null, projectInDB, costCategoryType, eligibleCosts, spendProfileFigures, generatedBy, generatedDate, true);
    }

    private CostCategoryType createCostCategoryType() {

        CostCategoryGroup costCategoryGroup = createCostCategoryGroup();

        return new CostCategoryType("Cost Category Type for Categories Labour, Materials, Other costs", costCategoryGroup);
    }

    private CostCategoryGroup createCostCategoryGroup() {

        List<CostCategory> costCategories = createCostCategories(Arrays.asList(1L, 2L, 3L));

        return new CostCategoryGroup("Cost Category Group for Categories Labour, Materials, Other costs", costCategories);
    }

    private List<CostCategory> createCostCategories(List<Long> categories) {

        List<CostCategory> costCategories = new ArrayList<>();

        categories.forEach(category -> {
            CostCategory costCategory = new CostCategory();
            costCategory.setId(category);
            costCategories.add(costCategory);
        });

        return costCategories;
    }

    private List<Cost> buildEligibleCostsForCategories(Map<Long, BigDecimal> categoryCost, CostCategoryGroup costCategoryGroup) {

        List<Cost> eligibleCostForAllCategories = new ArrayList<>();

        categoryCost.forEach((category, value) ->
                eligibleCostForAllCategories.add(createEligibleCost(category, value, costCategoryGroup)));

        return eligibleCostForAllCategories;
    }

    private Cost createEligibleCost(Long categoryId, BigDecimal value, CostCategoryGroup costCategoryGroup) {

        CostCategory costCategory = new CostCategory();
        costCategory.setId(categoryId);
        costCategory.setCostCategoryGroup(costCategoryGroup);

        Cost cost = new Cost();
        cost.setCostCategory(costCategory);
        cost.setValue(value);

        return cost;
    }

    private void assertCostForCategoryForGivenMonth(SpendProfile spendProfileInDB, Long category, Integer whichMonth, BigDecimal expectedValue) {

        boolean thisCostShouldExist = spendProfileInDB.getSpendProfileFigures().getCosts().stream()
                .anyMatch(cost -> Objects.equals(cost.getCostCategory().getId(), category)
                        && cost.getCostTimePeriod().getOffsetAmount().equals(whichMonth)
                        && cost.getValue().equals(expectedValue));
        Assert.assertTrue(thisCostShouldExist);
    }

    private List<Cost> buildCostsForCategories(List<Long> categories, int totalMonths) {

        List<Cost> costForAllCategories = new ArrayList<>();

        categories.forEach(category -> {

            // Intentionally insert in the reverse order of months to ensure that the sorting functionality actually works
            for (int index = totalMonths - 1; index >= 0; index--) {
                costForAllCategories.add(createCost(category, index, BigDecimal.ONE, null));
            }
        });

        return costForAllCategories;
    }

    private List<Cost> buildCostsForCategoriesWithGivenValues(Map<Long, List<BigDecimal>> categoryCosts, int totalMonths, CostCategoryGroup costCategoryGroup) {

        List<Cost> costForAllCategories = new ArrayList<>();

        categoryCosts.forEach((category, costs) -> {

            for (int index = 0; index < totalMonths; index++) {
                costForAllCategories.add(createCost(category, index, costs.get(index), costCategoryGroup));
            }
        });

        return costForAllCategories;
    }

    private Cost createCost(Long categoryId, Integer offsetAmount, BigDecimal value, CostCategoryGroup costCategoryGroup) {

        CostCategory costCategory = new CostCategory();
        costCategory.setId(categoryId);
        costCategory.setCostCategoryGroup(costCategoryGroup);

        //CostTimePeriod(Integer offsetAmount, TimeUnit offsetUnit, Integer durationAmount, TimeUnit durationUnit)
        CostTimePeriod costTimePeriod = new CostTimePeriod(offsetAmount, TimeUnit.MONTH, 1, TimeUnit.MONTH);

        Cost cost = new Cost();
        cost.setCostCategory(costCategory);
        cost.setCostTimePeriod(costTimePeriod);
        cost.setValue(value);

        return cost;
    }

    private SpendProfile spendProfileExpectations(SpendProfile expectedSpendProfile) {
        return createLambdaMatcher(spendProfile -> {

            assertEquals(expectedSpendProfile.getOrganisation(), spendProfile.getOrganisation());
            assertEquals(expectedSpendProfile.getProject(), spendProfile.getProject());
            assertEquals(expectedSpendProfile.getCostCategoryType(), spendProfile.getCostCategoryType());

            CostGroup expectedEligibles = expectedSpendProfile.getEligibleCosts();
            CostGroup actualEligibles = spendProfile.getEligibleCosts();
            assertCostGroupsEqual(expectedEligibles, actualEligibles);

            CostGroup expectedSpendFigures = expectedSpendProfile.getSpendProfileFigures();
            CostGroup actualSpendFigures = spendProfile.getSpendProfileFigures();
            assertCostGroupsEqual(expectedSpendFigures, actualSpendFigures);

            assertEquals(expectedSpendProfile.getGeneratedBy(), spendProfile.getGeneratedBy());
            assertTrue(spendProfile.getGeneratedDate().getTimeInMillis() - expectedSpendProfile.getGeneratedDate().getTimeInMillis() < 100);
        });
    }

    private void assertCostGroupsEqual(CostGroup expected, CostGroup actual) {
        assertEquals(expected.getDescription(), actual.getDescription());
        assertEquals(expected.getCosts().size(), actual.getCosts().size());
        expected.getCosts().forEach(expectedCost ->
                assertTrue(simpleFindFirst(actual.getCosts(), actualCost -> costsMatch(expectedCost, actualCost)).isPresent())
        );
    }

    private boolean costsMatch(Cost expectedCost, Cost actualCost) {
        CostGroup expectedCostGroup = expectedCost.getCostGroup();
        CostGroup actualCostGroup = actualCost.getCostGroup();
        if ((expectedCostGroup != null) != (actualCostGroup != null)) {
            return false;
        }
        if (expectedCostGroup != null) {
            if (!(expectedCostGroup.getDescription().equals(actualCostGroup.getDescription()))) {
                return false;
            }
        }

        if (!(expectedCost.getValue().equals(actualCost.getValue()))) {
            return false;
        }
        if (!(expectedCost.getCostCategory().equals(actualCost.getCostCategory()))) {
            return false;
        }

        CostTimePeriod expectedTimePeriod = expectedCost.getCostTimePeriod();
        CostTimePeriod actualTimePeriod = actualCost.getCostTimePeriod();
        if ((expectedTimePeriod != null) != (actualTimePeriod != null)) {
                return false;
        }

        if (expectedTimePeriod != null) {
            if (!(expectedTimePeriod.getOffsetAmount().equals(actualTimePeriod.getOffsetAmount()))) {
                return false;
            }
            if (!(expectedTimePeriod.getOffsetUnit().equals(actualTimePeriod.getOffsetUnit()))) {
                return false;
            }
            if (!(expectedTimePeriod.getDurationAmount().equals(actualTimePeriod.getDurationAmount()))) {
                return false;
            }
            if (!(expectedTimePeriod.getDurationUnit().equals(actualTimePeriod.getDurationUnit()))) {
                return false;
            }
        }

        return true;
    }

    private List<SpendProfile> getSpendProfilesAndSetWhenSpendProfileRepositoryMock(Long projectId) {
        List<SpendProfile> spendProfileList = newSpendProfile().build(2);
        when(spendProfileRepository.findByProjectId(projectId)).thenReturn(spendProfileList);

        return spendProfileList;
    }

    @Override
    protected SpendProfileServiceImpl supplyServiceUnderTest() {
        SpendProfileServiceImpl spendProfileService = new SpendProfileServiceImpl();
        ReflectionTestUtils.setField(spendProfileService, "webBaseUrl", webBaseUrl);
        return spendProfileService;
    }

    private class GenerateSpendProfileData {

        private Project project;
        private Organisation organisation1;
        private Organisation organisation2;
        private CostCategoryType costCategoryType1;
        private CostCategoryType costCategoryType2;

        private CostCategory type1Cat1;
        private CostCategory type1Cat2;
        private CostCategory type2Cat1;
        private User user;

        public Project getProject() {
            return project;
        }

        public Organisation getOrganisation1() {
            return organisation1;
        }

        public Organisation getOrganisation2() {
            return organisation2;
        }

        public CostCategoryType getCostCategoryType1() {
            return costCategoryType1;
        }

        public CostCategoryType getCostCategoryType2() {
            return costCategoryType2;
        }

        public User getUser() {
            return user;
        }

        public GenerateSpendProfileData build() {

            UserResource loggedInUser = newUserResource().build();
            user = newUser().withId(loggedInUser.getId()).build();
            setLoggedInUser(loggedInUser);
            when(userRepository.findById(loggedInUser.getId())).thenReturn(Optional.of(user));

            organisation1 = newOrganisation().withOrganisationType(BUSINESS).build();
            organisation2 = newOrganisation().withOrganisationType(OrganisationTypeEnum.RTO).build();

            PartnerOrganisation partnerOrganisation1 = newPartnerOrganisation().withOrganisation(organisation1).build();
            PartnerOrganisation partnerOrganisation2 = newPartnerOrganisation().withOrganisation(organisation2).build();

            Competition competition = newCompetition()
                    .withName("Competition 1")
                    .withProjectStages(newProjectStages().withProjectSetupStage(ProjectSetupStage.SPEND_PROFILE).build(1))
                    .build();

            Application application = newApplication()
                    .withName("Application 1")
                    .withCompetition(competition)
                    .build();

            project = newProject().
                    withId(projectId).
                    withTargetStartDate(LocalDate.now().plusMonths(1)).
                    withDuration(3L).
                    withPartnerOrganisations(asList(partnerOrganisation1, partnerOrganisation2)).
                    withApplication(application).
                    build();

            // First cost category type and everything that goes with it.
            type1Cat1 = newCostCategory().withName(LABOUR.getDisplayName()).build();
            type1Cat2 = newCostCategory().withName(MATERIALS.getDisplayName()).build();

            costCategoryType1 = newCostCategoryType()
                    .withName("Type 1")
                    .withCostCategoryGroup(
                            newCostCategoryGroup()
                                    .withDescription("Group 1")
                                    .withCostCategories(asList(type1Cat1, type1Cat2))
                                    .build())
                    .build();

            // Second cost category type and everything that goes with it.
            type2Cat1 = newCostCategory().build();

            costCategoryType2 = newCostCategoryType()
                    .withName("Type 2")
                    .withCostCategoryGroup(
                            newCostCategoryGroup()
                                    .withDescription("Group 2")
                                    .withCostCategories(singletonList(type2Cat1))
                                    .build())
                    .build();

            // set basic repository lookup expectations
            when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
            when(organisationRepository.findById(organisation1.getId())).thenReturn(Optional.of(organisation1));
            when(organisationRepository.findById(organisation2.getId())).thenReturn(Optional.of(organisation2));
            when(costCategoryRepository.findById(type1Cat1.getId())).thenReturn(Optional.of(type1Cat1));
            when(costCategoryRepository.findById(type1Cat2.getId())).thenReturn(Optional.of(type1Cat2));
            when(costCategoryRepository.findById(type2Cat1.getId())).thenReturn(Optional.of(type2Cat1));
            when(costCategoryTypeRepository.findById(costCategoryType1.getId())).thenReturn(Optional.of(costCategoryType1));
            when(costCategoryTypeRepository.findById(costCategoryType2.getId())).thenReturn(Optional.of(costCategoryType2));

            return this;
        }
    }
}


text:
<!DOCTYPE html>

    <title>Exhibition</title>
    <link rel="stylesheet" href="style/style.css" type="text/css">

{{media.info.title}}

{{media.info.description}}

redpajama_set_name: RedPajamaGithub


text:
class CreateChoirs < ActiveRecord::Migration
  def change
    create_table :choirs do |t|
      t.string  :name, null: false

      t.string  :street_name
      t.string  :house_no
      t.integer :zipcode
      t.string  :city, default: "Berlin"
      t.string  :country, default: "DE"

      t.string  :website, default: ""

      t.float   :latitude
      t.float   :longitude

      t.timestamps null: false
    end
  end
end


text:
/*jshint unused:false*/
// Note: ttls are in seconds
var cache_manager = require('../');
var memory_cache = cache_manager.caching({store: 'memory', max: 100, ttl: 10});
var memory_cache2 = cache_manager.caching({store: 'memory', max: 100, ttl: 100});
var ttl; //Can't use a different ttl per set() call with memory cache

//
// Basic usage
//
memory_cache.set('foo', 'bar', ttl, function (err) {
    if (err) { throw err; }

    memory_cache.get('foo', function (err, result) {
        console.log(result);
        // >> 'bar'
        memory_cache.del('foo', function (err) {
            if (err) {
                console.log(err);
            }
        });
    });
});

function get_user(id, cb) {
    setTimeout(function () {
        console.log("Fetching user from slow database.");
        cb(null, {id: id, name: 'Bob'});
    }, 100);
}

var user_id = 123;
var key = 'user_' + user_id;

//
// wrap() example
//

// Instead of manually managing the cache like this:
function get_cached_user_manually(id, cb) {
    memory_cache.get(id, function (err, result) {
        if (err) { return cb(err); }

        if (result) {
            return cb(null, result);
        }

        get_user(id, function (err, result) {
            if (err) { return cb(err); }
            memory_cache.set(id, result);
            cb(null, result);
        });
    });
}

// ... you can instead use the `wrap` function:
function get_cached_user(id, cb) {
    memory_cache.wrap(id, function (cache_callback) {
        get_user(id, cache_callback);
    }, cb);
}

get_cached_user(user_id, function (err, user) {
    // First time fetches the user from the (fake) database:
    console.log(user);

    get_cached_user(user_id, function (err, user) {
        // Second time fetches from cache.
        console.log(user);
    });
});

// Outputs:
// Returning user from slow database.
// { id: 123, name: 'Bob' }
// { id: 123, name: 'Bob' }

// Same as above, but written differently:
memory_cache.wrap(key, function (cb) {
    get_user(user_id, cb);
}, function (err, user) {
    console.log(user);

    // Second time fetches user from memory_cache
    memory_cache.wrap(key, function (cb) {
        get_user(user_id, cb);
    }, function (err, user) {
        console.log(user);
    });
});

//
// multi-cache example
//
var multi_cache = cache_manager.multi_caching([memory_cache, memory_cache2]);
var user_id2 = 456;
var key2 = 'user_' + user_id;
var ttl2; //Can't use a different ttl per set() call with memory cache

multi_cache.wrap(key2, function (cb) {
    get_user(user_id2, cb);
}, function (err, user) {
    console.log(user);

    // Second time fetches user from memory_cache, since it's highest priority.
    // If the data expires in the memory cache, the next fetch would pull it from
    // the Redis cache, and set the data in memory again.
    multi_cache.wrap(key2, function (cb) {
        get_user(user_id2, cb);
    }, function (err, user) {
        console.log(user);
    });

    // Sets in all caches.
    multi_cache.set('foo2', 'bar2', ttl2, function (err) {
        if (err) { throw err; }

        // Fetches from highest priority cache that has the key.
        multi_cache.get('foo2', function (err, result) {
            console.log(result);
            // >> 'bar2'

            // Delete from all caches
            multi_cache.del('foo2', function (err) {
                if (err) {
                    console.log(err);
                }
                process.exit();
            });
        });
    });
});


text:
package org.apache.heron.common.utils.metrics;

import org.apache.heron.api.metric.CountMetric;
import org.apache.heron.api.metric.CumulativeCountMetric;
import org.apache.heron.api.metric.MeanReducer;
import org.apache.heron.api.metric.MeanReducerState;
import org.apache.heron.api.metric.ReducedMetric;
import org.apache.heron.common.basics.SingletonRegistry;
import org.apache.heron.common.config.SystemConfig;
import org.apache.heron.common.utils.misc.PhysicalPlanHelper;
import org.apache.heron.common.utils.topology.TopologyContextImpl;

/**
 * Spout's metrics to be collect
 * We need to:
 * 1. Define the metrics to be collected
 * 2. New them in the constructor
 * 3. Register them in registerMetrics(...) by using MetricsCollector's registerMetric(...)
 * 4. Expose methods which could be called externally to change the value of metrics
 *
 * This is a fast spout metrics object and it is NOT used in heron core. To use this
 * metrics object, go to SpoutInstance.java and replace "new FullSpoutMetrcs" to "new SpoutMetrics"
 */

public class SpoutMetrics implements ISpoutMetrics {
  private final CountMetric ackCount;
  private final ReducedMetric<MeanReducerState, Number, Double> completeLatency;
  private final ReducedMetric<MeanReducerState, Number, Double> failLatency;
  private final CountMetric failCount;
  private final CountMetric timeoutCount;
  private final CountMetric emitCount;
  private final ReducedMetric<MeanReducerState, Number, Double> nextTupleLatency;
  private final CountMetric nextTupleCount;
  private final CountMetric tupleAddedToQueue;

  // The # of times back-pressure happens on outStreamQueue so instance could not
  // produce more tuples
  private final CountMetric outQueueFullCount;

  // The mean # of pending-to-be-acked tuples in spout if acking is enabled
  private final ReducedMetric<MeanReducerState, Number, Double> pendingTuplesCount;
  /*
   * Metrics for how many times spout instance task is run.
   */
  private CumulativeCountMetric taskRunCount;
  /*
   * Metrics for how many times spout produceTuple is called.
   */
  private CumulativeCountMetric produceTupleCount;
  /*
   * Metrics for how many times spout continue work is true.
   */
  private CumulativeCountMetric continueWorkCount;

  public SpoutMetrics() {
    ackCount = new CountMetric();
    completeLatency = new ReducedMetric<>(new MeanReducer());
    failLatency = new ReducedMetric<>(new MeanReducer());
    failCount = new CountMetric();
    timeoutCount = new CountMetric();
    emitCount = new CountMetric();
    nextTupleLatency = new ReducedMetric<>(new MeanReducer());
    nextTupleCount = new CountMetric();
    outQueueFullCount = new CountMetric();
    pendingTuplesCount = new ReducedMetric<>(new MeanReducer());
    tupleAddedToQueue = new CountMetric();
    taskRunCount = new CumulativeCountMetric();
    produceTupleCount = new CumulativeCountMetric();
    continueWorkCount = new CumulativeCountMetric();
  }

  public void registerMetrics(TopologyContextImpl topologyContext) {
    SystemConfig systemConfig =
        (SystemConfig) SingletonRegistry.INSTANCE.getSingleton(SystemConfig.HERON_SYSTEM_CONFIG);

    int interval = (int) systemConfig.getHeronMetricsExportInterval().getSeconds();

    topologyContext.registerMetric("__ack-count/default", ackCount, interval);
    topologyContext.registerMetric("__complete-latency/default", completeLatency, interval);
    topologyContext.registerMetric("__fail-latency/default", failLatency, interval);
    topologyContext.registerMetric("__fail-count/default", failCount, interval);
    topologyContext.registerMetric("__timeout-count/default", timeoutCount, interval);
    topologyContext.registerMetric("__emit-count/default", emitCount, interval);
    topologyContext.registerMetric("__next-tuple-latency", nextTupleLatency, interval);
    topologyContext.registerMetric("__next-tuple-count", nextTupleCount, interval);
    topologyContext.registerMetric("__out-queue-full-count", outQueueFullCount, interval);
    topologyContext.registerMetric("__pending-acked-count", pendingTuplesCount, interval);
    topologyContext.registerMetric("__data-tuple-added-to-outgoing-queue/default",
        tupleAddedToQueue, interval);
    topologyContext.registerMetric("__task-run-count", taskRunCount, interval);
    topologyContext.registerMetric("__produce-tuple-count", produceTupleCount, interval);
    topologyContext.registerMetric("__continue-work-count", continueWorkCount, interval);
  }

  // For MultiCountMetrics, we need to set the default value for all streams.
  // Otherwise, it is possible one metric for a particular stream is null.
  // For instance, the fail-count on a particular stream could be undefined
  // causing metrics not be exported.
  // However, it will not set the Multi Reduced/Assignable Metrics,
  // since we could not have default values for them
  public void initMultiCountMetrics(PhysicalPlanHelper helper) {
    // For spout, we would consider the output stream
  }

  public void ackedTuple(String streamId, long latency) {
    ackCount.incr();
    completeLatency.update(latency);
  }

  public void failedTuple(String streamId, long latency) {
    failCount.incr();
    failLatency.update(latency);
  }

  public void timeoutTuple(String streamId) {
    timeoutCount.incr();
  }

  public void emittedTuple(String streamId) {
    emitCount.incr();
  }

  public void addTupleToQueue(int size) {
    tupleAddedToQueue.incr();
  }

  public void nextTuple(long latency) {
    nextTupleLatency.update(latency);
    nextTupleCount.incr();
  }

  public void updateOutQueueFullCount() {
    outQueueFullCount.incr();
  }

  public void updatePendingTuplesCount(long count) {
    pendingTuplesCount.update(count);
  }

  public void serializeDataTuple(String streamId, long latency) {
  }

  public void updateTaskRunCount() {
    taskRunCount.incr();
  }

  public void updateProduceTupleCount() {
    produceTupleCount.incr();
  }

  public void updateContinueWorkCount() {
    continueWorkCount.incr();
  }
}


text:
![GeneralAssemb.ly](../../img/icons/FEWD_Logo.png)

#FEWD - jQuery Review

A whirlwind review of jQuery.

---

##Best Practices

- Put your jQuery scripts at the bottom of your page. This will help ensure the HTML document has time to load and your script will actually have something to target.
- Wrap your functions and your jQuery in `$(document).ready(function{ your jQuery awesomeness in here });`
- **DON'T FORGET TO INCLUDE THE JQUERY LIBRARY IN THE `<head></head>` OF YOUR PAGE!** Remember, you can Google "Google jQuery script" and get the link from the Google Developer Library.

---

##Selectors in jQuery

Remember, in jQuery, you must target something or else your script is for naught!

You can target a tag, ID, or class. Remember, the selector goes at the beginning of your jQuery statement:

```
$("#id-here")
$(".class-here")
$("p")
```
---

##Useful jQuery Functions

There are a myriad of functions you can do in jQuery once you've selected something to manipulate. You can change a CSS property, add an attribute, add or remove a class to an element, hide it, show it, etc.

---

##Useful jQuery Functions

If you want to manipulate the CSS of an element, you would use ```.css```:

```
$("p").css("color", "#333");
```

The syntax when using ```.css``` is:
.css("attribute", "value");

---

##Useful jQuery Functions

If you wanted to add an attribute to an element, you would use ```.attr```:

```
$("li.menu").attr("class", "open");
```

The syntax when using ```.attr``` is:
.attr("attributename", "value");

---

##Useful jQuery Functions

To show and hide elements, there are two different sets of functions you can use.

```.show()``` / ```.hide()``` / ```.toggle()```

or

```.slideUp()``` / ```.slideDown()``` / ```.slideToggle()```

These are the equivalent of setting ```display: block``` (show) and ```display: none``` (hide) in CSS.

---

##Useful jQuery Functions

The only difference between show/hide and slideUp/slideDown is how the element appears and disappears.

Show/Hide is like taking the bottom right corner of the box and dragging it out to reveal the item or hide the item.

SlideUp/SlideDown is the equivalent of taking the bottom edge of the element and dragging it downward or upward to show or hide it.

---

##Useful jQuery Functions

Remember that ```.toggle()``` toggles only between ```.show()``` and ```.hide()```.

Conversely, ```.slideToggle()``` toggles between ```.slideUp()``` and ```.slideDown()```.

---

##Useful jQuery Functions

The show/hide and slide functions can take a speed parameter. This controls how fast the item appears and disappears.

This speed is measured in milliseconds. So, 1 second is 1000 milliseconds. The fastest speed you can indicate is 100. The syntax is as follows:

```
$(".open").hide(100);
```

You can also use 'fast' or 'slow':

```
$(".peekaboo").slideToggle('fast');
```

---

##Useful jQuery Functions

You can add, remove, and toggle classes in jQuery. This is helpful if you want to add an active state in a menu or want to toggle between classes.

The functions are:

```
.addClass()
.removeClass()
.toggleClass()
```

You only need to put the name of the class you want to work with - with no period - in the parentheses:

```
$("p").toggleClass("red");
```

---

##Useful jQuery Functions

**Tip:** If you wish to add and remove classes, it's best to write a line of jQuery that removes the class from all similar elements first, then write another line of jQuery immediately after that adds the class to the particular element you're targeting in the function.

Same goes for when you want to show and hide items. Hide everything first, then show the thing you want.

Remember, write the general rule first, then the exceptions to the rule.

---

##Useful jQuery Functions

You can also detect the presence of a class in a particular element. You can use ```.hasClass()``` to do this. This is useful for writing if/else statements in jQuery.

---

##Keyword: "This"

In jQuery "this" refers to the selected object.

---

##This in jQuery

How about this code?

```
$('a').on('click',function(e){
    $(this).fadeOut(500);
});
```

In the above code, this means that when you click on a link, it will fade that link out at a speed of 500 milliseconds.

---

##Tree Traversal

Using ```$(this)``` is especially useful for traversing the DOM tree - in other words, navigating through the tags you have written in your HTML document.

You can use several jQuery functions to help you select nearby or related elements.

---

##Tree Traversal

The first method of traversal is using ```.prev()``` and ```.next()```.

This looks at the first selector you declared and then selects the next tag or previous tag from your starting point.

---

##Tree Traversal

For instance, if you had HTML like this:

``

Something

Something else

``

And you wrote this jQuery statement:

```
$("li.menu-item").prev()
```

You would wind up working with the ```ul``` tag.

---

##Tree Traversal

If you had HTML like this:

`

Something

Something else

`

And you wrote this jQuery statement:

```
$("li.menu-item").next()
```

You would wind up working with the ```li``` tag immediately after the one with the class of .menu-item.

---

##Tree Traversal

The other means of traversal is by using these three functions:

```
.parent()
.children()
.siblings()
```

In the case of an unordered list:

- ```ul``` would be the parent
- ```li``` would be the child of ```ul```
- all the ```li``` items within that ```ul``` are siblings

---

##Useful jQuery Scripts

You don't have to write jQuery from scratch, though you might for simple menus or other simple tasks.

But if you want to use some jQuery plugins, there are several out there that are really nifty.

---

##jQuery Slideshows

There are a billion slideshow plugins out there, but here are a few of my favorites:

- **jQuery Cycle2:** http://jquery.malsup.com/cycle2/
- **Nivo Slider:** http://dev7studios.com/plugins/nivo-slider/
- **FlexSlider:** http://www.woothemes.com/flexslider/

---

##UI Elements

Sometimes, you might want fancy elements. Maybe tabs for people to navigate through? Accordions? Carousels?

These are two really nifty libraries that have a wide range of stuff you could use:

- **jQuery UI:** http://jqueryui.com/
- **jQuery Tools:** http://jquerytools.org/

---

##Pop Me Up

You ever see those fancy popup boxes that black out the background of a page? These are often used in photo galleries, but you see other uses for them. They are called modal windows. Modal yourself here:

- **Lightbox** (actually a JavaScript library, not jQuery): http://lokeshdhakar.com/projects/lightbox2/
- **Colorbox:** http://www.jacklmoore.com/colorbox/
- **FancyBox:** http://fancyapps.com/

---

##Fancy Scrollers

You may see some nifty "one-page" sites out there, that have sliding panels or links that scroll smoothly to a different part of the page.

- **One Page Nav Sample:** http://davist11.github.io/jQuery-One-Page-Nav/
- **Another One Page Demo:** http://www.thepetedesign.com/demos/onepage_scroll_demo.html

---

##Sticky Stuff

Sometimes, you have a header or footer or other element that you want to "stick" in place. jQuery is a nifty way to do this.

- **HCSticky:** http://someweblog.com/hcsticky-jquery-floating-sticky-plugin/

---

##Equalize me!

Sometimes, you have multiple columns that you need to make sure are the same size. You could use CSS to set a min-width, but what if you're never sure what the height of your content is going to be? No problem, Equalize will rescue you:

- **Equalize:** http://tsvensen.github.io/equalize.js/

---

##Fun With Grids

Sometimes, you'll see a really cool grid layout where all the boxes aren't the same height or length, but everything fits nicely together like bricks. It makes for a nice collage effect. To achieve that, use Masonry:

- **Masonry:** http://masonry.desandro.com/

---

#...And So Much More!

Remember, Google is your friend. Chances are, if you need something in jQuery, someone has created it. Most really good jQuery plugins come with documentation that should tell you how to implement and use the plugin. So have fun (but don't go overboard)!


text:
package fileutils

import (
	"io/ioutil"
	"os"
)

func TempDir(namePrefix string, cb func(tmpDir string, err error)) {
	tmpDir, err := ioutil.TempDir("", namePrefix)

	defer func() {
		os.RemoveAll(tmpDir)
	}()

	cb(tmpDir, err)
}

func TempFile(namePrefix string, cb func(tmpFile *os.File, err error)) {
	tmpFile, err := ioutil.TempFile("", namePrefix)

	defer func() {
		tmpFile.Close()
		os.Remove(tmpFile.Name())
	}()

	cb(tmpFile, err)
}