package org.todo;

import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.todo.controller.TodoCompleteDto;
import org.todo.controller.TodoCreateDto;
import org.todo.model.todo.Todo;
import org.todo.model.todo.Todo.Category;
import org.todo.model.user.User;

import java.time.LocalDate;
import java.util.Random;

import static io.restassured.RestAssured.given;
import static io.restassured.http.ContentType.JSON;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

public class TodoRestServiceIT {

    @BeforeAll
    public static void setBaseURI() {
        RestAssured.baseURI = "http://localhost:8080/api";
    }

    @Test
    public void registerUser() {
        User user = new User("user" + new Random().nextInt(100), "12345");
        given().contentType(JSON).body(user)
                .when().post("/users")
                .then().statusCode(201);
    }

    @Test
    public void registerUserWithInvalidUsername() {
        User user = new User("user$", "12345");
        given().contentType(JSON).body(user)
                .when().post("/users")
                .then().statusCode(400);
    }

    @Test
    public void registerUserWithInvalidPassword() {
        User user = new User("user", "1234");
        given().contentType(JSON).body(user)
                .when().post("/users")
                .then().statusCode(400);
    }

    @Test
    public void registerExistingUser() {
        User user = new User("test", "12345");
        given().contentType(JSON).body(user)
                .when().post("/users")
                .then().statusCode(409);
    }

    @Test
    public void authenticateUser() {
        given().auth().preemptive().basic("test", "12345")
                .when().head("/auth")
                .then().statusCode(204);
    }

    @Test
    public void authenticateNonExistingUser() {
        given().auth().preemptive().basic("unknown", "12345")
                .when().head("/auth")
                .then().statusCode(401);
    }

    @Test
    public void authenticateUserWithInvalidPassword() {
        given().auth().preemptive().basic("test", "invalid")
                .when().head("/auth")
                .then().statusCode(401);
    }

    @Test
    public void getCategories() {
        given().accept(JSON)
                .when().get("/categories")
                .then().statusCode(200).body("$", hasSize(5));
    }

    @Test
    public void addTodo() {
        TodoCreateDto todoDto = new TodoCreateDto("Something", Category.HOME, LocalDate.now());
        givenBasicAuth("test", "12345").contentType(JSON).body(todoDto)
                .when().post("/todos")
                .then().statusCode(201).body("id", notNullValue()).body("completed", equalTo(false));
    }

    @Test
    public void addTodoWithoutTitle() {
        TodoCreateDto todoDto = new TodoCreateDto(null, Category.HOME, LocalDate.now());
        givenBasicAuth("test", "12345").contentType(JSON).body(todoDto)
                .when().post("/todos")
                .then().statusCode(400);
    }

    @Test
    public void addTodoWithoutAuthentication() {
        TodoCreateDto todoDto = new TodoCreateDto("Something", Category.HOME, LocalDate.now());
        given().contentType(JSON).body(todoDto)
                .when().post("/todos")
                .then().statusCode(401);
    }

    @Test
    public void getTodos() {
        givenBasicAuth("test", "12345").accept(JSON)
                .when().get("/todos")
                .then().statusCode(200).body("$", hasItem(hasEntry("title", "Test")));
    }

    @Test
    public void getTodosWithCategory() {
        givenBasicAuth("test", "12345").accept(JSON).param("category", Category.WORK)
                .when().get("/todos")
                .then().statusCode(200).body("$", not(empty()));
        givenBasicAuth("test", "12345").accept(JSON).param("category", Category.PERSONAL)
                .when().get("/todos")
                .then().statusCode(200).body("$", empty());
    }

    @Test
    public void getTodosWithoutAuthentication() {
        given().accept(JSON)
                .when().get("/todos")
                .then().statusCode(401);
    }

    @Test
    public void getTodo() {
        givenBasicAuth("test", "12345").accept(JSON)
                .when().get("/todos/" + 1)
                .then().statusCode(200).body("title", equalTo("Test"));
    }

    @Test
    public void getNonExistingTodo() {
        givenBasicAuth("test", "12345").accept(JSON)
                .when().get("/todos/" + 0)
                .then().statusCode(404);
    }

    @Test
    public void getTodoWithoutAuthentication() {
        given().accept(JSON)
                .when().get("/todos/" + 1)
                .then().statusCode(401);
    }

    @Test
    public void getTodoWithoutAuthorization() {
        givenBasicAuth("test2", "12345").accept(JSON)
                .when().get("/todos/" + 1)
                .then().statusCode(403);
    }

    @Test
    public void updateTodo() {
        TodoCreateDto todoDto = new TodoCreateDto("Something", Category.HOME, LocalDate.now());
        Todo todo = givenBasicAuth("test", "12345").contentType(JSON).body(todoDto).post("/todos").as(Todo.class);
        LocalDate tomorrow = LocalDate.now().plusDays(1);
        todo.setDueDate(tomorrow);
        givenBasicAuth("test", "12345").contentType(JSON).body(todo)
                .when().put("/todos/" + todo.getId())
                .then().statusCode(200).body("dueDate", equalTo(tomorrow.toString()));
    }

    @Test
    public void updateNonExistingTodo() {
        Todo todo = new Todo(0, "Something", Category.HOME, LocalDate.now());
        givenBasicAuth("test", "12345").contentType(JSON).body(todo)
                .when().put("/todos/" + 0)
                .then().statusCode(404);
    }

    @Test
    public void updateTodoWithMismatchingId() {
        Todo todo = new Todo(0, "Something", Category.HOME, LocalDate.now());
        givenBasicAuth("test", "12345").contentType(JSON).body(todo)
                .when().put("/todos/" + 1)
                .then().statusCode(400);
    }

    @Test
    public void updateTodoWithoutAuthentication() {
        Todo todo = new Todo(1, "Something", Category.HOME, LocalDate.now());
        given().contentType(JSON).body(todo)
                .when().put("/todos/" + 1)
                .then().statusCode(401);
    }

    @Test
    public void updateTodoWithoutAuthorization() {
        Todo todo = new Todo(1, "Something", Category.HOME, LocalDate.now());
        givenBasicAuth("test2", "12345").contentType(JSON).body(todo)
                .when().put("/todos/" + 1)
                .then().statusCode(403);
    }

    @Test
    public void completeTodo() {
        givenBasicAuth("test", "12345").contentType(JSON).body(new TodoCompleteDto(true))
                .when().patch("/todos/" + 1)
                .then().statusCode(200).body("completed", equalTo(true));
    }

    @Test
    public void completeNonExistingTodo() {
        givenBasicAuth("test", "12345").contentType(JSON).body(new TodoCompleteDto(true))
                .when().patch("/todos/" + 0)
                .then().statusCode(404);
    }

    @Test
    public void completeTodoWithoutAuthentication() {
        given().contentType(JSON).body(new TodoCompleteDto(true))
                .when().patch("/todos/" + 1)
                .then().statusCode(401);
    }

    @Test
    public void completeTodoWithoutAuthorization() {
        givenBasicAuth("test2", "12345").contentType(JSON).body(new TodoCompleteDto(true))
                .when().patch("/todos/" + 1)
                .then().statusCode(403);
    }

    @Test
    public void deleteTodo() {
        TodoCreateDto todoDto = new TodoCreateDto("Something", Category.HOME, LocalDate.now());
        Todo todo = givenBasicAuth("test", "12345").contentType(JSON).body(todoDto).post("/todos").as(Todo.class);
        givenBasicAuth("test", "12345")
                .when().delete("/todos/" + todo.getId())
                .then().statusCode(204);
        givenBasicAuth("test", "12345").accept(JSON).when().get("/todos/" + todo.getId()).then().statusCode(404);
    }

    @Test
    public void deleteNonExistingTodo() {
        givenBasicAuth("test", "12345")
                .when().delete("/todos/" + 0)
                .then().statusCode(404);
    }

    @Test
    public void deleteTodoWithoutAuthentication() {
        given()
                .when().delete("/todos/" + 1)
                .then().statusCode(401);
    }

    @Test
    public void deleteTodoWithoutAuthorization() {
        givenBasicAuth("test2", "12345")
                .when().delete("/todos/" + 1)
                .then().statusCode(403);
    }

    private RequestSpecification givenBasicAuth(String username, String password) {
        return given().auth().preemptive().basic(username, password);
    }
}
