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.EntityDto
import dev.fyloz.trial.colorrecipesexplorer.model.Model
import dev.fyloz.trial.colorrecipesexplorer.model.NamedModel
import dev.fyloz.trial.colorrecipesexplorer.repository.NamedJpaRepository
import org.junit.jupiter.api.*
import org.springframework.data.jpa.repository.JpaRepository
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

abstract class AbstractServiceTest<E, N : EntityDto<E>, U : EntityDto<E>, S : Service<E, N, U, *>, R : JpaRepository<E, *>> {
    protected abstract val repository: R
    protected abstract val service: S

    protected abstract val entity: E
    protected abstract val anotherEntity: E
    protected abstract val entitySaveDto: N
    protected abstract val entityUpdateDto: U

    protected val entityList: List<E>
        get() = listOf(
                entity,
                anotherEntity
        )

    @AfterEach
    open fun afterEach() {
        reset(repository, service, entitySaveDto, entityUpdateDto)
    }

    @Nested
    inner class GetAll {
        @Test
        fun `returns all available entities`() {
            whenever(repository.findAll()).doReturn(entityList)

            val found = service.getAll()

            assertEquals(entityList, found)
        }

        @Test
        fun `returns empty list when there is no entities`() {
            whenever(repository.findAll()).doReturn(listOf())

            val found = service.getAll()

            assertTrue { found.isEmpty() }
        }
    }

    @Nested
    inner class Save {
        @Test
        fun `calls and returns save() in the repository`() {
            whenever(repository.save(entity)).doReturn(entity)

            val found = service.save(entity)

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

    @Nested
    inner class SaveDto {
        @Test
        fun `calls and returns save() with the created entity`() {
            doReturn(entity).whenever(service).save(entity)
            doReturn(entity).whenever(entitySaveDto).toEntity()

            val found = service.save(entitySaveDto)

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

    @Nested
    inner class Update {
        @Test
        fun `calls and returns save() in the repository`() {
            whenever(repository.save(entity)).doReturn(entity)

            val found = service.update(entity)

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

    @Nested
    inner class UpdateDto {
        @Test
        fun `calls and returns update() with the created entity`() {
            doReturn(entity).whenever(service).update(entity)
            doReturn(entity).whenever(entityUpdateDto).toEntity()

            val found = service.update(entityUpdateDto)

            verify(service).update(entity)
            assertEquals(entity, found)
        }
    }

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

            verify(repository).delete(entity)
        }
    }
}

abstract class AbstractModelServiceTest<E : Model, N : EntityDto<E>, U : EntityDto<E>, S : ModelService<E, N, U, *>, R : JpaRepository<E, Long>> : AbstractServiceTest<E, N, U, S, R>() {
    @Nested
    inner class ExistsById {
        @Test
        fun `returns true when an entity with the given id exists in the repository`() {
            whenever(repository.existsById(entity.id!!)).doReturn(true)

            val found = service.existsById(entity.id!!)

            assertTrue(found)
        }

        @Test
        fun `returns false when no entity with the given id exists in the repository`() {
            whenever(repository.existsById(entity.id!!)).doReturn(false)

            val found = service.existsById(entity.id!!)

            assertFalse(found)
        }
    }

    @Nested
    inner class GetById {
        @Test
        fun `returns the entity with the given id from the repository`() {
            whenever(repository.findById(entity.id!!)).doReturn(Optional.of(entity))

            val found = service.getById(entity.id!!)

            assertEquals(entity, found)
        }

        @Test
        fun `throws EntityNotFoundRestException when no entity with the given id exists in the repository`() {
            whenever(repository.findById(entity.id!!)).doReturn(Optional.empty())

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

    @Nested
    inner class SaveModel {
        @Test
        fun `throws EntityAlreadyExistsRestException when an entity with the given id exists in the repository`() {
            doReturn(true).whenever(repository).existsById(entity.id!!)

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

    @Nested
    inner class Update {
        @Test
        fun `calls and returns save() in the repository`() {
            whenever(repository.save(entity)).doReturn(entity)
            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`() {
            doReturn(false).whenever(service).existsById(entity.id!!)

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

    @Nested
    inner class DeleteById {
        @Test
        fun `calls deleteById() in the repository with the given id`() {
            service.deleteById(entity.id!!)

            verify(repository).deleteById(entity.id!!)
        }
    }
}

abstract class AbstractNamedModelServiceTest<E : NamedModel, N : EntityDto<E>, U : EntityDto<E>, S : NamedModelService<E, N, U, *>, R : NamedJpaRepository<E>> : AbstractModelServiceTest<E, N, U, S, R>() {
    protected abstract val entityWithEntityName: E

    @Nested
    inner class ExistsByName {
        @Test
        fun `returns true when an entity with the given name exists`() {
            whenever(repository.existsByName(entity.name)).doReturn(true)

            val found = service.existsByName(entity.name)

            assertTrue(found)
        }

        @Test
        fun `returns false when no entity with the given name exists`() {
            whenever(repository.existsByName(entity.name)).doReturn(false)

            val found = service.existsByName(entity.name)

            assertFalse(found)
        }
    }

    @Nested
    inner class GetByName {
        @Test
        fun `returns the entity with the given name`() {
            whenever(repository.findByName(entity.name)).doReturn(entity)

            val found = service.getByName(entity.name)

            assertEquals(entity, found)
        }

        @Test
        fun `throws EntityNotFoundRestException when no entity with the given name exists`() {
            whenever(repository.findByName(entity.name)).doReturn(null)

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

    @Nested
    inner class SaveNamedModel {
        @Test
        fun `throws EntityAlreadyExistsRestException when an entity with the given name exists`() {
            doReturn(true).whenever(service).existsByName(entity.name)

            val exception = assertThrows<EntityAlreadyExistsRestException> { service.save(entity) }
            assertEquals(entity.name, 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)
            doReturn(true).whenever(service).existsById(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)
            doReturn(false).whenever(service).existsById(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)

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

    @Nested
    inner class DeleteByName {
        @Test
        fun `calls deleteByName() in the repository with the given name`() {
            service.deleteByName(entity.name)

            verify(repository).deleteByName(entity.name)
        }
    }
}
