package dev.fyloz.trial.colorrecipesexplorer.service

import com.nhaarman.mockitokotlin2.*
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.trial.colorrecipesexplorer.service.files.FilesService
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockMultipartFile
import java.io.File
import java.nio.file.NoSuchFileException
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class RecipeServiceTest :
    AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
    override val repository: RecipeRepository = mock()
    private val companyService: CompanyService = mock()
    private val mixService: MixService = mock()
    override val service: RecipeService = spy(RecipeServiceImpl(repository, companyService, mixService))

    private val company: Company = company(id = 0L)
    override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)
    override val anotherEntity: Recipe = recipe(id = 1L, name = "another recipe", company = company)
    override val entitySaveDto: RecipeSaveDto = spy(recipeSaveDto(name = entity.name, companyId = entity.company.id!!))
    override val entityUpdateDto: RecipeUpdateDto = spy(recipeUpdateDto(id = entity.id!!, name = entity.name))

    @Nested
    inner class ExistsByCompany {
        @Test
        fun `returns true when at least one recipe exists for the given company`() {
            whenever(repository.existsByCompany(company)).doReturn(true)

            val found = service.existsByCompany(company)

            assertTrue(found)
        }

        @Test
        fun `returns false when no recipe exists for the given company`() {
            whenever(repository.existsByCompany(company)).doReturn(false)

            val found = service.existsByCompany(company)

            assertFalse(found)
        }
    }

    @Nested
    inner class GetAllByCompany {
        @Test
        fun `returns the recipes with the given company`() {
            val companies = listOf(entity, anotherEntity)
            whenever(repository.findAllByCompany(company)).doReturn(companies)

            val found = service.getAllByCompany(company)

            assertEquals(companies, found)
        }
    }

    @Nested
    inner class SaveDto {
        @Test
        fun `calls and returns save() with the created entity`() {
            whenever(companyService.getById(company.id!!)).doReturn(company)
            saveDtoTest(entity, entitySaveDto, service)
        }
    }

    @Nested
    inner class UpdatePublicDate {
        @Test
        fun `calls update with the updated note`() {
            val publicDataDto = recipePublicDataDto(id = entity.id!!, note = "newNote", mixesLocation = null)
            val expected = entity.apply { note = publicDataDto.note!! }
            doReturn(entity).whenever(service).getById(entity.id!!)
            doReturn(expected).whenever(service).update(expected)

            service.updatePublicData(publicDataDto)

            verify(service).update(expected)
        }

        @Test
        fun `calls mixService_updateLocation() with every mix and locations`() {
            val mix = mix(id = 0L)
            val mixRecipe = entity.apply { mixes.add(mix) }
            val location = "location"
            val mixLocation = mapOf(mix.id!! to location)
            val publicDataDto = recipePublicDataDto(id = mixRecipe.id!!, note = null, mixesLocation = mixLocation)
            doReturn(mixRecipe).whenever(service).getById(mixRecipe.id!!)

            service.updatePublicData(publicDataDto)

            verify(mixService).updateLocation(mix, location)
        }
    }

    @Nested
    inner class AddMix {
        @Test
        fun `adds the given mix to the given recipe and updates it`() {
            val mix = mix(id = 0L)
            val recipe = recipe(id = 0L, mixes = mutableListOf())

            doAnswer { it.arguments[0] }.whenever(service).update(any<Recipe>())

            val found = service.addMix(recipe, mix)

            verify(service).update(any<Recipe>())

            assertEquals(recipe.id, found.id)
            assertTrue(found.mixes.contains(mix))
        }
    }

    @Nested
    inner class RemoveMix {
        @Test
        fun `removes the given mix from its recipe and updates it`() {
            val recipe = recipe(id = 0L, mixes = mutableListOf())
            val mix = mix(id = 0L, recipe = recipe)
            recipe.mixes.add(mix)

            doAnswer { it.arguments[0] }.whenever(service).update(any<Recipe>())

            val found = service.removeMix(mix)

            verify(service).update(any<Recipe>())

            assertEquals(recipe.id, found.id)
            assertFalse(found.mixes.contains(mix))
        }
    }
}

class RecipeImageServiceTest {
    private val recipeService: RecipeService = mock()
    private val fileService: FilesService = mock()
    private val service = spy(RecipeImageServiceImpl(recipeService, fileService))

    private val recipeId = 1L
    private val imageId = 5L
    private val imagePath = "$RECIPE_IMAGES_DIRECTORY/$recipeId/$imageId"
    private val recipe = recipe(id = recipeId)
    private val recipeDirectory: File = mock()
    private val imagesIds = listOf(1L, 3L, 10L, 21L)
    private val imageData = byteArrayOf(64, 32, 16, 8, 4, 2, 1)
    private val image = MockMultipartFile("$imageId", imageData)

    @AfterEach
    internal fun tearDown() {
        reset(recipeService, fileService, service, recipeDirectory)
    }

    @Nested
    inner class GetByIdForRecipe {
        @Test
        fun `returns data for the given recipe and image id red by the file service`() {
            whenever(fileService.getPath(imagePath)).doReturn(imagePath)
            whenever(fileService.readAsBytes(imagePath)).doReturn(imageData)

            val found = service.getByIdForRecipe(imageId, recipeId)

            assertEquals(imageData, found)
        }

        @Test
        fun `throws EntityNotFoundRestException when no image with the given recipe and image id exists`() {
            doReturn(imagePath).whenever(service).getPath(imageId, recipeId)
            whenever(fileService.readAsBytes(imagePath)).doThrow(NoSuchFileException(imagePath))

            val exception =
                assertThrows<EntityNotFoundRestException> { service.getByIdForRecipe(imageId, recipeId) }
            assertEquals("$recipeId/$imageId", exception.value)
        }
    }

    @Nested
    inner class GetAllIdsForRecipe {
        @Test
        fun `returns a list containing all image's identifier of the images of the given recipe`() {
            val expectedFiles = imagesIds.map { File(it.toString()) }.toTypedArray()

            whenever(recipeService.getById(recipeId)).doReturn(recipe)
            whenever(recipeDirectory.exists()).doReturn(true)
            whenever(recipeDirectory.isDirectory).doReturn(true)
            whenever(recipeDirectory.listFiles()).doReturn(expectedFiles)
            doReturn(recipeDirectory).whenever(service).getRecipeDirectory(recipeId)

            val found = service.getAllIdsForRecipe(recipeId)

            assertEquals(imagesIds, found)
        }

        @Test
        fun `returns an empty list when the given recipe's directory does not exists`() {
            whenever(recipeService.getById(recipeId)).doReturn(recipe)
            whenever(recipeDirectory.exists()).doReturn(false)
            whenever(recipeDirectory.isDirectory).doReturn(true)
            doReturn(recipeDirectory).whenever(service).getRecipeDirectory(recipeId)

            val found = service.getAllIdsForRecipe(recipeId)

            assertTrue(found.isEmpty())
        }

        @Test
        fun `returns an empty list when the given recipe's directory is not a directory`() {
            whenever(recipeService.getById(recipeId)).doReturn(recipe)
            whenever(recipeDirectory.exists()).doReturn(true)
            whenever(recipeDirectory.isDirectory).doReturn(false)
            doReturn(recipeDirectory).whenever(service).getRecipeDirectory(recipeId)

            val found = service.getAllIdsForRecipe(recipeId)

            assertTrue(found.isEmpty())
        }
    }

    @Nested
    inner class Save {
        @Test
        fun `writes the given image to the file service with the expected path`() {
            val expectedNextAvailableId = imagesIds.maxOrNull()!! + 1
            val imagePath = "$RECIPE_IMAGES_DIRECTORY/$recipeId/$expectedNextAvailableId"

            doReturn(imagesIds).whenever(service).getAllIdsForRecipe(recipeId)
            doReturn(imagePath).whenever(service).getPath(expectedNextAvailableId, recipeId)

            service.save(image, recipeId)

            verify(fileService).write(image, imagePath)
        }
    }

    @Nested
    inner class Delete {
        @Test
        fun `deletes the image with the given recipe and image id from the file service`() {
            doReturn(imagePath).whenever(service).getPath(imageId, recipeId)

            service.delete(imageId, recipeId)

            verify(fileService).delete(imagePath)
        }
    }

    @Nested
    inner class GetRecipeDirectory {
        @Test
        fun `returns a file with the expected path`() {
            val recipeDirectoryPath = "$RECIPE_IMAGES_DIRECTORY/$recipeId"
            whenever(fileService.getPath(recipeDirectoryPath)).doReturn(recipeDirectoryPath)

            val found = service.getRecipeDirectory(recipeId)

            assertEquals(recipeDirectoryPath, found.path)
        }
    }

    @Nested
    inner class GetPath {
        @Test
        fun `returns the expected path`() {
            whenever(fileService.getPath(any())).doAnswer { it.arguments[0] as String }

            val found = service.getPath(imageId, recipeId)

            assertEquals(imagePath, found)
        }
    }
}
