# Copyright 2020 Google LLC
# Modifications Copyright 2022-2023 Aerleon Project Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Unittest for junipermsmpc acl rendering module."""

import datetime
import re
import textwrap
from unittest import mock

from absl.testing import absltest, parameterized

from aerleon.lib import junipermsmpc, nacaddr, naming, policy
from tests.regression_utils import capture

GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter inet
}
"""
GOOD_HEADER_V6 = """
header {
  target:: msmpc test-filter inet6
}
"""
GOOD_HEADER_MIXED = """
header {
  target:: msmpc test-filter mixed
}
"""
GOOD_HEADER_MIXED_IMPLICIT = """
header {
  target:: msmpc test-filter
}
"""
GOOD_NOVERBOSE_V4_HEADER = """
header {
  target:: msmpc test-filter noverbose inet
}
"""
GOOD_NOVERBOSE_V6_HEADER = """
header {
  target:: msmpc test-filter inet6 noverbose
}
"""
GOOD_HEADER_INGRESS = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter ingress inet
}
"""
GOOD_HEADER_EGRESS = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter egress inet
}
"""
BAD_HEADER_DIRECTION = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter ingress egress inet
}
"""
BAD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: cisco test-filter
}
"""
BAD_HEADER_2 = """
header {
  target:: msmpc test-filter inetpoop
}
"""
BAD_HEADER_3 = """
header {
  target:: msmpc test-filter inet inet6
}
"""
EXPIRED_TERM = """
term is_expired {
  expiration:: 2001-01-01
  action:: accept
}
"""
EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}

term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_1_V6 = """
term good-term-1 {
  protocol:: icmpv6
  action:: accept
}

term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: icmp
  icmp-type:: echo-reply information-reply information-request
  icmp-type:: router-solicitation timestamp-request
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  protocol:: icmp
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_8 = """
term good-term-8 {
  source-prefix:: foo_prefix_list
  destination-prefix:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  verbatim:: msmpc "mary had a little lamb"
  verbatim:: iptables "mary had a second lamb"
  verbatim:: cisco "mary had a third lamb"
  verbatim:: juniper "mary had a fourth lamb"
}
"""
GOOD_TERM_17 = """
term owner-term {
  owner:: foo@google.com
  action:: accept
}
"""
GOOD_TERM_18_SRC = """
term address-exclusions {
  source-address:: INTERNAL
  source-exclude:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_18_DST = """
term address-exclusions {
  destination-address:: INTERNAL
  destination-exclude:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_19 = """
term minimize-prefix-list {
  source-address:: INCLUDES
  source-exclude:: EXCLUDES
  action:: accept
}
"""
GOOD_TERM_25 = """
term good_term_25 {
  protocol:: tcp
  source-port:: DNS
  action:: accept
}
"""
GOOD_TERM_26 = """
term good_term_26 {
  protocol:: tcp
  source-port:: DNS
  action:: deny
}
"""
GOOD_TERM_26_V6 = """
term good_term_26-v6 {
  protocol:: tcp
  source-port:: DNS
  action:: deny
}
"""
GOOD_TERM_26_V6_REJECT = """
term good_term_26-v6 {
  protocol:: tcp
  source-port:: DNS
  action:: reject
}
"""
GOOD_TERM_30 = """
term good-term-30 {
  source-prefix-except:: foo_prefix_list
  destination-prefix-except:: bar_prefix_list
  action:: accept
}
"""
GOOD_TERM_31 = """
term good-term-31 {
  source-prefix:: foo_prefix
  source-prefix-except:: foo_except
  destination-prefix:: bar_prefix
  destination-prefix-except:: bar_except
  action:: accept
}
"""
GOOD_TERM_35 = """
term good_term_35 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
GOOD_TERM_36 = """
term good-term-36 {
  protocol:: tcp
  destination-address:: SOME_HOST
  destination-address:: SOME_HOST
  option:: inactive
  action:: accept
}
"""
GOOD_TERM_NUMERIC_PROTOCOL = """
term good-term-numeric {
  protocol:: %s
  action:: accept
}
"""
GOOD_TERM_COMMENT = """
term good-term-comment {
  comment:: "This is a COMMENT"
  action:: accept
}
"""
ESTABLISHED_TERM_1 = """
term established-term-1 {
  protocol:: tcp
  source-port:: DNS
  option:: established
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_1 = """
term icmptype-mismatch {
  comment:: "error when icmpv6 paired with inet filter"
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_2 = """
term icmptype-mismatch {
  comment:: "error when icmp paired with inet6 filter"
  protocol:: icmp
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
DEFAULT_TERM_1 = """
term default-term-1 {
  action:: deny
}
"""
RANGE_PORTS_TERM = """
term ranged-ports-1 {
  protocol:: udp
  destination-port:: BOOTPS
  destination-port:: BOOTPC
  action:: accept
}
"""
MIXED_TESTING_TERM = """
term good-term {
  protocol:: tcp
  source-address:: SOME_HOST
  destination-port:: SMTP
  destination-address:: SOME_OTHER_HOST
  action:: accept
}
"""

MIXED_TESTING_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

term good-term-icmp-2 {
  protocol:: icmp
  action:: accept
}

term good-term-icmpv6 {
  protocol:: icmpv6
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

term good-term-icmpv6-2 {
  protocol:: icmpv6
  action:: accept
}

term good-term-both-icmp-and-icmpv6 {
  protocol:: icmp
  protocol:: icmpv6
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

term good-term-both-icmp-and-icmpv6-2 {
  protocol:: icmp
  protocol:: icmpv6
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

"""
LOGGING_TERM = """
term good-term-1 {
  protocol:: icmp
  action:: accept
  logging:: %s
}
"""
TERM_NAME_COLLISION = """
term good-term-1%s {
  protocol:: icmp
  action:: accept
}

term hood-term-1%s {
  protocol:: tcp
  action:: accept
}
"""

SUPPORTED_TOKENS = frozenset(
    [
        'action',
        'comment',
        'destination_address',
        'destination_address_exclude',
        'destination_port',
        'destination_prefix',
        'destination_prefix_except',
        'expiration',
        'icmp_code',
        'icmp_type',
        'stateless_reply',
        'logging',
        'name',
        'option',
        'owner',
        'platform',
        'platform_exclude',
        'protocol',
        'source_address',
        'source_address_exclude',
        'source_port',
        'source_prefix',
        'source_prefix_except',
        'translated',
        'verbatim',
    ]
)

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {
        'established',
        'inactive',
        '.*',  # not actually a lex token!
        'tcp-established',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class JuniperMSMPCTest(parameterized.TestCase):
    def setUp(self):
        super().setUp()
        self.naming = naming.Naming()

    @capture.stdout
    def testTermAndFilterName(self):
        self.naming._ParseLine('SOME_HOST = 10.0.0.0/8', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn('term good-term-1 {', output, output)
        self.assertIn('rule test-filter {', output, output)

        print(output)

    def testBadFilterType(self):
        self.naming._ParseLine('SOME_HOST = 10.0.0.0/8', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        pol = policy.ParsePolicy(BAD_HEADER_2 + GOOD_TERM_1, self.naming)
        self.assertRaises(
            junipermsmpc.UnsupportedHeaderError, junipermsmpc.JuniperMSMPC, pol, EXP_INFO
        )

    def testMultipleFilterType(self):
        self.naming._ParseLine('SOME_HOST = 10.0.0.0/8', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        pol = policy.ParsePolicy(BAD_HEADER_3 + GOOD_TERM_1, self.naming)
        self.assertRaises(
            junipermsmpc.ConflictingTargetOptionsError, junipermsmpc.JuniperMSMPC, pol, EXP_INFO
        )

    @capture.stdout
    def testMixedv4(self):
        self.naming._ParseLine('SOME_HOST = 192.168.0.0/24', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        expected = (
            '                    term good-term-2 {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                192.168.0.0/24;\n'
            + '                            }'
        )

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn(expected, output, output)
        print(output)

    @capture.stdout
    def testMixedv6(self):
        self.naming._ParseLine('SOME_HOST = 2001::/33', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        expected = (
            '                    term good-term-2 {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                2001::/33;\n'
            + '                            }'
        )

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn(expected, output, output)
        print(output)

    @capture.stdout
    def testMixedBoth(self):
        self.naming._ParseLine('SOME_HOST = 192.168.0.0/24 2001::/33', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        expectedv4 = (
            '                    term good-term-2-inet {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                192.168.0.0/24;\n'
            + '                            }'
        )
        expectedv6 = (
            '                    term good-term-2-inet6 {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                2001::/33;\n'
            + '                            }'
        )

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn(expectedv4, output, output)
        self.assertIn(expectedv6, output, output)
        print(output)

    @capture.stdout
    def testCommentShrinking(self):
        long_comment = ' this is a very descriptive comment ' * 10
        expected = (
            ' ' * 32
            + '/* this is a very descriptive comment  this\n'
            + ' ' * 33
            + '** is a very descriptive comment  this is a\n'
            + ' ' * 33
            + '** very descriptive comment  this is a very\n'
            + ' ' * 33
            + '** descripti */'
        )
        self.naming._ParseLine(f'SOME_HOST = 10.0.0.0/8 # {long_comment}', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn(expected, output, output)

        print(output)

    @capture.stdout
    def testDefaultDeny(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + DEFAULT_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertNotIn('from {', output, output)
        self.assertIn('discard;', output, output)
        print(output)

    @capture.stdout
    def testIcmpType(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3, self.naming), EXP_INFO
        )
        output = str(msmpc)
        # verify proper translation from policy icmp-type text to juniper-esque
        self.assertIn('icmp-type 0;', output, output)
        self.assertIn('icmp-type 15;', output, output)
        self.assertIn('icmp-type 10;', output, output)
        self.assertIn('icmp-type 13;', output, output)
        self.assertIn('icmp-type 16;', output, output)
        print(output)

    @capture.stdout
    def testIcmpCode(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_35, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn('icmp-code [ 3 4 ];', output, output)
        print(output)

    @capture.stdout
    def testInactiveTerm(self):
        self.naming._ParseLine(f'SOME_HOST = 10.0.0.0/8', 'networks')
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_36, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn('inactive: term good-term-36 {', output)
        print(output)

    @capture.stdout
    def testInet6(self):
        self.naming._ParseLine(f'SOME_HOST = 2001::/33', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_V6 + GOOD_TERM_1_V6, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertTrue('protocol icmp6;' in output and 'protocol tcp;' in output, output)

        print(output)

    @parameterized.named_parameters(
        dict(testcase_name='WithApplyGroups', apply_groups=True),
        dict(testcase_name='WithoutApplyGroups', apply_groups=False),
    )
    @capture.stdout
    def testProtocolCase(self, apply_groups):
        header = GOOD_HEADER
        if not apply_groups:
            header = re.sub(r'(target:: msmpc .+)', r'\1 no-apply-groups', header)
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(header + GOOD_TERM_5, self.naming), EXP_INFO
        )
        output = str(msmpc)
        expected_output = (
            '    application test-filtergood-term-5-app1 {\n'
            '        protocol icmp;\n'
            '    }\n'
            '    application test-filtergood-term-5-app2 {\n'
            '        protocol tcp;\n'
            '        destination-port 1-65535;\n'
            '    }\n'
        )
        if apply_groups:
            expected_output = textwrap.indent(expected_output, '        ')
        self.assertIn(expected_output, output, output)
        print(output)

    @capture.stdout
    def testPrefixList(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_8, self.naming), EXP_INFO
        )
        spfx_re = re.compile(r'source-prefix-list foo_prefix_list;')
        dpfx_re = re.compile(
            r'destination-prefix-list bar_prefix_list;\W+destination-prefix-list baz_prefix_list;'
        )
        output = str(msmpc)
        self.assertTrue(spfx_re.search(output), output)
        self.assertTrue(dpfx_re.search(output), output)
        print(output)

    @capture.stdout
    def testPrefixListExcept(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_30, self.naming), EXP_INFO
        )
        spfx_re = re.compile(r'source-prefix-list foo_prefix_list except;')
        dpfx_re = re.compile(r'destination-prefix-list bar_prefix_list except;')
        output = str(msmpc)
        self.assertTrue(spfx_re.search(output), output)
        self.assertTrue(dpfx_re.search(output), output)
        print(output)

    @capture.stdout
    def testPrefixListMixed(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_31, self.naming), EXP_INFO
        )
        spfx_re = re.compile(
            r'source-prefix-list foo_prefix;\W+' r'source-prefix-list foo_except except;'
        )
        dpfx_re = re.compile(
            r'destination-prefix-list bar_prefix;\W+' r'destination-prefix-list bar_except except;'
        )
        output = str(msmpc)
        self.assertTrue(spfx_re.search(output), output)
        self.assertTrue(dpfx_re.search(output), output)
        print(output)

    @capture.stdout
    def testVerbatimTerm(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_11, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn('mary had a little lamb', output, output)
        # check if other platforms verbatim shows up in output
        self.assertNotIn('mary had a second lamb', output, output)
        self.assertNotIn('mary had a third lamb', output, output)
        self.assertNotIn('mary had a fourth lamb', output, output)
        print(output)

    @capture.stdout
    def testAccept(self):
        self.naming._ParseLine('DNS = 53/tcp', 'services')

        policy_text = GOOD_HEADER + GOOD_TERM_25
        msmpc = junipermsmpc.JuniperMSMPC(policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
        output = str(msmpc)
        self.assertIn('then {', output, output)
        self.assertIn('accept;', output, output)

        print(output)

    @capture.stdout
    def testDiscardIPv4(self):
        self.naming._ParseLine('DNS = 53/tcp', 'services')

        policy_text = GOOD_HEADER + GOOD_TERM_26
        msmpc = junipermsmpc.JuniperMSMPC(policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
        output = str(msmpc)
        self.assertIn('then {', output, output)
        self.assertIn('discard;', output, output)

        print(output)

    @capture.stdout
    def testDiscardIPv6(self):
        self.naming._ParseLine('DNS = 53/tcp', 'services')

        policy_text = GOOD_HEADER_V6 + GOOD_TERM_26_V6
        msmpc = junipermsmpc.JuniperMSMPC(policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
        output = str(msmpc)
        self.assertIn('then {', output, output)
        self.assertIn('discard;', output, output)

        print(output)

    @capture.stdout
    def testRejectIPv6(self):
        self.naming._ParseLine('DNS = 53/tcp', 'services')

        policy_text = GOOD_HEADER_V6 + GOOD_TERM_26_V6_REJECT
        msmpc = junipermsmpc.JuniperMSMPC(policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
        output = str(msmpc)
        self.assertIn('then {', output, output)
        self.assertIn('reject;', output, output)

        print(output)

    @capture.stdout
    def testTcpEstablished(self):
        self.naming._ParseLine('DNS = 53/tcp', 'services')

        policy_text = GOOD_HEADER + ESTABLISHED_TERM_1
        msmpc = junipermsmpc.JuniperMSMPC(policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
        output = str(msmpc)
        self.assertNotIn('term established-term-1', output, output)
        self.assertNotIn('tcp-established', output, output)

        print(output)

    @capture.stdout
    def testStatelessReply(self):
        self.naming._ParseLine(f'SOME_HOST = 10.0.0.1/32', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        ret = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming)

        _, terms = ret.filters[0]
        for term in terms:
            if term.protocol[0] == 'icmp':
                term.stateless_reply = True

        msmpc = junipermsmpc.JuniperMSMPC(ret, EXP_INFO)

        output = str(msmpc)
        self.assertNotIn('term good-term-1 {', output, output)
        self.assertIn('term good-term-2 {', output, output)
        print(output)

    @capture.stdout
    def testNoVerboseV4(self):
        addr_list = list()
        for octet in range(0, 256):
            net = nacaddr.IP(f"192.168.{octet!s}.64/27")
            addr_list.append(str(net))
        self.naming._ParseLine(f'SOME_HOST = {" ".join(addr_list)}', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(
                GOOD_NOVERBOSE_V4_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT, self.naming
            ),
            EXP_INFO,
        )
        self.assertIn('192.168.0.64/27;', str(msmpc))
        self.assertNotIn('COMMENT', str(msmpc))
        print(msmpc)

    @capture.stdout
    def testNoVerboseV6(self):
        addr_list = list()
        for octet in range(0, 256):
            net = nacaddr.IPv6(f"2001:db8:1010:{octet!s}::64/64", strict=False)
            addr_list.append(str(net))
        self.naming._ParseLine(f'SOME_HOST = {" ".join(addr_list)}', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(
                GOOD_NOVERBOSE_V6_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT, self.naming
            ),
            EXP_INFO,
        )
        self.assertIn('2001:db8:1010:90::/61;', str(msmpc))
        self.assertNotIn('COMMENT', str(msmpc))
        print(msmpc)

    def testTermTypeIndexKeys(self):
        # ensure an _INET entry for each _TERM_TYPE entry
        self.assertCountEqual(junipermsmpc.Term._TERM_TYPE.keys(), junipermsmpc.Term.AF_MAP.keys())

    @mock.patch.object(junipermsmpc.logging, 'warning')
    def testIcmpv6InetMismatch(self, mock_warning):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + BAD_ICMPTYPE_TERM_1, self.naming), EXP_INFO
        )
        # output happens in __str_
        str(msmpc)

        mock_warning.assert_called_once_with(
            'Term icmptype-mismatch will not be rendered,'
            ' as it has icmpv6 match specified but '
            'the ACL is of inet address family.'
        )

    @mock.patch.object(junipermsmpc.logging, 'warning')
    def testIcmpInet6Mismatch(self, mock_warning):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_V6 + BAD_ICMPTYPE_TERM_2, self.naming), EXP_INFO
        )
        # output happens in __str_
        str(msmpc)

        mock_warning.assert_called_once_with(
            'Term icmptype-mismatch will not be rendered,'
            ' as it has icmp match specified but '
            'the ACL is of inet6 address family.'
        )

    @mock.patch.object(junipermsmpc.logging, 'warning')
    def testExpiredTerm(self, mock_warn):
        _ = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO
        )

        mock_warn.assert_called_once_with(
            'WARNING: Term %s in policy %s is expired and will not be rendered.',
            'is_expired',
            'test-filter',
        )

    @mock.patch.object(junipermsmpc.logging, 'info')
    def testExpiringTerm(self, mock_info):
        exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
        _ = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(
                GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'), self.naming
            ),
            EXP_INFO,
        )

        mock_info.assert_called_once_with(
            'INFO: Term %s in policy %s expires in ' 'less than two weeks.',
            'is_expiring',
            'test-filter',
        )

    @capture.stdout
    def testOwnerTerm(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_17, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn(
            '                    /*\n'
            '                     ** Owner: foo@google.com\n'
            '                     */',
            output,
            output,
        )
        print(output)

    @capture.stdout
    def testOwnerNoVerboseTerm(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_NOVERBOSE_V4_HEADER + GOOD_TERM_17, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertNotIn('** Owner: ', output, output)
        print(output)

    @capture.stdout
    def testAddressExclude(self):
        self.naming._ParseLine('INTERNAL = 0.0.0.0/1 172.16.0.0/12', 'networks')
        self.naming._ParseLine('SOME_HOST = 10.0.0.0/8', 'networks')

        terms = (GOOD_TERM_18_SRC, GOOD_TERM_18_DST)

        for term in terms:
            msmpc = junipermsmpc.JuniperMSMPC(
                policy.ParsePolicy(GOOD_HEADER + term, self.naming), EXP_INFO
            )
            output = str(msmpc)
            expected_output = (
                '                            '
                + ('source' if term == GOOD_TERM_18_SRC else 'destination')
                + '-address {\n'
                + '                                0.0.0.0/1;\n'
                + '                                172.16.0.0/12;\n'
                + '                                10.0.0.0/8 except;\n'
                + '                            }'
            )
            self.assertIn(expected_output, output, output)
            self.assertNotIn('10.0.0.0/8;', output, output)
            self.assertNotIn('172.16.0.0/12 except;', output, output)

        print(output)

    @capture.stdout
    def testMinimizePrefixes(self):
        self.naming._ParseLine('INCLUDES = 1.0.0.0/8 2.0.0.0/8', 'networks')
        self.naming._ParseLine('EXCLUDES = 1.1.1.1/32 2.0.0.0/8 3.3.3.3/32', 'networks')

        expected = ['1.0.0.0/8;', '1.1.1.1/32 except;']
        unexpected = ['2.0.0.0/8;', '2.0.0.0/8 except;', '3.3.3.3/32']

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19, self.naming), EXP_INFO
        )
        output = str(msmpc)
        for result in expected:
            self.assertIn(result, output, f'expected "{result}" in {output}')
        for result in unexpected:
            self.assertNotIn(result, output, f'unexpected "{result}" in {output}')

        print(output)

    @capture.stdout
    def testNoMatchReversal(self):
        self.naming._ParseLine('INCLUDES = 10.0.0.0/8 10.0.0.0/10', 'networks')
        self.naming._ParseLine('EXCLUDES = 10.0.0.0/9', 'networks')

        expected = ['10.0.0.0/8;', '10.0.0.0/10;', '10.0.0.0/9 except;']

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19, self.naming), EXP_INFO
        )
        output = str(msmpc)
        for result in expected:
            self.assertIn(result, output)
        print(output)

    def testBuildTokens(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_35, self.naming), EXP_INFO
        )
        st, sst = msmpc._BuildTokens()
        self.assertSetEqual(st, SUPPORTED_TOKENS)
        self.assertDictEqual(sst, SUPPORTED_SUB_TOKENS)

    @capture.stdout
    def testRangedPorts(self):
        self.naming._ParseLine('BOOTPC = 67/udp', 'services')
        self.naming._ParseLine('BOOTPS = 68/udp', 'services')
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + RANGE_PORTS_TERM, self.naming), EXP_INFO
        )
        self.assertIn('destination-port 67-68;', str(msmpc))
        print(msmpc)

    @capture.stdout
    def testNotRangedPorts(self):
        self.naming._ParseLine('BOOTPC = 67/udp', 'services')
        self.naming._ParseLine('BOOTPS = 69/udp', 'services')
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + RANGE_PORTS_TERM, self.naming), EXP_INFO
        )
        self.assertNotIn('destination-port 67-68;', str(msmpc))
        self.assertIn('destination-port 67;', str(msmpc))
        self.assertIn('destination-port 69;', str(msmpc))
        print(msmpc)

    @parameterized.named_parameters(
        dict(testcase_name='WithApplyGroups', apply_groups=True),
        dict(testcase_name='WithoutApplyGroups', apply_groups=False),
    )
    @capture.stdout
    def testApplicationSets(self, apply_groups):
        header = GOOD_HEADER
        if not apply_groups:
            header = re.sub(r'(target:: msmpc .+)', r'\1 no-apply-groups', header)
        self.naming._ParseLine('BOOTPC = 67/udp', 'services')
        self.naming._ParseLine('BOOTPS = 69/udp', 'services')
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(header + RANGE_PORTS_TERM, self.naming), EXP_INFO
        )
        expected = (
            '        applications {\n'
            '            application test-filterranged-ports-1-app1 {\n'
            '                protocol udp;\n'
            '                destination-port 67;\n'
            '            }\n'
            '            application test-filterranged-ports-1-app2 {\n'
            '                protocol udp;\n'
            '                destination-port 69;\n'
            '            }\n'
            '            application-set test-filterranged-ports-1-app {\n'
            '                application test-filterranged-ports-1-app1;\n'
            '                application test-filterranged-ports-1-app2;\n'
            '            }\n'
            '        }\n'
        )
        if not apply_groups:
            expected = textwrap.dedent(expected)
        self.assertIn(expected, str(msmpc))
        print(msmpc)

    @capture.stdout
    def testGroup(self):
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + DEFAULT_TERM_1, self.naming), EXP_INFO
        )
        self.assertEqual('b;', msmpc._Group(['B']))
        self.assertEqual('B;', msmpc._Group(['B'], lc=False))
        self.assertEqual('b;', msmpc._Group(['B'], lc=True))
        self.assertEqual('100;', msmpc._Group([100]))
        self.assertEqual('100-200;', msmpc._Group([(100, 200)]))
        self.assertEqual('[ b a ];', msmpc._Group(['b', 'A']))
        self.assertEqual('[ 99 101-199 ];', msmpc._Group([99, (101, 199)]))
        self.assertEqual('[ 99 101-199 ];', msmpc._Group([99, (101, 199)]))
        print(msmpc)

    @parameterized.named_parameters(
        dict(
            testcase_name='MIXED_TO_V4',
            addresses=[
                ['0.0.0.0/1', '2001::/33'],
                ['192.168.0.0/24'],
            ],
            expected=[
                '                    term good-term-inet {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                0.0.0.0/1;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                192.168.0.0/24;\n'
                + '                            }'
            ],
            notexpected=['2001::/33'],
        ),
        dict(
            testcase_name='V4_TO_MIXED',
            addresses=[
                ['192.168.0.0/24'],
                ['0.0.0.0/1', '2001::/33'],
            ],
            expected=[
                '                    term good-term-inet {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                192.168.0.0/24;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                0.0.0.0/1;\n'
                + '                            }'
            ],
            notexpected=['2001::/33'],
        ),
        dict(
            testcase_name='MIXED_TO_V6',
            addresses=[
                ['0.0.0.0/1', '2001::/33'],
                ['2201::/48'],
            ],
            expected=[
                '                    term good-term-inet6 {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                2001::/33;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                2201::/48;\n'
                + '                            }'
            ],
            notexpected=['0.0.0.0/1'],
        ),
        dict(
            testcase_name='V6_TO_MIXED',
            addresses=[
                ['2201::/48'],
                ['0.0.0.0/1', '2001::/33'],
            ],
            expected=[
                '                    term good-term-inet6 {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                2201::/48;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                2001::/33;\n'
                + '                            }'
            ],
            notexpected=['0.0.0.0/1'],
        ),
        dict(
            testcase_name='MIXED_TO_MIXED',
            addresses=[
                ['0.0.0.0/1', '2001::/33'],
                ['192.168.0.0/24', '2201::/48'],
            ],
            expected=[
                '                    term good-term-inet {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                0.0.0.0/1;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                192.168.0.0/24;\n'
                + '                            }',
                '                    term good-term-inet6 {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                2001::/33;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                2201::/48;\n'
                + '                            }',
            ],
            notexpected=[],
        ),
        dict(
            testcase_name='V4_TO_V4',
            addresses=[['0.0.0.0/1'], ['192.168.0.0/24']],
            expected=[
                '                    term good-term {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                0.0.0.0/1;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                192.168.0.0/24;\n'
                + '                            }'
            ],
            notexpected=[],
        ),
        dict(
            testcase_name='V6_TO_V6',
            addresses=[['2001::/33'], ['2201::/48']],
            expected=[
                '                    term good-term {\n'
                + '                        from {\n'
                + '                            source-address {\n'
                + '                                2001::/33;\n'
                + '                            }\n'
                + '                            destination-address {\n'
                + '                                2201::/48;\n'
                + '                            }'
            ],
            notexpected=[],
        ),
        dict(
            testcase_name='V4_TO_V6',
            addresses=[['0.0.0.0/1'], ['2201::/48']],
            expected=[],
            notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
        ),
        dict(
            testcase_name='V6_TO_V4',
            addresses=[['2001::/33'], ['192.168.0.0/24']],
            expected=[],
            notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
        ),
        dict(
            testcase_name='PARTLY_UNSPECIFIED',
            addresses=[['2001::/33'], ['192.168.0.0/24']],
            expected=['term good_term_25 '],
            notexpected=[
                '0.0.0.0/1',
                '192.168.0.0/24',
                '2001::/33',
                '2201::/48',
                'term good-term-both-icmp-and-icmpv6-',
            ],
        ),
    )
    def testMixed(self, addresses, expected, notexpected):
        self.naming._ParseLine(f'SOME_HOST = {" ".join(addresses[0])}', 'networks')
        self.naming._ParseLine(f'SOME_OTHER_HOST = {" ".join(addresses[1])}', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        self.naming._ParseLine('DNS = 53/tcp', 'services')
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED + MIXED_TESTING_TERM + GOOD_TERM_25, self.naming),
            EXP_INFO,
        )
        output = str(msmpc)
        for expect in expected:
            self.assertIn(expect, output, output)
        for notexpect in notexpected:
            self.assertNotIn(notexpect, output, output)

    @parameterized.named_parameters(
        dict(
            testcase_name='MIXED_TO_V4',
            addresses=[
                ['0.0.0.0/1', '2001::/33'],
                ['192.168.0.0/24'],
            ],
            expected=[
                '                    term good-term-icmp-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=['2001::/33'],
        ),
        dict(
            testcase_name='V4_TO_MIXED',
            addresses=[
                ['192.168.0.0/24'],
                ['0.0.0.0/1', '2001::/33'],
            ],
            expected=[
                '                    term good-term-icmp-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=['2001::/33'],
        ),
        dict(
            testcase_name='MIXED_TO_V6',
            addresses=[
                ['0.0.0.0/1', '2001::/33'],
                ['2201::/48'],
            ],
            expected=[
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=['0.0.0.0/1'],
        ),
        dict(
            testcase_name='V6_TO_MIXED',
            addresses=[
                ['2201::/48'],
                ['0.0.0.0/1', '2001::/33'],
            ],
            expected=[
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=['0.0.0.0/1'],
        ),
        dict(
            testcase_name='MIXED_TO_MIXED',
            addresses=[
                ['0.0.0.0/1', '2001::/33'],
                ['192.168.0.0/24', '2201::/48'],
            ],
            expected=[
                '                    term good-term-icmp-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2-inet {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2-inet6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=[],
        ),
        dict(
            testcase_name='V4_TO_V4',
            addresses=[['0.0.0.0/1'], ['192.168.0.0/24']],
            expected=[
                '                    term good-term-icmp {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                0.0.0.0/1;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                192.168.0.0/24;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=[],
        ),
        dict(
            testcase_name='V6_TO_V6',
            addresses=[['2001::/33'], ['2201::/48']],
            expected=[
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
                '                    term good-term-both-icmp-and-icmpv6-2 {\n'
                '                        from {\n'
                '                            source-address {\n'
                '                                2001::/33;\n'
                '                            }\n'
                '                            destination-address {\n'
                '                                2201::/48;\n'
                '                            }\n'
                '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=[],
        ),
        dict(
            testcase_name='V4_TO_V6',
            addresses=[['0.0.0.0/1'], ['2201::/48']],
            expected=[
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
        ),
        dict(
            testcase_name='V6_TO_V4',
            addresses=[['2001::/33'], ['192.168.0.0/24']],
            expected=[
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
        ),
        dict(
            testcase_name='PARTLY_UNSPECIFIED',
            addresses=[['2001::/33'], ['192.168.0.0/24']],
            expected=[
                '                    term good-term-icmp-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmp-app;\n'
                '                        }',
                '                    term good-term-icmpv6-2 {\n'
                '                        from {\n'
                '                            application-sets test-filtergood-term-icmpv6-app;\n'
                '                        }',
            ],
            notexpected=[
                '0.0.0.0/1',
                '192.168.0.0/24',
                '2001::/33',
                '2201::/48',
                'term good-term-icmp-i',
                'term good-term-icmpv6-i',
                'term good-term-both-icmp-and-icmpv6-',
            ],
        ),
    )
    def testMixedICMP(self, addresses, expected, notexpected):
        self.naming._ParseLine(f'SOME_HOST = {" ".join(addresses[0])}', 'networks')
        self.naming._ParseLine(f'SOME_OTHER_HOST = {" ".join(addresses[1])}', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        self.naming._ParseLine('DNS = 53/tcp', 'services')
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED + MIXED_TESTING_TERM_ICMP, self.naming), EXP_INFO
        )
        output = str(msmpc)
        for expect in expected:
            self.assertIn(expect, output, output)
        for notexpect in notexpected:
            self.assertNotIn(notexpect, output, output)

    @parameterized.named_parameters(
        dict(
            testcase_name='true-apply-groups', option='true', want_logging=True, apply_groups=True
        ),
        dict(
            testcase_name='True-apply-groups', option='True', want_logging=True, apply_groups=True
        ),
        dict(
            testcase_name='syslog-apply-groups',
            option='syslog',
            want_logging=True,
            apply_groups=True,
        ),
        dict(
            testcase_name='local-apply-groups',
            option='local',
            want_logging=True,
            apply_groups=True,
        ),
        dict(
            testcase_name='disable-apply-groups',
            option='disable',
            want_logging=False,
            apply_groups=True,
        ),
        dict(
            testcase_name='log-both-apply-groups',
            option='log-both',
            want_logging=True,
            apply_groups=True,
        ),
        dict(
            testcase_name='true-no-apply-groups',
            option='true',
            want_logging=True,
            apply_groups=False,
        ),
        dict(
            testcase_name='True-no-apply-groups',
            option='True',
            want_logging=True,
            apply_groups=False,
        ),
        dict(
            testcase_name='syslog-no-apply-groups',
            option='syslog',
            want_logging=True,
            apply_groups=False,
        ),
        dict(
            testcase_name='local-no-apply-groups',
            option='local',
            want_logging=True,
            apply_groups=False,
        ),
        dict(
            testcase_name='disable-no-apply-groups',
            option='disable',
            want_logging=False,
            apply_groups=False,
        ),
        dict(
            testcase_name='log-both-no-apply-groups',
            option='log-both',
            want_logging=True,
            apply_groups=False,
        ),
    )
    def testLogging(self, option, want_logging, apply_groups):
        self.naming._ParseLine('SOME_HOST = 192.168.0.0/24', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        expected_output = (
            '    test-filter {\n'
            + '        services {\n'
            + '            stateful-firewall {\n'
            + '                rule test-filter {\n'
            + '                    match-direction input-output;\n'
            + '                    term good-term-1 {\n'
            + '                        from {\n'
            + '                            application-sets '
            'test-filtergood-term-1-app;\n'
            + '                        }\n'
            + '                        then {\n'
            + '                            accept;\n'
            + ('                            syslog;\n' if want_logging else '')
            + '                        }\n'
            + '                    }\n'
            + '                }\n'
            + '            }\n'
            + '        }\n'
            + '        applications {\n'
            + '            application test-filtergood-term-1-app1 {\n'
            + '                protocol icmp;\n'
            + '            }\n'
            + '            application-set test-filtergood-term-1-app {\n'
            + '                application test-filtergood-term-1-app1;\n'
            + '            }\n'
            + '        }\n'
            + '    }\n'
            + '}\n'
            + 'apply-groups test-filter;'
        )
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED_IMPLICIT + (LOGGING_TERM % option), self.naming),
            EXP_INFO,
        )
        output = str(msmpc)
        self.assertIn(expected_output, output, output)

    @parameterized.named_parameters(
        dict(
            testcase_name='default-apply-groups',
            header=GOOD_HEADER,
            apply_groups=True,
            direction='input-output',
        ),
        dict(
            testcase_name='ingress-apply-groups',
            header=GOOD_HEADER_INGRESS,
            apply_groups=True,
            direction='input',
        ),
        dict(
            testcase_name='egress-apply-groups',
            header=GOOD_HEADER_EGRESS,
            apply_groups=True,
            direction='output',
        ),
        dict(
            testcase_name='default-no-apply-groups',
            header=GOOD_HEADER,
            apply_groups=False,
            direction='input-output',
        ),
        dict(
            testcase_name='ingress-no-apply-groups',
            header=GOOD_HEADER_INGRESS,
            apply_groups=False,
            direction='input',
        ),
        dict(
            testcase_name='egress-no-apply-groups',
            header=GOOD_HEADER_EGRESS,
            apply_groups=False,
            direction='output',
        ),
    )
    def testDirection(self, header, apply_groups, direction):
        if not apply_groups:
            header = re.sub(r'(target:: msmpc .+)', r'\1 no-apply-groups', header)
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(header + GOOD_TERM_3, self.naming), EXP_INFO
        )
        output = str(msmpc)
        expected_output = '        rule test-filter {\n' '            match-direction %s;'
        if apply_groups:
            expected_output = textwrap.indent(expected_output, '        ')
        self.assertIn(expected_output % direction, output, output)

    def testBadDirectionCombo(self):
        pol = policy.ParsePolicy(BAD_HEADER_DIRECTION + GOOD_TERM_3, self.naming)
        self.assertRaises(
            junipermsmpc.ConflictingTargetOptionsError, junipermsmpc.JuniperMSMPC, pol, EXP_INFO
        )

    def testTermNameCollision(self):
        short_append = '1' * (junipermsmpc.MAX_IDENTIFIER_LEN // 2 - len('?ood-term-1'))
        long_append = f"{short_append}1"
        not_too_long_name = TERM_NAME_COLLISION % (short_append, short_append)
        too_long_name = TERM_NAME_COLLISION % (long_append, long_append)
        pol = policy.ParsePolicy(GOOD_HEADER + too_long_name, self.naming)
        self.assertRaises(
            junipermsmpc.ConflictingApplicationSetsError, junipermsmpc.JuniperMSMPC, pol, EXP_INFO
        )
        _ = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER + not_too_long_name, self.naming), EXP_INFO
        )

    @capture.stdout
    def testSlashZeroReplacement(self):
        self.naming._ParseLine('SOME_HOST = 0.0.0.0/0 ::/0', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        expectedv4 = (
            '                    term good-term-2-inet {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                any-ipv4;\n'
            + '                            }'
        )
        expectedv6 = (
            '                    term good-term-2-inet6 {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                any-ipv6;\n'
            + '                            }'
        )

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn(expectedv4, output, output)
        self.assertIn(expectedv6, output, output)
        print(output)

    @capture.stdout
    def testV6SlashFourteenReplacement(self):
        self.naming._ParseLine('SOME_HOST = 0.0.0.0/1 ::/14', 'networks')
        self.naming._ParseLine('SMTP = 25/tcp', 'services')
        expectedv4 = (
            '                    term good-term-2-inet {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                0.0.0.0/1;\n'
            + '                            }'
        )
        expectedv6 = (
            '                    term good-term-2-inet6 {\n'
            + '                        from {\n'
            + '                            destination-address {\n'
            + '                                ::/16;\n'
            + '                                1::/16;\n'
            + '                                2::/16;\n'
            + '                                3::/16;\n'
            + '                            }'
        )

        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming), EXP_INFO
        )
        output = str(msmpc)
        self.assertIn(expectedv4, output, output)
        self.assertIn(expectedv6, output, output)
        print(output)

    @parameterized.named_parameters(
        dict(testcase_name='tcp-apply-groups', protoname='tcp', protonum='tcp', apply_groups=True),
        dict(
            testcase_name='hopopt-apply-groups',
            protoname='hopopt',
            protonum='0',
            apply_groups=True,
        ),
        dict(
            testcase_name='vrrp-apply-groups', protoname='vrrp', protonum='112', apply_groups=True
        ),
        dict(
            testcase_name='tcp-no-apply-groups',
            protoname='tcp',
            protonum='tcp',
            apply_groups=False,
        ),
        dict(
            testcase_name='hopopt-no-apply-groups',
            protoname='hopopt',
            protonum='0',
            apply_groups=False,
        ),
        dict(
            testcase_name='vrrp-no-apply-groups',
            protoname='vrrp',
            protonum='112',
            apply_groups=False,
        ),
    )
    def testProtocolAsNumber(self, protoname, protonum, apply_groups):
        expected = (
            '    application test-filtergood-term-numeric-app1 {\n' '        protocol %s;'
        ) % protonum

        header = GOOD_HEADER_MIXED
        if not apply_groups:
            header = re.sub(r'(target:: msmpc .+)', r'\1 no-apply-groups', header)
        msmpc = junipermsmpc.JuniperMSMPC(
            policy.ParsePolicy(header + GOOD_TERM_NUMERIC_PROTOCOL % protoname, self.naming),
            EXP_INFO,
        )
        output = str(msmpc)
        if apply_groups:
            expected = textwrap.indent(expected, '        ')
        self.assertIn(expected, output, output)

    def testSupportedNamedProtocols(self):
        supported_as_names = junipermsmpc.Term._SUPPORTED_PROTOCOL_NAMES
        all_supported_protocols = junipermsmpc.Term.PROTO_MAP.keys()
        for want in supported_as_names:
            self.assertIn(want, all_supported_protocols)


if __name__ == '__main__':
    absltest.main()
