package dev.fyloz.trial.colorrecipesexplorer.service

import com.nhaarman.mockitokotlin2.*
import dev.fyloz.trial.colorrecipesexplorer.config.SecurityConfigurationProperties
import dev.fyloz.trial.colorrecipesexplorer.config.defaultGroupCookieName
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.EmployeeGroupRepository
import dev.fyloz.trial.colorrecipesexplorer.repository.EmployeeRepository
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Test
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import java.util.*
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest
import kotlin.test.*

class EmployeeServiceTest : AbstractModelServiceTest<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeService, EmployeeRepository>() {
    private val passwordEncoder = BCryptPasswordEncoder()

    override val entity: Employee = employee(passwordEncoder, id = 0L)
    override val anotherEntity: Employee = employee(passwordEncoder, id = 1L)
    private val entityDefaultGroupUser = employee(passwordEncoder, id = 2L, isDefaultGroupUser = true)
    private val entitySystemUser = employee(passwordEncoder, id = 3L, isSystemUser = true)
    private val group = employeeGroup(id = 0L)
    override val entitySaveDto: EmployeeSaveDto = spy(employeeSaveDto(passwordEncoder, id = 0L))
    override val entityUpdateDto: EmployeeUpdateDto = spy(employeeUpdateDto(id = 0L))

    override val repository: EmployeeRepository = mock()
    override val service: EmployeeService = spy(EmployeeServiceImpl(repository, passwordEncoder))

    private val entitySaveDtoEmployee = Employee(entitySaveDto.id, entitySaveDto.firstName, entitySaveDto.lastName, passwordEncoder.encode(entitySaveDto.password), isDefaultGroupUser = false, isSystemUser = false, group = null, permissions = entitySaveDto.permissions)

    @AfterEach
    override fun afterEach() {
        reset(entitySaveDto, entityUpdateDto)
        super.afterEach()
    }

    @Nested
    inner class ExistsByFirstNameAndLastName {
        @Test
        fun `returns true when an employee with the given first name and last name exists`() {
            whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(true)

            val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName)

            assertTrue(found)
        }

        @Test
        fun `returns false when no employee with the given first name and last name exists`() {
            whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(false)

            val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName)

            assertFalse(found)
        }
    }

    @Nested
    inner class GetById {
        @Test
        fun `throws EntityNotFoundRestException when the corresponding employee is a default group user`() {
            whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser))

            val exception = assertThrows<EntityNotFoundRestException> { service.getById(entityDefaultGroupUser.id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false) }
            assertTrue(exception.value is Long)
            assertEquals(entityDefaultGroupUser.id, exception.value as Long)
        }

        @Test
        fun `throws EntityNotFoundRestException when the corresponding employee is a system user`() {
            whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser))

            val exception = assertThrows<EntityNotFoundRestException> { service.getById(entitySystemUser.id, ignoreDefaultGroupUsers = false, ignoreSystemUsers = true) }
            assertTrue(exception.value is Long)
            assertEquals(entitySystemUser.id, exception.value as Long)
        }
    }

    @Nested
    inner class GetByGroup {
        @Test
        fun `returns all the employees with the given group from the repository`() {
            whenever(repository.findAllByGroup(group)).doReturn(entityList)

            val found = service.getByGroup(group)

            assertTrue(found.containsAll(entityList))
            assertTrue(entityList.containsAll(found))
        }

        @Test
        fun `returns an empty list when there is no employee with the given group in the repository`() {
            whenever(repository.findAllByGroup(group)).doReturn(listOf())

            val found = service.getByGroup(group)

            assertTrue(found.isEmpty())
        }
    }

    @Nested
    inner class GetDefaultGroupUser {
        @Test
        fun `returns the default employee of the given group from the repository`() {
            whenever(repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)).doReturn(entityDefaultGroupUser)

            val found = service.getDefaultGroupEmployee(group)

            assertEquals(entityDefaultGroupUser, found)
        }
    }

    @Nested
    inner class Save {
        @Test
        fun `calls save() in the repository`() {
            whenever(repository.save(entity)).doReturn(entity)
            doReturn(false).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName)

            val found = service.save(entity)

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

        @Test
        fun `throws EntityAlreadyExistsException when firstName and lastName exists`() {
            doReturn(true).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName)

            val exception = assertThrows<EntityAlreadyExistsRestException> { service.save(entity) }
            assertEquals("${entity.firstName} ${entity.lastName}", exception.value)
        }
    }

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

            val found = service.save(entitySaveDto)

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

    @Nested
    inner class Update {
        @Test
        fun `throws EntityAlreadyExistsRestException when a different employee with the given first name and last name exists`() {
            whenever(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(entityDefaultGroupUser)
            doReturn(entity).whenever(service).getById(eq(entity.id), any(), any())

            val exception = assertThrows<EntityAlreadyExistsRestException> { service.update(entity, true, ignoreSystemUsers = true) }
            assertTrue(exception.value is String)
            assertEquals("${entity.firstName} ${entity.lastName}", exception.value as String)
        }
    }
}

class EmployeeGroupServiceTest : AbstractModelServiceTest<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupServiceImpl, EmployeeGroupRepository>() {
    private val employeeService: EmployeeService = mock()
    override val repository: EmployeeGroupRepository = mock()
    override val service: EmployeeGroupServiceImpl = spy(EmployeeGroupServiceImpl(repository, employeeService))

    override val entity: EmployeeGroup = employeeGroup(id = 0L, name = "group")
    override val anotherEntity: EmployeeGroup = employeeGroup(id = 1L, name = "another group")
    override val entitySaveDto: EmployeeGroupSaveDto = spy(employeeGroupSaveDto(name = "group"))
    override val entityUpdateDto: EmployeeGroupUpdateDto = spy(employeeGroupUpdateDto(id = 0L, name = "groupL"))

    private val groupEmployeeId = 1000000L + entity.id!!
    private val groupEmployee = employee(BCryptPasswordEncoder(), id = groupEmployeeId, group = entity)

    @BeforeEach
    override fun afterEach() {
        reset(employeeService, entitySaveDto, entityUpdateDto)
        super.afterEach()
    }

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

            val found = service.existsByName(entity.name)

            assertTrue(found)
        }

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

            val found = service.existsByName(entity.name)

            assertFalse(found)
        }
    }

    @Nested
    inner class GetEmployeesForGroup {
        @Test
        fun `returns all employees in the given group`() {
            val group = employeeGroup(id = 1L, employees = mutableSetOf(groupEmployee))

            doReturn(group).whenever(service).getById(group.id!!)

            val found = service.getEmployeesForGroup(group.id!!)

            assertTrue(found.contains(groupEmployee))
            assertTrue(found.size == 1)
        }

        @Test
        fun `returns empty collection when the given group contains any employee`() {
            doReturn(entity).whenever(service).getById(entity.id!!)

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

            assertTrue(found.isEmpty())
        }
    }

    @Nested
    inner class GetRequestDefaultGroup {
        @Test
        fun `returns the group contained in the cookie of the HTTP request`() {
            val cookies: Array<Cookie> = arrayOf(Cookie(defaultGroupCookieName, groupEmployeeId.toString()))
            val request: HttpServletRequest = mock()

            whenever(request.cookies).doReturn(cookies)
            whenever(employeeService.getById(eq(groupEmployeeId), any(), any())).doReturn(groupEmployee)

            val found = service.getRequestDefaultGroup(request)

            assertEquals(entity, found)
        }

        @Test
        fun `throws EntityNotFoundRestException when the HTTP request does not contains a cookie for the default group`() {
            val request: HttpServletRequest = mock()

            whenever(request.cookies).doReturn(arrayOf())

            val exception = assertThrows<EntityNotFoundRestException> { service.getRequestDefaultGroup(request) }
            assertEquals("defaultGroup", exception.value)
        }
    }

    @Nested
    inner class SetResponseDefaultGroup {
        @Test
        fun `the default group cookie has been added to the given HTTP response with the given group id`() {
            val response = MockHttpServletResponse()

            whenever(employeeService.getDefaultGroupEmployee(entity)).doReturn(groupEmployee)
            doReturn(entity).whenever(service).getById(entity.id!!)

            service.setResponseDefaultGroup(entity.id!!, response)
            val found = response.getCookie(defaultGroupCookieName)

            assertNotNull(found)
            assertEquals(defaultGroupCookieName, found.name)
            assertEquals(groupEmployeeId.toString(), found.value)
            assertEquals(defaultGroupCookieMaxAge, found.maxAge)
            assertTrue(found.isHttpOnly)
            assertTrue(found.secure)
        }
    }

    @Nested
    inner class AddEmployeeToGroup {
        @Test
        fun `calls addEmployeeToGroup() with the group of the given groupId and the employee of the given employeeId`() {
            whenever(employeeService.getById(groupEmployeeId)).doReturn(groupEmployee)
            doReturn(entity).whenever(service).getById(entity.id!!)

            service.addEmployeeToGroup(entity.id!!, groupEmployeeId)

            verify(service).addEmployeeToGroup(entity, groupEmployee)
        }

        @Test
        fun `calls update() and employeeService_update() with the updated entities`() {
            val group = employeeGroup()
            val employee = employee()

            whenever(employeeService.update(employee)).doReturn(employee)
            doReturn(group).whenever(service).update(group)

            service.addEmployeeToGroup(group, employee)

            verify(service).update(group)
            verify(employeeService).update(employee)

            assertTrue(group.employees.contains(employee))
            assertEquals(group, employee.group)
        }

        @Test
        fun `do nothing when the given employee is already in the given group`() {
            val group = employeeGroup()
            val employee = employee(group = group)

            service.addEmployeeToGroup(group, employee)

            verify(service, times(0)).update(group)
            verify(employeeService, times(0)).update(employee)
        }

        @Test
        fun `remove previous group from the given employee and add it the the given group`() {
            val group = employeeGroup(id = 0L)
            val previousGroup = employeeGroup(id = 1L)
            val employee = employee(group = previousGroup)

            whenever(employeeService.update(employee)).doReturn(employee)
            doReturn(group).whenever(service).update(group)
            doReturn(group).whenever(service).update(previousGroup)

            service.addEmployeeToGroup(group, employee)

            verify(service).removeEmployeeFromGroup(previousGroup, employee)
            verify(service).update(group)
            verify(employeeService, times(2)).update(employee)

            assertTrue(group.employees.contains(employee))
            assertEquals(group, employee.group)
        }
    }

    @Nested
    inner class RemoveEmployeeFromGroup {
        @Test
        fun `calls removeEmployeeFromGroup() with the group of the given group id and the employee of the given employee id`() {
            whenever(employeeService.getById(groupEmployeeId)).doReturn(groupEmployee)
            doReturn(entity).whenever(service).getById(entity.id!!)
            doAnswer { it.arguments[0] }.whenever(service).update(any<EmployeeGroup>())

            service.removeEmployeeFromGroup(entity.id!!, groupEmployeeId)

            verify(service).removeEmployeeFromGroup(entity, groupEmployee)
        }

        @Test
        fun `calls update() and employeeService_update() with the updated entities`() {
            val employee = employee()
            val group = employeeGroup(employees = mutableSetOf(employee))
            employee.group = group

            whenever(employeeService.update(employee)).doReturn(employee)
            doReturn(group).whenever(service).update(group)

            service.removeEmployeeFromGroup(group, employee)

            verify(service).update(group)
            verify(employeeService).update(employee)

            assertFalse(group.employees.contains(employee))
            assertNull(employee.group)
        }

        @Test
        fun `do nothing when the given employee is not in the given group`() {
            val employee = employee()
            val group = employeeGroup(id = 0L)
            val anotherGroup = employeeGroup(id = 1L, employees = mutableSetOf(employee))
            employee.group = anotherGroup

            service.removeEmployeeFromGroup(group, employee)

            verify(service, times(0)).update(anotherGroup)
            verify(employeeService, times(0)).update(employee)
        }

        @Test
        fun `do nothing when the given employee is not in a group`() {
            val employee = employee()
            val group = employeeGroup()

            service.removeEmployeeFromGroup(group, employee)

            verify(service, times(0)).update(group)
            verify(employeeService, times(0)).update(employee)
        }
    }
}

class EmployeeUserDetailsServiceTest {
    private val employeeService: EmployeeService = mock()
    private val securityConfigurationProperties = SecurityConfigurationProperties().apply {
        jwtSecret = "secret"
        jwtDuration = 1000L
        root = SecurityConfigurationProperties.SystemUserCredentials(999L, "root")
    }
    private val service = spy(EmployeeUserDetailsServiceImpl(employeeService, securityConfigurationProperties))

    private val employee = employee(id = 0L)

    @BeforeEach
    fun beforeEach() {
        reset(employeeService, service)
    }

    @Nested
    inner class LoadUserByUsername {
        @Test
        fun `calls loadUserByEmployeeId() with the given username as an id`() {
            whenever(employeeService.getById(eq(employee.id), any(), any())).doReturn(employee)
            doReturn(User(employee.id.toString(), employee.password, listOf())).whenever(service).loadUserByEmployeeId(employee.id)

            service.loadUserByUsername(employee.id.toString())

            verify(service).loadUserByEmployeeId(eq(employee.id), any())
        }

        @Test
        fun `throws UsernameNotFoundException when no employee with the given id exists`() {
            whenever(employeeService.getById(eq(employee.id), any(), any())).doThrow(EntityNotFoundRestException(employee.id))

            assertThrows<UsernameNotFoundException> { service.loadUserByUsername(employee.id.toString()) }
        }
    }

    @Nested
    inner class LoadUserByEmployeeId {
        @Test
        fun `returns an User corresponding to the employee with the given id`() {
            whenever(employeeService.getById(eq(employee.id), any(), any())).doReturn(employee)

            val found = service.loadUserByEmployeeId(employee.id)

            assertEquals(employee.id, found.username.toLong())
            assertEquals(employee.password, found.password)
        }
    }
}
