package Zonemaster::Engine::Test::Basic;

use v5.16.0;
use warnings;

use version; our $VERSION = version->declare("v1.0.19");

use Carp;
use List::MoreUtils qw[any none uniq];
use Locale::TextDomain qw[Zonemaster-Engine];
use Readonly;

use Zonemaster::Engine::Profile;
use Zonemaster::Engine::Constants qw[:ip :name];
use Zonemaster::Engine::Test::Address;
use Zonemaster::Engine::Test::Syntax;
use Zonemaster::Engine::TestMethods;
use Zonemaster::Engine::Util qw[info name ns should_run_test];

=head1 NAME

Zonemaster::Engine::Test::Basic - Module implementing tests focused on basic zone functionality

=head1 SYNOPSIS

    my @results = Zonemaster::Engine::Test::Basic->all( $zone );

=head1 METHODS

=over

=item all()

    my @logentries = Zonamester::Engine::Test::Basic->all( $zone );

Runs the test cases in the Basic test module.
A test is skipped if it is not included in
L<Zonemaster::Engine::Profile/"test_cases"> or if a previous test case has found
a condition that renders it superfluous.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub all {
    my ( $class, $zone ) = @_;

    my @results;

    if ( should_run_test( q{basic01} ) ) {
        push @results, $class->basic01( $zone );
        if ( none { $_->tag eq q{B01_CHILD_FOUND} } @results ) {
            return @results;
        }
    }

    my $auth_response_soa = 0;
    if ( should_run_test( q{basic02} ) ) {
        push @results, $class->basic02( $zone );
        $auth_response_soa = any { $_->tag eq q{B02_AUTH_RESPONSE_SOA} } @results;
    }

    if ( should_run_test( q{basic03} ) ) {
        # Perform BASIC3 if BASIC2 failed
        if ( $auth_response_soa ) {
            push @results,
              _emit_log(
                HAS_NAMESERVER_NO_WWW_A_TEST => {
                    zname => $zone->name,
                }
              );
        }
        else {
            push @results, $class->basic03( $zone );
        }
    }

    return @results;
} ## end sub all

=over

=item can_continue()

    my $bool = can_continue( $zone, @logentry_array );

Determines if further evaluation of the given zone is possible based on the results from the Basic Test Cases.

Takes a L<Zonemaster::Engine::Zone> object and an array of L<Zonemaster::Engine::Logger::Entry> objects.

Returns a boolean.

=back

=cut

sub can_continue {
    my ( $class, $zone, @results ) = @_;

    if ( should_run_test( 'basic02' ) ) {
        my %tag = map { $_->tag => 1 } @results;
        return !$tag{B02_NO_DELEGATION} && $tag{B02_AUTH_RESPONSE_SOA};
    }

    return 1;
}

=over

=item metadata()

    my $hash_ref = metadata();

Returns a reference to a hash, the keys of which are the names of all Test Cases in the module, and the corresponding values are references to
an array containing all the message tags that the Test Case can use in L<log entries|Zonemaster::Engine::Logger::Entry>.

=back

=cut

sub metadata {
    my ( $class ) = @_;

    return {
        basic01 => [
            qw(
              B01_CHILD_IS_ALIAS
              B01_CHILD_FOUND
              B01_INCONSISTENT_ALIAS
              B01_INCONSISTENT_DELEGATION
              B01_NO_CHILD
              B01_PARENT_DISREGARDED
              B01_PARENT_FOUND
              B01_PARENT_NOT_FOUND
              B01_PARENT_UNDETERMINED
              B01_ROOT_HAS_NO_PARENT
              B01_SERVER_ZONE_ERROR
              TEST_CASE_END
              TEST_CASE_START
              )
        ],
        basic02 => [
            qw(
              B02_AUTH_RESPONSE_SOA
              B02_NO_DELEGATION
              B02_NO_WORKING_NS
              B02_NS_BROKEN
              B02_NS_NOT_AUTH
              B02_NS_NO_IP_ADDR
              B02_NS_NO_RESPONSE
              B02_UNEXPECTED_RCODE
              IPV4_DISABLED
              IPV6_DISABLED
              IPV4_ENABLED
              IPV6_ENABLED
              TEST_CASE_END
              TEST_CASE_START
              )
        ],
        basic03 => [
            qw(
              A_QUERY_NO_RESPONSES
              HAS_A_RECORDS
              IPV4_DISABLED
              IPV4_ENABLED
              IPV6_DISABLED
              IPV6_ENABLED
              NO_A_RECORDS
              TEST_CASE_END
              TEST_CASE_START
              )
        ],
    };
} ## end sub metadata

Readonly my %TAG_DESCRIPTIONS => (
    BASIC01 => sub {
        __x    # BASIC:BASIC01
          'The domain must have a parent domain';
    },
    BASIC02 => sub {
        __x    # BASIC:BASIC02
          'The domain must have at least one working name server';
    },
    BASIC03 => sub {
        __x    # BASIC:BASIC03
          'The Broken but functional test';
    },
    A_QUERY_NO_RESPONSES => sub {
        __x    # BASIC:A_QUERY_NO_RESPONSES
          'Nameservers did not respond to A query.';
    },
    B01_CHILD_IS_ALIAS => sub {
        __x    # BASIC:B01_CHILD_IS_ALIAS
          '"{domain_child}" is not a zone. It is an alias for "{domain_target}". Run a test for "{domain_target}" instead. '
          . 'Returned from name servers "{ns_list}".', @_;
    },
    B01_CHILD_FOUND => sub {
        __x    # BASIC:B01_CHILD_FOUND
          'The zone "{domain}" is found.', @_;
    },
    B01_INCONSISTENT_ALIAS => sub {
        __x    # BASIC:B01_INCONSISTENT_ALIAS
          'The alias for "{domain}" is inconsistent between name servers.', @_;
    },
    B01_INCONSISTENT_DELEGATION => sub {
        __x    # BASIC:B01_INCONSISTENT_DELEGATION
          'The name servers for parent zone "{domain_parent}" give inconsistent delegation of "{domain_child}". '
          . 'Returned from name servers "{ns_list}".', @_;
    },
    B01_NO_CHILD => sub {
        __x    # BASIC:B01_NO_CHILD
          '"{domain_child}" does not exist as a DNS zone. Try to test "{domain_super}" instead.', @_;
    },
    B01_PARENT_DISREGARDED  => sub {
        __x    # BASIC:B01_PARENT_DISREGARDED
          'This is a test of an undelegated domain so finding the parent zone is disregarded.';
    },
    B01_PARENT_FOUND => sub {
        __x    # BASIC:B01_PARENT_FOUND
          'The parent zone is "{domain}" as returned from name servers "{ns_list}".', @_;
    },
    B01_PARENT_NOT_FOUND => sub {
        __x    # BASIC:B01_PARENT_NOT_FOUND
          'The parent zone cannot be found.';
    },
    B01_PARENT_UNDETERMINED => sub {
        __x    # BASIC:B01_PARENT_UNDETERMINED
          'The parent zone cannot be determined on name servers "{ns_list}".', @_;
    },
    B01_ROOT_HAS_NO_PARENT => sub {
        __x    # BASIC:B01_ROOT_HAS_NO_PARENT
          'This is a test of the root zone which has no parent zone.';
    },
    B01_SERVER_ZONE_ERROR => sub {
        __x    # BASIC:B01_SERVER_ZONE_ERROR
          'Unexpected response on query for "{query_name}" with query type "{rrtype}" to "{ns}".', @_;
    },
    B02_AUTH_RESPONSE_SOA => sub {
        __x    # BASIC:B02_AUTH_RESPONSE_SOA
          'Authoritative answer on SOA query for "{domain}" is returned by name servers "{ns_list}".', @_;
    },
    B02_NO_DELEGATION => sub {
        __x    # BASIC:B02_NO_DELEGATION
          'There is no delegation (name servers) for "{domain}" which means it does not exist as a zone.', @_;
    },
    B02_NO_WORKING_NS => sub {
        __x    # BASIC:B02_NO_WORKING_NS
          'There is no working name server for "{domain}" so it is unreachable.', @_;
    },
    B02_NS_BROKEN => sub {
        __x    # BASIC:B02_NS_BROKEN
          'Broken response from name server "{ns}" on an SOA query.', @_;
    },
    B02_NS_NOT_AUTH => sub {
        __x    # BASIC:B02_NS_NOT_AUTH
          'Name server "{ns}" does not give an authoritative answer on an SOA query.', @_;
    },
    B02_NS_NO_IP_ADDR => sub {
        __x    # BASIC:B02_NS_NO_IP_ADDR
          'Name server "{nsname}" cannot be resolved into an IP address.', @_;
    },
    B02_NS_NO_RESPONSE => sub {
        __x    # BASIC:B02_NS_NO_RESPONSE
          'Name server "{ns}" does not respond to an SOA query.', @_;
    },
    B02_UNEXPECTED_RCODE => sub {
        __x    # BASIC:B02_UNEXPECTED_RCODE
          'Name server "{ns}" responds with an unexpected RCODE name ("{rcode}") on an SOA query.', @_;
    },
    DOMAIN_NAME_LABEL_TOO_LONG => sub {
        __x    # BASIC:DOMAIN_NAME_LABEL_TOO_LONG
          'Domain name ({domain}) has a label ({label}) too long ({dlength}/{max}).', @_;
    },
    DOMAIN_NAME_TOO_LONG => sub {
        __x    # BASIC:DOMAIN_NAME_TOO_LONG
          'Domain name is too long ({fqdnlength}/{max}).', @_;
    },
    DOMAIN_NAME_ZERO_LENGTH_LABEL => sub {
        __x    # BASIC:DOMAIN_NAME_ZERO_LENGTH_LABEL
          'Domain name ({domain}) has a zero-length label.', @_;
    },
    HAS_A_RECORDS => sub {
        __x    # BASIC:HAS_A_RECORDS
          'Nameserver {ns} returned "A" record(s) for {domain}.', @_;
    },
    HAS_NAMESERVER_NO_WWW_A_TEST => sub {
        __x    # BASIC:HAS_NAMESERVER_NO_WWW_A_TEST
          'Functional nameserver found. "A" query for www.{zname} test skipped.', @_;
    },
    IPV4_DISABLED => sub {
        __x    # BASIC:IPV4_DISABLED
          'IPv4 is disabled, not sending "{rrtype}" query to {ns}.', @_;
    },
    IPV4_ENABLED => sub {
        __x    # BASIC:IPV4_ENABLED
          'IPv4 is enabled, can send "{rrtype}" query to {ns}.', @_;
    },
    IPV6_DISABLED => sub {
        __x    # BASIC:IPV6_DISABLED
          'IPv6 is disabled, not sending "{rrtype}" query to {ns}.', @_;
    },
    IPV6_ENABLED => sub {
        __x    # BASIC:IPV6_ENABLED
          'IPv6 is enabled, can send "{rrtype}" query to {ns}.', @_;
    },
    NO_A_RECORDS => sub {
        __x    # BASIC:NO_A_RECORDS
          'Nameserver {ns} did not return "A" record(s) for {domain}.', @_;
    },
    TEST_CASE_END => sub {
        __x    # BASIC:TEST_CASE_END
          'TEST_CASE_END {testcase}.', @_;
    },
    TEST_CASE_START => sub {
        __x    # BASIC:TEST_CASE_START
          'TEST_CASE_START {testcase}.', @_;
    },
);

=over

=item tag_descriptions()

    my $hash_ref = tag_descriptions();

Used by the L<built-in translation system|Zonemaster::Engine::Translator>.

Returns a reference to a hash, the keys of which are the message tags and the corresponding values are strings (message IDs).

=back

=cut

sub tag_descriptions {
    return \%TAG_DESCRIPTIONS;
}

=over

=item version()

    my $version_string = version();

Returns a string containing the version of the current module.

=back

=cut

sub version {
    return "$Zonemaster::Engine::Test::Basic::VERSION";
}

=head1 INTERNAL METHODS

=over

=item _emit_log()

    my $log_entry = _emit_log( $message_tag_string, $hash_ref );

Adds a message to the L<logger|Zonemaster::Engine::Logger> for this module.
See L<Zonemaster::Engine::Logger::Entry/add($tag, $argref, $module, $testcase)> for more details.

Takes a string (message tag) and a reference to a hash (arguments).

Returns a L<Zonemaster::Engine::Logger::Entry> object.

=back

=cut

sub _emit_log { my ( $tag, $argref ) = @_; return Zonemaster::Engine->logger->add( $tag, $argref, 'Basic' ); }

=over

=item _ip_disabled_message()

    my $bool = _ip_disabled_message( $logentry_array_ref, $ns, @query_type_array );

Checks if the IP version of a given name server is allowed to be queried. If not, it adds a logging message and returns true. Else, it returns false.
Used in Test Cases in combination with L<_ip_enabled_message()>.

Takes a reference to an array of L<Zonemaster::Engine::Logger::Entry> objects, a L<Zonemaster::Engine::Nameserver> object and an array of strings (query type).

Returns a boolean.

=back

=cut

sub _ip_disabled_message {
    my ( $results_array, $ns, @rrtypes ) = @_;

    if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv4}) and $ns->address->version == $IP_VERSION_4 ) {
        push @$results_array, map {
          _emit_log(
            IPV4_DISABLED => {
                ns     => $ns->string,
                rrtype => $_,
            }
          )
        } @rrtypes;
        return 1;
    }

    if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv6}) and $ns->address->version == $IP_VERSION_6 ) {
        push @$results_array, map {
          _emit_log(
            IPV6_DISABLED => {
                ns     => $ns->string,
                rrtype => $_,
            }
          )
        } @rrtypes;
        return 1;
    }
    return 0;
}

=over

=item _ip_enabled_message()

    _ip_enabled_message( $array_ref, $ns, @query_type_array );

Adds a logging message if the IP version of a given name server is allowed to be queried.
Used in Test Cases in combination with L<_ip_disabled_message()>.

Takes a reference to an array of L<Zonemaster::Engine::Logger::Entry> objects, a L<Zonemaster::Engine::Nameserver> object and an array of strings (query type).

=back

=cut

sub _ip_enabled_message {
    my ( $results_array, $ns, @rrtypes ) = @_;

    if ( Zonemaster::Engine::Profile->effective->get(q{net.ipv4}) and $ns->address->version == $IP_VERSION_4 ) {
        push @$results_array, map {
          _emit_log(
            IPV4_ENABLED => {
                ns     => $ns->string,
                rrtype => $_,
            }
          )
        } @rrtypes;
    }

    if ( Zonemaster::Engine::Profile->effective->get(q{net.ipv6}) and $ns->address->version == $IP_VERSION_6 ) {
        push @$results_array, map {
          _emit_log(
            IPV6_ENABLED => {
                ns     => $ns->string,
                rrtype => $_,
            }
          )
        } @rrtypes;
    }
}

=head1 TESTS

=over

=item basic01()

    my @logentry_array = basic01( $zone );

Runs the L<Basic01 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/Basic-TP/basic01.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub basic01 {
    my ( $class, $zone ) = @_;
    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'Basic01';

    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    if ( $zone->name eq '.' ) {
        push @results,
          _emit_log(
             B01_CHILD_FOUND => {
                domain => $zone->name->string
             }
          );

        push @results,
          _emit_log(
             B01_ROOT_HAS_NO_PARENT => {}
          );

        return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
    }

    if ( Zonemaster::Engine::Recursor->has_fake_addresses( $zone->name->string ) ) {
        push @results,
          _emit_log(
              B01_CHILD_FOUND => {
                domain => $zone->name->string
              }
          );

        push @results,
          _emit_log(
              B01_PARENT_DISREGARDED => {}
          );

        return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
    }

    my %handled_servers;
    my %parent_found;
    my %delegation_found;
    my %aa_nxdomain;
    my %aa_soa;
    my %aa_cname;
    my %cname_with_referral;
    my %aa_dname;
    my %aa_nodata;

    my %rrs_ns;
    my $type_soa = q{SOA};
    my $type_ns = q{NS};
    my $type_dname = q{DNAME};

    my %all_servers = ( '.' => [ Zonemaster::Engine::Recursor->root_servers ] );
    my @all_labels = ( '.' );
    my @remaining_labels = ( '.' );

    while ( my $zone_name = shift @remaining_labels ) {
        my @remaining_servers = @{ $all_servers{$zone_name} };

        CUR_SERVERS:
        while ( my $ns = shift @remaining_servers ) {
            next CUR_SERVERS if grep { $_ eq $ns->address->short } @{ $handled_servers{$zone_name} };
            push @{ $handled_servers{$zone_name} }, $ns->address->short;

            if ( _ip_disabled_message( \@results, $ns, ( $type_soa, $type_ns, $type_dname ) ) ) {
                next CUR_SERVERS;
            }
            _ip_enabled_message( \@results, $ns, ( $type_soa, $type_ns, $type_dname ) );

            my $p_soa = $ns->query( $zone_name, $type_soa );

            unless ( $p_soa and $p_soa->rcode eq 'NOERROR' and $p_soa->aa and scalar $p_soa->get_records_for_name( $type_soa, $zone_name, 'answer' ) == 1 ) {
                push @results,
                  _emit_log(
                        B01_SERVER_ZONE_ERROR => {
                            query_name => $zone_name,
                            rrtype => $type_soa,
                            ns => $ns->string
                        }
                  );

                next CUR_SERVERS;
            }

            my $p_ns = $ns->query( $zone_name, $type_ns );

            unless ( $p_ns and $p_ns->rcode eq 'NOERROR' and $p_ns->aa and scalar $p_ns->get_records( $type_ns, 'answer' ) > 0
                and scalar $p_ns->get_records( $type_ns, 'answer' ) == scalar $p_ns->get_records_for_name( $type_ns, $zone_name, 'answer' )
            ) {
                push @results,
                  _emit_log(
                        B01_SERVER_ZONE_ERROR => {
                            query_name => $zone_name,
                            rrtype => $type_ns,
                            ns => $ns->string
                        }
                  );

                next CUR_SERVERS;
            }

            $rrs_ns{name( $_->nsdname )->string} = [] for $p_ns->get_records_for_name( $type_ns, $zone_name, 'answer' );

            foreach my $rr ( $p_ns->get_records( 'A', 'additional' ), $p_ns->get_records( 'AAAA', 'additional' ) ) {
                if ( exists $rrs_ns{name( $rr->owner )->string} ) {
                    push @{ $rrs_ns{name( $rr->owner )->string} }, $rr->address;
                }
            }

            foreach my $ns_name ( keys %rrs_ns ) {
                unless ( scalar @{ $rrs_ns{$ns_name} } ) {
                    my $p_a = Zonemaster::Engine::Recursor->recurse( $ns_name, q{A} );

                    if ( $p_a and $p_a->rcode eq 'NOERROR' ) {
                        push @{ $rrs_ns{$ns_name} }, $_->address for $p_a->get_records_for_name( 'A', $ns_name );
                    }

                    my $p_aaaa = Zonemaster::Engine::Recursor->recurse( $ns_name, q{AAAA} );

                    if ( $p_aaaa and $p_aaaa->rcode eq 'NOERROR' ) {
                        push @{ $rrs_ns{$ns_name} }, $_->address for $p_aaaa->get_records_for_name( 'AAAA', $ns_name );
                    }
                }

                foreach my $ns_ip ( @{ $rrs_ns{$ns_name} } ) {
                    unless ( grep { $_ eq $ns_ip } @{ $handled_servers{$zone_name} } ) {
                        push @{ $all_servers{$zone_name} }, ns( $ns_name, $ns_ip );

                        unless ( grep { $_ eq $zone_name } @all_labels ) {
                            push @remaining_labels, $zone_name;
                            push @all_labels, $zone_name;
                        }
                    }
                }
            }

            my $intermediate_query_name = name( $zone_name );
            my $loop_zone_name = $zone_name;
            my $loop_counter = 0;

            LOOP:
            while() {
                $loop_counter += 1;
                if ( $loop_counter >= 1000 ) {
                    Zonemaster::Engine->logger->add( LOOP_PROTECTION => {
                        caller => 'Zonemaster::Engine::Test::Basic->basic01',
                        child_zone_name => $zone->name,
                        name => $loop_zone_name,
                        intermediate_query_name => $intermediate_query_name
                      }
                    );

                    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
                }

                last if scalar @{ $intermediate_query_name->labels } >= scalar @{ $zone->name->labels };
                $intermediate_query_name = name( @{ $zone->name->labels }[ ( scalar @{ $zone->name->labels } - scalar @{ $intermediate_query_name->labels } ) - 1 ] . '.' . $intermediate_query_name->string );

                $p_soa = $ns->query( $intermediate_query_name, $type_soa );

                unless ( $p_soa ) {
                    push @results,
                      _emit_log(
                            B01_SERVER_ZONE_ERROR => {
                                query_name => $intermediate_query_name,
                                rrtype => $type_soa,
                                ns => $ns->string
                            }
                      );

                    next CUR_SERVERS;
                }

                if ( $p_soa->rcode eq 'NOERROR' and $p_soa->aa and scalar $p_soa->get_records_for_name( $type_soa, $intermediate_query_name, 'answer' ) == 1 ) {
                    if ( $intermediate_query_name->string eq $zone->name->string ) {
                        push @{ $parent_found{$loop_zone_name} }, $ns->string;
                        push @{ $aa_soa{$loop_zone_name} }, $ns->string;
                    }
                    else {
                        $p_ns = $ns->query( $intermediate_query_name, $type_ns );

                        unless ( $p_ns and $p_ns->rcode eq 'NOERROR' and $p_ns->aa and scalar $p_ns->get_records( $type_ns, 'answer' ) > 0
                            and scalar $p_ns->get_records( $type_ns, 'answer' ) == scalar $p_ns->get_records_for_name( $type_ns, $intermediate_query_name, 'answer' )
                        ) {
                            push @results,
                              _emit_log(
                                    B01_SERVER_ZONE_ERROR => {
                                        query_name => $intermediate_query_name,
                                        rrtype => $type_ns,
                                        ns => $ns->string
                                    }
                              );

                            next CUR_SERVERS;
                        }

                        my %rrs_ns_bis;
                        $rrs_ns_bis{name( $_->nsdname )->string} = [] for $p_ns->get_records_for_name( $type_ns, $intermediate_query_name, 'answer' );

                        foreach my $rr ( $p_ns->get_records( 'A', 'additional' ), $p_ns->get_records( 'AAAA', 'additional' ) ) {
                            if ( exists $rrs_ns_bis{name( $rr->owner )->string} ) {
                                push @{ $rrs_ns_bis{name( $rr->owner )->string} }, $rr->address;
                            }
                        }

                        foreach my $ns_name ( keys %rrs_ns_bis ) {
                            unless ( scalar @{ $rrs_ns_bis{$ns_name} } > 0 ) {
                                my $p_a = Zonemaster::Engine::Recursor->recurse( $ns_name, q{A} );

                                if ( $p_a and $p_a->rcode eq 'NOERROR' ) {
                                    push @{ $rrs_ns_bis{$ns_name} }, $_->address for $p_a->get_records_for_name( 'A', $ns_name );
                                }

                                my $p_aaaa = Zonemaster::Engine::Recursor->recurse( $ns_name, q{AAAA} );

                                if ( $p_aaaa and $p_aaaa->rcode eq 'NOERROR' ) {
                                    push @{ $rrs_ns_bis{$ns_name} }, $_->address for $p_aaaa->get_records_for_name( 'AAAA', $ns_name );
                                }
                            }

                            foreach my $ns_ip ( @{ $rrs_ns_bis{$ns_name} } ) {
                                unless ( grep { $_ eq $ns_ip } @{ $handled_servers{$intermediate_query_name} } ) {
                                    push @{ $all_servers{$intermediate_query_name} }, ns( $ns_name, $ns_ip );

                                    unless ( grep { $_ eq $intermediate_query_name } @all_labels ) {
                                        push @remaining_labels, $intermediate_query_name;
                                        push @all_labels, $intermediate_query_name;
                                    }
                                }
                            }
                        }

                        $loop_zone_name = $intermediate_query_name->string;
                        next LOOP;
                    }
                }
                elsif ( $p_soa->rcode eq 'NXDOMAIN' and $p_soa->aa ) {
                    push @{ $parent_found{$loop_zone_name} }, $ns->string;
                    push @{ $aa_nxdomain{$loop_zone_name} }, $ns->string;
                }
                elsif ( $p_soa->is_redirect and scalar $p_soa->get_records_for_name( $type_ns, $intermediate_query_name, 'authority' ) ) {
                    if ( $intermediate_query_name->string eq $zone->name->string ) {
                        push @{ $parent_found{$loop_zone_name} }, $ns->string;
                        push @{ $delegation_found{$loop_zone_name} }, $ns->string;
                    }
                    else {
                        my %rrs_ns_bis;
                        $rrs_ns_bis{name( $_->nsdname )->string} = [] for $p_soa->get_records_for_name( $type_ns, $intermediate_query_name, 'authority' );

                        foreach my $rr ( $p_soa->get_records( 'A', 'additional' ), $p_soa->get_records( 'AAAA', 'additional' ) ) {
                            if ( exists $rrs_ns_bis{name( $rr->owner )->string} ) {
                                push @{ $rrs_ns_bis{name( $rr->owner )->string} }, $rr->address;
                            }
                        }

                        foreach my $ns_name ( keys %rrs_ns_bis ) {
                            unless ( scalar @{ $rrs_ns_bis{$ns_name} } > 0 ) {
                                my $p_a = Zonemaster::Engine::Recursor->recurse( $ns_name, q{A} );

                                if ( $p_a and $p_a->rcode eq 'NOERROR' ) {
                                    push @{ $rrs_ns_bis{$ns_name} }, $_->address for $p_a->get_records_for_name( 'A', $ns_name );
                                }

                                my $p_aaaa = Zonemaster::Engine::Recursor->recurse( $ns_name, q{AAAA} );

                                if ( $p_aaaa and $p_aaaa->rcode eq 'NOERROR' ) {
                                    push @{ $rrs_ns_bis{$ns_name} }, $_->address for $p_aaaa->get_records_for_name( 'AAAA', $ns_name );
                                }
                            }

                            foreach my $ns_ip ( @{ $rrs_ns_bis{$ns_name} } ) {
                                unless ( grep { $_ eq $ns_ip } @{ $handled_servers{$intermediate_query_name} } ) {
                                    push @{ $all_servers{$intermediate_query_name} }, ns( $ns_name, $ns_ip );

                                    unless ( grep { $_ eq $intermediate_query_name } @all_labels ) {
                                        push @remaining_labels, $intermediate_query_name;
                                        push @all_labels, $intermediate_query_name;
                                    }
                                }
                            }
                        }
                    }
                }
                elsif ( $p_soa->rcode eq 'NOERROR' and $p_soa->aa ) {
                    next LOOP if $intermediate_query_name->string ne $zone->name->string;

                    if ( scalar $p_soa->get_records_for_name( 'CNAME', $zone->name, 'answer' ) ) {
                        push @{ $parent_found{$loop_zone_name} }, $ns->string;
                        push @{ $aa_cname{$loop_zone_name} }, $ns->string;
                    }
                    else {
                        my $p_dname = $ns->query( $zone->name->string , $type_dname );

                        if ( $p_dname and $p_dname->aa and $p_dname->rcode eq 'NOERROR' and scalar $p_dname->get_records_for_name( $type_dname, $zone->name, 'answer' ) == 1 ) {
                            push @{ $parent_found{$loop_zone_name} }, $ns->string;
                            push @{ $aa_dname{($p_dname->get_records_for_name( $type_dname, $zone->name, 'answer' ))[0]->dname}{$loop_zone_name} }, $ns->string;
                        }
                        else {
                            push @{ $parent_found{$loop_zone_name} }, $ns->string;
                            push @{ $aa_nodata{$loop_zone_name} }, $ns->string;
                        }
                    }
                }
                elsif ( $p_soa->is_redirect and scalar $p_soa->get_records_for_name( 'CNAME', $zone->name, 'answer' ) ) {
                    push @{ $parent_found{$loop_zone_name} }, $ns->string;
                    push @{ $cname_with_referral{$loop_zone_name} }, $ns->string;
                }
                else {
                    push @results,
                      _emit_log(
                            B01_SERVER_ZONE_ERROR => {
                                query_name => $intermediate_query_name,
                                rrtype => $type_soa,
                                ns => $ns->string
                            }
                      );
                }

                next CUR_SERVERS;
            }
        }
    }

    if ( scalar keys %parent_found ) {
        push @results, map {
          _emit_log(
              B01_PARENT_FOUND => {
                domain => $_,
                ns_list => join( q{;}, uniq sort @{ $parent_found{$_} } )
              }
          )
        } keys %parent_found;

        if ( scalar keys %parent_found > 1 ) {
          push @results,
            _emit_log(
                B01_PARENT_UNDETERMINED => {
                  ns_list => join( q{;}, uniq sort map { @{ $parent_found{$_} } } keys %parent_found )
                }
            );
        }
    }
    else {
        push @results,
          _emit_log(
              B01_PARENT_NOT_FOUND => {}
          );
    }

    if ( scalar keys %delegation_found or scalar keys %aa_soa ) {
        push @results,
          _emit_log(
              B01_CHILD_FOUND => {
                domain => $zone->name->string
              }
          );

        unless ( Zonemaster::Engine::Recursor->has_fake_addresses( $zone->name->string ) ) {
            my @hash_refs = ( \%aa_nxdomain, \%aa_cname, \%cname_with_referral, values %aa_dname, \%aa_nodata );
            foreach my $parent_domain ( uniq map { keys %$_ } @hash_refs ) {
                push @results,
                  _emit_log(
                      B01_INCONSISTENT_DELEGATION => {
                        domain_parent => $parent_domain,
                        domain_child => $zone->name->string,
                        ns_list => join( q{;}, uniq sort map { @{ $_->{$parent_domain} // [] } } @hash_refs )
                      }
                  );
            }
        }
    }

    if ( not scalar keys %delegation_found and not scalar keys %aa_soa ) {
        if ( Zonemaster::Engine::Recursor->has_fake_addresses( $zone->name->string ) ) {
            push @results,
              _emit_log(
                  B01_CHILD_NOT_EXIST => {
                    domain => $zone->name->string
                  }
              );
        }
        else {
            push @results,
              _emit_log(
                  B01_NO_CHILD => {
                    domain_child => $zone->name->string,
                    domain_super => $zone->name->next_higher
                  }
              );
        }
    }

    if ( scalar keys %aa_dname ) {
        push @results, map { my $target = $_;
          _emit_log(
              B01_CHILD_IS_ALIAS => {
                domain_child => $zone->name->string,
                domain_target => $target,
                ns_list => join( q{;}, uniq sort map { @{ $aa_dname{$target}{$_} } } keys %{ $aa_dname{$target} } )
              }
          )
        } keys %aa_dname;

        if ( scalar keys %aa_dname > 1 ) {
            push @results,
              _emit_log(
                  B01_INCONSISTENT_ALIAS => {
                    domain => $zone->name->string
                  }
              );
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub basic01

=over

=item basic02()

    my @logentry_array = basic02( $zone );

Runs the L<Basic02 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/Basic-TP/basic02.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub basic02 {
    my ( $class, $zone ) = @_;
    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'Basic02';

    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my $query_type = q{SOA};

    my %auth_response_soa;
    my %ns_broken;
    my %ns_not_auth;
    my %ns_cant_resolve;
    my %ns_no_response;
    my %unexpected_rcode;

    my @ns_names = @{ Zonemaster::Engine::TestMethods->method2( $zone ) };
    my @ns = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };

    if ( not scalar @ns_names ) {
        push @results,
            _emit_log(
                B02_NO_DELEGATION => {
                    domain => $zone->name
                }
            );

        return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
    }

    if ( not scalar @ns ) {
        my %found_ip;
        my @ns_ips = @{ $zone->glue_addresses };

        foreach my $ns_name ( @ns_names ) {
            $found_ip{$ns_name->string} = 0;

            foreach my $rr ( @ns_ips ) {
                if ( $rr->owner eq $ns_name ) {
                    $found_ip{$ns_name->string} = 1;
                    push @ns, Zonemaster::Engine::Nameserver->new({ name => $ns_name, address => $rr->address });
                }
            }
        }

        foreach my $ns_name ( keys %found_ip ) {
            if ( $found_ip{$ns_name} == 0 ) {
                $ns_cant_resolve{$ns_name} = 1;
            }
        }
    }

    foreach my $ns ( @ns ) {
        if ( _ip_disabled_message( \@results, $ns, $query_type ) ) {
            next;
        }
        _ip_enabled_message( \@results, $ns, $query_type );

        my $p = $ns->query( $zone->name, $query_type );

        if ( $p ) {
            if ( $p->rcode ne 'NOERROR' ) {
                $unexpected_rcode{$ns->string} = $p->rcode;
            }
            elsif ( not $p->aa ) {
                $ns_not_auth{$ns->string} = 1;
            }
            else {
                if ( $p->get_records_for_name( $query_type, $zone->name, q{answer} ) ) {
                    $auth_response_soa{$ns->string} = 1;
                }
                else {
                    $ns_broken{$ns->string} = 1;
                }
            }
        }
        else {
            $ns_no_response{$ns->string} = 1;
        }
    }

    if ( scalar keys %auth_response_soa ) {
        push @results,
            _emit_log(
                B02_AUTH_RESPONSE_SOA => {
                    domain => $zone->name,
                    ns_list => join( q{;}, sort keys %auth_response_soa )
                }
            );
    }
    else {
        push @results,
            _emit_log(
                B02_NO_WORKING_NS => {
                    domain => $zone->name
                }
            );

        if ( scalar keys %ns_broken ) {
            push @results, map {
                _emit_log(
                    B02_NS_BROKEN => {
                        ns => $_
                    }
                )
            } keys %ns_broken;
        }

        if ( scalar keys %ns_not_auth ) {
            push @results, map {
                _emit_log(
                    B02_NS_NOT_AUTH => {
                        ns => $_
                    }
                )
            } keys %ns_not_auth;
        }

        if ( scalar keys %ns_cant_resolve ) {
            push @results, map {
                _emit_log(
                    B02_NS_NO_IP_ADDR => {
                        nsname => $_
                    }
                )
            } keys %ns_cant_resolve;
        }

        if ( scalar keys %ns_no_response ) {
            push @results, map {
                _emit_log(
                    B02_NS_NO_RESPONSE => {
                        ns => $_
                    }
                )
            } keys %ns_no_response;
        }

        if ( scalar keys %unexpected_rcode ) {
            push @results, map {
                _emit_log(
                    B02_UNEXPECTED_RCODE => {
                        rcode => $unexpected_rcode{$_},
                        ns => $_
                    }
                )
            } keys %unexpected_rcode;
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub basic02

=over

=item basic03()

    my @logentry_array = basic03( $zone );

Runs the L<Basic03 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/Basic-TP/basic03.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub basic03 {
    my ( $class, $zone ) = @_;
    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'Basic03';

    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my $query_type = q{A};

    my $name        = q{www.} . $zone->name;
    my $response_nb = 0;
    foreach my $ns ( @{ Zonemaster::Engine::TestMethods->method4( $zone ) } ) {
        if ( _ip_disabled_message( \@results, $ns, $query_type ) ) {
            next;
        }
        _ip_enabled_message( \@results, $ns, $query_type );

        my $p = $ns->query( $name, $query_type );
        next if not $p;
        $response_nb++;
        if ( $p->has_rrs_of_type_for_name( $query_type, $name ) ) {
            push @results,
              _emit_log(
                HAS_A_RECORDS => {
                    ns     => $ns->string,
                    domain => $name,
                }
              );
        }
        else {
            push @results,
              _emit_log(
                NO_A_RECORDS => {
                    ns     => $ns->string,
                    domain => $name,
                }
              );
        }
    } ## end foreach my $ns ( @{ Zonemaster::Engine::TestMethods...})

    if ( scalar( @{ Zonemaster::Engine::TestMethods->method4( $zone ) } ) and not $response_nb ) {
        push @results, _emit_log( A_QUERY_NO_RESPONSES => {} );
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub basic03

1;
