package dst.ass2.service.courseplan.tests;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import dst.ass1.jpa.tests.TestData;
import dst.ass2.service.api.courseplan.CoursePlan;
import dst.ass2.service.courseplan.CoursePlanApplication;

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

    @Autowired
    private TestData testData;

    @LocalServerPort
    private int port;

    private TestRestTemplate restTemplate;
    private HttpHeaders headers;

    @Before
    public void setUp() {
        headers = new HttpHeaders();
        restTemplate = new TestRestTemplate();
    }

    @Test
    public void createCoursePlan_withWrongHttpMethod_returnsError() {
        String url = url("/courseplans");
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class, body("membershipId", testData.membership1Id));

        assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.CLIENT_ERROR));
    }

    @Test
    public void createCoursePlan_withNonExistingMembershipKey_returnsNotFoundError() {
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        String url = url("/courseplans");
        MultiValueMap<String, String> body = body("membershipId", 123456789);

        HttpEntity<?> request = new HttpEntity<>(body, headers);
        ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);

        assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND));
    }

    @Test
    public void createCoursePlan_withExistingMembershipKey_returnsCourseplanId() {
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        String url = url("/courseplans");
        MultiValueMap<String, String> body = body("membershipId", testData.membership1Id);

        HttpEntity<?> request = new HttpEntity<>(body, headers);
        ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);

        assertThat(response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL));
        assertThat(response.getBody(), notNullValue());

        try {
            Long.parseLong(response.getBody());
        } catch (NumberFormatException e) {
            throw new AssertionError("Response body of " + url + " should be a Long value (the courseplan ID)", e);
        }
    }

    @Test
    public void getCoursePlan_onNonExistingPlan_returns404() {
        String url = url("/courseplans/" + 12345678);
        ResponseEntity<?> response = restTemplate.getForEntity(url, String.class);
        assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND));
    }

    @Test
    public void getCoursePlan_onCreatedCoursePlan_returnsJsonEntity() {
        Long id = createCoursePlan(testData.membership1Id);

        String url = url("/courseplans/" + id);
        ResponseEntity<CoursePlan> response = restTemplate.getForEntity(url, CoursePlan.class);

        assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL));
        assertThat(response.getBody(), notNullValue());
        assertThat(response.getBody().getId(), is(id));
        assertThat(response.getBody().getMembershipId(), is(testData.membership1Id));
    }

    @Test
    public void addCourse_onCreatedCoursePlan_returnsOk() {
        Long id = createCoursePlan(testData.membership2Id);
        ResponseEntity<String> response = addCourse(id, testData.course2Id);
        assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL));
    }

    @Test
    public void addCourse_andThenGetCoursePlan_containsCourseInList() {
        Long id = createCoursePlan(testData.membership2Id);

        addCourse(id, testData.course2Id);
        CoursePlan coursePlan = getCoursePlan(id);

        assertThat(coursePlan.getCourses().size(), is(1));
        assertThat(coursePlan.getCourses(), hasItem(testData.course2Id));
    }

    @Test
    public void addCourse_onUnavailableCourse_returnsAppropriateError() {
        Long id = createCoursePlan(testData.membership1Id);
        ResponseEntity<String> response = addCourse(id, testData.course1Id);

        assertThat("Make use of an appropriate HTTP status code", response.getStatusCode(), allOf(
                is(not(HttpStatus.OK)),
                is(not(HttpStatus.NOT_FOUND)),
                is(not(HttpStatus.INTERNAL_SERVER_ERROR))
        ));
    }

    @Test
    public void addCourse_onNonExistingCourse_returns404() {
        Long id = createCoursePlan(testData.membership1Id);
        ResponseEntity<String> response = addCourse(id, 123456789L);
        assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND));
    }

    @Test
    public void submit_nonExistingCoursePlan_returns404() {
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        String url = url("/courseplans/123456789/submit");
        ResponseEntity<String> response = restTemplate.postForEntity(url, new LinkedMultiValueMap<>(), String.class);
        assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND));
    }

    @Test
    public void submit_onExistingCoursePlan_returnsOk() {
        Long id = createCoursePlan(testData.membership2Id);
        addCourse(id, testData.course3Id);

        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        String url = url("/courseplans/" + id + "/submit");
        ResponseEntity<String> response = restTemplate.postForEntity(url, new LinkedMultiValueMap<>(), String.class);
        assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL));
    }

    @Test
    public void delete_existingCoursePlan_returnsSuccessful() {
        Long id = createCoursePlan(testData.membership1Id);
        String url = url("/courseplans/" + id);

        HttpEntity<?> request = new HttpEntity<>(null, headers);

        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class);
        assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL));
    }

    @Test
    public void delete_nonExistingCoursePlan_returns404() {
        String url = url("/courseplans/12345678");
        HttpEntity<?> request = new HttpEntity<>(null, headers);

        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class);
        assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND));
    }

    private CoursePlan getCoursePlan(Long id) {
        String url = url("/courseplans/" + id);
        ResponseEntity<CoursePlan> response = restTemplate.getForEntity(url, CoursePlan.class);
        return response.getBody();
    }

    private Long createCoursePlan(Long membershipId) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        String url = url("/courseplans");
        MultiValueMap<String, String> body = body("membershipId", membershipId);
        HttpEntity<?> request = new HttpEntity<>(body, headers);
        ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);

        if(!response.getStatusCode().is2xxSuccessful()) {
            throw new AssertionError("Expected createCoursePlan to return 2xx, instead got: " + response);
        }

        try {
            return Long.parseLong(response.getBody());
        } catch (NumberFormatException e) {
            throw new AssertionError("Expected createCoursePlan to return an id, instead got: " + response);
        }
    }

    private ResponseEntity<String> addCourse(Long coursePlanId, Long courseId) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        String url = url("/courseplans/" + coursePlanId + "/courses");
        MultiValueMap<String, String> body = body("courseId", courseId);
        return restTemplate.postForEntity(url, new HttpEntity<>(body, headers), String.class);
    }

    private String url(String uri) {
        return "http://localhost:" + port + uri;
    }

    private MultiValueMap<String, String> body(String key, Object value) {
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add(key, String.valueOf(value));
        return map;
    }

}
