/*
 Copyright (c) 2012, Oracle and/or its affiliates. All rights
 reserved.
 
 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; version 2 of
 the License.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 02110-1301  USA
 */

/*global unified_debug, fs, path, util, assert, suites_dir,
         test_conn_properties
*/

"use strict";

/* This test harness is documented in the README file.
*/

var udebug = unified_debug.getLogger("harness.js");
var exec = require("child_process").exec;
var re_matching_test_case = /Test\.js$/;
var SQL = {};
var disabledTests = {};
try {
  disabledTests = require("../disabled-tests.conf").disabledTests;
}
catch(e) {}

/* Test  
*/
function Test(name, phase) {
  this.filename = "";
  this.name = name;
  this.phase = (typeof(phase) === 'number') ? phase : 2;
  this.errorMessages = '';
  this.index = 0;
  this.failed = null;
  this.skipped = false;
}

function SmokeTest(name) {
  this.name = name;
  this.phase = 0;
}
SmokeTest.prototype = new Test();

function ConcurrentTest(name) {
  this.name = name;
  this.phase = 1;
}
ConcurrentTest.prototype = new Test();


function SerialTest(name) {
  this.name = name;
  this.phase = 2;
}
SerialTest.prototype = new Test();


function ClearSmokeTest(name) {
  this.name = name;
  this.phase = 3;
}
ClearSmokeTest.prototype = new Test();


Test.prototype.test = function(result) {
  udebug.log_detail('test starting:', this.suite.name, this.name);
  this.result = result;
  result.listener.startTest(this);
  var runReturnCode;

  udebug.log_detail('test.run:', this.suite.name, this.name);
  try {
    runReturnCode = this.run();
  }
  catch(e) {
    console.log(this.name, 'threw exception & failed\n', e.stack);
    this.failed = true;
    result.fail(this, e);
    return;
  }

  if(! runReturnCode) {
    // async test must call Test.pass or Test.fail when done
    udebug.log(this.name, 'started.');
    return;
  }

  // Test ran synchronously.  Fail if any error messages have been reported.
  if(! this.skipped) {
    if (this.errorMessages === '') {
      udebug.log_detail(this.name, 'passed');
      result.pass(this);
    } else {
      this.failed = true;
      udebug.log_detail(this.name, 'failed');
      result.fail(this, this.errorMessages);
    }
  }
};

Test.prototype.pass = function() {
  if (this.failed !== null) {
    console.log('Error: pass called with status already ' + (this.failed?'failed':'passed'));
    assert(this.failed === null);
  } else {
    this.failed = false;
    this.result.pass(this);
  }
};

Test.prototype.fail = function(message) {
  if (this.failed !== null) {
    console.log('Error: pass called with status already ' + (this.failed?'failed':'passed'));
    assert(this.failed === null);
  } else {
    this.failed = true;
    if (message) {
      this.appendErrorMessage(message);
    }
    this.result.fail(this, { 'message' : this.errorMessages});
  }
};

Test.prototype.appendErrorMessage = function(message) {
  this.errorMessages += message;
  this.errorMessages += '\n';
};

Test.prototype.failOnError = function() {
  if (this.errorMessages !== '') {
    this.fail();
  } else {
    this.pass();
  }
};

Test.prototype.skip = function(message) {
  this.skipped = true;
  this.result.skip(this, message);
  return true;
};

Test.prototype.isTest = function() { return true; };

Test.prototype.fullName = function() {
  var n = "";
  if(this.suite)    { n = n + this.suite.name + " "; }
  if(this.filename) { n = n + path.basename(this.filename) + " "; }
  return n + this.name;
};

Test.prototype.run = function() {
  throw {
    "name" : "unimplementedTest",
    "message" : "this test does not have a run() method"
  };
};

function getType(obj) {
  var type = typeof(obj);
  console.log(util.inspect(obj));
  if (type === 'object') return obj.constructor.name;
  return type;
}

function compare(o1, o2) {
  if (o1 == o2) return true;
  if (o1 == null && o2 == null) return true;
  if (typeof(o1) === 'undefined' && typeof(o2) === 'undefined') return true;
  if (typeof(o1) !== typeof(o2)) return false;
  if (o1.toString() === o2.toString()) return true;
  return false;
}

Test.prototype.errorIfNotEqual = function(message, o1, o2) {
	if (!compare(o1, o2)) {
	  var o1type = getType(o1);
	  var o2type = getType(o2);
    message += ': expected (' + o1type + ') ' + o1 + '; actual (' + o2type + ') ' + o2 + '\n';
		this.errorMessages += message;
	}
};

Test.prototype.errorIfNotStrictEqual = function(message, o1, o2) {
  if(o1 !== o2) {
    var o1type = getType(o1);
    var o2type = getType(o2);
    message += ': expected (' + o1type + ') ' + o1 + '; actual (' + o2type + ') ' + o2 + '\n';
		this.errorMessages += message;
	}
};

Test.prototype.errorIfTrue = function(message, o1) {
  if (o1) {
    message += ': expected not true; actual ' + o1 + '\n';
    this.errorMessages += message;
  }
};

Test.prototype.errorIfNotTrue = function(message, o1) {
  if (o1 !== true) {
    message += ': expected true; actual ' + o1 + '\n';
    this.errorMessages += message;
  }
};

Test.prototype.errorIfNotError = function(message, o1) {
  if (!o1) {
    message += ' did not occur.\n';
    this.errorMessages += message;
  }
};

Test.prototype.errorIfNull = function(message, val) {
  if(val === null) {
    this.errorMessages += message;
  }
};

Test.prototype.errorIfNotNull = function(message, val) {
  if(val !== null) {
    this.errorMessages += message;
  }
};

/* Use this with the error argument in a callback */
Test.prototype.errorIfError = function(val) {
  if(typeof val !== 'undefined' && val !== null) {
    this.errorMessages += util.inspect(val);
  }
};

/* Value must be defined and not-null 
   Function returns true if there was no error; false on error 
*/
Test.prototype.errorIfUnset = function(message, value) {
  var r = (typeof value === 'undefined' || value === null); 
  if(r) {
    this.errorMessages += message;
  }
  return ! r;
};

Test.prototype.hasNoErrors = function() {
  return this.errorMessages.length === 0;
};

/** Suite
  *  A suite consists of all tests in all test programs in a directory 
  *
  */
function Suite(name, path) {
  this.name = name;
  this.path = path;
  this.tests = [];
  this.currentTest = 0;
  this.smokeTest = {};
  this.concurrentTests = [];
  this.numberOfConcurrentTests = 0;
  this.numberOfConcurrentTestsCompleted = 0;
  this.numberOfConcurrentTestsStarted = 0;
  this.firstConcurrentTestIndex = -1;
  this.serialTests = [];
  this.numberOfSerialTests = 0;
  this.firstSerialTestIndex = -1;
  this.nextSerialTestIndex = -1;
  this.clearSmokeTest = {};
  this.testInFile = null;
  this.suite = {};
  this.numberOfRunningConcurrentTests = 0;
  this.skipSmokeTest = false;
  this.skipClearSmokeTest = false;
  udebug.log_detail('Creating Suite for', name, path);
}

Suite.prototype.addTest = function(filename, test) {
  this.filename = path.relative(suites_dir, filename);
  udebug.log_detail('Suite', this.name, 'adding test', test.name, 'from', this.filename);
  test.filename = filename;
  test.suite = this;
  if(disabledTests && disabledTests[this.filename]) {
    udebug.log("Skipping ", this.filename, "[DISABLED]");
  }
  else {
    this.tests.push(test);
  }
  return test;
};

Suite.prototype.addTestsFromFile = function(f, onlyTests) {
  var t, i, j, k, testList, testHash;
  if(onlyTests) {
    onlyTests = String(onlyTests);
    testList = onlyTests.split(",");
    testHash = [];
    for(i = 0 ; i < testList.length ; i ++) {
      k = Number(testList[i]) - 1;
      testHash[k] = 1;
    }
  }
  if(re_matching_test_case.test(f)) {
    t = require(f);
    if(typeof(t.tests) === 'object' && t.tests instanceof Array) {
      for(j = 0 ; j < t.tests.length ; j++) {
        if(onlyTests === null || testHash[j] === 1) {
          this.addTest(f, t.tests[j]);
        }
      }
    }      
    else if(typeof(t.isTest) === 'function' && t.isTest()) {
      this.addTest(f, t);
    }
    else { 
      udebug.log_detail(t);
      throw "Module " + f + " does not export a Test.";
    }
  }
};

Suite.prototype.createTests = function() {  
  var stat = fs.statSync(this.path);
  var suite, i;
  
  if(stat.isFile()) {
    var testFile = this.path;
    this.path = path.dirname(testFile);
    try {
      this.addTestsFromFile(path.join(this.path, "SmokeTest.js"), null);
    } catch(e1) {}
    this.addTestsFromFile(testFile, this.testInFile);
    try {
      this.addTestsFromFile(path.join(this.path, "ClearSmokeTest.js"), null);
    } catch(e2) {}
  }
  else if(stat.isDirectory()) {
    var files = fs.readdirSync(this.path);
    for(i = 0; i < files.length ; i++) {
      this.addTestsFromFile(path.join(this.path, files[i]), null);
    }
  }

  udebug.log_detail('Suite', this.name, 'found', this.tests.length, 'tests.');

  this.tests.forEach(function(t, index) {
    t.original = index;
  });

  this.tests.sort(function(a,b) {
    // sort the tests by phase, preserving the original order within each phase
    if(a.phase < b.phase)  { return -1; }
    if(a.phase === b.phase) { return (a.original < b.original)?-1:1;  }
    return 1;
  });

  suite = this;
  this.tests.forEach(function(t, index) {
    t.index = index;
    t.suite = suite;
    switch(t.phase) {
      case 0:
        suite.smokeTest = t;
        break;
      case 1:
        suite.concurrentTests.push(t);
        if (suite.firstConcurrentTestIndex === -1) {
          suite.firstConcurrentTestIndex = t.index;
          udebug.log_detail('Suite.createTests firstConcurrentTestIndex:', suite.firstConcurrentTestIndex);
        }
        break;
      case 2:
        suite.serialTests.push(t);
        if (suite.firstSerialTestIndex === -1) {
          suite.firstSerialTestIndex = t.index;
          udebug.log_detail('Suite.createTests firstSerialTestIndex:', suite.firstSerialTestIndex);
        }
        break;
      case 3:
        suite.clearSmokeTest = t;
        break;
    }
    udebug.log_detail('createTests sorted test case', t.name, ' ', t.phase, ' ', t.index);
    });
  suite.numberOfConcurrentTests = suite.concurrentTests.length;
  udebug.log_detail('numberOfConcurrentTests for', suite.name, 'is', suite.numberOfConcurrentTests);
  suite.numberOfSerialTests = suite.serialTests.length;
  udebug.log_detail('numberOfSerialTests for', suite.name, 'is', suite.numberOfSerialTests);
};


Suite.prototype.runTests = function(result) {
  var tc;
  if (this.tests.length === 0) { 
    return false;
  }
  this.currentTest = 0;
  tc = this.tests[this.currentTest];
  switch (tc.phase) {
    case 0:
      // smoke test
      // start the smoke test
      if(this.skipSmokeTest) {
        tc.result = result;
        tc.skip("skipping SmokeTest");
      }
      else {
        tc.test(result);
      }
      break;
    case 1:
      // concurrent test is the first test
      // start all concurrent tests
      this.startConcurrentTests(result);
      break;
    case 2:
      // serial test is the first test
      this.startSerialTests(result);
      break;
    case 3:
      // clear smoke test is the first test
      if(this.skipClearSmokeTest) {
       tc.result = result;
       tc.skip("skipping ClearSmokeTest");
      }
      else {
        tc.test(result);
      }
      break;
  }
  return true;
};


Suite.prototype.startConcurrentTests = function(result) {
  var self = this;
  udebug.log_detail('Suite.startConcurrentTests');
  if (this.firstConcurrentTestIndex !== -1) {
    this.concurrentTests.forEach(function(testCase) {
      udebug.log_detail('Suite.startConcurrentTests starting ', self.name, testCase.name);
      testCase.test(result);
      self.numberOfConcurrentTestsStarted++;
    });
    return false;    
  } 
  // else:
  return this.startSerialTests(result);
};


Suite.prototype.startSerialTests = function(result) {
  assert(result);
  udebug.log_detail('Suite.startSerialTests');
  if (this.firstSerialTestIndex !== -1) {
    this.startNextSerialTest(this.firstSerialTestIndex, result);
    return false;
  } 
  // else:
  return this.startClearSmokeTest(result);
};


Suite.prototype.startClearSmokeTest = function(result) {
  assert(result);
  udebug.log_detail('Suite.startClearSmokeTest');
  if (this.skipClearSmokeTest) {
    this.clearSmokeTest.result = result;
    this.clearSmokeTest.skip("skipping ClearSmokeTest");
  }
  else if (this.clearSmokeTest && this.clearSmokeTest.test) {
    this.clearSmokeTest.test(result);
    return false;
  } 
  return true;
};


Suite.prototype.startNextSerialTest = function(index, result) {
  assert(result);
  var testCase = this.tests[index];
  testCase.test(result);
};


/* Notify the suite that a test has completed.
   Returns false if there are more tests to be run,
   true if suite is complete.
 */
Suite.prototype.testCompleted = function(testCase) {
  var tc, index;

  udebug.log_detail('Suite.testCompleted for', this.name, testCase.name, 'phase', 
                    testCase.phase);
  var result = testCase.result;
  switch (testCase.phase) {
    case 0:     // the smoke test completed
      if (testCase.failed) {        // if the smoke test failed, we are done
        return true;          
      } 
      udebug.log_detail('Suite.testCompleted; starting concurrent tests');
      return this.startConcurrentTests(result);

    case 1:     // one of the concurrent tests completed
      udebug.log_detail('Completed ', this.numberOfConcurrentTestsCompleted, 
                 ' out of ', this.numberOfConcurrentTests);
      if (++this.numberOfConcurrentTestsCompleted === this.numberOfConcurrentTests) {
        return this.startSerialTests(result);   // go on to the serial tests
      }
      return false;

    case 2:     // one of the serial tests completed
      index = testCase.index + 1;
      if (index < this.tests.length) {
        tc = this.tests[index];
        if (tc.phase === 2) {     // start another serial test
          tc.test(result);
        } 
        else if (tc.phase === 3) {     // start the clear smoke test
          this.startClearSmokeTest(result);
        }
        return false;
      }
      /* Done */
      udebug.log_detail('Suite.testCompleted there is no ClearSmokeTest so we are done with ' + testCase.suite.name);
      return true;

    case 3:   // the clear smoke test completed
      udebug.log_detail('Suite.testCompleted completed ClearSmokeTest.');
      return true;
  }
};


/* Listener
*/
function Listener() {
  this.started = 0;
  this.ended   = 0;
  this.printStackTraces = false;
  this.runningTests = {};
}

Listener.prototype.startTest = function(t) { 
  this.started++;
  this.runningTests[t.fullName()] = 1;
};

Listener.prototype.pass = function(t) {
  this.ended++;
  delete this.runningTests[t.fullName()];
  console.log("[pass]", t.fullName() );
};

Listener.prototype.skip = function(t, message) {
  this.skipped++;
  delete this.runningTests[t.fullName()];
  console.log("[skipped]", t.fullName(), "\t", message);
};

Listener.prototype.fail = function(t, e) {
  var message = "";
  this.ended++;
  delete this.runningTests[t.fullName()];
  if (e) {
    if (typeof(e.stack) !== 'undefined') {
      t.stack = e.stack;
    }
    if (typeof(e.message) !== 'undefined') {
      message = e.message;
    } else {
      message = e.toString();
    }
  }
  if ((this.printStackTraces) && typeof(t.stack) !== 'undefined') {
    message = t.stack;
  }

  if(t.phase === 0) {
    console.log("[FailSmokeTest]", t.fullName(), "\t", message);
  }
  else {
    console.log("[FAIL]", t.fullName(), "\t", message);
  }
};

Listener.prototype.listRunningTests = function() {
  console.log(this.runningTests);
};


/* QuietListener */
function QuietListener() {
  this.started = 0;
  this.ended   = 0;
  this.runningTests = {};
}

QuietListener.prototype.startTest = Listener.prototype.startTest;

QuietListener.prototype.pass = function(t) {
  this.ended++;
  delete this.runningTests[t.fullName()];
};

QuietListener.prototype.skip = QuietListener.prototype.pass;
QuietListener.prototype.fail = QuietListener.prototype.pass;

QuietListener.prototype.listRunningTests = Listener.prototype.listRunningTests;

/* FailOnlyListener */
function FailOnlyListener() {
  this.fail = Listener.prototype.fail;
}

FailOnlyListener.prototype = new QuietListener();

  
/* Result 
*/
function Result(driver) {
  this.driver = driver;
  this.passed = [];
  this.failed = [];
  this.skipped = [];
}

Result.prototype.pass = function(t) {
  this.passed.push(t.name);
  this.listener.pass(t);
  this.driver.testCompleted(t);
};

Result.prototype.fail = function(t, e) {
  this.failed.push(t.name);
  this.listener.fail(t, e);
  this.driver.testCompleted(t);
};

Result.prototype.skip = function(t, reason) {
  this.skipped.push(t.name);
  this.listener.skip(t, reason);
  this.driver.testCompleted(t);
};

/* SQL DDL Utilities
*/
var runSQL = function(sqlPath, source, callback) {  

  function childProcess(error, stdout, stderr) {
    udebug.log_detail('harness runSQL process completed.');
    udebug.log_detail(source + ' stdout: ' + stdout);
    udebug.log_detail(source + ' stderr: ' + stderr);
    if (error !== null) {
      udebug.log(source + 'exec error: ' + error);
    } else {
      udebug.log_detail(source + ' exec OK');
    }
    if(callback) {
      callback(error);  
    }
  }

  // prepend the file containing the engine.sql (ndb.sql or innodb.sql) to the file containing the sql commands 
  var enginesqlPath = path.join(suites_dir, global.engine + '.sql ');
  var cmd = 'cat ' + enginesqlPath + ' ' + sqlPath + ' | mysql';
  
  var p = test_conn_properties;
  if(p) {
    if(p.mysql_socket)     { cmd += " --socket=" + p.mysql_socket; }
    else if(p.mysql_port)  { cmd += " --port=" + p.mysql_port; }
    if(p.mysql_host)     { cmd += " -h " + p.mysql_host; }
    if(p.mysql_user)     { cmd += " -u " + p.mysql_user; }
    if(p.mysql_password) { cmd += " --password=" + p.mysql_password; }
  }
  udebug.log_detail('harness runSQL forking process...' + cmd);
  var child = exec(cmd, childProcess);
};

SQL.create =  function(suite, callback) {
  var sqlPath = path.join(suite.path, 'create.sql');
  udebug.log_detail("createSQL path: " + sqlPath);
  runSQL(sqlPath, 'createSQL', callback);
};

SQL.drop = function(suite, callback) {
  var sqlPath = path.join(suite.path, 'drop.sql');
  udebug.log_detail("dropSQL path: " + sqlPath);
  runSQL(sqlPath, 'dropSQL', callback);
};


/* Exports from this module */
exports.Test              = Test;
exports.Suite             = Suite;
exports.Listener          = Listener;
exports.QuietListener     = QuietListener;
exports.FailOnlyListener  = FailOnlyListener;
exports.Result            = Result;
exports.SmokeTest         = SmokeTest;
exports.ConcurrentTest    = ConcurrentTest;
exports.SerialTest        = SerialTest;
exports.ClearSmokeTest    = ClearSmokeTest;
exports.SQL               = SQL;


