// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.labs.testing.environmentTest');
goog.setTestOnly('goog.labs.testing.environmentTest');

goog.require('goog.Promise');
goog.require('goog.labs.testing.Environment');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.testSuite');

var testCase = null;
var mockControl = null;
var replacer = null;

// Use this flag to control whether the global JsUnit lifecycle events are being
// called as part of the test lifecycle or as part of the "mocked" environment.
var testing = false;

// Bootstrap it because... why not.
var env = new goog.labs.testing.Environment();
// The outer test case.
var realTestCase = goog.labs.testing.EnvironmentTestCase_.getInstance();

function setUp() {
  // These methods end up being called by the test framework for these tests
  // as well as the part of the environment that is being tested as part
  // of the test.  Bail if the test is already running.
  if (testing) {
    // This value is used by the testSetupReturnsValue test below
    return 'hello';
  }

  // Temporarily override the initializeTestRunner method to avoid installing
  // our "test" TestCase.
  var initFn = goog.testing.TestCase.initializeTestRunner;
  goog.testing.TestCase.initializeTestRunner = function() {};
  testCase = new goog.labs.testing.EnvironmentTestCase_();
  goog.labs.testing.EnvironmentTestCase_.getInstance = function() {
    return testCase;
  };
  goog.testing.TestCase.initializeTestRunner = initFn;

  mockControl = new goog.testing.MockControl();

  replacer = new goog.testing.PropertyReplacer();
}

function tearDown() {
  if (testing) {
    return;
  }

  replacer.reset();

  mockControl.$resetAll();
  mockControl.$tearDown();
}

function testLifecycle() {
  testing = true;

  var envOne = mockControl.createStrictMock(goog.labs.testing.Environment);
  var envTwo = mockControl.createStrictMock(goog.labs.testing.Environment);
  var envThree = mockControl.createStrictMock(goog.labs.testing.Environment);
  var testMethod = mockControl.createFunctionMock('testMethod');

  testCase.addNewTest('testFake', testMethod);

  testCase.registerEnvironment_(envOne);
  testCase.registerEnvironment_(envTwo);
  testCase.registerEnvironment_(envThree);

  envOne.setUpPage();
  envTwo.setUpPage();
  envThree.setUpPage();

  envOne.setUp();
  envTwo.setUp();
  envThree.setUp();

  testMethod();

  envThree.tearDown();
  envTwo.tearDown();
  envOne.tearDown();

  envThree.tearDownPage();
  envTwo.tearDownPage();
  envOne.tearDownPage();

  mockControl.$replayAll();
  testCase.runTests();
  mockControl.$verifyAll();

  testing = false;
}

function testLifecycle_withObject() {
  testing = true;

  var envOne = mockControl.createStrictMock(goog.labs.testing.Environment);
  var envTwo = mockControl.createStrictMock(goog.labs.testing.Environment);
  var envThree = mockControl.createStrictMock(goog.labs.testing.Environment);
  var testObject = {
    configureEnvironment:
        mockControl.createFunctionMock('configureEnvironment1'),
    setUp: mockControl.createFunctionMock('setUp1'),
    testSuite: {
      configureEnvironment:
          mockControl.createFunctionMock('configureEnvironment2'),
      setUp: mockControl.createFunctionMock('setUp2'),
      test: mockControl.createFunctionMock('test'),
      tearDown: mockControl.createFunctionMock('tearDown2')
    },
    tearDown: mockControl.createFunctionMock('tearDown1')
  };

  testCase.setTestObj(testObject);

  testCase.registerEnvironment_(envOne);
  testCase.registerEnvironment_(envTwo);
  testCase.registerEnvironment_(envThree);

  envOne.setUpPage();
  envTwo.setUpPage();
  envThree.setUpPage();

  testObject.configureEnvironment();
  testObject.testSuite.configureEnvironment();
  envOne.setUp();
  envTwo.setUp();
  envThree.setUp();

  testObject.setUp();
  testObject.testSuite.setUp();
  testObject.testSuite.test();
  testObject.testSuite.tearDown();
  testObject.tearDown();

  envThree.tearDown();
  envTwo.tearDown();
  envOne.tearDown();

  envThree.tearDownPage();
  envTwo.tearDownPage();
  envOne.tearDownPage();

  mockControl.$replayAll();
  testCase.runTests();
  mockControl.$verifyAll();

  testing = false;
}

function testLifecycle_withPromises() {
  var mockClock = new goog.testing.MockClock(true /* autoinstall */);
  testing = true;

  var envOne = mockControl.createStrictMock(goog.labs.testing.Environment);
  testCase.registerEnvironment_(envOne);
  var envTwo = mockControl.createStrictMock(goog.labs.testing.Environment);
  testCase.registerEnvironment_(envTwo);
  var testObj = {
    'configureEnvironment':
        mockControl.createFunctionMock('configureEnvironment'),
    'setUpPage': mockControl.createFunctionMock('setUpPage'),
    'setUp': mockControl.createFunctionMock('setUp'),
    'tearDown': mockControl.createFunctionMock('tearDown'),
    'tearDownPage': mockControl.createFunctionMock('tearDownPage'),
    'testFoo': mockControl.createFunctionMock('testFoo'),
    'testBar': {
      'configureEnvironment':
          mockControl.createFunctionMock('configureEnvironmentTest'),
      'setUp': mockControl.createFunctionMock('setUpTest'),
      'test': mockControl.createFunctionMock('testBar'),
      'tearDown': mockControl.createFunctionMock('tearDownTest')
    }
  };
  testCase.setTestObj(testObj);

  // Make the testCase.runTestsReturningPromise() a pending operation so we can
  // use assertNextCall also for checking the first call.
  var resultPromise;
  var pendingOp = goog.Promise.withResolver();
  pendingOp.promise.then(function() {
    resultPromise = testCase.runTestsReturningPromise();
  });
  var nextOp = null;
  var finishPendingOp = function() {
    pendingOp.resolve();
    pendingOp = nextOp;
    nextOp = null;
    mockClock.tick();
  };
  var expectCallTo = function(expectedCall) {
    assertNull(nextOp);
    nextOp = goog.Promise.withResolver();
    expectedCall().$returns(nextOp.promise);
    mockControl.$replayAll();
    finishPendingOp();
    mockControl.$verifyAll();
    mockControl.$resetAll();
  };
  // Make sure there are no hanging expecations dropped by the first $resetAll.
  mockControl.$replayAll();
  mockControl.$verifyAll();
  mockControl.$resetAll();

  expectCallTo(envOne.setUpPage);
  expectCallTo(envTwo.setUpPage);
  expectCallTo(testObj.setUpPage);
  expectCallTo(testObj.configureEnvironment);
  expectCallTo(envOne.setUp);
  expectCallTo(envTwo.setUp);
  expectCallTo(testObj.setUp);
  expectCallTo(testObj.testFoo);

  mockControl.$resetAll();
  testObj.tearDown();
  envTwo.tearDown();
  envOne.tearDown();

  expectCallTo(testObj.configureEnvironment);
  expectCallTo(testObj.testBar.configureEnvironment);
  expectCallTo(envOne.setUp);
  expectCallTo(envTwo.setUp);
  expectCallTo(testObj.setUp);
  expectCallTo(testObj.testBar.setUp);
  expectCallTo(testObj.testBar.test);

  mockControl.$resetAll();
  testObj.testBar.tearDown();
  testObj.tearDown();
  envTwo.tearDown();
  envOne.tearDown();
  testObj.tearDownPage();
  envTwo.tearDownPage();
  envOne.tearDownPage();
  mockControl.$replayAll();
  finishPendingOp();
  var result = mockClock.tickPromise(resultPromise);
  mockControl.$verifyAll();
  assertTrue(result.complete);
  assertEquals(2, result.totalCount);
  assertEquals(2, result.runCount);
  assertEquals(2, result.successCount);
  assertEquals(0, result.errors.length);

  testing = false;
  mockClock.uninstall();
}

function testTearDownWithMockControl() {
  testing = true;

  var envWith = new goog.labs.testing.Environment();
  var envWithout = new goog.labs.testing.Environment();

  var mockControlMock = mockControl.createStrictMock(goog.testing.MockControl);
  var mockControlCtorMock =
      mockControl.createMethodMock(goog.testing, 'MockControl');
  mockControlCtorMock().$times(1).$returns(mockControlMock);
  // Expecting verify / reset calls twice since two environments use the same
  // mockControl, but only one created it and is allowed to tear it down.
  mockControlMock.$verifyAll();
  mockControlMock.$replayAll();
  mockControlMock.$verifyAll();
  mockControlMock.$resetAll();
  mockControlMock.$tearDown().$times(1);
  mockControlMock.$verifyAll();
  mockControlMock.$replayAll();
  mockControlMock.$verifyAll();
  mockControlMock.$resetAll();

  mockControl.$replayAll();
  envWith.withMockControl();
  envWithout.mockControl = mockControlMock;
  envWith.tearDown();
  envWithout.tearDown();
  mockControl.$verifyAll();
  mockControl.$resetAll();

  testing = false;
}

function testAutoDiscoverTests() {
  testing = true;

  var setUpPageFn = testCase.setUpPage;
  var setUpFn = testCase.setUp;
  var tearDownFn = testCase.tearDownFn;
  var tearDownPageFn = testCase.tearDownPageFn;

  testCase.autoDiscoverTests();

  assertEquals(setUpPageFn, testCase.setUpPage);
  assertEquals(setUpFn, testCase.setUp);
  assertEquals(tearDownFn, testCase.tearDownFn);
  assertEquals(tearDownPageFn, testCase.tearDownPageFn);

  // Note that this number changes when more tests are added to this file as
  // the environment reflects on the window global scope for JsUnit.
  assertEquals(10, testCase.tests_.length);

  testing = false;
}


// Verify "goog.testing.testSuite" integration
function testTestSuiteTests() {
  testing = true;

  // don't try to reinitialize the test runner, while a test is running.
  replacer.set(goog.testing.TestCase, 'initializeTestRunner', function() {});

  // with an active environment.
  var envOne = new goog.labs.testing.Environment();

  var setUpPageFn = testCase.setUpPage;
  var setUpFn = testCase.setUp;
  var tearDownFn = testCase.tearDownFn;
  var tearDownPageFn = testCase.tearDownPageFn;

  goog.testing.testSuite({
    // These lifecycle methods should not override the environment testcase
    // methods but they should be called, when the test runs.
    setUp: function() {},
    tearDown: function() {},
    setUpPage: function() {},
    tearDownPage: function() {},
    // This test method should be added.
    testMe: function() {},
    testMeToo: {
      setUp: function() {},
      testA: function() {},
      testB: function() {},
      tearDown: function() {}
    }
  });

  assertEquals(setUpPageFn, testCase.setUpPage);
  assertEquals(setUpFn, testCase.setUp);
  assertEquals(tearDownFn, testCase.tearDownFn);
  assertEquals(tearDownPageFn, testCase.tearDownPageFn);

  assertEquals(3, testCase.tests_.length);

  testing = false;
}

function testSetupReturnsValue() {
  testing = true;

  var env = new goog.labs.testing.Environment();

  // Expect the environment to pass on any value returned by the user defined
  // setUp method.
  assertEquals('hello', testCase.setUp());

  testCase.tearDown();
  testing = false;
}

function testMockClock() {
  testing = true;

  var env = new goog.labs.testing.Environment().withMockClock();

  testCase.addNewTest('testThatThrowsEventually', function() {
    setTimeout(function() { throw new Error('LateErrorMessage'); }, 200);
  });

  testCase.runTests();
  assertTestFailure(testCase, 'testThatThrowsEventually', 'LateErrorMessage');

  testing = false;
}

function testMockControl() {
  testing = true;

  var env = new goog.labs.testing.Environment().withMockControl();
  var test = env.mockControl.createFunctionMock('test');

  testCase.addNewTest('testWithoutVerify', function() {
    test();
    env.mockControl.$replayAll();
    test();
  });

  testCase.runTests();
  assertNull(env.mockClock);

  testing = false;
}

function testMock() {
  testing = true;

  var env = new goog.labs.testing.Environment().withMockControl();
  var mock = env.mock({test: function() {}});

  testCase.addNewTest('testMockCalled', function() {
    mock.test().$times(2);

    env.mockControl.$replayAll();
    mock.test();
    mock.test();
    env.mockControl.verifyAll();
  });

  testCase.runTests();

  testing = false;
}

function assertTestFailure(testCase, name, message) {
  assertContains(message, testCase.result_.resultsByName[name][0].toString());
}
