package com.ufund.api.ufundapi.service;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import com.ufund.api.ufundapi.DuplicateKeyException;
import com.ufund.api.ufundapi.model.Need;
import com.ufund.api.ufundapi.model.Need.GoalType;
import com.ufund.api.ufundapi.persistence.CupboardDAO;

@Tag("Service-tier")
class CupboardServiceTest {

    private CupboardDAO mockCupboardDAO;
    private CupboardService cupboardService;
    private AuthService mockAuthService;

    @BeforeEach
    void setupCupboardService() {
        mockCupboardDAO = mock(CupboardDAO.class);
        mockAuthService = mock(AuthService.class);
        cupboardService = new CupboardService(mockAuthService, mockCupboardDAO);
        this.mockAuthService = mock(AuthService.class);
    }

    @Test
    void testCreateNeed() throws IOException, DuplicateKeyException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.addNeed(any())).thenReturn(need);
        when(mockCupboardDAO.getNeeds()).thenReturn(new Need[0]);

        // Invoke
        Need response = cupboardService.createNeed(name, location, image, maxGoal, type, urgent, description);

        // Analyze
        assertNotNull(response);
        assertEquals(need, response);
    }

    @Test
    void testCreateNeedBadGoal() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = -100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.addNeed(any())).thenReturn(need);
        when(mockCupboardDAO.getNeeds()).thenReturn(new Need[0]);

        // Analyze
        assertThrows(IllegalArgumentException.class, () ->
                cupboardService.createNeed(name, location, image, maxGoal, type, urgent, description));
    }

    @Test
    void testCreateNeedBadPhysicalGoal() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 1.23;
        GoalType type = Need.GoalType.PHYSICAL;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.addNeed(any())).thenReturn(need);
        when(mockCupboardDAO.getNeeds()).thenReturn(new Need[0]);

        // Analyze
        assertThrows(IllegalArgumentException.class, () ->
                cupboardService.createNeed(name, location, image, maxGoal, type, urgent, description));
    }

    @Test
    void testCreateNeedBadBlankFields() throws IOException {
        // Setup
        String name = "";
        String location = "Atlantis";
        double maxGoal = 1.23;
        GoalType type = Need.GoalType.PHYSICAL;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.addNeed(any())).thenReturn(need);
        when(mockCupboardDAO.getNeeds()).thenReturn(new Need[0]);

        // Analyze
        assertThrows(IllegalArgumentException.class, () ->
                cupboardService.createNeed(name, location, image, maxGoal, type, urgent, description));
    }

    @Test
    void testCreateNeedNullFields() throws IOException {
        // Setup
        String name = "";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.addNeed(any())).thenReturn(need);
        when(mockCupboardDAO.getNeeds()).thenReturn(new Need[0]);

        // Analyze
        assertThrows(IllegalArgumentException.class, () ->
                cupboardService.createNeed(name, location, image, maxGoal, type, urgent, description));
    }

    @Test
    void testCreateNeedDuplicate() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        Need[] needs = { need };

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.addNeed(any())).thenReturn(need);
        when(mockCupboardDAO.getNeeds()).thenReturn(needs);

        // Analyze
        assertThrows(DuplicateKeyException.class, () ->
                cupboardService.createNeed(name, location, image, maxGoal, type, urgent, description));
    }

    @Test
    void testSearchNeeds() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        Need[] needs = { need };

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.getNeeds()).thenReturn(needs);

        // Invoke
        Need[] response = cupboardService.searchNeeds("Jelly");

        // Analyze
        assertEquals(need, response[0]);
        assertEquals(need.getName(), response[0].getName());
    }

    @Test
    void testSearchNeedsFail() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        Need[] needs = { need };

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.getNeeds()).thenReturn(needs);

        // Invoke
        Need[] response = cupboardService.searchNeeds("Octopus");

        // Analyze
        assertArrayEquals(new Need[0], response);
    }

    @Test
    void testGetNeed() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.getNeed(0)).thenReturn(need);

        // Invoke
        Need response = cupboardService.getNeed(need.getId());

        // Analyze
        assertEquals(need, response);
    }

    @Test
    void testUpdateNeed() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        Need newNeed = new Need("Octopus", image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.updateNeed(any())).thenReturn(newNeed);

        // Invoke
        Need response = cupboardService.updateNeed(newNeed, need.getId());

        // Analyze
        assertEquals(newNeed, response);
    }

    @Test
    void testUpdateNeedBadGoal() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        // goal should not be 0, should throw error
        Need newNeed = new Need(name, image, location, 0, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.updateNeed(any())).thenReturn(newNeed);

        // Invoke and Analyze
        assertThrows(IllegalArgumentException.class, () ->
            cupboardService.updateNeed(newNeed, need.getId()));
    }

    @Test
    void testUpdateNeedBadPhysicalGoal() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 10;
        GoalType type = Need.GoalType.PHYSICAL;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        // goal should be integer, should throw error
        Need newNeed = new Need(name, image, location, 1.23, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.updateNeed(any())).thenReturn(newNeed);

        // Invoke and Analyze
        assertThrows(IllegalArgumentException.class, () ->
            cupboardService.updateNeed(newNeed, need.getId()));
    }

    @Test
    void testUpdateNeedBlankFields() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 10;
        GoalType type = Need.GoalType.PHYSICAL;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        // passed in blank name, should throw error
        Need newNeed = new Need("", image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.updateNeed(any())).thenReturn(newNeed);

        // Invoke and Analyze
        assertThrows(IllegalArgumentException.class, () ->
            cupboardService.updateNeed(newNeed, need.getId()));
    }

    @Test
    void testUpdateNeedNotMatchingIDs() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 10;
        GoalType type = Need.GoalType.PHYSICAL;
        boolean urgent = false;
        String image = "";
        String description = "";
        // passed in blank name, should throw error
        Need newNeed = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.updateNeed(any())).thenReturn(newNeed);

        // Invoke and Analyze
        assertThrows(IllegalArgumentException.class, () ->
            cupboardService.updateNeed(newNeed, -1));
    }

    @Test
    void testDeleteNeed() throws IOException {
        // Setup
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);

        // When the same id is passed in, our mock User DAO will return the User object
        when(mockCupboardDAO.deleteNeed(0)).thenReturn(true);
        when(mockCupboardDAO.getNeeds()).thenReturn(new Need[0]);

        // Invoke
        boolean response = cupboardService.deleteNeed(need.getId());
        Need[] responseNeeds = cupboardService.getNeeds();

        // Analyze
        assertTrue(response);
        assertArrayEquals(new Need[0], responseNeeds);
    }

    @Test
    void checkoutNeed() throws IOException, IllegalAccessException {
        int id = 1;
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = Need.GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        String key = "anything";
        var amount = 10;

        doNothing().when(mockAuthService).keyIsValid(key);
        when(mockCupboardDAO.getNeed(id)).thenReturn(need);

        assertDoesNotThrow(() -> cupboardService.checkoutNeed(id, amount, key));
    }

    @Test
    void checkoutNeed2() {
        int id = 1;
        String key = "anything";
        var amount = -5;

        assertThrows(IllegalArgumentException.class, () -> cupboardService.checkoutNeed(id, amount, key));
    }

    @Test
    void checkoutNeed3() throws IOException {
        int id = 1;
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = GoalType.PHYSICAL;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        String key = "anything";
        var amount = 3.1459;

        when(mockCupboardDAO.getNeed(id)).thenReturn(need);

        assertThrows(IllegalArgumentException.class, () -> cupboardService.checkoutNeed(id, amount, key));
    }

    @Test
    void checkoutNeed4() throws IOException {
        int id = 1;
        String name = "Jellyfish";
        String location = "Atlantis";
        double maxGoal = 100.0;
        GoalType type = GoalType.MONETARY;
        boolean urgent = false;
        String image = "";
        String description = "";
        var need = new Need(name, image, location, maxGoal, type, urgent, description);
        String key = "anything";
        var amount = 3.1459;

        when(mockCupboardDAO.getNeed(id)).thenReturn(need);

        assertDoesNotThrow(() -> cupboardService.checkoutNeed(id, amount, key));
    }
}