 #
 page = require('webpage').create()
 
-# Capture console.log output to wrap the output
-# from the specs into a JSON message.
+# Used to collect log messages for later assignment to the spec
+#
+currentSpecId = 0
+logs = {}
+
+# Add logs to the given suite
+#
+# @param suite [Object} the suite result
+#
+page.addLogs = (suite) ->
+  for s in suite.suites
+    arguments.callee(s) if s
+
+  for spec in suite.specs
+    id = Number(spec['id'])
+    spec['logs'] = logs[id] if logs[id] && logs[id].length isnt 0
+    delete spec['id']
+
+  delete suite['id']
+  delete suite['parent']
+
+# Capture console.log output to add it to
+# the result when specs have finished.
 #
 page.onConsoleMessage = (msg, line, source) ->
-  if /^ConsoleReporter: ([\s\S]*)$/.test(msg)
-    console.log RegExp.$1
+  if /^RUNNER_RESULT: ([\s\S]*)$/.test(msg)
+    result = JSON.parse(RegExp.$1)
+
+    for suite in result.suites
+      page.addLogs(suite)
+
+    console.log JSON.stringify(result, undefined, 2)
+
+  else if /^SPEC_START: (\d+)$/.test(msg)
+    currentSpecId = Number(RegExp.$1)
+    logs[currentSpecId] = []
+
   else
-    console.log JSON.stringify({ log: msg, line: line, source: source })
+    logs[currentSpecId].push "#{ msg } in #{ source } (line #{ line })"
 
 # Initialize the page before the JavaScript is run.
 #
 page.onInitialized = ->
   page.evaluate ->
 
-    # Jasmine Reporter that logs reporter steps.
+    # Jasmine Reporter that logs reporter steps
+    # and results to the console.
     #
     class ConsoleReporter
 
+      runnerResult: {
+        passed: false
+        stats: {
+          specs: 0
+          failures: 0
+          time: 0.0
+        }
+        suites: []
+      }
+
+      currentSpecs: []
+      nestedSuiteResults: {}
+
+      # Report the start of a spec.
+      #
+      # @param spec [jasmine.Spec] the spec
+      #
+      reportSpecStarting: (spec) ->
+        console.log "SPEC_START: #{ spec.id }"
+
+      # Report results from a spec.
+      #
+      # @param spec [jasmine.Spec] the spec
+      #
       reportSpecResults: (spec) ->
-        @report { spec: spec.description, failed: spec.results().failedCount, skiped: spec.results().skipped }
-
+        unless spec.results().skipped
+          specResult = {
+            id: spec.id
+            description: spec.description
+            passed: spec.results().failedCount is 0
+          }
+
+          if spec.results().failedCount isnt 0
+            messages = []
+            messages.push result.message for result in spec.results().getItems()
+            specResult['messages'] = messages if messages.length isnt 0
+
+          @currentSpecs.push specResult
+
+      # Report results from a suite.
+      #
+      # @param suite [jasmine.Suite] the suite
+      #
       reportSuiteResults: (suite) ->
-        @report { suite: suite.getFullName(), failed: suite.results().failedCount, skiped: suite.results().skipped  }
-
+        unless suite.results().skipped
+          suiteResult = {
+            id: suite.id
+            parent: suite.parentSuite?.id
+            description: suite.description
+            passed: suite.results().failedCount is 0
+            specs: @currentSpecs
+            suites: []
+          }
+
+          if suite.parentSuite?
+            parent = suite.parentSuite.id
+            @nestedSuiteResults[parent] = [] unless @nestedSuiteResults[parent]
+            @nestedSuiteResults[parent].push suiteResult
+          else
+            @addNestedSuites suiteResult
+            @removeEmptySuites suiteResult
+
+            if suiteResult.specs.length isnt 0 || suiteResult.suites.length isnt 0
+              @runnerResult.suites.push suiteResult
+
+        @currentSpecs = []
+
+      # Report results from the runner.
+      #
+      # @param runner [jasmine.Runner] the runner
+      #
       reportRunnerResults: (runner) ->
-        @report { finish: runner.description, total: runner.results().totalCount, failed: runner.results().failedCount }
-
+        runtime = (new Date().getTime() - @startTime) / 1000
+
+        @runnerResult['passed'] = runner.results().failedCount is 0
+
+        @runnerResult['stats'] = {
+          specs: runner.results().totalCount
+          failures: runner.results().failedCount
+          time: runtime
+        }
+
+        console.log "RUNNER_RESULT: #{ JSON.stringify(@runnerResult) }"
+
+      # Report the start of the runner
+      #
+      # @param runner [jasmine.Runner] the runner
+      #
       reportRunnerStarting: (runner) ->
-      reportSpecStarting: (spec) ->
-      log: (str) ->
-
-      report: (response) ->
-        console.log "ConsoleReporter: #{ JSON.stringify(response) }"
+        @startTime = new Date().getTime()
+
+      # Add all nested suites that have previously
+      # been processed.
+      #
+      # @param suiteResult [Object] the suite result
+      #
+      addNestedSuites: (suiteResult) ->
+        if @nestedSuiteResults[suiteResult.id]
+          for suite in @nestedSuiteResults[suiteResult.id]
+            @addNestedSuites suite
+            suiteResult.suites.push suite
+
+      # Removes suites without child suites or specs.
+      #
+      # @param suiteResult [Object] the suite result
+      #
+      removeEmptySuites: (suiteResult) ->
+        suites = []
+
+        for suite in suiteResult.suites
+          @removeEmptySuites suite
+
+          suites.push suite if suite.suites.length isnt 0 || suite.specs.length isnt 0
+
+        suiteResult.suites = suites
+
+      # Log a message
+      #
+      # @param message [String] the log message
+      #
+      log: (message) ->
 
     # Attach the console reporter when the document is ready.
     #

#
page.open url, (status) ->
  if status isnt 'success'
    console.log JSON.stringify({ error: "Unable to access Jasmine specs at #{ url }" })
    phantom.exit()
  else
    waitFor specsReady, -> phantom.exit()

# This file is the script that runs within PhantomJS, requests the Jasmine specs
# and waits until they are ready.

# Wait until the test condition is true or a timeout occurs.
#
