package dev.fyloz.trial.colorrecipesexplorer.service

import com.nhaarman.mockitokotlin2.*
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityAlreadyExistsRestException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.repository.MaterialTypeRepository
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 kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class MaterialTypeServiceTest : AbstractNamedModelServiceTest<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService, MaterialTypeRepository>() {
    override val repository: MaterialTypeRepository = mock()
    private val materialService: MaterialService = mock()
    override val service: MaterialTypeService = spy(MaterialTypeServiceImpl(repository, materialService))
    override val entity: MaterialType = materialType(id = 0L, name = "material type", prefix = "MAT")
    override val anotherEntity: MaterialType = materialType(id = 1L, name = "another material type", prefix = "AMT")
    override val entityWithEntityName: MaterialType = materialType(2L, name = entity.name, prefix = "EEN")
    private val systemType = materialType(id = 3L, name = "systype", prefix = "SYS", systemType = true)
    private val anotherSystemType = materialType(id = 4L, name = "another systype", prefix = "ASY", systemType = true)
    override val entitySaveDto: MaterialTypeSaveDto = spy(materialTypeSaveDto(name = "material type", prefix = "MAT"))
    override val entityUpdateDto: MaterialTypeUpdateDto = spy(materialTypeUpdateDto(id = 0L, name = "material type", prefix = "MAT"))

    @AfterEach
    override fun afterEach() {
        reset(materialService)
        super.afterEach()
    }

    @Nested
    inner class ExistsByPrefix {
        @Test
        fun `returns true when a material type with the given prefix exists`() {
            whenever(repository.existsByPrefix(entity.prefix)).doReturn(true)

            val found = service.existsByPrefix(entity.prefix)

            assertTrue(found)
        }

        @Test
        fun `returns false when no material type with the given prefix exists`() {
            whenever(repository.existsByPrefix(entity.prefix)).doReturn(false)

            val found = service.existsByPrefix(entity.prefix)

            assertFalse(found)
        }
    }

    @Nested
    inner class IsUsedByMaterial {
        @Test
        fun `returns true when materialService_existsByMaterialType() returns true`() {
            whenever(materialService.existsByMaterialType(entity)).doReturn(true)

            val found = service.isUsedByMaterial(entity)

            assertTrue(found)
        }

        @Test
        fun `returns false when materialService_existsByMaterialType() returns false`() {
            whenever(materialService.existsByMaterialType(entity)).doReturn(false)

            val found = service.isUsedByMaterial(entity)

            assertFalse(found)
        }
    }

    @Nested
    inner class GetAllSystemTypes {
        @Test
        fun `returns all system types`() {
            whenever(repository.findAllBySystemTypeIs(true)).doReturn(listOf(systemType, anotherSystemType))

            val found = service.getAllSystemTypes()

            assertTrue(found.contains(systemType))
            assertTrue(found.contains(anotherSystemType))
        }
    }

    @Nested
    inner class GetAllNonSystemTypes {
        @Test
        fun `returns all non system types`() {
            whenever(repository.findAllBySystemTypeIs(false)).doReturn(listOf(entity, anotherEntity))

            val found = service.getAllNonSystemType()

            assertTrue(found.contains(entity))
            assertTrue(found.contains(anotherEntity))
        }
    }

    @Nested
    inner class SaveMaterialType {
        @Test
        fun `throws EntityAlreadyExistsRestException when a material type with the given prefix already exists`() {
            doReturn(true).whenever(service).existsByPrefix(entity.prefix)

            val exception = assertThrows<EntityAlreadyExistsRestException> { service.save(entity) }
            assertEquals(entity.prefix, exception.value)
        }
    }

    @Nested
    inner class Update {
        @Test
        fun `calls and returns save() in the repository`() {
            whenever(repository.save(entity)).doReturn(entity)
            whenever(repository.findByName(entity.name)).doReturn(null)
            whenever(repository.findByPrefix(entity.prefix)).doReturn(null)
            doReturn(true).whenever(service).existsById(entity.id!!)
            doReturn(entity).whenever(service).getById(entity.id!!)

            val found = service.update(entity)

            verify(repository).save(entity)
            assertEquals(entity, found)
        }

        @Test
        fun `throws EntityNotFoundRestException when no entity with the given id exists in the repository`() {
            whenever(repository.findByName(entity.name)).doReturn(null)
            whenever(repository.findByPrefix(entity.prefix)).doReturn(null)
            doReturn(false).whenever(service).existsById(entity.id!!)
            doReturn(null).whenever(service).getById(entity.id!!)

            val exception = assertThrows<EntityNotFoundRestException> { service.update(entity) }
            assertTrue(exception.value is Long)
            assertEquals(entity.id, exception.value as Long)
        }

        @Test
        fun `throws EntityAlreadyExistsRestException when an entity with the updated name exists`() {
            whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName)
            whenever(repository.findByPrefix(entity.prefix)).doReturn(null)
            doReturn(true).whenever(service).existsById(entity.id!!)
            doReturn(entity).whenever(service).getById(entity.id!!)

            val exception = assertThrows<EntityAlreadyExistsRestException> { service.update(entity) }
            assertEquals(entity.name, exception.value)
        }

        @Test
        fun `throws EntityAlreadyExistsRestException when an entity with the updated prefix exists`() {
            val anotherMaterialType = materialType(prefix = entity.prefix)
            whenever(repository.findByPrefix(entity.prefix)).doReturn(anotherMaterialType)
            doReturn(entity).whenever(service).getById(entity.id!!)

            val exception = assertThrows<EntityAlreadyExistsRestException> { service.update(entity) }
            assertEquals(entity.prefix, exception.value)
        }
    }

    @Nested
    inner class Delete {
        @Test
        fun `calls delete() in the repository`() {
            doReturn(false).whenever(service).isUsedByMaterial(entity)

            service.delete(entity)

            verify(repository).delete(entity)
        }

        @Test
        fun `throws CannotDeleteUsedMaterialTypeRestException when the material type is in use`() {
            doReturn(true).whenever(service).isUsedByMaterial(entity)

            assertThrows<CannotDeleteUsedMaterialTypeRestException> { service.delete(entity) }
        }
    }
}
