#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library 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
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
#if UNITTEST
using NUnit.Framework;
using NMock2;
namespace Nuclex.Support.Tracking {
  /// Unit Test for the transaction group class
  [TestFixture]
  public class TransactionGroupTest {
    #region interface ITransactionGroupSubscriber
    /// Interface used to test the transaction group
    public interface ITransactionGroupSubscriber {
      /// Called when the transaction group's progress changes
      /// Transaction group whose progress has changed
      /// Contains the new progress achieved
      void ProgressChanged(object sender, ProgressReportEventArgs arguments);
      /// Called when the transaction group has ended
      /// Transaction group that as ended
      /// Not used
      void Ended(object sender, EventArgs arguments);
    }
    #endregion // interface ITransactionGroupSubscriber
    #region class ProgressUpdateEventArgsMatcher
    /// Compares two ProgressUpdateEventArgsInstances for NMock validation
    private class ProgressUpdateEventArgsMatcher : Matcher {
      /// Initializes a new ProgressUpdateEventArgsMatcher 
      /// Expected progress update event arguments
      public ProgressUpdateEventArgsMatcher(ProgressReportEventArgs expected) {
        this.expected = expected;
      }
      /// 
      ///   Called by NMock to verfiy the ProgressUpdateEventArgs match the expected value
      /// 
      /// Actual value to compare to the expected value
      /// 
      ///   True if the actual value matches the expected value; otherwise false
      /// 
      public override bool Matches(object actualAsObject) {
        ProgressReportEventArgs actual = (actualAsObject as ProgressReportEventArgs);
        if(actual == null)
          return false;
        return (actual.Progress == this.expected.Progress);
      }
      /// Creates a string representation of the expected value
      /// Writer to write the string representation into
      public override void DescribeTo(TextWriter writer) {
        writer.Write(this.expected.Progress.ToString());
      }
      /// Expected progress update event args value
      private ProgressReportEventArgs expected;
    }
    #endregion // class ProgressUpdateEventArgsMatcher
    #region class TestTransaction
    /// Transaction used for testing in this unit test
    private class TestTransaction : Transaction, IProgressReporter {
      /// will be triggered to report when progress has been achieved
      public event EventHandler AsyncProgressChanged;
      /// Changes the testing transaction's indicated progress
      /// 
      ///   New progress to be reported by the testing transaction
      /// 
      public void ChangeProgress(float progress) {
        OnAsyncProgressChanged(progress);
      }
      /// Transitions the transaction into the ended state
      public void End() {
        OnAsyncEnded();
      }
      /// Fires the progress update event
      /// Progress to report (ranging from 0.0 to 1.0)
      /// 
      ///   Informs the observers of this transaction about the achieved progress.
      /// 
      protected virtual void OnAsyncProgressChanged(float progress) {
        OnAsyncProgressChanged(new ProgressReportEventArgs(progress));
      }
      /// Fires the progress update event
      /// Progress to report (ranging from 0.0 to 1.0)
      /// 
      ///   Informs the observers of this transaction about the achieved progress.
      ///   Allows for classes derived from the transaction class to easily provide
      ///   a custom event arguments class that has been derived from the
      ///   transaction's ProgressUpdateEventArgs class.
      /// 
      protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) {
        EventHandler copy = AsyncProgressChanged;
        if(copy != null)
          copy(this, eventArguments);
      }
    }
    #endregion // class TestTransaction
    #region class ChainEndingTransaction
    /// 
    ///   Transaction that ends another transaction when its Ended property is called
    /// 
    private class ChainEndingTransaction : Transaction {
      /// Initializes a new chain ending transaction
      public ChainEndingTransaction() {
        this.chainedTransaction = new TestTransaction();
      }
      /// Transitions the transaction into the ended state
      public void End() {
        OnAsyncEnded();
      }
      /// 
      ///   Transaction that will end when this transaction's ended property is accessed
      /// 
      public TestTransaction ChainedTransaction {
        get { return this.chainedTransaction; }
      }
      /// Whether the transaction has ended already
      public override bool Ended {
        get {
          if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) {
            this.chainedTransaction.End();
          }
          return base.Ended;
        }
      }
      /// 
      ///   Transaction that will end when this transaction's ended property is accessed
      /// 
      private TestTransaction chainedTransaction;
      /// Whether we already ended the chained transaction and ourselves
      private int endedCalled;
    }
    #endregion // class ChainEndingTransaction
    /// Initialization routine executed before each test is run
    [SetUp]
    public void Setup() {
      this.mockery = new Mockery();
    }
    /// Validates that the set transaction properly sums the progress
    [Test]
    public void TestSummedProgress() {
      using(
        TransactionGroup testTransactionGroup =
          new TransactionGroup(
            new TestTransaction[] { new TestTransaction(), new TestTransaction() }
          )
      ) {
        ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup);
        Expect.Once.On(mockedSubscriber).
          Method("ProgressChanged").
          With(
            new Matcher[] {
              new NMock2.Matchers.TypeMatcher(typeof(TransactionGroup)),
              new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.25f))
            }
          );
        testTransactionGroup.Children[0].Transaction.ChangeProgress(0.5f);
        this.mockery.VerifyAllExpectationsHaveBeenMet();
      }
    }
    /// Validates that the transaction group respects the weights
    [Test]
    public void TestWeightedSummedProgress() {
      using(
        TransactionGroup testTransactionGroup =
          new TransactionGroup(
            new WeightedTransaction[] {
              new WeightedTransaction(new TestTransaction(), 1.0f),
              new WeightedTransaction(new TestTransaction(), 2.0f)
            }
          )
      ) {
        ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup);
        Expect.Once.On(mockedSubscriber).
          Method("ProgressChanged").
          With(
            new Matcher[] {
              new NMock2.Matchers.TypeMatcher(typeof(TransactionGroup)),
              new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f / 3.0f))
            }
          );
        testTransactionGroup.Children[0].Transaction.ChangeProgress(0.5f);
        Expect.Once.On(mockedSubscriber).
          Method("ProgressChanged").
          With(
            new Matcher[] {
              new NMock2.Matchers.TypeMatcher(typeof(TransactionGroup)),
              new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f))
            }
          );
        testTransactionGroup.Children[1].Transaction.ChangeProgress(0.5f);
        this.mockery.VerifyAllExpectationsHaveBeenMet();
      }
    }
    /// 
    ///   Validates that the ended event is triggered when the last transaction out of
    ///   multiple transactions in the group ends.
    /// 
    [Test]
    public void TestEndedEventWithTwoTransactions() {
      using(
        TransactionGroup testTransactionGroup =
          new TransactionGroup(
            new TestTransaction[] { new TestTransaction(), new TestTransaction() }
          )
      ) {
        ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup);
        Expect.Exactly(2).On(mockedSubscriber).
          Method("ProgressChanged").
          WithAnyArguments();
        Expect.Once.On(mockedSubscriber).
          Method("Ended").
          WithAnyArguments();
        testTransactionGroup.Children[0].Transaction.End();
        testTransactionGroup.Children[1].Transaction.End();
        this.mockery.VerifyAllExpectationsHaveBeenMet();
      }
    }
    /// 
    ///   Validates that the ended event is triggered when a single transaction contained
    ///   in the group ends.
    /// 
    [Test]
    public void TestEndedEventWithSingleTransaction() {
      using(
        TransactionGroup testTransactionGroup =
          new TransactionGroup(
            new TestTransaction[] { new TestTransaction() }
          )
      ) {
        ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup);
        Expect.Once.On(mockedSubscriber).
          Method("ProgressChanged").
          WithAnyArguments();
        Expect.Once.On(mockedSubscriber).
          Method("Ended").
          WithAnyArguments();
        testTransactionGroup.Children[0].Transaction.End();
        this.mockery.VerifyAllExpectationsHaveBeenMet();
      }
    }
    /// 
    ///   Verifies that the transaction group immediately enters the ended state when
    ///   the contained transactions have already ended before the constructor
    /// 
    /// 
    ///   This was a bug at one time and should prevent a regression
    /// 
    [Test]
    public void TestAlreadyEndedTransactions() {
      using(
        TransactionGroup testTransactionGroup =
          new TransactionGroup(
            new Transaction[] { Transaction.EndedDummy, Transaction.EndedDummy }
          )
      ) {
        Assert.IsTrue(testTransactionGroup.Wait(1000));
      }
    }
    /// 
    ///   Verifies that the transaction group doesn't think it's already ended when
    ///   the first transaction being added is in the ended state
    /// 
    /// 
    ///   This was a bug at one time and should prevent a regression
    /// 
    [Test]
    public void TestAlreadyEndedTransactionAsFirstTransaction() {
      using(
        TransactionGroup testTransactionGroup =
          new TransactionGroup(
            new Transaction[] { Transaction.EndedDummy, new TestTransaction() }
          )
      ) {
        Assert.IsFalse(testTransactionGroup.Ended);
      }
    }
    /// 
    ///   Verifies that a transaction ending while the constructor is running doesn't
    ///   wreak havoc on the transaction group
    /// 
    [Test]
    public void TestTransactionEndingDuringConstructor() {
      ChainEndingTransaction chainTransaction = new ChainEndingTransaction();
      using(
        TransactionGroup testTransactionGroup =
          new TransactionGroup(
            new Transaction[] { chainTransaction.ChainedTransaction, chainTransaction }
          )
      ) {
        Assert.IsFalse(testTransactionGroup.Ended);
        chainTransaction.End();
        Assert.IsTrue(testTransactionGroup.Ended);
      }
    }
    /// Mocks a subscriber for the events of a transaction
    /// Transaction to mock an event subscriber for
    /// The mocked event subscriber
    private ITransactionGroupSubscriber mockSubscriber(Transaction transaction) {
      ITransactionGroupSubscriber mockedSubscriber =
        this.mockery.NewMock();
      transaction.AsyncEnded += new EventHandler(mockedSubscriber.Ended);
      (transaction as IProgressReporter).AsyncProgressChanged +=
        new EventHandler(mockedSubscriber.ProgressChanged);
      return mockedSubscriber;
    }
    /// Mock object factory
    private Mockery mockery;
  }
} // namespace Nuclex.Support.Tracking
#endif // UNITTEST