package net.joelinn.quartz;

import com.fasterxml.jackson.databind.ObjectMapper;
import net.joelinn.quartz.jobstore.RedisJobStoreSchema;
import net.joelinn.quartz.jobstore.AbstractRedisStorage;
import net.joelinn.quartz.jobstore.RedisStorage;
import net.joelinn.quartz.jobstore.RedisTriggerState;
import org.hamcrest.MatcherAssert;
import org.junit.Test;
import org.quartz.*;
import org.quartz.impl.calendar.WeeklyCalendar;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.quartz.spi.OperableTrigger;
import org.quartz.spi.SchedulerSignaler;
import org.quartz.spi.TriggerFiredResult;

import java.util.*;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsMapContaining.hasKey;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;

/**
 * Joe Linn
 * 7/15/2014
 */
public class StoreTriggerTest extends BaseTest{

    @Test
    public void storeTrigger() throws Exception {
        CronTriggerImpl trigger = getCronTrigger();
        trigger.getJobDataMap().put("foo", "bar");

        jobStore.storeTrigger(trigger, false);

        final String triggerHashKey = schema.triggerHashKey(trigger.getKey());
        Map<String, String> triggerMap = jedis.hgetAll(triggerHashKey);
        assertThat(triggerMap, hasKey("description"));
        assertEquals(trigger.getDescription(), triggerMap.get("description"));
        assertThat(triggerMap, hasKey("trigger_class"));
        assertEquals(trigger.getClass().getName(), triggerMap.get("trigger_class"));

        assertTrue("The trigger hash key is not a member of the triggers set.", jedis.sismember(schema.triggersSet(), triggerHashKey));
        assertTrue("The trigger group set key is not a member of the trigger group set.", jedis.sismember(schema.triggerGroupsSet(), schema.triggerGroupSetKey(trigger.getKey())));
        assertTrue(jedis.sismember(schema.triggerGroupSetKey(trigger.getKey()), triggerHashKey));
        assertTrue(jedis.sismember(schema.jobTriggersSetKey(trigger.getJobKey()), triggerHashKey));
        String triggerDataMapHashKey = schema.triggerDataMapHashKey(trigger.getKey());
        MatcherAssert.assertThat(jedis.exists(triggerDataMapHashKey), equalTo(true));
        MatcherAssert.assertThat(jedis.hget(triggerDataMapHashKey, "foo"), equalTo("bar"));
    }

    @Test(expected = JobPersistenceException.class)
    public void storeTriggerNoReplace() throws Exception {
        jobStore.storeTrigger(getCronTrigger(), false);
        jobStore.storeTrigger(getCronTrigger(), false);
    }

    @Test
    public void storeTriggerWithReplace() throws Exception {
        jobStore.storeTrigger(getCronTrigger(), true);
        jobStore.storeTrigger(getCronTrigger(), true);
    }

    @Test
    public void retrieveTrigger() throws Exception {
        CronTriggerImpl cronTrigger = getCronTrigger();
        jobStore.storeJob(getJobDetail(), false);
        jobStore.storeTrigger(cronTrigger, false);

        OperableTrigger operableTrigger = jobStore.retrieveTrigger(cronTrigger.getKey());

        assertThat(operableTrigger, instanceOf(CronTriggerImpl.class));
        assertThat(operableTrigger.getFireInstanceId(), notNullValue());
        CronTriggerImpl retrievedTrigger = (CronTriggerImpl) operableTrigger;

        assertEquals(cronTrigger.getCronExpression(), retrievedTrigger.getCronExpression());
        assertEquals(cronTrigger.getTimeZone(), retrievedTrigger.getTimeZone());
        assertEquals(cronTrigger.getStartTime(), retrievedTrigger.getStartTime());
    }

    @Test
    public void removeTrigger() throws Exception {
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup", job.getKey());
        trigger1.getJobDataMap().put("foo", "bar");
        CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup", job.getKey());

        jobStore.storeJob(job, false);
        jobStore.storeTrigger(trigger1, false);
        jobStore.storeTrigger(trigger2, false);

        jobStore.removeTrigger(trigger1.getKey());

        // ensure that the trigger was removed, but the job was not
        assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue());
        assertThat(jobStore.retrieveJob(job.getKey()), not(nullValue()));

        // remove the second trigger
        jobStore.removeTrigger(trigger2.getKey());

        //  ensure that both the trigger and job were removed
        assertThat(jobStore.retrieveTrigger(trigger2.getKey()), nullValue());
        assertThat(jobStore.retrieveJob(job.getKey()), nullValue());
        MatcherAssert.assertThat(jedis.exists(schema.triggerDataMapHashKey(trigger1.getKey())), equalTo(false));
    }

    @Test
    public void getTriggersForJob() throws Exception {
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup", job.getKey());
        CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup", job.getKey());

        jobStore.storeJob(job, false);
        jobStore.storeTrigger(trigger1, false);
        jobStore.storeTrigger(trigger2, false);

        List<OperableTrigger> triggers = jobStore.getTriggersForJob(job.getKey());
        assertThat(triggers, hasSize(2));
    }

    @Test
    public void getNumberOfTriggers() throws Exception {
        JobDetail job = getJobDetail();
        jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false);

        int numberOfTriggers = jobStore.getNumberOfTriggers();

        assertEquals(4, numberOfTriggers);
    }

    @Test
    public void getTriggerKeys() throws Exception {
        JobDetail job = getJobDetail();
        jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false);

        Set<TriggerKey> triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupEquals("group1"));

        assertThat(triggerKeys, hasSize(2));
        assertThat(triggerKeys, containsInAnyOrder(new TriggerKey("trigger2", "group1"), new TriggerKey("trigger1", "group1")));

        jobStore.storeTrigger(getCronTrigger("trigger4", "triggergroup1", job.getKey()), false);

        triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupContains("group"));

        assertThat(triggerKeys, hasSize(5));

        triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupEndsWith("1"));

        assertThat(triggerKeys, hasSize(3));
        assertThat(triggerKeys, containsInAnyOrder(new TriggerKey("trigger2", "group1"),
                new TriggerKey("trigger1", "group1"), new TriggerKey("trigger4", "triggergroup1")));

        triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupStartsWith("trig"));

        assertThat(triggerKeys, hasSize(1));
        assertThat(triggerKeys, containsInAnyOrder(new TriggerKey("trigger4", "triggergroup1")));
    }

    @Test
    public void getTriggerGroupNames() throws Exception {
        List<String> triggerGroupNames = jobStore.getTriggerGroupNames();

        assertThat(triggerGroupNames, not(nullValue()));
        assertThat(triggerGroupNames, hasSize(0));

        JobDetail job = getJobDetail();
        jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false);

        triggerGroupNames = jobStore.getTriggerGroupNames();

        assertThat(triggerGroupNames, hasSize(3));
        assertThat(triggerGroupNames, containsInAnyOrder("group3", "group2", "group1"));
    }

    @Test
    public void getTriggerState() throws Exception {
        SchedulerSignaler signaler = mock(SchedulerSignaler.class);
        AbstractRedisStorage storageDriver = new RedisStorage(new RedisJobStoreSchema(), new ObjectMapper(), signaler, "scheduler1", 2000);

        // attempt to retrieve the state of a non-existent trigger
        Trigger.TriggerState state = jobStore.getTriggerState(new TriggerKey("foobar"));
        assertEquals(Trigger.TriggerState.NONE, state);

        // store a trigger
        JobDetail job = getJobDetail();
        CronTriggerImpl cronTrigger = getCronTrigger("trigger1", "group1", job.getKey());
        jobStore.storeTrigger(cronTrigger, false);

        // the newly-stored trigger's state should be NONE
        state = jobStore.getTriggerState(cronTrigger.getKey());
        assertEquals(Trigger.TriggerState.NORMAL, state);

        // set the trigger's state
        storageDriver.setTriggerState(RedisTriggerState.WAITING, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis);

        // the trigger's state should now be NORMAL
        state = jobStore.getTriggerState(cronTrigger.getKey());
        assertEquals(Trigger.TriggerState.NORMAL, state);
    }

    @Test
    public void pauseTrigger() throws Exception {
        SchedulerSignaler signaler = mock(SchedulerSignaler.class);
        AbstractRedisStorage storageDriver = new RedisStorage(new RedisJobStoreSchema(), new ObjectMapper(), signaler, "scheduler1", 2000);

        // store a trigger
        JobDetail job = getJobDetail();
        CronTriggerImpl cronTrigger = getCronTrigger("trigger1", "group1", job.getKey());
        cronTrigger.setNextFireTime(new Date(System.currentTimeMillis()));
        jobStore.storeTrigger(cronTrigger, false);

        // set the trigger's state to COMPLETED
        storageDriver.setTriggerState(RedisTriggerState.COMPLETED, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis);
        jobStore.pauseTrigger(cronTrigger.getKey());

        // trigger's state should not have changed
        assertEquals(Trigger.TriggerState.COMPLETE, jobStore.getTriggerState(cronTrigger.getKey()));

        // set the trigger's state to BLOCKED
        storageDriver.setTriggerState(RedisTriggerState.BLOCKED, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis);
        jobStore.pauseTrigger(cronTrigger.getKey());

        // trigger's state should be PAUSED
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(cronTrigger.getKey()));

        // set the trigger's state to ACQUIRED
        storageDriver.setTriggerState(RedisTriggerState.ACQUIRED, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis);
        jobStore.pauseTrigger(cronTrigger.getKey());

        // trigger's state should be PAUSED
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(cronTrigger.getKey()));
    }

    @Test
    public void pauseTriggersEquals() throws Exception {
        // store triggers
        JobDetail job = getJobDetail();
        jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false);

        // pause triggers
        Collection<String> pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group1"));

        assertThat(pausedGroups, hasSize(1));
        assertThat(pausedGroups, containsInAnyOrder("group1"));

        // ensure that the triggers were actually paused
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger1", "group1")));
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger2", "group1")));
    }

    @Test
    public void pauseTriggersStartsWith() throws Exception {
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey());
        CronTriggerImpl trigger2 = getCronTrigger("trigger1", "group2", job.getKey());
        CronTriggerImpl trigger3 = getCronTrigger("trigger1", "foogroup1", job.getKey());
        storeJobAndTriggers(job, trigger1, trigger2, trigger3);

        Collection<String> pausedTriggerGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupStartsWith("group"));

        assertThat(pausedTriggerGroups, hasSize(2));
        assertThat(pausedTriggerGroups, containsInAnyOrder("group1", "group2"));

        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey()));
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey()));
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger3.getKey()));
    }

    @Test
    public void pauseTriggersEndsWith() throws Exception {
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey());
        CronTriggerImpl trigger2 = getCronTrigger("trigger1", "group2", job.getKey());
        CronTriggerImpl trigger3 = getCronTrigger("trigger1", "foogroup1", job.getKey());
        storeJobAndTriggers(job, trigger1, trigger2, trigger3);

        Collection<String> pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEndsWith("oup1"));

        assertThat(pausedGroups, hasSize(2));
        assertThat(pausedGroups, containsInAnyOrder("group1", "foogroup1"));

        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey()));
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger2.getKey()));
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger3.getKey()));
    }

    @Test
    public void resumeTrigger() throws Exception {
        // create and store a job and trigger
        JobDetail job = getJobDetail();
        jobStore.storeJob(job, false);
        CronTriggerImpl trigger = getCronTrigger("trigger1", "group1", job.getKey());
        trigger.computeFirstFireTime(new WeeklyCalendar());
        jobStore.storeTrigger(trigger, false);

        // pause the trigger
        jobStore.pauseTrigger(trigger.getKey());
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger.getKey()));

        // resume the trigger
        jobStore.resumeTrigger(trigger.getKey());
        // the trigger state should now be NORMAL
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey()));

        // attempt to resume the trigger, again
        jobStore.resumeTrigger(trigger.getKey());
        // the trigger state should not have changed
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey()));
    }

    @Test
    public void resumeTriggersEquals() throws Exception {
        // store triggers and job
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey());
        CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey());
        CronTriggerImpl trigger3 = getCronTrigger("trigger3", "group2", job.getKey());
        CronTriggerImpl trigger4 = getCronTrigger("trigger4", "group3", job.getKey());
        storeJobAndTriggers(job, trigger1, trigger2, trigger3, trigger4);

        // pause triggers
        Collection<String> pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group1"));

        assertThat(pausedGroups, hasSize(1));
        assertThat(pausedGroups, containsInAnyOrder("group1"));

        // ensure that the triggers were actually paused
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger1", "group1")));
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger2", "group1")));

        // resume triggers
        Collection<String> resumedGroups = jobStore.resumeTriggers(GroupMatcher.triggerGroupEquals("group1"));

        assertThat(resumedGroups, hasSize(1));
        assertThat(resumedGroups, containsInAnyOrder("group1"));

        // ensure that the triggers were resumed
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(new TriggerKey("trigger1", "group1")));
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(new TriggerKey("trigger2", "group1")));
    }

    @Test
    public void resumeTriggersEndsWith() throws Exception {
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey());
        CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey());
        CronTriggerImpl trigger3 = getCronTrigger("trigger3", "group2", job.getKey());
        CronTriggerImpl trigger4 = getCronTrigger("trigger4", "group3", job.getKey());
        storeJobAndTriggers(job, trigger1, trigger2, trigger3, trigger4);

        Collection<String> pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEndsWith("1"));

        assertThat(pausedGroups, hasSize(1));
        assertThat(pausedGroups, containsInAnyOrder("group1"));

        // ensure that the triggers were actually paused
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey()));
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey()));

        // resume triggers
        Collection<String> resumedGroups = jobStore.resumeTriggers(GroupMatcher.triggerGroupEndsWith("1"));

        assertThat(resumedGroups, hasSize(1));
        assertThat(resumedGroups, containsInAnyOrder("group1"));

        // ensure that the triggers were actually resumed
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger1.getKey()));
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger2.getKey()));
    }

    @Test
    public void resumeTriggersStartsWith() throws Exception {
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "mygroup1", job.getKey());
        CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey());
        CronTriggerImpl trigger3 = getCronTrigger("trigger3", "group2", job.getKey());
        CronTriggerImpl trigger4 = getCronTrigger("trigger4", "group3", job.getKey());
        storeJobAndTriggers(job, trigger1, trigger2, trigger3, trigger4);

        Collection<String> pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupStartsWith("my"));

        assertThat(pausedGroups, hasSize(1));
        assertThat(pausedGroups, containsInAnyOrder("mygroup1"));

        // ensure that the triggers were actually paused
        assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey()));

        // resume triggers
        Collection<String> resumedGroups = jobStore.resumeTriggers(GroupMatcher.triggerGroupStartsWith("my"));

        assertThat(resumedGroups, hasSize(1));
        assertThat(resumedGroups, containsInAnyOrder("mygroup1"));

        // ensure that the triggers were actually resumed
        assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger1.getKey()));
    }

    @Test
    public void getPausedTriggerGroups() throws Exception {
        // store triggers
        JobDetail job = getJobDetail();
        jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false);
        jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false);

        // pause triggers
        Collection<String> pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group1"));
        pausedGroups.addAll(jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group3")));

        assertThat(pausedGroups, hasSize(2));
        assertThat(pausedGroups, containsInAnyOrder("group3", "group1"));

        // retrieve paused trigger groups
        Set<String> pausedTriggerGroups = jobStore.getPausedTriggerGroups();
        assertThat(pausedTriggerGroups, hasSize(2));
        assertThat(pausedTriggerGroups, containsInAnyOrder("group1", "group3"));
    }

    @Test
    public void pauseAndResumeAll() throws Exception {
        // store some jobs with triggers
        Map<JobDetail, Set<? extends Trigger>> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2);
        jobStore.storeJobsAndTriggers(jobsAndTriggers, false);

        // ensure that all triggers are in the NORMAL state
        for (Map.Entry<JobDetail, Set<? extends Trigger>> jobDetailSetEntry : jobsAndTriggers.entrySet()) {
            for (Trigger trigger : jobDetailSetEntry.getValue()) {
                assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey()));
            }
        }

        jobStore.pauseAll();

        // ensure that all triggers were paused
        for (Map.Entry<JobDetail, Set<? extends Trigger>> jobDetailSetEntry : jobsAndTriggers.entrySet()) {
            for (Trigger trigger : jobDetailSetEntry.getValue()) {
                assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger.getKey()));
            }
        }

        // resume all triggers
        jobStore.resumeAll();

        // ensure that all triggers are again in the NORMAL state
        for (Map.Entry<JobDetail, Set<? extends Trigger>> jobDetailSetEntry : jobsAndTriggers.entrySet()) {
            for (Trigger trigger : jobDetailSetEntry.getValue()) {
                assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey()));
            }
        }
    }

    @Test
    @SuppressWarnings("unchecked")
    public void triggersFired() throws Exception {
        // store some jobs with triggers
        Map<JobDetail, Set<? extends Trigger>> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2, "* * * * * ?");

        // disallow concurrent execution for one of the jobs
        Map.Entry<JobDetail, Set<? extends Trigger>> firstEntry = jobsAndTriggers.entrySet().iterator().next();
        JobDetail nonConcurrentKey = firstEntry.getKey().getJobBuilder().ofType(TestJobNonConcurrent.class).build();
        Set<? extends Trigger> nonConcurrentTriggers = firstEntry.getValue();
        jobsAndTriggers.remove(firstEntry.getKey());
        jobsAndTriggers.put(nonConcurrentKey, nonConcurrentTriggers);

        jobStore.storeCalendar("testCalendar", new WeeklyCalendar(), false, true);
        jobStore.storeJobsAndTriggers(jobsAndTriggers, false);

        List<OperableTrigger> acquiredTriggers = jobStore.acquireNextTriggers(System.currentTimeMillis() - 1000, 500, 4000);
        assertThat(acquiredTriggers, hasSize(13));

        int lockedTriggers = 0;
        // ensure that all triggers are in the NORMAL state and have been ACQUIRED
        for (Map.Entry<JobDetail, Set<? extends Trigger>> jobDetailSetEntry : jobsAndTriggers.entrySet()) {
            for (Trigger trigger : jobDetailSetEntry.getValue()) {
                assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey()));
                String triggerHashKey = schema.triggerHashKey(trigger.getKey());
                if (jobDetailSetEntry.getKey().isConcurrentExectionDisallowed()) {
                    if (jedis.zscore(schema.triggerStateKey(RedisTriggerState.ACQUIRED), triggerHashKey) != null) {
                        assertThat("acquired trigger should be locked", jedis.get(schema.triggerLockKey(schema.triggerKey(triggerHashKey))), notNullValue());
                        lockedTriggers++;
                    } else {
                        assertThat("non-acquired trigger should not be locked", jedis.get(schema.triggerLockKey(schema.triggerKey(triggerHashKey))), nullValue());
                    }
                } else {
                    assertThat(jedis.zscore(schema.triggerStateKey(RedisTriggerState.ACQUIRED), triggerHashKey), not(nullValue()));
                }
            }
        }

        assertThat(lockedTriggers, equalTo(1));

        Set<? extends OperableTrigger> triggers = (Set<? extends  OperableTrigger>) new ArrayList<>(jobsAndTriggers.entrySet()).get(0).getValue();
        List<TriggerFiredResult> triggerFiredResults = jobStore.triggersFired(new ArrayList<>(triggers));
        assertThat("exactly one trigger fired for job with concurrent execution disallowed", triggerFiredResults, hasSize(1));

        triggers = (Set<? extends  OperableTrigger>) new ArrayList<>(jobsAndTriggers.entrySet()).get(1).getValue();
        triggerFiredResults = jobStore.triggersFired(new ArrayList<>(triggers));
        assertThat("all triggers fired for job with concurrent execution allowed", triggerFiredResults, hasSize(4));
    }

    @Test
    public void replaceTrigger() throws Exception {
        assertFalse(jobStore.replaceTrigger(TriggerKey.triggerKey("foo", "bar"), getCronTrigger()));

        // store triggers and job
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey());
        CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey());
        storeJobAndTriggers(job, trigger1, trigger2);

        CronTriggerImpl newTrigger = getCronTrigger("newTrigger", "group1", job.getKey());

        assertTrue(jobStore.replaceTrigger(trigger1.getKey(), newTrigger));

        // ensure that the proper trigger was replaced
        assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue());

        List<OperableTrigger> jobTriggers = jobStore.getTriggersForJob(job.getKey());

        assertThat(jobTriggers, hasSize(2));
        List<TriggerKey> jobTriggerKeys = new ArrayList<>(jobTriggers.size());
        for (OperableTrigger jobTrigger : jobTriggers) {
            jobTriggerKeys.add(jobTrigger.getKey());
        }

        assertThat(jobTriggerKeys, containsInAnyOrder(trigger2.getKey(), newTrigger.getKey()));
    }

    @Test
    public void replaceTriggerSingleTriggerNonDurableJob() throws Exception {
        // store trigger and job
        JobDetail job = getJobDetail();
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey());
        storeJobAndTriggers(job, trigger1);

        CronTriggerImpl newTrigger = getCronTrigger("newTrigger", "group1", job.getKey());

        assertTrue(jobStore.replaceTrigger(trigger1.getKey(), newTrigger));

        // ensure that the proper trigger was replaced
        assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue());

        List<OperableTrigger> jobTriggers = jobStore.getTriggersForJob(job.getKey());

        assertThat(jobTriggers, hasSize(1));

        // ensure that the job still exists
        assertThat(jobStore.retrieveJob(job.getKey()), not(nullValue()));
    }

    @Test(expected = JobPersistenceException.class)
    public void replaceTriggerWithDifferentJob() throws Exception {
        // store triggers and job
        JobDetail job = getJobDetail();
        jobStore.storeJob(job, false);
        CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey());
        jobStore.storeTrigger(trigger1, false);
        CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey());
        jobStore.storeTrigger(trigger2, false);

        CronTriggerImpl newTrigger = getCronTrigger("newTrigger", "group1", JobKey.jobKey("foo", "bar"));

        jobStore.replaceTrigger(trigger1.getKey(), newTrigger);
    }
}