use strict;
use warnings;
use Scalar::Util 'blessed';
use Test::Simple tests => 29;

use Farly::ASA::Parser;

my $parser = Farly::ASA::Parser->new();

ok( defined($parser), "constructor" );

my $string;
my $tree;
my $actual;
my $expected;

#
# hostname
#

$string = q{hostname test_fw};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'hostname' => 1,
	'STRING'   => 1
};

ok( equals( $expected, $actual ), "hostname" );

#
# name
#

$string = q{name 192.168.10.0 net1 description This is a test};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'REMARKS'      => 1,
	'named_ip'     => 1,
	'NAME_ID'      => 1,
	'name'         => 1,
	'name_comment' => 1,
	'IPADDRESS'    => 1
};

ok( equals( $expected, $actual ), "name" );

#
# interface nameif
#

$string = q{
interface Vlan2
 nameif outside
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'if_name'           => 1,
	'interface_options' => 2,
	'interface'         => 1,
	'STRING'            => 2
};

ok( equals( $expected, $actual ), "interface nameif" );

#
# interface ip
#

$string = q{
interface Vlan2
 ip address 10.2.19.8 255.255.255.248 standby 10.2.19.9
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'if_ip'             => 1,
	'if_mask'           => 1,
	'interface_options' => 2,
	'if_addr'           => 1,
	'interface'         => 1,
	'if_standby'        => 1,
	'MASK'              => 1,
	'IPADDRESS'         => 2,
	'STRING'            => 1
};

ok( equals( $expected, $actual ), "interface ip" );

#
# interface security-level
#

$string = q{
interface Vlan2
 security-level 0
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'DIGIT'             => 1,
	'sec_level'         => 1,
	'interface_options' => 2,
	'interface'         => 1,
	'STRING'            => 1
};

ok( equals( $expected, $actual ), "interface security-level" );

#
# object host
#

$string = q{
object network TestFW
 host 192.168.5.219
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'object'         => 1,
	'object_address' => 1,
	'STRING'         => 1,
	'OBJECT_ENTRY'    => 1,
	'object_id'      => 1,
	'OBJECT_TYPE'    => 1,
	'IPADDRESS'      => 1
};

ok( equals( $expected, $actual ), "object host" );

#
# object subnet
#

$string = q{
object network test_net1
 subnet 10.1.2.0 255.255.255.0
};

$tree = $parser->parse($string);

$actual = productions($tree);


$expected = {
	'object'         => 1,
	'IPNETWORK'      => 1,
	'object_address' => 1,
	'STRING'         => 1,
	'OBJECT_ENTRY'    => 1,
	'object_id'      => 1,
	'OBJECT_TYPE'    => 1
};

ok( equals( $expected, $actual ), "object subnet" );

#
# object range
#

$string = q{
object network test_net1_range
 range 10.1.2.13 10.1.2.28
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'IPRANGE'        => 1,
	'object'         => 1,
	'OBJECT_ENTRY'    => 1,
	'object_address' => 1,
	'object_id'      => 1,
	'OBJECT_TYPE'    => 1,
	'STRING'         => 1
};

ok( equals( $expected, $actual ), "object range" );

#
# object service src dst
#

$string = q{
object service web_https
 service tcp source gt 1024 destination eq 443
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'                 => 1,
	'object_service_protocol' => 1,
	'object'                  => 1,
	'port_gt'                 => 1,
	'object_service'          => 1,
	'PROTOCOL'                => 1,
	'PORT_ID'                 => 1,
	'port'                    => 2,
	'object_service_src'      => 1,
	'PORT_GT'                 => 1,
	'STRING'                  => 1,
	'OBJECT_ENTRY'             => 1,
	'object_service_dst'      => 1,
	'object_id'               => 1,
	'OBJECT_TYPE'             => 1
};

ok( equals( $expected, $actual ), "object service src dst" );

#
# object-group service src
#

$string = q{object-group service NFS
 service-object 6 source eq 2046
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'           => 1,
	'og_service_object' => 1,
	'PROTOCOL'          => 1,
	'og_so_protocol'    => 1,
	'PORT_ID'           => 1,
	'object_group'      => 1,
	'port'              => 1,
	'STRING'            => 1,
	'og_id'             => 1,
	'og_so_src_port'    => 1,
	'OBJECT_TYPE'       => 1,
	'og_object'         => 1,
	'GROUP_TYPE'        => 1
};

ok( equals( $expected, $actual ), "object-group service src" );

#
# object-group protocol
#

$string = q{
object-group protocol test65
 protocol-object tcp
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'og_id'              => 1,
	'OBJECT_TYPE'        => 1,
	'PROTOCOL'           => 1,
	'og_protocol_object' => 1,
	'og_object'          => 1,
	'object_group'       => 1,
	'GROUP_TYPE'         => 1,
	'STRING'             => 1
};

ok( equals( $expected, $actual ), "object-group protocol" );

#
# network-object named host
#
$string = q{
object-group network test_net
 network-object host server1
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'NAME'              => 1,
	'object_group'      => 1,
	'STRING'            => 1,
	'og_id'             => 1,
	'og_network_object' => 1,
	'OBJECT_TYPE'       => 1,
	'og_object'         => 1,
	'address'           => 1,
	'GROUP_TYPE'        => 1
};

ok( equals( $expected, $actual ), "network-object named host" );

#
# port-object
#

$string = q{
object-group service web tcp
 port-object eq www
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'        => 1,
	'PORT_ID'        => 1,
	'GROUP_PROTOCOL' => 1,
	'object_group'   => 1,
	'port'           => 1,
	'og_port_object' => 1,
	'STRING'         => 1,
	'og_protocol'    => 1,
	'og_id'          => 1,
	'OBJECT_TYPE'    => 1,
	'og_object'      => 1,
	'GROUP_TYPE'     => 1
};

ok( equals( $expected, $actual ), "port-object" );

#
# network group-object
#

$string = q{
object-group network test_net
 group-object server1
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'object_group'    => 1,
	'GROUP_REF'       => 1,
	'STRING'          => 1,
	'og_id'           => 1,
	'OBJECT_TYPE'     => 1,
	'og_group_object' => 1,
	'og_object'       => 1,
	'GROUP_TYPE'      => 1
};

ok( equals( $expected, $actual ), "network group-object" );

#
# object-group description
#

$string = q{
object-group network test_net
 description test network
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'REMARKS'        => 1,
	'og_id'          => 1,
	'OBJECT_TYPE'    => 1,
	'og_object'      => 1,
	'object_group'   => 1,
	'og_description' => 1,
	'GROUP_TYPE'     => 1,
	'STRING'         => 1
};

ok( equals( $expected, $actual ), "object-group description" );

#
# object-group service dst
#

$string = q{object-group service NFS
 service-object 6 destination eq 2046
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'           => 1,
	'og_service_object' => 1,
	'PROTOCOL'          => 1,
	'og_so_protocol'    => 1,
	'PORT_ID'           => 1,
	'object_group'      => 1,
	'port'              => 1,
	'og_so_dst_port'    => 1,
	'STRING'            => 1,
	'og_id'             => 1,
	'OBJECT_TYPE'       => 1,
	'og_object'         => 1,
	'GROUP_TYPE'        => 1
};

ok( equals( $expected, $actual ), "object-group service dst" );

#
# object-group service
#

$string = q{
object-group service www tcp
 group-object web
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'GROUP_PROTOCOL'  => 1,
	'object_group'    => 1,
	'GROUP_REF'       => 1,
	'STRING'          => 1,
	'og_protocol'     => 1,
	'og_id'           => 1,
	'OBJECT_TYPE'     => 1,
	'og_group_object' => 1,
	'og_object'       => 1,
	'GROUP_TYPE'      => 1
};

ok( equals( $expected, $actual ), "object-group service" );

#
# access-list 1
#

$string =
q{access-list acl-outside permit tcp OG_NETWORK customerX range 1024 65535 host server1 eq 80};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'      => 1,
	'PORT_RANGE'   => 1,
	'ACTIONS'      => 1,
	'PROTOCOL'     => 1,
	'acl_options'  => 1,
	'GROUP_REF'    => 1,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_src_port' => 1,
	'acl_protocol' => 1,
	'address'      => 2,
	'port_range'   => 1,
	'NAME'         => 1,
	'acl_dst_ip'   => 1,
	'PORT_ID'      => 1,
	'port'         => 2,
	'acl_dst_port' => 1,
	'access_list'  => 1,
	'acl_src_ip'   => 1
};

ok( equals( $expected, $actual ), "access-list 1" );

#
# access-list 2
#

$string =
q{access-list acl-outside line 1 extended permit ip host server1 eq 1024 any eq 80};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'      => 2,
	'ACTIONS'      => 1,
	'PROTOCOL'     => 1,
	'ACL_TYPES'    => 1,
	'acl_options'  => 1,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_src_port' => 1,
	'acl_protocol' => 1,
	'ANY'          => 1,
	'address'      => 2,
	'NAME'         => 1,
	'acl_dst_ip'   => 1,
	'PORT_ID'      => 2,
	'port'         => 2,
	'acl_dst_port' => 1,
	'DIGIT'        => 1,
	'acl_line'     => 1,
	'access_list'  => 1,
	'acl_src_ip'   => 1,
	'acl_type'     => 1
};

ok( equals( $expected, $actual ), "access-list 2" );

#
# access-list 3
#

$string =
q{access-list acl-outside permit tcp OG_NETWORK customerX OG_SERVICE high_ports host server1 eq 80};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'      => 1,
	'ACTIONS'      => 1,
	'PROTOCOL'     => 1,
	'acl_options'  => 1,
	'GROUP_REF'    => 2,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_src_port' => 1,
	'acl_protocol' => 1,
	'address'      => 2,
	'NAME'         => 1,
	'acl_dst_ip'   => 1,
	'PORT_ID'      => 1,
	'port'         => 2,
	'acl_dst_port' => 1,
	'access_list'  => 1,
	'acl_src_ip'   => 1
};

ok( equals( $expected, $actual ), "access-list 3" );

#
# access-list 4
#

$string =
q{access-list acl-outside permit OG_SERVICE srv2 OG_NETWORK customerX OG_SERVICE high_ports host server1 eq 80};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'      => 1,
	'NAME'         => 1,
	'acl_dst_ip'   => 1,
	'ACTIONS'      => 1,
	'PORT_ID'      => 1,
	'port'         => 2,
	'acl_options'  => 1,
	'GROUP_REF'    => 3,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_dst_port' => 1,
	'acl_src_port' => 1,
	'acl_protocol' => 1,
	'access_list'  => 1,
	'address'      => 2,
	'acl_src_ip'   => 1
};

ok( equals( $expected, $actual ), "access-list 4" );

#
# access-list 5
#

$string =
  q{access-list acl-outside permit object citrix any OG_NETWORK citrix_servers};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'acl_dst_ip'   => 1,
	'ACTIONS'      => 1,
	'OBJECT_REF'   => 1,
	'GROUP_REF'    => 1,
	'acl_options'  => 1,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_protocol' => 1,
	'access_list'  => 1,
	'ANY'          => 1,
	'address'      => 2,
	'acl_src_ip'   => 1
};

ok( equals( $expected, $actual ), "access-list 5" );

#
# access-list 6
#

$string =
q{access-list acl-outside permit OG_SERVICE srv2 OG_NETWORK customerX OG_SERVICE high_ports net1 255.255.255.0 eq www};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'port_eq'      => 1,
	'NAMED_NET'    => 1,
	'acl_dst_ip'   => 1,
	'ACTIONS'      => 1,
	'PORT_ID'      => 1,
	'port'         => 2,
	'acl_options'  => 1,
	'GROUP_REF'    => 3,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_dst_port' => 1,
	'acl_src_port' => 1,
	'acl_protocol' => 1,
	'access_list'  => 1,
	'address'      => 2,
	'acl_src_ip'   => 1
};

ok( equals( $expected, $actual ), "access-list 6" );

#
# access-list 7
#

$string =
  q{access-list acl-outside permit ip any range 1024 65535 host server1 gt www};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'PORT_RANGE'   => 1,
	'ACTIONS'      => 1,
	'PROTOCOL'     => 1,
	'acl_options'  => 1,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_src_port' => 1,
	'acl_protocol' => 1,
	'ANY'          => 1,
	'address'      => 2,
	'port_range'   => 1,
	'NAME'         => 1,
	'port_gt'      => 1,
	'acl_dst_ip'   => 1,
	'port'         => 2,
	'PORT_GT'      => 1,
	'acl_dst_port' => 1,
	'access_list'  => 1,
	'acl_src_ip'   => 1
};

ok( equals( $expected, $actual ), "access-list 7" );

#
# access-list 8
#

$string =
q{access-list acl-outside extended permit OG_PROTOCOL sip_transport OG_NETWORK voip_nets OG_SERVICE high_ports OG_NETWORK voip_srvs OG_SERVICE sip_ports};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'ACTIONS'      => 1,
	'ACL_TYPES'    => 1,
	'acl_options'  => 1,
	'GROUP_REF'    => 5,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'acl_src_port' => 1,
	'acl_protocol' => 1,
	'address'      => 2,
	'acl_dst_ip'   => 1,
	'port'         => 2,
	'acl_dst_port' => 1,
	'access_list'  => 1,
	'acl_src_ip'   => 1,
	'acl_type'     => 1
};

ok( equals( $expected, $actual ), "access-list 8" );

#
# access-list object service
#

$string =
q{access-list acl-outside line 1 extended permit object citrix object internal_net object citrix_net};

$tree   = $parser->parse($string);
$actual = productions($tree);

$expected = {
	'acl_dst_ip'   => 1,
	'ACTIONS'      => 1,
	'OBJECT_REF'   => 3,
	'ACL_TYPES'    => 1,
	'acl_options'  => 1,
	'STRING'       => 1,
	'acl_id'       => 1,
	'acl_action'   => 1,
	'DIGIT'        => 1,
	'acl_line'     => 1,
	'acl_protocol' => 1,
	'access_list'  => 1,
	'address'      => 2,
	'acl_src_ip'   => 1,
	'acl_type'     => 1
};

ok( equals( $expected, $actual ), "access-list object service" );

#
# access-list icmp-type
#

$string =
q{access-list acl-outside line 1 extended permit icmp any any OG_ICMP-TYPE safe-icmp};

$tree   = $parser->parse($string);
$actual = productions($tree);

$expected = {
	'ACTIONS'       => 1,
	'PROTOCOL'      => 1,
	'ACL_TYPES'     => 1,
	'GROUP_REF'     => 1,
	'acl_icmp_type' => 1,
	'acl_options'   => 1,
	'STRING'        => 1,
	'acl_id'        => 1,
	'acl_action'    => 1,
	'acl_protocol'  => 1,
	'ANY'           => 2,
	'address'       => 2,
	'acl_dst_ip'    => 1,
	'DIGIT'         => 1,
	'acl_dst_port'  => 1,
	'acl_line'      => 1,
	'access_list'   => 1,
	'acl_src_ip'    => 1,
	'acl_type'      => 1
};

ok( equals( $expected, $actual ), "access-list icmp-type" );

#
# access-group
#

$string = q{
access-group acl-outside in interface outside
};

$tree = $parser->parse($string);

$actual = productions($tree);

$expected = {
	'ag_interface'  => 1,
	'IF_REF'        => 1,
	'ACL_DIRECTION' => 1,
	'access_group'  => 1,
	'RULE_REF'      => 1,
	'ag_direction'  => 1,
	'ag_id'         => 1
};

ok(	equals( $expected, $actual ), "access-group" );

#
# Finished tests
#

sub productions {
	my ($node) = @_;

	my $result;

	# set s of explored vertices
	my %seen;

	#stack is all neighbors of s
	my @stack;
	push @stack, $node;

	while (@stack) {

		my $node = pop @stack;

		next if ( $seen{$node}++ );

		foreach my $key ( keys %$node ) {

			next if ( $key eq "EOL" );

			my $next = $node->{$key};

			if ( blessed($next) ) {

				$result->{ ref($next) }++;

				push @stack, $next;
			}
		}
	}

	return $result;
}

sub equals {
	my ( $hash1, $hash2 ) = @_;

	if ( scalar( keys %$hash1 ) != scalar( keys %$hash2 ) ) {
		return undef;
	}

	foreach my $key ( keys %$hash2 ) {
		if ( !defined( $hash1->{$key} ) ) {
			return undef;
		}
		if ( $hash1->{$key} ne $hash2->{$key} ) {
			return undef;
		}
	}
	return 1;
}
