/* globals jasmine, jasmineReporters, describe, beforeEach, it, expect, spyOn */
(function(){
    var env, suite, subSuite, subSubSuite, siblingSuite,
        reporter, writeCalls, suiteId=0, specId=0;
    function fakeSpec(ste, name) {
        var s = new jasmine.Spec({
            env: env,
            id: specId++,
            description: name
        });
        ste.addChild(s);
        return s;
    }
    function fakeSuite(name, parentSuite) {
        var s = new jasmine.Suite({
            env: env,
            id: suiteId++,
            description: name,
            parentSuite: parentSuite || jasmine.createSpy("pretend top suite") // I'm sure there's a good reason Jasmine does this...
        });
        if (parentSuite) {
            parentSuite.addChild(s);
        }
        else {

            env._suites = env._suites || [];
            env._suites.push(s);
        }
        return s;
    }

    function setupReporterWithOptions(options) {
        reporter = new jasmineReporters.NUnitXmlReporter(options);
        reporter.writeFile = jasmine.createSpy();
    }

    // make sure reporter is set before calling this
    function triggerRunnerEvents() {
        reporter.jasmineStarted();
        for (var i=0; i<env._suites.length; i++) {
            var s = env._suites[i];
            triggerSuiteEvents(s);
        }
        reporter.jasmineDone();

        // pre-parse some data to be used by various specs
        writeCalls = reporter.writeFile.calls.all();
        for (i=0; i<writeCalls.length; i++) {
            writeCalls[i].output = writeCalls[i].args[0];
            writeCalls[i].xmldoc = xmlDocumentFromString(writeCalls[i].output);
        }
    }
    function triggerSuiteEvents(ste) {
        reporter.suiteStarted(ste.result);
        var thing;
        for (var i=0; i<ste.children.length; i++) {
            thing = ste.children[i];
            if (thing instanceof jasmine.Suite) {
                triggerSuiteEvents(thing);
            } else {
                reporter.specStarted(thing.result);
                reporter.specDone(thing.result);
            }
        }
        reporter.suiteDone(ste.result);
    }

    function xmlDocumentFromString(str) {
        return (new DOMParser()).parseFromString(str, "text/xml");
    }

    describe("NUnitXmlReporter", function(){

        beforeEach(function(){
            env = new jasmine.Env();
            suite = fakeSuite("ParentSuite");
            subSuite = fakeSuite("SubSuite", suite);
            subSubSuite = fakeSuite("SubSubSuite", subSuite);
            siblingSuite = fakeSuite("SiblingSuite With Invalid Chars & < > \" ' | : \\ /");
            var spec = fakeSpec(suite, "should be a dummy with invalid characters: & < >");
            var failedSpec = fakeSpec(subSubSuite, "should be failed");
            failedSpec.result.status = "failed";
            failedSpec.result.failedExpectations.push({
                passed: false,
                message: "Expected true to be false.",
                expected: false,
                actual: true,
                matcherName: 'toBe',
                stack: "Stack trace! Stack trackes are cool & can have \"special\" characters <3\n\n Neat: yes."
            });
            var subSpec = fakeSpec(subSuite, "should be one level down");
            var subSubSpec1 = fakeSpec(subSubSuite, "(1) should be two levels down");
            var subSubSpec2 = fakeSpec(subSubSuite, "(2) should be two levels down");
            var subSubSpec3 = fakeSpec(subSubSuite, "(3) should be two levels down");
            var siblingSpec = fakeSpec(siblingSuite, "should be a sibling of Parent");
            setupReporterWithOptions({reportName: "<Bad Character Report>"});
        });

        describe("constructor", function(){
            beforeEach(function() {
                setupReporterWithOptions();
            });
            it("should default path to an empty string", function(){
                expect(reporter.savePath).toBe('');
            });
            it("should allow a custom path to be provided", function() {
                setupReporterWithOptions({savePath:'/tmp'});
                expect(reporter.savePath).toBe('/tmp');
            });
            it("should default filename to 'nunitresults.xml'", function(){
                expect(reporter.filename).toBe("nunitresults.xml");
            });
            it("should allow a custom filename to be provided", function() {
                setupReporterWithOptions({filename:'results.xml'});
                expect(reporter.filename).toBe('results.xml');
            });
            it("should default reportName to 'Jasmine Results'", function(){
                expect(reporter.reportName).toBe("Jasmine Results");
            });
            it("should allow a custom reportName to be provided", function() {
                setupReporterWithOptions({reportName:"Test Results"});
                expect(reporter.reportName).toBe("Test Results");
            });
        });

        describe("generated xml output", function(){
            var output, xmldoc;

            beforeEach(function(){
                triggerRunnerEvents();
                output = writeCalls[0].output;
                xmldoc = writeCalls[0].xmldoc;
            });
            it("should escape invalid xml chars from report name", function() {
                expect(output).toContain('name="&amp;lt;Bad Character Report&amp;gt;"');
            });
            it("should escape invalid xml chars from suite names", function() {
                expect(output).toContain('name="SiblingSuite With Invalid Chars &amp; &amp;lt; &amp;gt; &amp;quot; &amp;apos; | : \\ /"');
            });
            it("should escape invalid xml chars from spec names", function() {
                expect(output).toContain('name="should be a dummy with invalid characters: &amp; &amp;lt; &amp;gt;');
            });
            describe("xml structure", function() {
                var rootNode, suites, specs;
                beforeEach(function() {
                    rootNode = xmldoc.getElementsByTagName("test-results")[0];
                    suites = rootNode.getElementsByTagName("test-suite");
                    specs = rootNode.getElementsByTagName("test-case");
                });
                it("should report the date / time that the tests were run", function() {
                    var now = new Date();
                    expect(rootNode.getAttribute("date")).toMatch(/\d{4}-\d{2}-\d{2}/);
                    expect(rootNode.getAttribute("time")).toMatch(/\d{2}:\d{2}:\d{2}/);
                });
                it("should report the appropriate number of suites", function() {
                    expect(suites.length).toBe(4);
                });
                it("should order suites appropriately", function() {
                    expect(suites[0].getAttribute("name")).toContain("ParentSuite");
                    expect(suites[1].getAttribute("name")).toContain("SubSuite");
                    expect(suites[2].getAttribute("name")).toContain("SubSubSuite");
                    expect(suites[3].getAttribute("name")).toContain("SiblingSuite");
                });
                it("should nest suites appropriately", function() {
                    expect(suites[0].parentNode).toBe(rootNode);
                    expect(suites[1].parentNode).toBe(suites[0].getElementsByTagName("results")[0]);
                    expect(suites[2].parentNode).toBe(suites[1].getElementsByTagName("results")[0]);
                    expect(suites[3].parentNode).toBe(rootNode);
                });
                it("should report the execution time for test specs", function() {
                    var time;
                    for (var i = 0; i < specs.length; i++) {
                        time = specs[i].getAttribute("time");
                        expect(time.length).toBeGreaterThan(0);
                        expect(time).not.toContain(":"); // as partial seconds, not a timestamp
                    }
                });
                it("should include a test-case for each spec and report the total number of specs on the root node", function() {
                    expect(rootNode.getAttribute("total")).toBe(specs.length.toString());
                });
                describe("passed specs", function() {
                    it("should indicate that the test case was successful", function() {
                        expect(specs[1].getAttribute("success")).toBe("true");
                    });
                });
                describe("failed specs", function() {
                    var failedSpec;
                    beforeEach(function() {
                        failedSpec = rootNode.getElementsByTagName("message")[0].parentNode.parentNode;
                    });
                    it("should indicate that the test case was not successful", function() {
                        expect(failedSpec.getAttribute("success")).toBe("false");
                    });
                    it("should include the error for failed specs", function() {
                        expect(failedSpec.getElementsByTagName("message")[0].textContent).toBe('Expected true to be false.');
                    });
                    it("should include the stack trace for failed specs", function() {
                        expect(failedSpec.getElementsByTagName("stack-trace")[0].textContent).toContain('cool & can have "special" characters <3');
                    });
                    it("should report the failure on ancestor suite nodes", function() {
                        var parentSuite = failedSpec.parentNode.parentNode;
                        var grandparentSuite = parentSuite.parentNode.parentNode;
                        expect(parentSuite.getAttribute("success")).toBe("false");
                        expect(grandparentSuite.getAttribute("success")).toBe("false");
                    });
                    it("should report the number of failed specs on the root node", function() {
                        expect(rootNode.getAttribute("failures")).toBe("1");
                    });
                });
            });
        });
    });
})();

