/*
 * Copyright © 2014-2017 EntIT Software LLC, a Micro Focus company (L.P.)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package io.cloudslang.engine.node.services;

import io.cloudslang.engine.data.SimpleHiloIdentifierGenerator;
import io.cloudslang.engine.node.entities.WorkerNode;
import io.cloudslang.engine.node.repositories.WorkerNodeRepository;
import io.cloudslang.engine.versioning.services.VersionService;
import io.cloudslang.score.api.nodes.WorkerStatus;
import liquibase.integration.spring.SpringLiquibase;
import org.apache.commons.dbcp.BasicDataSource;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;


import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Created by IntelliJ IDEA.
 * User:
 * Date: 15/11/12
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = WorkerNodeServiceTest.Configurator.class)
@Transactional
@Rollback
// this test depend on repo - should mock it!
public class WorkerNodeServiceTest {
    private static final boolean SHOW_SQL = false;
    private static final String versionId = "123";

    @Autowired
    private WorkerNodeService workerNodeService;

    @Autowired
    private WorkerLockService workerLockService;

    @Autowired
    private WorkerNodeRepository workerNodeRepository;

    @Autowired
    private VersionService versionService;

    @Before
    public void initNodes() {
        workerNodeService.create("H1", "H1", "amit.levin", "c:/dir");

        workerNodeService.create("H2", "H2", "dima.rassin", "c:/dir");
    }

    @After
    public void reset() {
        Mockito.reset(versionService, workerLockService);
    }

    @Test
    public void keepAlive() throws Exception {
        when(versionService.getCurrentVersion(anyString())).thenReturn(5L);

        WorkerNode worker = workerNodeService.readByUUID("H1");
        Date origDate = worker.getAckTime();
        Assert.assertNull(origDate);
        workerNodeService.keepAlive("H1");
        workerNodeRepository.flush();
        worker = workerNodeService.readByUUID("H1");
        Assert.assertNotNull(worker.getAckTime());
        Assert.assertEquals(5, worker.getAckVersion());

    }

    @Test
    public void createNode() throws Exception {
        workerNodeService.create("H3", "H3", "amit.levin", "c:/dir");
        verify(workerLockService).create("H3");
        WorkerNode worker = workerNodeService.readByUUID("H3");
        Assert.assertNotNull(worker);
    }

    @Test
    public void login() throws Exception {
        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        WorkerNode worker = workerNodeService.readByUUID("H3");
        Assert.assertEquals(WorkerStatus.FAILED, worker.getStatus());
        workerNodeService.up("H3", "version", versionId, false);
        worker = workerNodeService.readByUUID("H3");
        Assert.assertEquals(WorkerStatus.RUNNING, worker.getStatus());
    }

    @Test
    public void readByUUID() throws Exception {
        WorkerNode worker = workerNodeService.readByUUID("H1");
        Assert.assertNotNull(worker);
    }

    @Test
    public void readAllWorkers() throws Exception {
        List<WorkerNode> workers = workerNodeService.readAllWorkers();
        Assert.assertEquals(2, workers.size());
    }

    @Test
    public void readAllNotDeletedWorkers() {
        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        List<WorkerNode> workers = workerNodeService.readAllNotDeletedWorkers();
        Assert.assertEquals(3, workers.size());
        workerNodeService.updateWorkerToDeleted("H3");
        workers = workerNodeService.readAllNotDeletedWorkers();
        Assert.assertEquals(2, workers.size());
    }

    @Test
    public void deleteRunningWorkerTest() {
        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        WorkerNode worker = workerNodeService.readByUUID("H3");
        Assert.assertEquals(WorkerStatus.FAILED, worker.getStatus());
        workerNodeService.up("H3", "version", versionId, false);
        Assert.assertEquals(WorkerStatus.RUNNING, worker.getStatus());
        workerNodeService.updateWorkerToDeleted("H3");
        Assert.assertEquals(WorkerStatus.IN_RECOVERY, worker.getStatus());
        Assert.assertEquals(false, worker.isActive());
        Assert.assertEquals(true, worker.isDeleted());
    }

    @Test
    public void restoreDeletedWorker() {
        workerNodeService.create("H3", "H3", "tirla.alin", "m:/y/imaginary/path");
        WorkerNode worker = workerNodeService.readByUUID("H3");
        worker.setActive(false);
        worker.setDeleted(true);
        worker.setStatus(WorkerStatus.IN_RECOVERY);

        workerNodeService.updateWorkerToNotDeleted("H3");

        Assert.assertEquals(WorkerStatus.IN_RECOVERY, worker.getStatus());
        Assert.assertEquals(false, worker.isActive());
        Assert.assertEquals(false, worker.isDeleted());
    }

    @Test
    public void readNonRespondingWorkers() throws Exception {
        List<String> workers = workerNodeService.readNonRespondingWorkers();
        Assert.assertEquals(0, workers.size());

        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        workers = workerNodeService.readNonRespondingWorkers();
        Assert.assertEquals(0, workers.size());

        // only activate workers can be non responding
        workerNodeService.activate("H3");
        workers = workerNodeService.readNonRespondingWorkers();
        Assert.assertEquals(0, workers.size());//still it not "non responding" because it yet to login(first login)

        // after login version is current system version.
        workerNodeService.up("H3", "version", versionId, false);
        workers = workerNodeService.readNonRespondingWorkers();
        Assert.assertEquals(0, workers.size());

        //when the worker version is too far from the system version its NonResponding
        when(versionService.getCurrentVersion(anyString())).thenReturn(100L);
        workers = workerNodeService.readNonRespondingWorkers();
        Assert.assertEquals(3, workers.size());

        //after up the worker version will be aligned with current system version.
        workerNodeService.up("H3", "version", versionId, false);
        workers = workerNodeService.readNonRespondingWorkers();
        Assert.assertEquals(2, workers.size());
        Assert.assertFalse(workers.contains("H3"));

    }

    @Test
    public void readWorkersByActivation() throws Exception {

        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        List<WorkerNode> workers = workerNodeService.readWorkersByActivation(true);
        Assert.assertEquals(0, workers.size());
        workers = workerNodeService.readWorkersByActivation(false);
        Assert.assertEquals(3, workers.size());

        // activate worker
        workerNodeService.activate("H3");
        workers = workerNodeService.readWorkersByActivation(true);
        Assert.assertEquals(1, workers.size());
        workers = workerNodeService.readWorkersByActivation(false);
        Assert.assertEquals(2, workers.size());

        // deactivate worker
        workerNodeService.deactivate("H3");
        workers = workerNodeService.readWorkersByActivation(true);
        Assert.assertEquals(0, workers.size());
        workers = workerNodeService.readWorkersByActivation(false);
        Assert.assertEquals(3, workers.size());

    }

    @Test
    public void updateEnvironmentParams() throws Exception {
        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        workerNodeService.updateEnvironmentParams("H3", "Window", "7.0", "4");
        WorkerNode worker = workerNodeService.readByUUID("H3");
        Assert.assertEquals("Window", worker.getOs());
        Assert.assertEquals("7.0", worker.getJvm());
        Assert.assertEquals("4", worker.getDotNetVersion());

    }

    @Test
    public void updateStatus() {
        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        WorkerNode worker = workerNodeService.readByUUID("H3");
        Assert.assertEquals(WorkerStatus.FAILED, worker.getStatus());

        workerNodeService.updateStatus("H3", WorkerStatus.RUNNING);
        worker = workerNodeService.readByUUID("H3");
        Assert.assertEquals(WorkerStatus.RUNNING, worker.getStatus());
    }

    @Test
    public void updateBulkNumber() {
        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");

        workerNodeService.updateBulkNumber("H3", versionId);

        WorkerNode worker = workerNodeService.readByUUID("H3");
        Assert.assertEquals(versionId, worker.getBulkNumber());
    }

    @Test
    public void readAllWorkerGroups() {
        List<String> groups = workerNodeService.readAllWorkerGroups();
        Assert.assertEquals(WorkerNode.DEFAULT_WORKER_GROUPS.length, groups.size());

        workerNodeService.create("H3", "H3", "dima.rassin", "c:/dir");
        workerNodeService.updateWorkerGroups("H3", "group 1", "group 2");
        workerNodeService.updateWorkerGroups("H1", "group 1");
        groups = workerNodeService.readAllWorkerGroups();
        Assert.assertEquals(WorkerNode.DEFAULT_WORKER_GROUPS.length + 2, groups.size());

        workerNodeService.updateWorkerGroups("H3");
        WorkerNode workerNode = workerNodeService.readByUUID("H3");
        Assert.assertTrue(workerNode.getGroups().isEmpty());
    }

    @Test
    public void checkIfGroupsExist() {
        workerNodeService.updateWorkerGroups("H1", "group 1", "group 2");
        workerNodeService.updateWorkerGroups("H2", "group 1");

        List<String> result = workerNodeService.readWorkerGroups(Arrays.asList("group 1"));
        Assert.assertEquals(Arrays.asList("group 1"), result);
    }

    @Test
    public void addWorkerGroup() {
        WorkerNode workerNode = workerNodeService.readByUUID("H1");
        int groupSize = workerNode.getGroups().size();

        workerNodeService.addGroupToWorker("H1", "aaa");
        workerNode = workerNodeService.readByUUID("H1");

        Assert.assertEquals(groupSize + 1, workerNode.getGroups().size());
    }

    @Test
    public void addWorkerInDuplicateGroups() {
        List<String> groups = workerNodeService.readAllWorkerGroups();
        Assert.assertEquals(WorkerNode.DEFAULT_WORKER_GROUPS.length, groups.size());

        List<String> list;
        HashSet<String> expected;

        workerNodeService.create("PLM", "PLM", "dan.filip", "c:/plm");

        workerNodeService.updateWorkerGroups("PLM", "c1", "c2", "c2", "c3");
        list = workerNodeService.readWorkerGroups("PLM");

        expected = new HashSet<>(Arrays.asList("c2", "c1", "c3"));
        org.junit.Assert.assertTrue("worker groups contain duplicates?", new HashSet<>(list).equals(expected));

        // test null is not allowed + duplicates
        workerNodeService.updateWorkerGroups("PLM", null, "1", null, "1", "1");
        list = workerNodeService.readWorkerGroups("PLM");

        expected = new HashSet<>(Arrays.asList("1"));
        org.junit.Assert.assertTrue("worker groups contain duplicates?", new HashSet<>(list).equals(expected));

        // test add worker in the same group twice
        workerNodeService.addGroupToWorker("PLM", "g1");
        workerNodeService.addGroupToWorker("PLM", "g1");

        list = workerNodeService.readWorkerGroups("PLM");
        expected = new HashSet<>(Arrays.asList("1", "g1"));
        org.junit.Assert.assertTrue("worker groups contain duplicates?", new HashSet<>(list).equals(expected));
    }

    @Test
    public void updateVersionTest() {
        workerNodeService.create("worker_1", "password", "stamHost", "c:/dir");
        WorkerNode workerNode = workerNodeService.readByUUID("H1");
        Assert.assertEquals("", workerNode.getVersion());

        workerNodeService.updateVersion("H1", "VERSION", versionId);

        workerNode = workerNodeService.readByUUID("H1");

        Assert.assertEquals("Version not updated!", "VERSION", workerNode.getVersion());
    }

    @Test
    public void updateMigratedPasswordTest() {
        workerNodeService.create("worker_1", "password", "stamHost", "c:/dir");
        WorkerNode workerNode = workerNodeService.readByUUID("H1");
        Assert.assertNull(workerNode.getMigratedPassword());

        workerNodeService.updateMigratedPassword("H1", "newPassword");

        workerNode = workerNodeService.readByUUID("H1");

        Assert.assertEquals("Version not updated!", "newPassword", workerNode.getMigratedPassword());
    }

    @Configuration
    @EnableJpaRepositories("io.cloudslang.engine.node.repositories")
    @EnableTransactionManagement
    static class Configurator {
        @Bean
        DataSource dataSource() {
            BasicDataSource ds = new BasicDataSource();
            ds.setDriverClassName("org.h2.Driver");
            ds.setUrl("jdbc:h2:mem:test");
            ds.setUsername("sa");
            ds.setPassword("sa");
            ds.setDefaultAutoCommit(false);
            return new TransactionAwareDataSourceProxy(ds);
        }

        @Bean
        SpringLiquibase liquibase(DataSource dataSource) {
            SpringLiquibase liquibase = new SpringLiquibase();
            liquibase.setDataSource(dataSource);
            //liquibase.setChangeLog("classpath:/META-INF/database/test.changes.xml");
            liquibase.setChangeLog("classpath:/META-INF/database/score.changes.xml");
            SimpleHiloIdentifierGenerator.setDataSource(dataSource);
            return liquibase;
        }


        @Bean
        Properties hibernateProperties() {
            return new Properties() {{
                setProperty("hibernate.format_sql", "true");
                setProperty("hibernate.hbm2ddl.auto", "validate");
                setProperty("hibernate.cache.use_query_cache", "false");
                setProperty("hibernate.generate_statistics", "false");
                setProperty("hibernate.cache.use_second_level_cache", "false");
                setProperty("hibernate.order_updates", "true");
                setProperty("hibernate.order_inserts", "true");
                setProperty("hibernate.dialect_resolvers", "io.cloudslang.engine.dialects.ScoreDialectResolver");
            }};
        }

        @Bean
        JpaVendorAdapter jpaVendorAdapter() {
            HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
            adapter.setShowSql(SHOW_SQL);
            adapter.setGenerateDdl(true);
            return adapter;
        }

        @Bean(name = "entityManagerFactory")
        @DependsOn("liquibase")
        LocalContainerEntityManagerFactoryBean emf(JpaVendorAdapter jpaVendorAdapter) {
            LocalContainerEntityManagerFactoryBean fb = new LocalContainerEntityManagerFactoryBean();
            fb.setJpaProperties(hibernateProperties());
            fb.setDataSource(dataSource());
            fb.setPersistenceProviderClass(HibernatePersistenceProvider.class);
            fb.setPackagesToScan("io.cloudslang.engine.node");
            fb.setJpaVendorAdapter(jpaVendorAdapter);
            return fb;
        }

        @Bean
        PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
            return new JpaTransactionManager(emf);
        }

        @Bean
        VersionService versionService() {
            VersionService versionService = mock(VersionService.class);
            when(versionService.getCurrentVersion(anyString())).thenReturn(1L);
            return versionService;
        }

        @Bean
        WorkerNodeService workerNodeService() {
            return new WorkerNodeServiceImpl();
        }

        @Bean
        WorkerLockService workerLockService() {
            return mock(WorkerLockService.class);
        }

        @Bean
        QueueConfigurationDataService queueConfigurationDataService() {
            return mock(QueueConfigurationDataService.class);
        }

    }
}
