# $Id: Fixture.pm,v 1.14 2005/04/27 15:53:47 tonyb Exp $
#
# Copyright (c) 2002-2005 Cunningham & Cunningham, Inc.
# Released under the terms of the GNU General Public License version 2 or later.
#
# Perl translation by Dave W. Smith <dws@postcognitive.com>
# Modified by Tony Byrne <fit4perl@byrnehq.com>

package Test::C2FIT::Fixture;

use strict;
use Error qw( :try );

my %summary;

our $yellow	= '#ffffcf';
our $green	= '#cfffcf';
our $red	= '#ffcfcf';
our $gray	= '#808080';
our $ignore	= '#efefef';
our $info	= $gray;
our $label	= '#c08080';

sub new
{
	my $pkg = shift;
	return bless { counts => new Test::C2FIT::Counts(), @_ }, $pkg;
}

sub counts
{
	my $self = shift;
	$self->{'counts'} = $_[0] if @_;
	return $self->{'counts'};
}

sub doTables
{
	my $self = shift;
	my($tables) = @_;

	$Test::C2FIT::Fixture::summary{'run date'} = scalar localtime(time());
	$Test::C2FIT::Fixture::summary{'run elapsed time'} = Test::C2FIT::Runtime->new();

	while ( $tables )
	{
		my $heading = $tables->at(0,0,0);
		if ( $heading ) {
			try
			{
				my $pkg = $heading->text();
				my $fixture = $self->loadFixture($pkg);
				$fixture->counts($self->counts());
				$fixture->doTable($tables);
			}
			otherwise
			{
				my $e = shift;
				$self->exception($heading, $e);
			};
		}
		$tables = $tables->more();
	}
}

sub doTable
{
	my $self = shift;
	my($table) = @_;
	$self->doRows($table->parts()->more());
}

sub doRows
{
	my $self = shift;
	my($rows) = @_;
	while ( $rows )
	{
		$self->doRow($rows);
		$rows = $rows->more();
	}
}

sub doRow
{
	my $self = shift;
	my($row) = @_;
	$self->doCells($row->parts());
}

sub doCells
{
	my $self = shift;
	my($cells) = @_;
	my $columnNumber = 0;
	
	while ( $cells )
	{
		try
		{
			$self->doCell($cells, $columnNumber);
		}
		otherwise
		{
			my $e = shift;
			$self->exception($cells, $e);
		};
		$cells = $cells->more();
		++$columnNumber;
	}
}

sub doCell
{
 	my $self = shift;
	my($cell, $columnNumber) = @_;
	$self->ignore($cell);
}

# Annotations

sub right
{
	my $self = shift;
	my($cell) = @_;
	$cell->addToTag(qq| bgcolor="$green"|);
	$self->counts()->{'right'} += 1;
}

sub wrong
{
	my $self = shift;
	my ($cell, $actual) = @_;
	$cell->addToTag(qq| bgcolor="$red"|);
	$cell->{'body'} = $self->escape($cell->text());	
	$cell->addToBody($self->label("expected") . "<hr>" . $self->escape($actual) . $self->label("actual"))
		if defined($actual);
	$self->counts()->{'wrong'} += 1;	
}

sub ignore
{
	my $self = shift;
	my($cell) = @_;
	$cell->addToTag(qq| bgcolor="$ignore"|);
	$self->counts()->{'ignores'} += 1;
}

sub error
{
	my $self = shift;
	my ($cell,$message) = @_;
	$cell->{'body'} = $self->escape($cell->text());
	$cell->addToBody("<hr><pre>" . $self->escape($message) . "</pre>");
	$cell->addToTag(' bgcolor="' . $yellow . '"');
	$self->counts()->{'exceptions'}++;
}

sub info
{
	my $self = shift;
	my ($cell, $message);
	if (scalar @_ == 2)
	{
		($cell, $message) = @_;
		$cell->addToBody($self->info($message));
	}
	else
	{
		$message = shift;
		return qq| <font color="$info">| . $self->escape($message) . qq|</font>|;
	}
}

sub exception
{
	my $self = shift;
	my($cell, $exception) = @_;

	#TBD how to include a stack trace is an open issue

	$cell->addToTag(' bgcolor="ffffcf"');
	$cell->addToBody('<hr><font size=-2><pre>' .
		$exception .
		"</pre></font>");
	$self->counts()->{'exceptions'} += 1;
}

# Utilities

sub label
{
	my $self = shift;
	my($string) = @_;
	return '' unless $string;
	return qq| <font size=-1 color="$label"><i>$string</i></font>|;
}

sub gray
{
	my $self = shift;
	my($string) = @_;
	return '' unless $string;
	return qq|<font color="$gray">$string</font>|;
}

sub escape
{
	my $self = shift;
	my($string) = @_;

	return $string unless $string;

	$string =~ s/\&/&amp;/g;
	$string =~ s/</&lt;/g;
	
	$string =~ s/  / &nbsp;/g;
	$string =~ s|\r\n|<br />|g;
	$string =~ s|\r|<br \/>|g;
	$string =~ s|\n|<br \/>|g;
	return $string;
}

sub camel
{
	my($string) = @_;
	$string =~ s/\s(\S)/uc($1)/eg;
	return $string;
}

sub parse
{
	my $self = shift;
	my($string, $type) = @_;
	die "can't yet parse $type\n" if $type ne "generic";
	return $string;
}

sub check
{
	my $self = shift;
	my($cell, $adapter) = @_;

	my $text = $cell->text();
	return if $text eq "";

	if ( not defined($adapter) )
	{
		$self->ignore($cell);
	}
	elsif ( $text eq "error" )
	{
		try
		{
			my $result = $adapter->invoke();
			$self->wrong($cell, $adapter->toString($result));
		}
		otherwise
		{
			#TBD The Java source distinguishes between illegal access
			# and "normal" exceptions. 
			$self->right($cell);
		};
	}
	else
	{
		try
		{
			my $result = $adapter->get();
			if ( $adapter->equals($adapter->parse($text), $result) )
			{
				$self->right($cell);
			}
			else
			{
				$self->wrong($cell, $adapter->toString($result));
			}
		}
		otherwise
		{
			my $e = shift;
			$self->exception($cell, $e);
		};
	}
}

sub fixtureName
{
	my $self = shift;
	my $tables = shift;
	return $tables->at(0, 0, 0);
}

sub loadFixture
{
	my $self = shift;
	my $fixtureName = shift;

	my $notFound = qq|The fixture "$fixtureName" was not found.\n|;
	my $foundButNotFixture = qq|"$fixtureName" was found, but it's not a fixture.\n|;

	my $fixture;

	$fixtureName = $self->_java2PerlFixtureName($fixtureName);

	try
	{
		require "$fixtureName.pm";
	}
	otherwise
	{
		my $e = shift;
		throw Test::C2FIT::Exception("$notFound: $e");
	};

	try
	{
		$fixtureName =~ s/\//::/g;
		$fixture = $fixtureName->new();
		throw Test::C2FIT::Exception("not a fixture")
			unless $fixture->isa('Test::C2FIT::Fixture');
		
	}
	otherwise
	{
		throw Test::C2FIT::Exception($foundButNotFixture);
	};




	return $fixture;

}

sub _java2PerlFixtureName
{
	my ($self, $fixtureName) = @_;
	$fixtureName =~ s/^fit\./Test\.C2FIT\./;

	# Need this because example and fat packages are in our namespace - prevents
	# creation of top level namespace, frowned upon by CPAN indexer.
	$fixtureName =~ s/^eg\./Test\.C2FIT\.eg\./;
	$fixtureName =~ s/^fat\./Test\.C2FIT\.fat\./;

	$fixtureName =~ s/\./\//g;
	return $fixtureName;
}

package Test::C2FIT::Counts;

sub new
{
	my $pkg = shift;
	bless
	{
		right		=> 0,
		wrong		=> 0,
		ignores		=> 0,
		exceptions	=> 0
	}, $pkg;
}

sub toString
{
	my $self = shift;
	join(", ", map { $self->{$_} . " " . $_ } qw(right wrong ignores exceptions))
}

sub tally
{
	my $self = shift;
	my($counts) = @_;

	$self->{'right'} += $counts->{'right'};
	$self->{'wrong'} += $counts->{'wrong'};
	$self->{'ignores'} += $counts->{'ignores'};
	$self->{'exceptions'} += $counts->{'exceptions'};
}

package Test::C2FIT::Runtime;

use overload '""' => \&toString;

sub new
{
	use Benchmark;
	my $pkg = shift;
	bless
	{
		start => new Benchmark()
	}, $pkg;
}

sub toString
{
	my $self = shift;
	my $end = new Benchmark();
	my $timeDiff = timediff($end,$self->{start});
	my $timeStr = timestr($timeDiff);
	return $timeStr;
}

1;

__END__

package fit;

// Copyright (c) 2002-2005 Cunningham & Cunningham, Inc.
// Released under the terms of the GNU General Public License version 2 or later.

import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import java.text.DateFormat;

public class Fixture {

    public Map summary = new HashMap();
    public Counts counts = new Counts();
    protected String[] args;

    public class RunTime {
        long start = System.currentTimeMillis();
        long elapsed = 0;

        public String toString() {
            elapsed = (System.currentTimeMillis()-start);
            if (elapsed > 600000) {
                return d(3600000)+":"+d(600000)+d(60000)+":"+d(10000)+d(1000);
            } else {
                return d(60000)+":"+d(10000)+d(1000)+"."+d(100)+d(10);
            }
        }

        String d(long scale) {
            long report = elapsed / scale;
            elapsed -= report * scale;
            return Long.toString(report);
        }
    }



    // Traversal //////////////////////////

	/* Altered by Rick Mugridge to dispatch on the first Fixture */
    public void doTables(Parse tables) {
        summary.put("run date", new Date());
        summary.put("run elapsed time", new RunTime());
        if (tables != null) {
        	Parse fixtureName = fixtureName(tables);
            if (fixtureName != null) {
                try {
                    Fixture fixture = getLinkedFixtureWithArgs(tables);
                    fixture.interpretTables(tables);
                } catch (Exception e) {
                    exception (fixtureName, e);
                    interpretFollowingTables(tables);
                }
            }
        }
    }

    /* Added by Rick Mugridge to allow a dispatch into DoFixture */
    protected void interpretTables(Parse tables) {
  		try { // Don't create the first fixture again, because creation may do something important.
  			getArgsForTable(tables); // get them again for the new fixture object
  			doTable(tables);
  		} catch (Exception ex) {
  			exception(fixtureName(tables), ex);
  			return;
  		}
  		interpretFollowingTables(tables);
  	}

    /* Added by Rick Mugridge */
    private void interpretFollowingTables(Parse tables) {
        //listener.tableFinished(tables);
            tables = tables.more;
        while (tables != null) {
            Parse fixtureName = fixtureName(tables);
            if (fixtureName != null) {
                try {
                    Fixture fixture = getLinkedFixtureWithArgs(tables);
                    fixture.doTable(tables);
                } catch (Throwable e) {
                    exception(fixtureName, e);
		        }
		    }
            //listener.tableFinished(tables);
            tables = tables.more;
        }
    }

    /* Added from FitNesse*/
	protected Fixture getLinkedFixtureWithArgs(Parse tables) throws Exception {
		Parse header = tables.at(0, 0, 0);
        Fixture fixture = loadFixture(header.text());
		fixture.counts = counts;
		fixture.summary = summary;
		fixture.getArgsForTable(tables);
		return fixture;
	}
	
	public Parse fixtureName(Parse tables) {
		return tables.at(0, 0, 0);
	}

	public Fixture loadFixture(String fixtureName)
	throws InstantiationException, IllegalAccessException {
		String notFound = "The fixture \"" + fixtureName + "\" was not found.";
		try {
			return (Fixture)(Class.forName(fixtureName).newInstance());
		}
		catch (ClassCastException e) {
			throw new RuntimeException("\"" + fixtureName + "\" was found, but it's not a fixture.", e);
		}
		catch (ClassNotFoundException e) {
			throw new RuntimeException(notFound, e);
		}
		catch (NoClassDefFoundError e) {
			throw new RuntimeException(notFound, e);
		}
	}

	/* Added by Rick Mugridge, from FitNesse */
	protected void getArgsForTable(Parse table) {
	    ArrayList argumentList = new ArrayList();
	    Parse parameters = table.parts.parts.more;
	    for (; parameters != null; parameters = parameters.more)
	        argumentList.add(parameters.text());
	    args = (String[]) argumentList.toArray(new String[0]);
	}

    public void doTable(Parse table) {
        doRows(table.parts.more);
    }

    public void doRows(Parse rows) {
        while (rows != null) {
            Parse more = rows.more;
            doRow(rows);
            rows = more;
        }
    }

    public void doRow(Parse row) {
        doCells(row.parts);
    }

    public void doCells(Parse cells) {
        for (int i=0; cells != null; i++) {
            try {
                doCell(cells, i);
            } catch (Exception e) {
                exception(cells, e);
            }
            cells=cells.more;
        }
    }

    public void doCell(Parse cell, int columnNumber) {
        ignore(cell);
    }


    // Annotation ///////////////////////////////

    public static String green = "#cfffcf";
    public static String red = "#ffcfcf";
    public static String gray = "#efefef";
    public static String yellow = "#ffffcf";

    public  void right (Parse cell) {
        cell.addToTag(" bgcolor=\"" + green + "\"");
        counts.right++;
    }

    public void wrong (Parse cell) {
        cell.addToTag(" bgcolor=\"" + red + "\"");
		cell.body = escape(cell.text());
        counts.wrong++;
    }

    public void wrong (Parse cell, String actual) {
        wrong(cell);
        cell.addToBody(label("expected") + "<hr>" + escape(actual) + label("actual"));
    }

	public void info (Parse cell, String message) {
		cell.addToBody(info(message));
	}

	public String info (String message) {
		return " <font color=\"#808080\">" + escape(message) + "</font>";
	}

    public void ignore (Parse cell) {
        cell.addToTag(" bgcolor=\"" + gray + "\"");
        counts.ignores++;
    }

	public void error (Parse cell, String message) {
		cell.body = escape(cell.text());
		cell.addToBody("<hr><pre>" + escape(message) + "</pre>");
		cell.addToTag(" bgcolor=\"" + yellow + "\"");
		counts.exceptions++;
	}

    public void exception (Parse cell, Throwable exception) {
        while(exception.getClass().equals(InvocationTargetException.class)) {
            exception = ((InvocationTargetException)exception).getTargetException();
        }
        final StringWriter buf = new StringWriter();
        exception.printStackTrace(new PrintWriter(buf));
        error(cell, buf.toString());
    }

    // Utility //////////////////////////////////

    public String counts() {
        return counts.toString();
    }

    public static String label (String string) {
        return " <font size=-1 color=\"#c08080\"><i>" + string + "</i></font>";
    }

    public static String escape (String string) {
    	string = string.replaceAll("&", "&amp;");
    	string = string.replaceAll("<", "&lt;");
    	string = string.replaceAll("  ", " &nbsp;");
		string = string.replaceAll("\r\n", "<br />");
		string = string.replaceAll("\r", "<br />");
		string = string.replaceAll("\n", "<br />");
    	return string;
    }

    public static String camel (String name) {
        StringBuffer b = new StringBuffer(name.length());
        StringTokenizer t = new StringTokenizer(name);
        if (!t.hasMoreTokens())
            return name;
        b.append(t.nextToken());
        while (t.hasMoreTokens()) {
            String token = t.nextToken();
            b.append(token.substring(0, 1).toUpperCase());      // replace spaces with camelCase
            b.append(token.substring(1));
        }
        return b.toString();
    }

    public Object parse (String s, Class type) throws Exception {
        if (type.equals(String.class))              {return s;}
        if (type.equals(Date.class))                {return DateFormat.getDateInstance().parse(s);}
        if (type.equals(ScientificDouble.class))    {return ScientificDouble.valueOf(s);}
        throw new Exception("can't yet parse "+type);
    }

    public void check(Parse cell, TypeAdapter a) {
        String text = cell.text();
        if (text.equals("")) {
            try {
                info(cell, a.toString(a.get()));
            } catch (Exception e) {
                info(cell, "error");
            }
        } else if (a == null) {
            ignore(cell);
        } else  if (text.equals("error")) {
            try {
                Object result = a.invoke();
                wrong(cell, a.toString(result));
            } catch (IllegalAccessException e) {
                exception (cell, e);
            } catch (Exception e) {
                right(cell);
            }
        } else {
            try {
                Object result = a.get();
                if (a.equals(a.parse(text), result)) {
                    right(cell);
                } else {
                    wrong(cell, a.toString(result));
                }
            } catch (Exception e) {
                exception(cell, e);
            }
        }
    }

	/* Added by Rick, from FitNesse */
    public String[] getArgs() {
        return args;
    }

}
