package com.mockobjects.io;

import com.mockobjects.ExpectationCounter;
import com.mockobjects.ExpectationSegment;
import com.mockobjects.Verifiable;
import com.mockobjects.util.Verifier;

import java.io.IOException;
import java.io.Writer;

/**
  * A mock {@link java.io.Writer Writer}.
  * <h3>Example usage</h3>
  * <p>
  * You may use the <code>MockWriter</code> like this:
  * <pre>
  * public void testSomething() throws IOException {
  *     MockWriter out = new MockWriter();
  *     out.setExpectedSegment("some string");
  *     out.setExpectedFlushCalls(1);
  *     out.setExpectedCloseCalls(1);
  *
  *     ObjectUnderTest testee = new ObjectUnderTest(out);
  *     out.verify();
  *
  *     // If we get here, the mock's flush() and close() methods were
  *     // called exactly once each (order cannot be determined) and
  *     // the write() method was called with the string "some string" in it.
  * }</pre>
  * </p>
  * @author Francois Beausoleil, fbos@users.sourceforge.net
  */
public class MockWriter extends Writer implements Verifiable {
	private ExpectationSegment segment =
		new ExpectationSegment("String segment");
	private ExpectationCounter flushCallsCount =
		new ExpectationCounter("flush calls");
	private ExpectationCounter closeCallsCount =
		new ExpectationCounter("close calls");

	private boolean writeShouldThrowException = false;
	private boolean flushShouldThrowException = false;
	private boolean closeShouldThrowException = false;

	/**
	 * Instantiates a new mock writer which will act as a data sink.
	 * Once instantiated, mocks of this class do not expect anything  special.
	 * Once the object is instantiated, you should set your expectations using
	 * the provided methods.
	 */
	public MockWriter() {
		// NOTE:  We *have* to use the Writer(Object lock) constructor because
		// Writer() will use this as a lock.  When we call
		// Verifier.verifyObject(MockWriter), it will find the lock field,
		// which is Verifiable, resulting in an infinite recursion loop...
		super(new Object());
	}

	/**
	 * Sets the mock's behavior when writing.
	 * If this method has been called, then
	 * {@link #write(char[],int,int) write(char[], int, int)} will throw an
	 * {@link java.io.IOException IOException}.
	 */
	public void setWriteShouldThrowException() {
		writeShouldThrowException = true;
	}

	/**
	 * Sets the mock's behavior when flushing. If this method has been called,
     * then {@link #flush() flush()} will throw 
     * an {@link java.io.IOException IOException}.
     */
	public void setFlushShouldThrowException() {
		flushShouldThrowException = true;
	}

	/**
	 * Sets the mock's behavior when closing. 
     * If this method has been called, then {@link #close() close()} will 
     * throw an {@link java.io.IOException IOException}. 
     */
	public void setCloseShouldThrowException() {
		closeShouldThrowException = true;
	}
    
	/**
	 * Sets the expected number of times that the {@link #flush() flush()}
	 * method will be called.
	 * @see #flush()
	 */
	public void setExpectedFlushCalls(int calls) {
		flushCallsCount.setExpected(calls);
	}

	/**
	 * Sets the expected number of times that the {@link #close() close()}
	 * method will be called.
	 * @see #close()
	 */
	public void setExpectedCloseCalls(int calls) {
		closeCallsCount.setExpected(calls);
	}

	/**
	 * Sets the value of the expected string segment.
	 * When the {@link #write(char[], int, int) write(char[], int, int)} method
	 * is called, a string is instantiated with the passed array and compared
	 * to the <code>aString</code> parameter of this method.  If the two strings
	 * differ, an {@link junit.framework.AssertionFailedError} will be thrown.
	 * @see com.mockobjects.ExpectationSegment
	 * @see #write(char[], int, int)
	 */
	public void setExpectedSegment(String aString) {
		segment.setExpected(aString);
	}

	/**
	 * Either throws an exception or asserts a string segment for equality.
	 * @see com.mockobjects.ExpectationSegment
	 * @see #setWriteShouldThrowException(boolean)
	 */
	public void write(char cbuf[], int off, int len) throws IOException {
		if (writeShouldThrowException) {
			throw new IOException("Mock Exception");
		}

		segment.setActual(new String(cbuf, off, len));
	}

    /**
     * This method will also throw an {@link java.io.IOException IOException}
     * if asked to do so by calling
     * {@link #setFlushShouldThrowException() setFlushShouldThrowException()}.
     * Please note that the call count will be incremented <em>before</em> the
     * check for the exception is done.
     * @see #setExpectedFlushCalls(int)
     * @see #setFlushShouldThrowException()
     */
    public void flush() throws IOException {
    	flushCallsCount.inc();
    
    	if (flushShouldThrowException) {
    		throw new IOException("Mock Exception");
    	}
    }

    /**
     * Increments the close counter and asserts that this method was not
     * called too many times.
     * This method will also throw an {@link java.io.IOException IOException}
     * if asked to do so by calling
     * {@link #setCloseShouldThrowException() setCloseShouldThrowException()}.
     * Please note that the call count will be incremented <em>before</em> the
     * check for the exception is done.
     * @see #setExpectedCloseCalls(int)
     * @see #setCloseShouldThrowException()
     */
    public void close() throws IOException {
    	closeCallsCount.inc();
    
    	if (closeShouldThrowException) {
    		throw new IOException("Mock Exception");
    	}
    }
    
	public void verify() {
		// WARNING:  If the MockWriter calls its parent no-arg constructor,
		// this call will fail with a StackOverflowError. See constructor
		// for details.
		Verifier.verifyObject(this);
	}
}
