/** \file fish_tests.c
	Various bug and feature tests. Compiled and run by make test.
*/

#include "config.h"


#include <stdlib.h>
#include <stdio.h>
#include <wchar.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <unistd.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdarg.h>		

#ifdef HAVE_GETOPT_H
#include <getopt.h>
#endif

#include <signal.h>

#include <locale.h>
#include <dirent.h>

#include "fallback.h"
#include "util.h"

#include "common.h"
#include "proc.h"
#include "reader.h"
#include "builtin.h"
#include "function.h"
#include "complete.h"
#include "wutil.h"
#include "env.h"
#include "expand.h"
#include "parser.h"
#include "tokenizer.h"
#include "output.h"
#include "exec.h"
#include "event.h"
#include "halloc_util.h"

/**
   Number of laps to run performance testing loop
*/
#define LAPS 50

/**
   The result of one of the test passes
*/
#define NUM_ANS L"-7 99999999 1234567 deadbeef DEADBEEFDEADBEEF"

/**
   Number of encountered errors
*/
static int err_count=0;

/**
   Print formatted output
*/
static void say( wchar_t *blah, ... )
{
	va_list va;
	va_start( va, blah );
	vwprintf( blah, va );
	va_end( va );	
	wprintf( L"\n" );
}

/**
   Print formatted error string
*/
static void err( wchar_t *blah, ... )
{
	va_list va;
	va_start( va, blah );
	err_count++;
	
	wprintf( L"Error: " );
	vwprintf( blah, va );
	va_end( va );	
	wprintf( L"\n" );
}

/**
   Print ok message
*/
static void ok()
{
	wprintf( L"OK\n" );
}

/**
   Compare two pointers
*/
static int pq_compare( void *e1, void *e2 )
{
	return e1-e2;
}

/**
   Test priority queue functionality
*/
static void pq_test( int elements )
{
	int i;
	int prev;
	
	int *count = calloc( sizeof(int), 100 );
	
	priority_queue_t q;
	pq_init( &q, pq_compare );

	
	for( i=0; i<elements; i++ )
	{
		int foo = rand() % 100;
//		printf( "Adding %d\n", foo );
		pq_put( &q, (void *)foo );
		count[foo]++;
	}
	
	prev = 100;
	
	for( i=0; i<elements; i++ )
	{
		int pos = (int)pq_get( &q );
		count[ pos ]--;
		if( pos > prev )
			err( L"Wrong order of elements in priority_queue_t" );
		prev = pos;
		
	}

	for( i=0; i<100; i++ )
	{
		if( count[i] != 0 )
		{
			err( L"Wrong number of elements in priority_queue_t" );
		}
	}
}

/**
   Test stack functionality
*/
static int stack_test( int elements )
{
	long i;

	int res=1;
	
	array_list_t s;

	al_init( &s );	

	for( i=0; i<elements; i++ )
	{
		long foo;

		al_push_long( &s, i);
		al_push_long( &s, i);
		
		if( (foo=al_pop_long( &s )) != i )
		{
			err( L"Unexpected data" );
			res = 0;			
			break;
		}
	}

	for( i=0; i<elements; i++ )
	{
		long foo;
		
		if( (foo=al_pop_long( &s )) != (elements-i-1) )
		{
			err( L"Unexpected data" );
			res = 0;
			break;
		}		
	}


	al_destroy( &s );
	
	return res;
}

/**
   Hash function for pointers
*/
static int hash_func( void *data )
{
/*	srand( (int)data );
	return rand();
*/
	int foo = (int)data;
	return 127*((foo^0xefc7e214)) ^(foo<<11);	
}

/**
   Pointer hash comparison function
*/
static int compare_func( void *key1, void *key2 )
{
	return key1==key2;
}


/**
   Hashtable test
*/
static int hash_test( int elements )
{
	int i;
	int res=1;
		
	hash_table_t h;

	hash_init( &h, hash_func, compare_func );
	
	for( i=1; i< elements+1; i++ )
	{
		hash_put( &h, (void*)i, (void*)100-i );
	}
	
	for( i=1; i< elements+1; i++ )
	{
		if( (int)hash_get( &h, (void*)i ) != (100-i) )
		{
			err( L"Key %d gave data %d, expected data %d",
				 i, 
				 (int)hash_get( &h, (void*)i ),
				 100-i );
			res = 0;
			
			break;
		}
	}

	if( hash_get_count( &h ) != elements )
	{
		err( L"Table holds %d elements, should hold %d elements",
			 hash_get_count( &h ),
			 elements );
		res = 0;
		
	}
	
	
	for( i=1; i<elements+1; i+=2 )
	{
		hash_remove( &h, (void*)i, 0, 0 );
				
	}

	if( hash_get_count( &h ) != ((elements)/2) )
	{
		err( L"Table contains %d elements, should contain %d elements",
			 hash_get_count( &h ),
			 elements/2 );
		res = 0;
	}
	
	for( i=1; i<elements+1; i++ )
	{
		if( hash_contains( &h, (void*)i) != (i+1)%2 )
		{
			if( i%2 )
				err( L"Key %d remains, should be deleted",
						i );
			else
				err( L"Key %d does not exist",
					 i );
			res = 0;
			break;
			}
	}

	hash_destroy( &h );

	return res;
	
}

/**
   Arraylist test
*/
static void al_test( int sz)
{
	long i;	
	array_list_t l;

	

	al_init( &l );
	
	al_set_long( &l, 1, 7L );
	al_set_long( &l, sz, 7L );
	
	if( al_get_count( &l ) != maxi( sz+1, 2 ) )
		err( L"Wrong number of elements in array list" );
	
	for( i=0; i<al_get_count( &l ); i++ )
	{
		long val = al_get_long( &l, i );
		if( (i == 1) || (i==sz))
		{
			if( val != 7 )
				err( L"Canary changed to %d at index %d", val, i );
		}
		else
		{
			if( val != 0 )
				err( L"False canary %d found at index %d", val, i );
		}
	}
}

/**
   Stringbuffer test
*/
static void sb_test()
{
	string_buffer_t b;
	int res;
	
	sb_init( &b );
	
	if( (res=sb_printf( &b, L"%ls%s", L"Testing ", "string_buffer_t " )) == -1 )
	{
		err( L"Error %d while testing stringbuffers", res );
	}
	
	if( (res=sb_printf( &b, L"%ls", L"functionality" ))==-1)	
	{
		err( L"Error %d while testing stringbuffers", res );
	}

	say( (wchar_t *)b.buff );

	sb_clear( &b );

	sb_printf( &b, L"%d %u %o %x %llX", -7, 99999999, 01234567, 0xdeadbeef, 0xdeadbeefdeadbeefll );
	if( wcscmp( (wchar_t *)b.buff,  NUM_ANS) != 0 )
	{
		err( L"numerical formating is broken, '%ls' != '%ls'", (wchar_t *)b.buff, NUM_ANS );
	}
	else
		say( L"numerical formating works" );	

	
}

/**
   Performs all tests of the util library
*/
static void test_util()
{
	int i;

	say( L"Testing utility library" );
	
	for( i=0; i<18; i++ )
	{
		long t1, t2;
		pq_test( 1<<i );
		stack_test( 1<<i );
		t1 = get_time();
		hash_test( 1<<i );
		t2 = get_time();
		if( i > 8 )
			say( L"Hashtable uses %f microseconds per element at size %d",
				 ((double)(t2-t1))/(1<<i),
				1<<i );
		al_test( 1<<i );
	}

	sb_test();
	
	
/*
	int i;
	for( i=2; i<10000000; i*=2 )
	{
		
		printf( "%d", i );
		
		t1 = get_time();
		
		if(!hash_test(i))
			exit(0);
	
		t2 = get_time();
		
		printf( " %d\n", (t2-t1)/i );
		
		
	}
*/	
}


/**
   Test the tokenizer
*/
static void test_tok()
{
	tokenizer t;
	
	say( L"Testing tokenizer" );

	
	say( L"Testing invalid input" );
	tok_init( &t, 0, 0 );

	if( tok_last_type( &t ) != TOK_ERROR )
	{
		err(L"Invalid input to tokenizer was undetected" );
	}
	
	say( L"Testing use of broken tokenizer" );
	if( !tok_has_next( &t ) )
	{
		err( L"tok_has_next() should return 1 once on broken tokenizer" );
	}
	
	tok_next( &t );
	if( tok_last_type( &t ) != TOK_ERROR )
	{
		err(L"Invalid input to tokenizer was undetected" );
	}

	/*
	  This should crash if there is a bug. No reliable way to detect otherwise.
	*/
	say( L"Test destruction of broken tokenizer" );
	tok_destroy( &t );
	
	{

		wchar_t *str = L"string <redirection  2>&1 'nested \"quoted\" '(string containing subshells ){and,brackets}$as[$well (as variable arrays)]";
		const int types[] = 
		{
			TOK_STRING, TOK_REDIRECT_IN, TOK_STRING, TOK_REDIRECT_FD, TOK_STRING, TOK_STRING, TOK_END
		}
		;
		int i;
		
		say( L"Test correct tokenization" );

		for( i=0, tok_init( &t, str, 0 ); i<(sizeof(types)/sizeof(int)); i++,tok_next( &t ) )
		{
			if( types[i] != tok_last_type( &t ) )
			{
				err( L"Tokenization error:");
				wprintf( L"Token number %d of string \n'%ls'\n, expected token type %ls, got token '%ls' of type %ls\n", 
						 i+1,
						 str,
						 tok_get_desc(types[i]),
						 tok_last(&t),
						 tok_get_desc(tok_last_type( &t )) );
			}
		}
	}
	

		
}

/**
   Test the parser
*/
static void test_parser()
{
	say( L"Testing parser" );
	
	
	say( L"Testing null input to parser" );
	if( !parser_test( 0, 0, 0, 0 ) )
	{
		err( L"Null input to parser_test undetected" );
	}

	say( L"Testing block nesting" );
	if( !parser_test( L"if; end", 0, 0, 0 ) )
	{
		err( L"Incomplete if statement undetected" );			
	}
	if( !parser_test( L"if test; echo", 0, 0, 0 ) )
	{
		err( L"Missing end undetected" );			
	}
	if( !parser_test( L"if test; end; end", 0, 0, 0 ) )
	{
		err( L"Unbalanced end undetected" );			
	}

	say( L"Testing detection of invalid use of builtin commands" );
	if( !parser_test( L"case foo", 0, 0, 0 ) )
	{
		err( L"'case' command outside of block context undetected" );		
	}
	if( !parser_test( L"switch ggg; if true; case foo;end;end", 0, 0, 0 ) )
	{
		err( L"'case' command outside of switch block context undetected" );		
	}
	if( !parser_test( L"else", 0, 0, 0 ) )
	{
		err( L"'else' command outside of conditional block context undetected" );		
	}
	if( !parser_test( L"break", 0, 0, 0 ) )
	{
		err( L"'break' command outside of loop block context undetected" );		
	}
	if( !parser_test( L"exec ls|less", 0, 0, 0 ) || !parser_test( L"echo|return", 0, 0, 0 ))
	{
		err( L"Invalid pipe command undetected" );		
	}	

	say( L"Testing basic evaluation" );
	if( !eval( 0, 0, TOP ) )
	{
		err( L"Null input when evaluating undetected" );
	}	
	if( !eval( L"ls", 0, WHILE ) )
	{
		err( L"Invalid block mode when evaluating undetected" );
	}
	
}
	
/**
   Perform parameter expansion and test if the output equals the zero-terminated parameter list supplied.

   \param in the string to expand
   \param flags the flags to send to expand_string
*/

static int expand_test( const wchar_t *in, int flags, ... )
{
	array_list_t out;	
	va_list va;
	int i=0;
	int res=1;
	wchar_t *arg;
	
	al_init( &out );
	expand_string( 0, wcsdup(in), &out, flags);
	
	va_start( va, flags );

	while( (arg=va_arg(va, wchar_t *) )!= 0 ) 
	{
		if( al_get_count( &out ) == i )
		{
			res=0;
			break;
		}
		
		if( wcscmp( al_get( &out, i ),arg) != 0 )
		{
			res=0;
			break;
		}
		
		i++;		
	}
	va_end( va );
	
	al_foreach( &out, &free );
	return res;
		
}

/**
   Test globbing and other parameter expansion
*/
static void test_expand()
{
	say( L"Testing parameter expansion" );
	
	if( !expand_test( L"foo", 0, L"foo", 0 ))
	{
		err( L"Strings do not expand to themselves" );
	}

	if( !expand_test( L"a{b,c,d}e", 0, L"abe", L"ace", L"ade", 0 ) )
	{
		err( L"Bracket expansion is broken" );
	}

	if( !expand_test( L"a*", EXPAND_SKIP_WILDCARDS, L"a*", 0 ) )
	{
		err( L"Cannot skip wildcard expansion" );
	}
	
}

/**
   Test speed of completion calculations
*/
void perf_complete()
{
	wchar_t c;
	array_list_t out;
	long long t1, t2;
	int matches=0;
	double t;
	wchar_t str[3]=
	{
		0, 0, 0 
	}
	;
	int i;
	
	
	say( L"Testing completion performance" );
	al_init( &out );
	
	reader_push(L"");
	say( L"Here we go" );
	 	
	t1 = get_time();
	
	
	for( c=L'a'; c<=L'z'; c++ )
	{
		str[0]=c;
		reader_set_buffer( str, 0 );
		
		complete( str, &out );
	
		matches += al_get_count( &out );
		
		al_foreach( &out, &free );
		al_truncate( &out, 0 );
	}
	t2=get_time();
	
	t = (double)(t2-t1)/(1000000*26);
	
	say( L"One letter command completion took %f seconds per completion, %f microseconds/match", t, (double)(t2-t1)/matches );
	
	matches=0;
	t1 = get_time();
	for( i=0; i<LAPS; i++ )
	{
		str[0]='a'+(rand()%26);
		str[1]='a'+(rand()%26);
		
		reader_set_buffer( str, 0 );
		
		complete( str, &out );
	
		matches += al_get_count( &out );
		
		al_foreach( &out, &free );
		al_truncate( &out, 0 );
	}
	t2=get_time();
	
	t = (double)(t2-t1)/(1000000*LAPS);
	
	say( L"Two letter command completion took %f seconds per completion, %f microseconds/match", t, (double)(t2-t1)/matches );
	
	al_destroy( &out );

	reader_pop();
	
}

/**
   Main test 
*/
int main( int argc, char **argv )
{

	program_name=L"(ignore)";
	
	say( L"Testing low-level functionality");
	say( L"Lines beginning with '(ignore):' are not errors, they are warning messages\ngenerated by the fish parser library when given broken input, and can be\nignored. All actual errors begin with 'Error:'." );

	proc_init();	
	halloc_util_init();
	event_init();	
	parser_init();
	function_init();
	builtin_init();
	reader_init();
	env_init();

	test_util();
	test_tok();
	test_parser();
	test_expand();
		
	say( L"Encountered %d errors in low-level tests", err_count );

	/*
	  Skip performance tests for now, since they seem to hang when running from inside make (?)
	*/
//	say( L"Testing performance" );
//	perf_complete();
		
	env_destroy();
	reader_destroy();	
	parser_destroy();
	function_destroy();
	builtin_destroy();
	wutil_destroy();
	event_destroy();
	proc_destroy();
	halloc_util_destroy();
	
}