function callPeriodicallyUntil(block, delay, condition, i) {  // i is optional
    i = i || 0;
    block(i);
    waits(delay);
    runs(function () {
        if (!condition()) {
            callPeriodicallyUntil(block, delay, condition, i + 1);
        }
    });
}

describe("Formula Equation Preview", function () {
    beforeEach(function () {
        // Simulate an environment conducive to a FormulaEquationInput
        var $fixture = this.$fixture = $('\
<section class="problems-wrapper" data-url="THE_URL">\
  <section class="formulaequationinput">\
    <input type="text" id="input_THE_ID" data-input-id="THE_ID"\
        value="prefilled_value"/>\
    <div id="input_THE_ID_preview" class="equation">\
      \[\]\
      <img class="loading" style="visibility:hidden"/>\
    </div>\
  </section>\
</section>');

        // Modify $ for the test to search the fixture.
        var old$find = this.old$find = $.find;
        $.find = function () {
            // Given the default context, swap it out for the fixture.
            if (arguments[1] == document) {
                arguments[1] = $fixture[0];
            }

            // Call old function.
            return old$find.apply(this, arguments);
        }
        $.find.matchesSelector = old$find.matchesSelector;

        this.oldDGEBI = document.getElementById;
        document.getElementById = function (id) {
            return $("*#" + id)[0] || null;
        };

        // Catch the AJAX requests
        var ajaxTimes = this.ajaxTimes = [];
        this.oldProblem = window.Problem;

        window.Problem = {};
        Problem.inputAjax = jasmine.createSpy('Problem.inputAjax')
            .andCallFake(function () {
                ajaxTimes.push(Date.now());
            });

        // Spy on MathJax
        this.jax = 'OUTPUT_JAX';
        this.oldMathJax = window.MathJax;

        window.MathJax = {Hub: {}};
        MathJax.Hub.getAllJax = jasmine.createSpy('MathJax.Hub.getAllJax')
            .andReturn([this.jax]);
        MathJax.Hub.Queue = jasmine.createSpy('MathJax.Hub.Queue');
    });

    it('(the test) should be able to swap out the behavior of $', function () {
        // This was a pain to write, make sure it doesn't get screwed up.

        // Find the DOM element using DOM methods.
        var legitInput = this.$fixture[0].getElementsByTagName("input")[0];

        // Use the (modified) jQuery.
        var jqueryInput = $('.formulaequationinput input');
        var byIdInput = $("#input_THE_ID");

        expect(jqueryInput[0]).toEqual(legitInput);
        expect(byIdInput[0]).toEqual(legitInput);
    });

    describe('Ajax requests', function () {
        it('has an initial request with the correct parameters', function () {
            formulaEquationPreview.enable();

	    expect(MathJax.Hub.Queue).toHaveBeenCalled();
	    // Do what Queue would've done--call the function.
	    var args = MathJax.Hub.Queue.mostRecentCall.args;
	    args[1].call(args[0]);

            // This part may be asynchronous, so wait.
            waitsFor(function () {
                return Problem.inputAjax.wasCalled;
            }, "AJAX never called initially", 1000);

	    runs(function () {
		expect(Problem.inputAjax.callCount).toEqual(1);

		// Use `.toEqual` rather than `.toHaveBeenCalledWith`
		// since it supports `jasmine.any`.
		expect(Problem.inputAjax.mostRecentCall.args).toEqual([
                    "THE_URL",
                    "THE_ID",
                    "preview_formcalc",
                    {formula: "prefilled_value",
                     request_start: jasmine.any(Number)},
                    jasmine.any(Function)
		]);
	    });
        });

        it('makes a request on user input', function () {
            formulaEquationPreview.enable();
            $('#input_THE_ID').val('user_input').trigger('input');

            // This part is probably asynchronous
            waitsFor(function () {
                return Problem.inputAjax.wasCalled;
            }, "AJAX never called on user input", 1000);

            runs(function () {
                expect(Problem.inputAjax.mostRecentCall.args[3].formula
                      ).toEqual('user_input');
            });
        });

        it("shouldn't be requested for empty input", function () {
            formulaEquationPreview.enable();
	    MathJax.Hub.Queue.reset();

            // When we make an input of '',
            $('#input_THE_ID').val('').trigger('input');

            // Either it makes a request or jumps straight into displaying ''.
            waitsFor(function () {
                // (Short circuit if `inputAjax` is indeed called)
                return Problem.inputAjax.wasCalled || 
                    MathJax.Hub.Queue.wasCalled;
            }, "AJAX never called on user input", 1000);

            runs(function () {
                // Expect the request not to have been called.
                expect(Problem.inputAjax).not.toHaveBeenCalled();
            });
        });

        it('should limit the number of requests per second', function () {
            formulaEquationPreview.enable();

	    var minDelay = formulaEquationPreview.minDelay;
            var end = Date.now() + minDelay * 1.1;
            var step = 10;  // ms

            var $input = $('#input_THE_ID');
            var value;
            function inputAnother(iter) {
                value = "math input " + iter;
                $input.val(value).trigger('input');
            }

            callPeriodicallyUntil(inputAnother, step, function () {
                return Date.now() > end;  // Stop when we get to `end`.
            });

            waitsFor(function () {
                return Problem.inputAjax.wasCalled &&
                    Problem.inputAjax.mostRecentCall.args[3].formula == value;
            }, "AJAX never called with final value from input", 1000);

            runs(function () {
                // There should be 2 or 3 calls (depending on leading edge).
                expect(Problem.inputAjax.callCount).not.toBeGreaterThan(3);

                // The calls should happen approximately `minDelay` apart.
                for (var i =1; i < this.ajaxTimes.length; i ++) {
                    var diff = this.ajaxTimes[i] - this.ajaxTimes[i - 1];
                    expect(diff).toBeGreaterThan(minDelay - 10);
                }
            });
        });
    });

    describe("Visible results (icon and mathjax)", function () {
        it('should display a loading icon when requests are open', function () {
            formulaEquationPreview.enable();
            var $img = $("img.loading");
            expect($img.css('visibility')).toEqual('hidden');

            $("#input_THE_ID").val("different").trigger('input');
            expect($img.css('visibility')).toEqual('visible');

            // Don't let it fail later.
            waitsFor(function () {
		return Problem.inputAjax.wasCalled;
            });
        });

        it('should update MathJax and loading icon on callback', function () {
            formulaEquationPreview.enable();
            $('#input_THE_ID').val('user_input').trigger('input');

            waitsFor(function () {
                return Problem.inputAjax.wasCalled;
            }, "AJAX never called initially", 1000);

            runs(function () {
                var args = Problem.inputAjax.mostRecentCall.args;
                var callback = args[4];
                callback({
                    preview: 'THE_FORMULA',
                    request_start: args[3].request_start
                });

                // The only request returned--it should hide the loading icon.
                expect($("img.loading").css('visibility')).toEqual('hidden');

                // We should look in the preview div for the MathJax.
                var previewDiv = $("div")[0];
                expect(MathJax.Hub.getAllJax).toHaveBeenCalledWith(previewDiv);

                // Refresh the MathJax.
                expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
                    ['Text', this.jax, 'THE_FORMULA'],
                    ['Reprocess', this.jax]
                );
            });
        });

        it('should display errors from the server well', function () {
            var $img = $("img.loading");
            formulaEquationPreview.enable();
	    MathJax.Hub.Queue.reset();
            $("#input_THE_ID").val("different").trigger('input');

            waitsFor(function () {
                return Problem.inputAjax.wasCalled;
            }, "AJAX never called initially", 1000);

            runs(function () {
                var args = Problem.inputAjax.mostRecentCall.args;
                var callback = args[4];
                callback({
                    error: 'OOPSIE',
                    request_start: args[3].request_start
                });
                expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
                expect($img.css('visibility')).toEqual('visible');
            });

            var errorDelay = formulaEquationPreview.errorDelay * 1.1;
            waitsFor(function () {
                return MathJax.Hub.Queue.wasCalled;
            }, "Error message never displayed", 2000);

            runs(function () {
                // Refresh the MathJax.
                expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
                    ['Text', this.jax, '\\text{OOPSIE}'],
                    ['Reprocess', this.jax]
                );
                expect($img.css('visibility')).toEqual('hidden');
            });
        });
    });

    describe('Multiple callbacks', function () {
        beforeEach(function () {
            formulaEquationPreview.enable();
	    MathJax.Hub.Queue.reset();
            $('#input_THE_ID').val('different').trigger('input');

	    waitsFor(function () {
		return Problem.inputAjax.wasCalled;
	    });

	    runs(function () {
		$("#input_THE_ID").val("different2").trigger('input');
	    });

            waitsFor(function () {
                return Problem.inputAjax.callCount > 1;
            });

            runs(function () {
                var args = Problem.inputAjax.argsForCall;
                var response0 = {
                    preview: 'THE_FORMULA_0',
                    request_start: args[0][3].request_start
                };
                var response1 = {
                    preview: 'THE_FORMULA_1',
                    request_start: args[1][3].request_start
                };

                this.callbacks = [args[0][4], args[1][4]];
                this.responses = [response0, response1];
            });
        });

        it('should update requests sequentially', function () {
            var $img = $("img.loading");

            expect($img.css('visibility')).toEqual('visible');

            this.callbacks[0](this.responses[0]);
            expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
                ['Text', this.jax, 'THE_FORMULA_0'],
                ['Reprocess', this.jax]
            );
            expect($img.css('visibility')).toEqual('visible');

            this.callbacks[1](this.responses[1]);
            expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
                ['Text', this.jax, 'THE_FORMULA_1'],
                ['Reprocess', this.jax]
            );
            expect($img.css('visibility')).toEqual('hidden')
        });

        it("shouldn't display outdated information", function () {
            var $img = $("img.loading");

            expect($img.css('visibility')).toEqual('visible');

            // Switch the order (1 returns before 0)
            this.callbacks[1](this.responses[1]);
            expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
                ['Text', this.jax, 'THE_FORMULA_1'],
                ['Reprocess', this.jax]
            );
            expect($img.css('visibility')).toEqual('hidden')

            MathJax.Hub.Queue.reset();
            this.callbacks[0](this.responses[0]);
            expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
            expect($img.css('visibility')).toEqual('hidden')
        });

        it("shouldn't show an error if the responses are close together",
           function () {
               this.callbacks[0]({
                   error: 'OOPSIE',
                   request_start: this.responses[0].request_start
               });
               expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
               // Error message waiting to be displayed

               this.callbacks[1](this.responses[1]);
               expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
                   ['Text', this.jax, 'THE_FORMULA_1'],
                   ['Reprocess', this.jax]
               );

               // Make sure that it doesn't indeed show up later
               MathJax.Hub.Queue.reset();
               var errorDelay = formulaEquationPreview.errorDelay * 1.1;
               waits(errorDelay);

               runs(function () {
                   expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
               })
           });
    });

    afterEach(function () {
        // Return jQuery
        $.find = this.old$find;
        document.getElementById = this.oldDGEBI;

        // Return Problem
        Problem = this.oldProblem;
        if (Problem === undefined) {
            delete Problem;
        }

        // Return MathJax
        MathJax = this.oldMathJax;
        if (MathJax === undefined) {
            delete MathJax;
        }
    });
});
