package dst.ass2.service.courseplan.tests;

import dst.ass1.jpa.dao.IDAOFactory;
import dst.ass1.jpa.dao.IParticipantDAO;
import dst.ass1.jpa.model.*;
import dst.ass1.jpa.tests.TestData;
import dst.ass2.service.api.courseplan.CourseNotAvailableException;
import dst.ass2.service.api.courseplan.CoursePlan;
import dst.ass2.service.api.courseplan.EntityNotFoundException;
import dst.ass2.service.api.courseplan.ICoursePlanService;
import dst.ass2.service.courseplan.CoursePlanApplication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Collection;
import java.util.Optional;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = CoursePlanApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@org.springframework.transaction.annotation.Transactional
@ActiveProfiles("testdata")
public class CoursePlanServiceTest implements ApplicationContextAware {

    private static final Logger LOG = LoggerFactory.getLogger(CoursePlanServiceTest.class);

    private ApplicationContext ctx;

    @PersistenceContext
    private EntityManager em;

    private ICoursePlanService service;
    private IDAOFactory daoFactory;
    private IModelFactory modelFactory;
    private TestData testData;
    private IParticipantDAO participantDAO;

    private Long p1cp1p;
    private Long p1cp2np;
    private Long p2cp1np;
    private Long p2cp2p;
    private Long course4Id;

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.ctx = ctx;
    }

    @Before
    public void setUp() {
        LOG.info("Test resolving beans from application context");
        daoFactory = ctx.getBean(IDAOFactory.class);
        modelFactory = ctx.getBean(IModelFactory.class);
        service = ctx.getBean(ICoursePlanService.class);
        testData = ctx.getBean(TestData.class);
        participantDAO = daoFactory.createParticipantDAO();

        // course4Id is wrong in TestData, find out the correct ID
        course4Id = daoFactory.createCourseDAO().findAll().stream()
                .filter(c -> c.getName().equals(TestData.COURSE_4_NAME)).findFirst().get().getId();

        // Create an additional membership for tests
        IMembership membership4 = modelFactory.createMembership();
        membership4.setDiscount(10.0);
        membership4.setPremium(true);
        membership4.setParticipant(participantDAO.findById(testData.participant2Id));
        membership4.setCoursePlatform(daoFactory.createCoursePlatformDAO().findById(testData.coursePlatform2Id));
        IParticipant participant2 = participantDAO.findById(testData.participant2Id);
        participant2.getMemberships().add(membership4);
        em.persist(membership4);
        em.persist(participant2);

        // find the IDs of the memberships
        Collection<IMembership> memberships1 = participantDAO.findById(testData.participant1Id).getMemberships();
        Collection<IMembership> memberships2 = participantDAO.findById(testData.participant2Id).getMemberships();

        // participant 1, course platform 1, premium
        p1cp1p = memberships1.stream().filter(m -> m.getPremium()).findFirst().get().getId();

        // participant 1, course platform 2, non-premium
        p1cp2np = memberships1.stream().filter(m -> !m.getPremium()).findFirst().get().getId();

        // participant 2, course platform 1, non-premium
        p2cp1np = memberships2.stream().filter(m -> !m.getPremium()).findFirst().get().getId();

        // participant 2, course platform 2, premium
        p2cp2p = memberships2.stream().filter(m -> m.getPremium()).findFirst().get().getId();
    }

    @Test
    public void testCreateWithValidMembership_returnsNewCoursePlan() throws EntityNotFoundException {
        CoursePlan plan = service.create(p1cp1p);
        assertNotNull(plan);
        assertNotNull(plan.getId());
        assertEquals(p1cp1p, plan.getMembershipId());
    }

    @Test(expected = EntityNotFoundException.class)
    public void testCreateWithInvalidMembership_throwsException() throws EntityNotFoundException {
        service.create(1337L);
    }

    @Test(expected = EntityNotFoundException.class)
    public void testDeleteInvalidId_throwsException() throws EntityNotFoundException {
        service.delete(1337L);
    }

    @Test
    public void testDelete_findReturnsNull() throws EntityNotFoundException {
        CoursePlan plan = service.create(p1cp1p);
        assertNotNull(plan);
        assertNotNull(plan.getId());
        service.delete(plan.getId());
        assertNull(service.find(plan.getId()));
    }

    @Test
    public void testFind_returnsCorrectPlan() throws EntityNotFoundException {
        CoursePlan plan = service.create(p1cp1p);
        CoursePlan plan1 = service.find(plan.getId());
        assertEquals(plan, plan1);
    }

    @Test
    public void testFindInvalidId_returnsNull() {
        assertNull(service.find(1337L));
    }

    @Test
    public void testAdd_courseAdded() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p1cp2np);
        assertTrue(service.addCourse(plan, testData.course2Id));
        assertFalse(plan.getCourses().isEmpty());
        assertEquals(testData.course2Id, plan.getCourses().get(0));
    }

    @Test
    public void testAddPremiumCourseWithPremium_courseAdded() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p2cp2p);
        assertTrue(service.addCourse(plan, course4Id));
        assertFalse(plan.getCourses().isEmpty());
        assertEquals(course4Id, plan.getCourses().get(0));
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testAddCourseFromDifferentPlatform_throwsException() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p2cp1np);
        service.addCourse(plan, course4Id);
    }

    @Test
    public void testAddExistingCourse_returnsFalse() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p1cp2np);
        assertTrue(service.addCourse(plan, testData.course2Id));
        assertFalse(service.addCourse(plan, testData.course2Id));
    }

    @Test(expected = EntityNotFoundException.class)
    public void testAddInvalidCourse_throwsException() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p2cp1np);
        service.addCourse(plan, 1337L);
    }

    @Test(expected = EntityNotFoundException.class)
    public void testAddInvalidMembership_throwsException() throws EntityNotFoundException {
        service.create(1337L);
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testAddPremiumCourseWithoutPremium_throwsException() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p2cp1np);
        service.addCourse(plan, testData.course1Id);
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testAddCourseWithoutCapacity_throwsException() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p1cp1p);
        service.addCourse(plan, testData.course3Id);
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testAddCourseAlreadyInDB_throwsException() throws CourseNotAvailableException, EntityNotFoundException {
        CoursePlan plan = service.create(p1cp1p);
        service.addCourse(plan, testData.course1Id);
    }

    @Test
    public void testRemoveCourse_courseRemoved() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p2cp2p);
        assertTrue(service.addCourse(plan, course4Id));
        assertTrue(service.removeCourse(plan, course4Id));
        CoursePlan plan1 = service.find(plan.getId());
        assertTrue(plan1.getCourses().isEmpty());
    }

    @Test
    public void testRemoveNotAddedCourse_returnsFalse() throws EntityNotFoundException {
        CoursePlan plan = service.create(p1cp1p);
        assertFalse(service.removeCourse(plan, testData.course1Id));
    }

    @Test
    public void testCommit_committed() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p1cp2np);
        assertTrue(service.addCourse(plan, testData.course2Id));
        service.commit(plan);
        assertTrue(isEnrolled(testData.participant1Id, testData.course2Id));
    }

    @Test
    public void testCommitPremiumCourseWithPremium_committed() throws EntityNotFoundException, CourseNotAvailableException {
        CoursePlan plan = service.create(p2cp2p);
        assertTrue(service.addCourse(plan, course4Id));
        service.commit(plan);
        assertTrue(isEnrolled(testData.participant2Id, course4Id));
    }

    @Test(expected = EntityNotFoundException.class)
    public void testCommitWithInvalidMembership_throwsException() throws CourseNotAvailableException, EntityNotFoundException {
        CoursePlan plan = service.create(p1cp2np);
        assertTrue(service.addCourse(plan, testData.course2Id));
        em.remove(daoFactory.createMembershipDAO().findById(p1cp2np));
        service.commit(plan);
    }

    @Test(expected = EntityNotFoundException.class)
    public void testCommitWithInvalidCourse_throwsException() throws CourseNotAvailableException, EntityNotFoundException {
        CoursePlan plan = service.create(p1cp2np);
        assertTrue(service.addCourse(plan, testData.course2Id));
        em.remove(daoFactory.createCourseDAO().findById(testData.course2Id));
        service.commit(plan);
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testCommitPremiumCourseWithoutPremium_throwsException() throws CourseNotAvailableException, EntityNotFoundException {
        CoursePlan plan = service.create(p2cp2p);
        assertTrue(service.addCourse(plan, course4Id));
        IMembership membership = daoFactory.createMembershipDAO().findById(p2cp2p);
        membership.setPremium(false);
        em.persist(membership);
        service.commit(plan);
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testCommitCourseFromDifferentPlatform_throwsException() throws CourseNotAvailableException, EntityNotFoundException {
        CoursePlan plan = service.create(p2cp2p);
        assertTrue(service.addCourse(plan, course4Id));
        IMembership membership = daoFactory.createMembershipDAO().findById(p2cp2p);
        membership.setCoursePlatform(daoFactory.createCoursePlatformDAO().findById(testData.coursePlatform1Id));
        em.persist(membership);
        service.commit(plan);
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testCommitCourseWithoutCapacity_throwsException() throws CourseNotAvailableException, EntityNotFoundException {
        CoursePlan plan = service.create(p1cp2np);
        assertTrue(service.addCourse(plan, testData.course2Id));
        ICourse course = daoFactory.createCourseDAO().findById(testData.course2Id);
        course.setCapacity(0);
        em.persist(course);
        service.commit(plan);
    }

    @Test(expected = CourseNotAvailableException.class)
    public void testCommitCourseAlreadyInDB_throwsException() throws CourseNotAvailableException, EntityNotFoundException {
        CoursePlan plan = service.create(p1cp2np);
        service.addCourse(plan, testData.course2Id);
        service.commit(plan);
        service.commit(plan);
    }

    private boolean isEnrolled(Long participantId, Long courseId) {
        Optional<IEnrollment> first = participantDAO.findById(participantId).getEnrollments().stream().filter(e -> e.getId().getCourse().getId().equals(courseId)).findFirst();
        return first.isPresent();
    }
}
