package ca.odell.glazedlists.matchers;

import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.GlazedLists;
import junit.framework.TestCase;

import java.util.Arrays;

/**
 * Test the {@link ThreadedMatcherEditor}.
 *
 * @author James Lemieux
 */
public class ThreadedMatcherEditorTest extends TestCase {

    // The amount of time (in ms) to wait until the CountingMatcherEditorListener is done processing and begins delaying
    private static final long SIMULATED_PROCESSING_DELAY_STARTS = 100;
    // The amount of time (in ms) for the CountingMatcherEditorListener to delay
    private static final long SIMULATED_PROCESSING_DELAY = 250;
    // The amount of time (in ms) to wait until the CountingMatcherEditorListener completes processing AND delaying
    private static final long SIMULATED_PROCESSING_DELAY_WAIT = 300;

    private MatcherEditor.Event matchAll;
    private MatcherEditor.Event matchNone;
    private MatcherEditor.Event matchRelaxed;
    private MatcherEditor.Event matchConstrained;
    private MatcherEditor.Event matchChanged;

    /** combine multiple matcher editors */
    private ThreadedMatcherEditor threadedMatcherEditor;

    /** a matcher editor to help test the threadedMatcherEditor */
    private TextMatcherEditor textMatcherEditor;

    private FilterList filterList;

    /**
     * Prepare for the test.
     */
    public void setUp() {
        textMatcherEditor = new TextMatcherEditor(GlazedLists.toStringTextFilterator());
        threadedMatcherEditor = new ThreadedMatcherEditor(textMatcherEditor);
        filterList = new FilterList(new BasicEventList(), threadedMatcherEditor);

        matchAll = new MatcherEditor.Event(threadedMatcherEditor, MatcherEditor.Event.MATCH_ALL, Matchers.trueMatcher());
        matchNone = new MatcherEditor.Event(threadedMatcherEditor, MatcherEditor.Event.MATCH_NONE, Matchers.falseMatcher());
        matchRelaxed = new MatcherEditor.Event(threadedMatcherEditor, MatcherEditor.Event.RELAXED, threadedMatcherEditor.getMatcher());
        matchConstrained = new MatcherEditor.Event(threadedMatcherEditor, MatcherEditor.Event.CONSTRAINED, threadedMatcherEditor.getMatcher());
        matchChanged = new MatcherEditor.Event(threadedMatcherEditor, MatcherEditor.Event.CHANGED, threadedMatcherEditor.getMatcher());
    }

    /**
     * Clean up after the test.
     */
    public void tearDown() {
        threadedMatcherEditor = null;
        textMatcherEditor = null;
        filterList = null;

        matchAll = null;
        matchNone = null;
        matchRelaxed = null;
        matchConstrained = null;
        matchChanged = null;
    }

    public void testSimpleCoalescing() {
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchAll}, MatcherEditor.Event.MATCH_ALL);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchNone}, MatcherEditor.Event.MATCH_NONE);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchRelaxed}, MatcherEditor.Event.RELAXED);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchConstrained}, MatcherEditor.Event.CONSTRAINED);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchChanged}, MatcherEditor.Event.CHANGED);
    }

    public void testCoalescingSameElements() {
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchAll, matchAll, matchAll}, MatcherEditor.Event.MATCH_ALL);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchNone, matchNone, matchNone}, MatcherEditor.Event.MATCH_NONE);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchRelaxed, matchRelaxed, matchRelaxed}, MatcherEditor.Event.RELAXED);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchConstrained, matchConstrained, matchConstrained}, MatcherEditor.Event.CONSTRAINED);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchChanged, matchChanged, matchChanged}, MatcherEditor.Event.CHANGED);
    }

    public void testCoalescingMatchAll() {
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchNone, matchRelaxed, matchConstrained, matchChanged, matchAll}, MatcherEditor.Event.MATCH_ALL);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchNone, matchAll}, MatcherEditor.Event.MATCH_ALL);
    }

    public void testCoalescingMatchNone() {
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchAll, matchRelaxed, matchConstrained, matchChanged, matchNone}, MatcherEditor.Event.MATCH_NONE);
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchAll, matchNone}, MatcherEditor.Event.MATCH_NONE);
    }

    public void testCoalescingMatchChanged() {
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchAll, matchChanged});
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchNone, matchChanged});

        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchRelaxed, matchConstrained});
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchConstrained, matchRelaxed});

        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchRelaxed, matchRelaxed, matchConstrained});
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchConstrained, matchConstrained, matchRelaxed});

        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchChanged, matchConstrained});
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchChanged, matchRelaxed});

        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchAll, matchConstrained});
        this.runCoalescingMatchChangedTest(new MatcherEditor.Event[] {matchNone, matchRelaxed});
    }

    private void runCoalescingMatchChangedTest(MatcherEditor.Event[] events) {
        this.runCoalescingMatchChangedTest(events, MatcherEditor.Event.CHANGED);
    }

    private void runCoalescingMatchChangedTest(MatcherEditor.Event[] events, int expectedType) {
        final MatcherEditor.Event coalescedMatcherEvent = coalesceMatcherEvents(threadedMatcherEditor, events);
        // ensure the expectedType is received
        assertEquals(expectedType, coalescedMatcherEvent.getType());

        // ensure the MatcherEditor returned is == to the threadedMatcherEditor (the MatcherEditor which wraps the source)
        // (that is, we rebrand the coalescedMatcherEvent to look like it originates from the ThreadedMatcherEditor
        // rather than the underlying decorated MatcherEditor)
        assertTrue(threadedMatcherEditor == coalescedMatcherEvent.getMatcherEditor());

        // ensure the Matcher returned is == to the last MatcherEvent's Matcher
        assertTrue(events[events.length-1].getMatcher() == coalescedMatcherEvent.getMatcher());
    }

    protected <E> MatcherEditor.Event<E> coalesceMatcherEvents(ThreadedMatcherEditor<E> threadedMatcherEditor, MatcherEditor.Event<E>[] matcherEvents) {
        return threadedMatcherEditor.coalesceMatcherEvents(Arrays.asList(matcherEvents));
    }


    public void testFiltering() throws InterruptedException {
        filterList.addAll(Arrays.asList(new Object[] {"Andy", "Barry", "Colin", "James", "Jesse", "Jesus", "Trevor", "Ursula", "Vanessa", "Zack"}));
        assertEquals(10, filterList.size());

        textMatcherEditor.setFilterText(new String[] {"J"});
        Thread.sleep(SIMULATED_PROCESSING_DELAY_STARTS);
        assertEquals(3, filterList.size());

        textMatcherEditor.setFilterText(new String[] {"ss"});
        Thread.sleep(SIMULATED_PROCESSING_DELAY_STARTS);
        assertEquals(2, filterList.size());
    }

    public void testQueuingConstraints() throws InterruptedException {
        final CountingMatcherEditorListener counter =
            new CountingMatcherEditorListener(SIMULATED_PROCESSING_DELAY);
        threadedMatcherEditor.addMatcherEditorListener(counter);

        textMatcherEditor.setFilterText(new String[] {"J"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "J"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_STARTS);

        // now fill the queue with constraints one at a time, as through the user were typing "James"
        textMatcherEditor.setFilterText(new String[] {"Ja"});
        textMatcherEditor.setFilterText(new String[] {"Jam"});
        textMatcherEditor.setFilterText(new String[] {"Jame"});
        textMatcherEditor.setFilterText(new String[] {"James"});

        // ensure the matching finishes, and then check if each of the methods were fired the expected number of times
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(0, 0, 0, 2, 0);
    }

    public void testQueuingRelaxations() throws InterruptedException {
        final CountingMatcherEditorListener counter =
            new CountingMatcherEditorListener(SIMULATED_PROCESSING_DELAY);
        threadedMatcherEditor.addMatcherEditorListener(counter);

        textMatcherEditor.setFilterText(new String[] {"James"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "James"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_STARTS);

        // now fill the queue with relaxations one at a time, as through the user were deleting "James"
        textMatcherEditor.setFilterText(new String[] {"Jame"});
        textMatcherEditor.setFilterText(new String[] {"Jam"});
        textMatcherEditor.setFilterText(new String[] {"Ja"});
        textMatcherEditor.setFilterText(new String[] {"J"});

        // ensure the matching finishes, and then check if each of the methods were fired the expected number of times
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(0, 0, 0, 1, 1);
    }

    public void testQueuingMatchAll() throws InterruptedException {
        final CountingMatcherEditorListener counter =
            new CountingMatcherEditorListener(SIMULATED_PROCESSING_DELAY);
        threadedMatcherEditor.addMatcherEditorListener(counter);

        textMatcherEditor.setFilterText(new String[] {"James"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "James"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_STARTS);

        // simulate changing, then clearing the filter text
        textMatcherEditor.setFilterText(new String[] {"Scott"});
        textMatcherEditor.setFilterText(new String[0]);

        // ensure the matching finishes, and then check if each of the methods were fired the expected number of times
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(1, 0, 0, 1, 0);
    }

    public void testQueuingChanged() throws InterruptedException {
        final CountingMatcherEditorListener counter =
            new CountingMatcherEditorListener(SIMULATED_PROCESSING_DELAY);
        threadedMatcherEditor.addMatcherEditorListener(counter);

        textMatcherEditor.setFilterText(new String[] {"James"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "James"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_STARTS);

        textMatcherEditor.setFilterText(new String[] {"Scott"});
        textMatcherEditor.setFilterText(new String[] {"Jesse"});
        textMatcherEditor.setFilterText(new String[] {"Kevin"});

        // ensure the matching finishes, and then check if each of the methods were fired the expected number of times
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(0, 0, 1, 1, 0);
    }

    public void testQueuingAllSorts_WithPause() throws InterruptedException {
        final CountingMatcherEditorListener counter =
            new CountingMatcherEditorListener(SIMULATED_PROCESSING_DELAY);
        threadedMatcherEditor.addMatcherEditorListener(counter);

        textMatcherEditor.setFilterText(new String[] {"James"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "James"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_STARTS);
        counter.assertCounterState(0, 0, 0, 1, 0);

        textMatcherEditor.setFilterText(new String[] {"Ja"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "Ja"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(0, 0, 0, 1, 1);

        textMatcherEditor.setFilterText(new String[] {"Col"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "Col"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(0, 0, 1, 1, 1);

        textMatcherEditor.setFilterText(new String[] {"Colin"});
        // ensure we pause to let the time slice end and the Queue Thread to start and begin processing the "Colin"
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(0, 0, 1, 2, 1);

        textMatcherEditor.setFilterText(new String[0]);

        // ensure the matching finishes, and then check if each of the methods were fired the expected number of times
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        counter.assertCounterState(1, 0, 1, 2, 1);

        // since we wait for each change to the filter text to clear, we should
        // expect to find exactly the same number of total changes
        assertEquals(counter.getChangeCount(), 5);
    }

    public void testQueuingAllSorts_WithoutPause() throws InterruptedException {
        final CountingMatcherEditorListener counter =
            new CountingMatcherEditorListener(SIMULATED_PROCESSING_DELAY);
        threadedMatcherEditor.addMatcherEditorListener(counter);

        textMatcherEditor.setFilterText(new String[] {"James"});
        textMatcherEditor.setFilterText(new String[] {"Ja"});
        textMatcherEditor.setFilterText(new String[] {"Col"});
        textMatcherEditor.setFilterText(new String[] {"Colin"});
        textMatcherEditor.setFilterText(new String[0]);

        // ensure the matching finishes, and then check if each of the methods were fired the expected number of times
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);
        Thread.sleep(SIMULATED_PROCESSING_DELAY_WAIT);

        // because of modern multi-core processors, we can't predict EXACTLY
        // how the ThreadedMatcherEditor combined filters, but we do know that
        // SOMETHING must have been combined, and thus the number of changes
        // should be less than the number of times we changed the filter text
        assertTrue(counter.getChangeCount() < 5);
    }
}