#!/usr/bin/perl

use Getopt::Long;

$__isns_verbose = 1;
$__isns_security = 1;

$__isns_bin = "../";
$__isns_seq = 0;
$__isns_test_base = '/tmp/isns-test';
$__isns_test_dir = '/tmp/isns-test/test';
$__isns_stage = 1;
$__isns_test_data = '';
$__isns_test_dump = '';
$__isns_passed = 0;
$__isns_failed = 0;
$__isns_warned = 0;
@__isns_servers = ();

%__isns_ignore_tag = (
	"0004"	=> 1,		# Timestamp
	"0603v"	=> 1,		# DSA public key
);

sub isns_fail {
	
	print "*** FAILURE ***\n";
	$__isns_failed++;

	my $line;
	foreach $line (@_) {
		print "*** $line ***\n";
	}
}

sub isns_pass {

	print "*** SUCCESS ***\n" if ($__isns_verbose > 1);
	$__isns_passed++;
}

sub isns_warn {

	printf "*** WARNING: %s ***\n", join(' ', @_);
	$__isns_warned++;
}

sub isns_die {

	printf "*** TERMINAL FAILURE: %s ***\n", join(' ', @_);
	$__isns_failed++;

	&isns_finish;
	die "Test aborted\n";
}

sub isns_finish {

	my $pid_str;
	my $pid;
    
	foreach $pid_str (@__isns_servers) {
		$pid = $pid_str + 0;
		kill 15, $pid or &isns_warn("Cannot kill server process (pid=$pid): $!\n");
	}

	&isns_report;
}

sub isns_report {

	print "*** Test $__isns_test_name complete.";
	print " PASSED: $__isns_passed" if ($__isns_passed);
	print " FAILED: $__isns_failed" if ($__isns_failed);
	print " WARNINGS: $__isns_warned" if ($__isns_warned);
	print " ***\n";
}

sub isns_info {

	print @_ if ($__isns_verbose > 1);
}

sub isns_notice {

	print @_ if ($__isns_verbose > 0);
}

sub isns_stage {

	local($name, @msg) = @_;

	if ($name =~ m/^[0-9]/o) {
		$__isns_stage_name = $name;
	} else {
		$__isns_stage_name = sprintf "%02d-%s",
			$__isns_stage++, $name;
	}
	&isns_notice("*** $__isns_stage_name: ", @msg, " ***\n");
}

sub build_config {

	local($src_file, $dst_file, *__subst) = @_;
	my $key;
	my $okey;
	my $value;
	my $sepa;
	my %subst;

	&isns_info("*** Building $src_file -> $dst_file\n");

	# Translate all keys to lower case.
	foreach $key (keys(%__subst)) {
		$value = $__subst{$key};
		$key =~ tr/A-Z/a-z/;
		$subst{$key} = $value;
	}
#	foreach $key (keys(%subst)) {
#		printf "  %s -> %s\n", $key, $subst{$key};
#	}

	open IN, "<$src_file" or die "$src_file: $!\n";
	open OUT, ">$dst_file" or die "$dst_file: $!\n";

	while (<IN>) {
		$line = $_;
		if (m:(\S+)(\s*=\s*)(.*):o) {
			($okey, $sepa, $value) = ($1, $2, $3);

			$key = $okey;
			$key =~ tr/A-Z/a-z/;

			if ($subst{$key}) {
				$line = "$okey$sepa$subst{$key}\n";
			}
		}

		# Ignore unconfigured lines.
		next if ($line =~ m/\@[A-Z_]*\@/o);
		print OUT $line;
	}
	close OUT;
	close IN;
}

sub get_config_value {
	local($cfg_file, $item_name) = @_;
	my $result;
	my $name;
	my $value;

	$item_name =~ tr/A-Z/a-z/;

	open IN, "<$cfg_file" or die "$cfg_file: $!\n";
	while (<IN>) {
		chop;
		($name, $value) = split(/\s+=\s+/, $_);

		$name =~ tr/A-Z/a-z/;
		if ($name eq $item_name) {
			$result = $value;
			last;
		}
	}
	close IN;

	return $result;
}

sub create_key {

	local($keyfile) = @_;

	if ($__isns_security) {
		&isns_info("*** Creating key at $keyfile\n");
		system "./genkey -fsk $keyfile 2048 >${keyfile}.log 2>&1";
	}
	return $keyfile;
}

sub create_server {

	local(*override) = @_;
	my %local_config;
	my $my_dir;
	my $handle;
	my $config;

	$handle = sprintf "server%d", $__isns_seq++;
	$my_dir = "$__isns_test_dir/${handle}";

	mkdir $my_dir, 0700 or die "Cannot create $my_dir: $!\n";

	$server_addr = "127.0.0.1:7770" unless ($server_addr);

	$config = "$my_dir/config";

	$local_config{"SourceName"} = "isns.$handle";
	$local_config{"Database"} = "$my_dir/database";
	$local_config{"BindAddress"} = "$server_addr";
	$local_config{"PIDFile"} = "$my_dir/pid";
	$local_config{"ControlSocket"} = "$my_dir/control";
	$local_config{"Security"} = $__isns_security;
	$local_config{"AuthKeyFile"} = &create_key("$my_dir/auth_key");

	foreach $key (keys(%override)) {
		$local_config{$key} = $override{$key};
	}

	&build_config('server.conf', $config, \%local_config);
	return $config;
}

sub create_client {

	local($server_config, $client_address) = @_;
	my %local_config;
	my $server_key;
	my $control_socket;
	my $server_addr;
	my $my_dir;
	my $handle;
	my $config;

	$handle = sprintf "client%d", $__isns_seq++;
	$my_dir = "$__isns_test_dir/${handle}";

	mkdir $my_dir, 0700 or die "Cannot create $my_dir: $!\n";

	$control_socket = &get_config_value($server_config, "ControlSocket");
	$server_addr = &get_config_value($server_config, "BindAddress");
	$server_addr = "127.0.0.1" unless ($server_addr);

	$config = "$my_dir/config";

	$local_config{"SourceName"} = "isns.$handle";
	$local_config{"AuthName"} = "$handle.isns-test.eu";
	$local_config{"ServerAddress"} = $server_addr;
	$local_config{"ControlSocket"} = $control_socket;
	$local_config{"BindAddress"} = $client_address if ($client_address);
	$local_config{"server_config"} = $server_config;
	$local_config{"Security"} = $__isns_security;
	$local_config{"AuthKeyFile"} = &create_key("$my_dir/auth_key");
	$local_config{"ServerKeyFile"} =
		&get_config_value($server_config, "AuthKeyFile") . ".pub";

	&build_config('client.conf', $config, \%local_config);

	$__isns_data{$config,"server_config"} = $server_config;
	$__isns_data{$config} = %local_config;
	return $config;
}

sub get_logfile {

	local($config) = @_;
	my $dir;

	$dir = $config;
	$dir =~ s|/+[^/]+$||o;

	return "$dir/logfile";
}

sub run_command {

	local(@cmd) = @_;
	my $status;
	my $cmd;

	$cmd = join(' ', @cmd);
	&isns_info("$cmd\n");

	system "$cmd";

	$status = $?;
	if ($status) {
		&isns_warn("Command failed, exit status $status");
		print "*** Command was: $cmd ***\n";
		return undef;
	}

	return 1;
}

sub isns_start_server {

	local($server_config) = @_;
	my $logfile;
	my $pidfile;
	my $pid;

	die "restart_server: missing server config argument!\n"
		unless(-f $server_config);
	$logfile = &get_logfile($server_config);
	$pidfile = &get_config_value($server_config, "PIDFile");

	&isns_info("*** Starting server (logging to $logfile)\n");

	$pid = fork();
	if ($pid) {
		my $retry;

		if ($pidfile) {
			for ($retry = 0; $retry < 5; $retry++) {
				last if (-f $pidfile);
				sleep 1;
			}
			$pid = `cat $pidfile`;
			chop($pid);
		}
		&isns_info("*** Started server (pid=$pid) ***\n");
		push(@__isns_servers, $pid);
		return $pid;
	}

	&isns_info("${__isns_bin}isnsd -c $server_config -f -d all\n");
	exec "${__isns_bin}isnsd -c $server_config -f -d all >$logfile 2>&1 &"
		or die "Unable to run isnsd: $!\n";
}

sub isns_stop_server {

	local($pid) = @_;
	my @list;
	my $p;

	kill 15, $pid or &isns_warn("Cannot kill server process (pid=$pid): $!\n");
	foreach $p (@__isns_servers) {
		append(@list, $p) unless ($p == $pid);
	}
	@__isns_servers = @list;
}

sub isns_restart_server {

	local($pid, $server_config);

	if ($_[0] =~ m:^\d+$:o) {
		$pid = shift(@_);
	} else {
		if ($#__isns_servers < 0) {
			&isns_warn("isns_restart_server: no server running\n");
			return 0;
		}
		$pid = $__isns_servers[0];
	}
	$server_config = shift(@_);

	&isns_stop_server($pid);
	# give it some time to stop
	&isns_idle(5);

	return &isns_start_server($server_config);
}

sub isns_verify_db {

	local($stage, $server_config);
	my $dump_file;
	my $data_file;

	if ($_[0] =~ m/^\d/o) {
		$stage = shift(@_);
	} else {
		$stage = $__isns_stage_name;
	}
	$server_config = shift(@_);

	die "Test case forgot to call test_prep" unless($__isns_test_data);

	$dump_file = "$__isns_test_dump/$stage";
	unless (&run_command("${__isns_bin}isnsd -c $server_config --dump-db > $dump_file")) {
		&isns_fail;
		return 0;
	}

	# See if the reference data file exists. If it
	# doesn't, this means we're priming the test case.
	# Just copy the dump file.
	$data_file = "$__isns_test_data/$stage";
	$data_file = "${data_file}-no-security"	unless ($__isns_security);
	unless (-f $data_file) {
		print "*** Saving database dump for stage $stage ***\n";
		mkdir $__isns_test_data, 0755;
		system "cp $dump_file $data_file";
		return 1;
	}

	&isns_info("*** Verifying database dump for stage $stage ***\n");
	if (&verify_dump($stage, $data_file, $dump_file)) {
		&isns_pass;
	} else {
		if ($__isns_verbose > 1) {
			system("diff -u -ITimestamp -I'DSA security key' $data_file $dump_file");
		}
		&isns_fail;
	}

	return 1;
}

sub verify_db {

	&isns_verify_db(@_);
}

sub verify_response {

	local($stage, $client_config) = @_;
	my $dump_file;
	my $data_file;

	die "Test case forgot to call test_prep" unless($__isns_test_data);

	$dump_file = &get_logfile($client_config);

	# See if the reference data file exists. If it
	# doesn't, this means we're priming the test case.
	# Just copy the dump file.
	$data_file = "$__isns_test_data/$stage";
	$data_file = "${data_file}-no-security"	unless ($__isns_security);
	unless (-f $data_file) {
		print "*** Saving data for stage $stage ***\n";
		mkdir $__isns_test_data, 0755;
		system "cp $dump_file $data_file";
		return 1;
	}

	&isns_info("*** Verifying data for stage $stage ***\n");
	if (&verify_query($stage, $data_file, $dump_file)) {
		&isns_pass;
	} else {
		&isns_fail("Query response returns unexpected data");
		system "cp $dump_file $__isns_test_dump/$stage";
		print "*** Saved dump as $__isns_test_dump/$stage\n";
		print "*** Reference data in $data_file\n";
		if ($__isns_verbose > 1) {
			system("diff -u -ITimestamp -I'DSA security key' $data_file $dump_file");
		}
	}

	return 1;
}

sub verify_dump {

	local($stage, $data_file, $dump_file) = @_;
	my $line;
	my @dump;
	my @data;
	my @obj1;
	my @obj2;

	@dump = &load_dump($dump_file);
	@data = &load_dump($data_file);

	&skip_header(\@dump);
	&skip_header(\@data);

	while (1) {
		$line++;

		@obj1 = &get_next_object(\@dump);
		@obj2 = &get_next_object(\@data);

		last unless(@obj1 || @obj2);

		unless (@obj1 && @obj2) {
			print STDERR "*** $stage: Excess data at end of dump\n";
			return 0;
		}

		unless (&compare_objects(\@obj1, \@obj2)) {
			print STDERR "*** Object mismatch (object $line):\n";
			print STDERR "Expected:\n  ";
			print STDERR join("\n  ", @obj2), "\n";
			print STDERR "Got:\n  ";
			print STDERR join("\n  ", @obj1), "\n";
			return 0;
		}
	}

	if (@data) {
		print STDERR "*** $stage: Unexpected end of dump at line $line\n";
		return 0;
	}

	return 1;
}

sub skip_header {

	local(*list) = @_;
	local($_);

	while ($_ = shift(@list)) {
		last if (/^-/o);
	}
}

sub get_next_object {

	local(*list) = @_;
	local($_, $header, @result);
	my @tags;

	while ($_ = shift(@list)) {
		next if (/^-/o);
		if (/^\s+([0-9a-fv]+)\s+/o) {
			next if ($__isns_ignore_tag{$1});
			push(@tags, $_);
		} else {
			if (@result) {
				unshift(@list, $_);
				last;
			}
			push(@result, $_);
		}
		#print "### $_\n";
	}

	if (@tags) {
		push(@result, sort(@tags));
	}
	return @result;
}

sub compare_objects {

	local(*a, *b) = @_;
	local($i);

	return 0 unless ($#a == $#b);
	for ($i = 0; $i <= $#a; $i++) {
		return 0 unless ($a[$i] eq $b[$i]);
	}

	return 1;
}


sub verify_query {

	local($stage, $data_file, $dump_file) = @_;
	my $line;
	my @dump;
	my @data;

	@dump = &load_dump($dump_file);
	@data = &load_dump($data_file);

	while (@dump) {
		$line++;
		unless (@data) {
			print STDERR "*** $stage: Excess data in dump at line $line\n";
			return 0;
		}

		$a = shift(@dump);
		$b = shift(@data);
		if ($a =~ /^\S/o) {
			next if ($a eq $b);
			print STDERR "*** $stage: Mismatch at line $line ***\n";
			print STDERR "*** Found:    $a\n";
			print STDERR "*** Expected: $b\n";
			return 0;
		}

		($nix, $a_tag, $a_value) = split(/\s+/, $a, 3);
		($nix, $b_tag, $b_value) = split(/\s+/, $b, 3);
		if ($a_tag ne $b_tag) {
			print STDERR "*** $stage: Tag mismatch at line $line\n";
			print STDERR "*** Found:    $a\n";
			print STDERR "*** Expected: $b\n";
			return 0;
		}

		next if ($__isns_ignore_tag{$a_tag});
		if ($a_value ne $b_value) {
			print STDERR "*** $stage: Value mismatch at line $line (tag $a_tag)\n";
			print STDERR "*** Found:    $a\n";
			print STDERR "*** Expected: $b\n";
			return 0;
		}
	}

	if (@data) {
		print STDERR "*** $stage: Unexpected end of dump at line $line\n";
		return 0;
	}

	return 1;
}

sub load_dump {

	local($filename) = @_;
	my @result;

	open IN, $filename or die "Unable to open $filename: $!\n";
	while (<IN>) {
		chop;
		push(@result, $_);
	}
	close IN;
	return @result;
}


sub run_client {

	local($config, @args) = @_;
	my $logfile;
	my $cmd;

	$logfile = &get_logfile($config);

	$cmd = "${__isns_bin}isnsadm -c $client_config " . join(' ', @args);
	if (&run_command("$cmd >$logfile")) {
		return $logfile;
	}
	return undef;
}

sub __isns_enroll_client {

	local($client_config, @extra_args) = @_;
	my $source_name;
	my $auth_name;
	my $auth_key;
	my @args;

	$source_name = &get_config_value($client_config, "SourceName");
	$auth_name = &get_config_value($client_config, "AuthName");
	$auth_key = &get_config_value($client_config, "AuthKeyFile");

	push(@args, "--local --enroll $auth_name node-name=$source_name");
	push(@args, " key=${auth_key}.pub") if ($auth_key);
	push(@args, @extra_args) if (@extra_args);

	&run_client($client_config, @args);
}

sub isns_enroll_client {

	local($client, @args) = @_;
	my $server;

	$server = $__isns_data{$client,"server_config"};
	&isns_stage("enroll", "Enrolling client");
	&__isns_enroll_client($client, @args);
	&verify_db($__isns_stage_name, $server);
}

sub enroll_client {

	print "*** Enrolling client ***\n";
	&__isns_enroll_client(@_);
}

sub __isns_register_client {

	local($client_config, @extra_args) = @_;
	my @args;

	push(@args, "--register");
	push(@args, @extra_args) if (@extra_args);

	&run_client($client_config, @args);
}

sub isns_register_client {

	local($client, @args) = @_;
	my $server;

	$server = $__isns_data{$client,"server_config"};
	&isns_stage("registration", "Registering client " . join(' ', @args));
	&__isns_register_client($client, @args);
	&verify_db($__isns_stage_name, $server);
}

sub register_client {

	print "*** Registering client ***\n";
	&__isns_register_client(@_);
}

sub __isns_query_objects {

	local($client_config, @extra_args) = @_;
	my @args;

	push(@args, "--query");
	push(@args, @extra_args) if (@extra_args);

	return &run_client($client_config, @args);
}

sub isns_query_objects {

	local($client, @args) = @_;

	&isns_stage("query", "Querying " . join(' ', @args));
	&__isns_query_objects($client, @args);
	&verify_response($__isns_stage_name, $client);
}

sub query_objects {

	print "*** Querying objects ***\n";
	__isns_query_objects(@_);
}

sub isns_query_eid {

	local($client_config, @extra_args) = @_;
	my $logfile;
	my @args;
	local($eid);

	push(@args, "--query-eid");
	push(@args, @extra_args) if (@extra_args);

	&isns_info("*** Querying for EID ***\n");
	$logfile = &run_client($client_config, @args);

	if ($logfile) {
		$eid = `cat $logfile`;
		unless ($eid) {
			&isns_fail("Server reports empty EID");
		}
		chop($eid);
	}

	return $eid;
}

sub __isns_unregister_client {

	local($client_config, @extra_args) = @_;
	my @args;

	push(@args, "--deregister");
	push(@args, @extra_args) if (@extra_args);

	&run_client($client_config, @args);
}

sub isns_unregister_client {

	my $stage = 0;
	my $client;
	my $server;
	my $eid;

	if ($_[0] =~ m/^\d/o) {
		&isns_stage(shift(@_), "Unregister client");
	} else {
		&isns_stage("unregistration", "Unregister client");
	}

	$client = shift(@_);

	unless (@_) {
		$eid = &isns_query_eid($client);
		push(@_, "eid=$eid");
	}

	&__isns_unregister_client($client, @_);

	$server = $__isns_data{$client,"server_config"};
	&verify_db($__isns_stage_name, $server);
}

sub unregister_client {

	&isns_info("*** Unregistering client ***\n");
	&__isns_unregister_client(@_);
}

sub __isns_register_domain {

	local($client_config, @extra_args) = @_;
	my @args;

	push(@args, "--local --dd-register");
	push(@args, @extra_args) if (@extra_args);

	&run_client($client_config, @args);
}

sub isns_register_domain {

	local($client, @args) = @_;
	my $server;

	&isns_stage("dd-registration", "Registering DD " . join(' ', @args));
	&__isns_register_domain($client, @args);

	$server = $__isns_data{$client,"server_config"};
	&isns_verify_db($server);
}

sub register_domain {

	&isns_info("*** Registering DD ***\n");
	&__isns_register_domain(@_);
}

sub __isns_deregister_domain {

	local($client_config, @extra_args) = @_;
	my @args;

	push(@args, "--local --dd-deregister");
	push(@args, @extra_args) if (@extra_args);

	&run_client($client_config, @args);
}

sub isns_deregister_domain {

	local($client, @args) = @_;
	my $server;

	&isns_stage("dd-deregistration", "Deregistering DD (members)" . join(' ', @args));
	&__isns_deregister_domain($client, @args);

	$server = $__isns_data{$client,"server_config"};
	&isns_verify_db($server);
}

sub isns_external_test {

	local($client, @args) = @_;
	my $logfile;
	my $stage;
	my $cmd;

	$logfile = &get_logfile($client);

	$cmd = shift(@args);
	$stage = $cmd;
	$stage =~ s:.*/::o;

	$cmd = "${__isns_bin}$cmd -c $client " . join(' ', @args);

	&isns_stage($stage, "Running external $cmd " . join(' ', @args));
	unless (&run_command("$cmd >$logfile")) {
		return undef;
	}

	$server = $__isns_data{$client,"server_config"};
	&isns_verify_db($server);
}

sub __isns_prep_test {

	local($name, $duration, @ARGV) = @_;

	GetOptions('verbose+' => \$__isns_verbose,
		   "quiet"    => \$__isns_quiet,
		   "fast"     => \$__isns_quick,
		   "insecure" => \$__isns_insecure);
	$__isns_verbose = 0 if ($__isns_quiet);
	$__isns_security = 0 if ($__isns_insecure);

	if ($__isns_quick && $duration > 15) {
		print "*** Skipping $name (duration ~ $duration seconds) ***\n";
		exit(0);
	}

	print "*** Starting $name ***\n";
	printf "*** This test case will take about %u sec ***\n", $duration
		if ($duration);
	$__isns_test_name = $name;
	$__isns_test_dir = "$__isns_test_base/$name";
	$__isns_test_dump = "$__isns_test_dir/dump";
	$__isns_test_data = "data/$name";

	# Be careful when removing test dir
	system "rm -rf $__isns_test_dir" if ($__isns_test_dir =~ m:/tmp/:o);

	mkdir $__isns_test_base, 0700;
	mkdir $__isns_test_dir, 0700;
	mkdir $__isns_test_dump, 0700;
}

sub test_prep {

	local($name, @args) = @_;

	__isns_prep_test($name, 0, @args);
}

sub isns_prep_slow_test {

	__isns_prep_test(@_);
}

# Sleep for a few seconds, giving the user some dots to keep
# him occupied.
sub isns_idle {

	local($time) = @_;

	if ($__isns_verbose == 0) {
		sleep $time;
		return;
	}

	$| = 1;
	print "Snooze";
	while ($time--) {
		print ".";
		sleep 1;
	}
	print "\n";
	$| = 0;
}

sub main {

	my $server_config;
	my $client_config;

	&test_prep;

	$server_config = &create_server;
	$client_config = &create_client($server_config);
}

#&main;
1;
