"""Test anonymization of IP addresses and related functions."""
#   Copyright 2018 Intentionet
#
#   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
#
#       http://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.

from __future__ import unicode_literals

import ipaddress
import re

import pytest

from netconan.ip_anonymization import IpAnonymizer, IpV6Anonymizer, anonymize_ip_addr

ip_v4_classes = [
    "0.0.0.0/1",  # Class A
    "128.0.0.0/2",  # Class B
    "192.0.0.0/3",  # Class C
    "224.0.0.0/4",  # Class D (implies class E)
]

ip_v4_list = [
    ("12.13.14.15"),
    ("237.73.212.5"),
    ("123.45.67.89"),
    ("92.210.0.255"),
    ("128.7.55.12"),
    ("223.123.21.99"),
    ("193.99.99.99"),
    ("225.99.99.99"),
    ("241.99.99.99"),
    ("249.99.99.99"),
    ("254.254.254.254"),
    ("009.010.011.012"),
    ("1.2.3.0000014"),
]

ip_v6_list = [
    ("1234::5678"),
    ("::1"),
    ("1::"),
    ("1::1"),
    ("2001:db8:85a3:7:8:8a2e:370:7334"),
    ("2001:db8:a0b:12f0::1"),
    ("ffff:ffff::ffff:ffff"),
    ("a:b:c:d:e:f:1:2"),
    ("aAaA:bBbB:cCcC:dDdD:eEeE:fFfF:1010:2929"),
    ("ffff:eeee:dddd:cccc:bbbb:AaAa:9999:8888"),
]

# Private-use blocks defined at https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
# Tuples consist of: start of block, end of block, block subnet
private_blocks = [
    ("10.0.0.0", "10.255.255.255", "10.0.0.0/8"),
    ("172.16.0.0", "172.31.255.255", "172.16.0.0/12"),
    ("192.168.0.0", "192.168.255.255", "192.168.0.0/16"),
]

SALT = "saltForTest"


@pytest.fixture(scope="module")
def anonymizer_v4():
    """Most tests in this module use a single IPv4 anonymizer."""
    return IpAnonymizer(SALT)


@pytest.fixture(scope="module")
def anonymizer_v4_preserving_host_bits():
    """IPv4 anonymizer which also preserves some host bits."""
    return IpAnonymizer(SALT, preserve_suffix=8)


@pytest.fixture(scope="module")
def anonymizer_v6():
    """All tests in this module use a single IPv6 anonymizer."""
    return IpV6Anonymizer(SALT)


@pytest.fixture(scope="module")
def anonymizer(request):
    """Create a generic fixture for different types of anonymizers."""
    if request.param == "v4":
        return IpAnonymizer(SALT)
    elif request.param == "v6":
        return IpV6Anonymizer(SALT)
    elif request.param == "flipv4":
        return IpAnonymizer(SALT, salter=lambda a, b: 1)
    else:
        raise ValueError("Invalid anonymizer type {}".format(request.param))


@pytest.fixture(scope="module")
def flip_anonymizer_v4():
    """Create an anonymizer that flips every bit except for class bits."""
    # Don't preserve private blocks, because that reduces the number of bits flipped
    return IpAnonymizer(SALT, preserve_prefixes=ip_v4_classes, salter=lambda a, b: 1)


def anonymize_line_general(anonymizer, line, ip_addrs):
    """Test IP address removal from config lines."""
    line_w_ip = line.format(*ip_addrs)
    anon_line = anonymize_ip_addr(anonymizer, line_w_ip)

    # Now anonymize each IP address individually & build another anonymized line
    anon_ip_addrs = [anonymize_ip_addr(anonymizer, ip_addr) for ip_addr in ip_addrs]
    individually_anon_line = line.format(*anon_ip_addrs)

    # Make sure anonymizing each address individually is the same as
    # anonymizing all at once
    assert anon_line == individually_anon_line

    for ip_addr in ip_addrs:
        # Make sure the original ip address(es) are removed from the anonymized line
        assert ip_addr not in anon_line


@pytest.mark.parametrize(
    "line, ip_addrs",
    [
        ("ip address {} 255.255.255.254", ["123.45.67.89"]),
        ("ip address {} 255.0.0.0", ["10.0.0.0"]),
        ("ip address {}/16", ["10.0.0.0"]),
        ("tacacs-server host {}", ["10.1.1.17"]),
        ("tacacs-server host {}", ["001.021.201.012"]),
        ("syscon address {} Password", ["10.73.212.5"]),
        ("1 permit tcp host {} host {} eq 2", ["1.2.3.4", "1.2.3.45"]),
        ("1 permit tcp host {} host {} eq 2", ["1.2.123.4", "11.2.123.4"]),
        ("1 permit tcp host {} host {} eq 2", ["1.2.30.45", "1.2.30.4"]),
        ("1 permit tcp host {} host {} eq 2", ["11.20.3.4", "1.20.3.4"]),
        ("something host {} host {} host {}", ["1.2.3.4", "1.2.3.5", "1.2.3.45"]),
        # These formats may occur in Batfish output
        ('"{}"', ["1.2.3.45"]),
        ("({})", ["1.2.3.45"]),
        ("[IP addresses:{},{}]", ["1.2.3.45", "1.2.3.5"]),
        ("flow:{}->{}", ["1.2.3.45", "1.2.3.5"]),
        ("something={}", ["1.2.3.45"]),
        ("something <{}>", ["1.2.3.45"]),
        ("something '{}'", ["1.2.3.45"]),
    ],
)
def test_v4_anonymize_line(anonymizer_v4, line, ip_addrs):
    """Test IPv4 address removal from config lines."""
    anonymize_line_general(anonymizer_v4, line, ip_addrs)


@pytest.mark.parametrize("enclosing", "_:;[]$~!@#$%^&*()-+=[]|<>?")
def test_v4_anonymize_enclosed_addr(anonymizer_v4, enclosing):
    """Test IPv4 address removal from config lines with different enclosing characters."""
    ip_addr = "1.2.3.4"
    line = enclosing + "{}" + enclosing
    anonymize_line_general(anonymizer_v4, line, [ip_addr])


@pytest.mark.parametrize(
    "line, ip_addrs",
    [
        ("ip address {} something::something", ["1234::5678"]),
        ("ip address {} blah {}", ["1234::", "1234:5678::9abc:def0"]),
        ("ip address {} blah {} blah", ["::1", "1234:5678:abcd:dcba::9abc:def0"]),
        ("ip address {}/16 blah", ["::1"]),
        ("ip address {}/16 blah", ["1::"]),
        ("ip address {}/16 blah", ["1::1"]),
        ("ip address {}/16 blah", ["ffff:ffff::ffff:ffff"]),
    ],
)
def test_v6_anonymize_line(anonymizer_v6, line, ip_addrs):
    """Test IPv6 address removal from config lines."""
    anonymize_line_general(anonymizer_v6, line, ip_addrs)


@pytest.mark.parametrize("enclosing", ".;[]$~!@#$%^&*()-+=[]|<>?")
def test_v6_anonymize_enclosed_addr(anonymizer_v6, enclosing):
    """Test IPv6 address removal from config lines with different enclosing characters."""
    ip_addr = "1::1"
    line = enclosing + "{}" + enclosing
    anonymize_line_general(anonymizer_v6, line, [ip_addr])


def get_ip_v4_class(ip_int):
    """Return the letter corresponding to the IP class the ip_int is in."""
    if (ip_int & 0x80000000) == 0x00000000:
        return "A"
    elif (ip_int & 0xC0000000) == 0x80000000:
        return "B"
    elif (ip_int & 0xE0000000) == 0xC0000000:
        return "C"
    elif (ip_int & 0xF0000000) == 0xE0000000:
        return "D"
    else:
        return "E"


def get_ip_v4_class_mask(ip_int):
    """Return a mask indicating bits preserved when preserving class."""
    if (ip_int & 0xE0000000) == 0xE0000000:
        return 0xF0000000
    elif (ip_int & 0xC0000000) == 0xC0000000:
        return 0xE0000000
    elif (ip_int & 0x80000000) == 0x80000000:
        return 0xC0000000
    else:
        return 0x80000000


@pytest.mark.parametrize(
    "ip_addr",
    [
        "0.0.0.0",
        "127.255.255.255",  # Class A
        "128.0.0.0",
        "191.255.255.255",  # Class B
        "192.0.0.0",
        "223.255.255.255",  # Class C
        "224.0.0.0",
        "239.255.255.255",  # Class D
        "240.0.0.0",
        "247.255.255.255",  # Class E
    ],
)
def test_v4_class_preserved(flip_anonymizer_v4, ip_addr):
    """Test that IPv4 classes are preserved."""
    ip_int = int(flip_anonymizer_v4.make_addr(ip_addr))
    ip_int_anon = flip_anonymizer_v4.anonymize(ip_int)

    # IP v4 class should match after anonymization
    assert get_ip_v4_class(ip_int) == get_ip_v4_class(ip_int_anon)

    # Anonymized ip address should not match the original ip address
    assert ip_int != ip_int_anon

    # All bits that are not forced to be preserved are flipped
    class_mask = get_ip_v4_class_mask(ip_int)
    assert 0xFFFFFFFF ^ class_mask == ip_int ^ ip_int_anon


def test_preserve_custom_prefixes():
    """Test that a custom prefix is preserved correctly."""
    subnet = "170.0.0.0/8"