#!/usr/bin/env python3
"""Test generator for xips."""
import sys

# --- Test format ---
# run_test(cmd, suffix, expected):
#   Generates: {cmd} | ./xips {suffix} > /dev/null
#   Checks:    exit code == expected
#
# Suffix helpers (come after ./xips):
#   lines(n)       : | wc -l | awk '...'   → expected = 0 when count matches
#   cnt(n)         : --count | awk '...'   → expected = 0 when count matches
#   has('ip')      : | grep -Fx 'ip'       → expected = 0 (present)
#   [with expected=1 for absent checks]

def run_test(cmd, suffix, expected):
    print(cmd, "| ./xips", suffix, "> /dev/null")
    print("if [ $? -eq", expected, "]; then\ntrue\nelse\n\techo FAIL.  Test was",
          cmd.replace("|", "pipe"), "with suffix:", suffix.replace("|", "pipe"), "\nfi\n")


def lines(n):
    """Suffix that checks xips outputs exactly n lines.  expected=0."""
    return f"| wc -l | awk '{{if ($1 == {n}) exit 0; else exit 1}}'"


def cnt(n):
    """Suffix for --count: checks the printed count equals n.  expected=0."""
    return f"--count | awk '{{if ($1 == {n}) exit 0; else exit 1}}'"


def has(ip):
    """Suffix that checks output contains the exact line.  expected=0 (found)."""
    return f"| grep -Fx '{ip}'"


# ---------------------------------------------------------------------------
# Basic IPv4 expansion
# ---------------------------------------------------------------------------
expansion_tests = (
    # single IPs pass through unchanged
    ("echo '192.168.1.1'",                    lines(1), 0),
    ("echo '8.8.8.8'",                        lines(1), 0),
    # non-IP produces no output
    ("echo 'rabbit'",                         lines(0), 0),
    ("echo ''",                               lines(0), 0),
    # /30: 4 addresses (network, 2 hosts, broadcast)
    ("echo '192.168.1.0/30'",                 lines(4), 0),
    ("echo '192.168.1.0/30'",                 has("192.168.1.0"), 0),    # network addr
    ("echo '192.168.1.0/30'",                 has("192.168.1.1"), 0),    # host
    ("echo '192.168.1.0/30'",                 has("192.168.1.2"), 0),    # host
    ("echo '192.168.1.0/30'",                 has("192.168.1.3"), 0),    # broadcast
    # /24: 256 addresses
    ("echo '192.168.1.0/24'",                 lines(256), 0),
    ("echo '192.168.1.0/24'",                 has("192.168.1.0"), 0),
    ("echo '192.168.1.0/24'",                 has("192.168.1.255"), 0),
    # /32 single address
    ("echo '192.168.1.5/32'",                 lines(1), 0),
    ("echo '192.168.1.5/32'",                 has("192.168.1.5"), 0),
    # /29: 8 addresses
    ("echo '10.0.0.0/29'",                    lines(8), 0),
    # short range notation: 192.168.1.1-5
    ("echo '192.168.1.1-5'",                  lines(5), 0),
    ("echo '192.168.1.1-5'",                  has("192.168.1.1"), 0),
    ("echo '192.168.1.1-5'",                  has("192.168.1.5"), 0),
    # full range notation
    ("echo '192.168.1.1-192.168.1.5'",        lines(5), 0),
    # mask notation
    ("echo '192.168.1.0 255.255.255.252'",    lines(4), 0),
    ("echo '10.0.0.0 255.255.255.0'",         lines(256), 0),
    # 'to' notation
    ("echo '192.168.1.1 to 192.168.1.3'",     lines(3), 0),
    # no dedup by default: same IP twice → two output lines
    ("printf '192.168.1.1\\n192.168.1.1\\n'", lines(2), 0),
)

# ---------------------------------------------------------------------------
# IPv6 expansion
# ---------------------------------------------------------------------------
ipv6_expansion_tests = (
    ("echo '::1'",              lines(1), 0),
    ("echo '::1'",              has("::1"), 0),
    ("echo '::1/128'",          lines(1), 0),
    # short suffix range
    ("echo 'fe80::1-fe80::4'",  lines(4), 0),
    ("echo 'fe80::1-fe80::4'",  has("fe80::1"), 0),
    ("echo 'fe80::1-fe80::4'",  has("fe80::4"), 0),
    # /127: 2 addresses
    ("echo 'fe80::/127'",       lines(2), 0),
    # non-IPv6 produces no output
    ("echo 'gggg::1'",          lines(0), 0),
)

# ---------------------------------------------------------------------------
# -4 / -6 protocol filter
# ---------------------------------------------------------------------------
ipv4_ipv6_filter_tests = (
    # -4 passes IPv4, blocks IPv6
    ("echo '192.168.1.1'",                    f"-4 {lines(1)}", 0),
    ("echo '::1'",                            f"-4 {lines(0)}", 0),
    ("echo '192.168.1.0/30'",                 f"-4 {lines(4)}", 0),
    # -6 passes IPv6, blocks IPv4
    ("echo '::1'",                            f"-6 {lines(1)}", 0),
    ("echo '192.168.1.1'",                    f"-6 {lines(0)}", 0),
    ("echo '192.168.1.0/30'",                 f"-6 {lines(0)}", 0),
    ("echo 'fe80::1-fe80::4'",                f"-6 {lines(4)}", 0),
    # mixed input, filtered
    ("printf '192.168.1.1\\n::1\\n'",         f"-4 {lines(1)}", 0),
    ("printf '192.168.1.1\\n::1\\n'",         f"-6 {lines(1)}", 0),
)

# ---------------------------------------------------------------------------
# --sort: deduplicate and sort numerically
# ---------------------------------------------------------------------------
sort_tests = (
    # deduplication
    ("printf '192.168.1.1\\n192.168.1.1\\n'",         f"--sort {lines(1)}", 0),
    ("printf '192.168.1.0/30\\n192.168.1.0/30\\n'",   f"--sort {lines(4)}", 0),
    # numerical order (first element)
    ("printf '192.168.1.3\\n192.168.1.1\\n192.168.1.2\\n'",
     "--sort | head -1 | grep -Fx '192.168.1.1'", 0),
    # numerical order (last element)
    ("printf '192.168.1.3\\n192.168.1.1\\n192.168.1.2\\n'",
     "--sort | tail -1 | grep -Fx '192.168.1.3'", 0),
    # /30 sort gives 4 unique addresses
    ("echo '192.168.1.0/30'",         f"--sort {lines(4)}", 0),
    # sort across range + extra IP de-dupes
    ("printf '192.168.1.1\\n192.168.1.0/30\\n'", f"--sort {lines(4)}", 0),
    # IPv6 sort deduplication
    ("printf 'fe80::1\\nfe80::1\\n'", f"--sort {lines(1)}", 0),
)

# ---------------------------------------------------------------------------
# --count: count unique IPs instead of listing them
# ---------------------------------------------------------------------------
count_tests = (
    ("echo '192.168.1.1'",                       cnt(1), 0),
    ("echo '192.168.1.0/30'",                    cnt(4), 0),
    ("echo '10.0.0.0/29'",                       cnt(8), 0),
    # duplicates are deduplicated before counting
    ("printf '192.168.1.1\\n192.168.1.1\\n'",    cnt(1), 0),
    ("printf '192.168.1.1\\n192.168.1.2\\n'",    cnt(2), 0),
    # /24 = 256 — compare using awk, not foo, to handle exit-code wrapping
    ("echo '192.168.1.0/24'",
     "| wc -l | awk '{if ($1 == 256) exit 0; else exit 1}'", 0),
    # IPv6 count
    ("echo '::1'",                               cnt(1), 0),
    ("printf 'fe80::1\\nfe80::2\\n'",            cnt(2), 0),
)

# ---------------------------------------------------------------------------
# --shuffle: randomise order (deduplicates like --sort; test count, not order)
# ---------------------------------------------------------------------------
shuffle_tests = (
    ("echo '192.168.1.0/30'",                     f"--shuffle {lines(4)}", 0),
    # duplicates collapsed
    ("printf '192.168.1.1\\n192.168.1.1\\n'",     f"--shuffle {lines(1)}", 0),
    ("printf '192.168.1.1\\n192.168.1.2\\n'",     f"--shuffle {lines(2)}", 0),
    ("echo '::1'",                                f"--shuffle {lines(1)}", 0),
)

# ---------------------------------------------------------------------------
# --port N: append :N (IPv4) or [ip]:N (IPv6)
# ---------------------------------------------------------------------------
port_tests = (
    ("echo '192.168.1.1'",  "| grep -Fx '192.168.1.1'",       0),   # baseline
    ("echo '192.168.1.1'",  "--port 80  | grep -Fx '192.168.1.1:80'",   0),
    ("echo '192.168.1.1'",  "--port 443 | grep -Fx '192.168.1.1'",      1),  # bare absent
    ("echo '::1'",          "--port 443 | grep -Fx '[::1]:443'",         0),
    ("echo '::1'",          "--port 443 | grep -Fx '::1'",              1),  # bare absent
    # port with range: count unchanged
    ("echo '192.168.1.0/30'", f"--port 8080 {lines(4)}", 0),
    # port suffix shows in --sort output
    ("echo '192.168.1.1'",    "--port 22 --sort | grep -Fx '192.168.1.1:22'", 0),
)

# ---------------------------------------------------------------------------
# --private: only output RFC1918 / loopback / link-local addresses
# ---------------------------------------------------------------------------
private_tests = (
    # IPv4 private ranges in
    ("echo '10.0.0.1'",          f"--private {lines(1)}", 0),
    ("echo '10.0.0.0'",          f"--private {lines(1)}", 0),
    ("echo '10.255.255.255'",     f"--private {lines(1)}", 0),
    ("echo '172.16.0.1'",         f"--private {lines(1)}", 0),
    ("echo '172.31.255.255'",     f"--private {lines(1)}", 0),
    ("echo '192.168.1.1'",        f"--private {lines(1)}", 0),
    ("echo '127.0.0.1'",          f"--private {lines(1)}", 0),
    ("echo '169.254.1.1'",        f"--private {lines(1)}", 0),
    # IPv4 public out
    ("echo '8.8.8.8'",            f"--private {lines(0)}", 0),
    ("echo '1.1.1.1'",            f"--private {lines(0)}", 0),
    ("echo '172.15.255.255'",     f"--private {lines(0)}", 0),
    ("echo '172.32.0.0'",         f"--private {lines(0)}", 0),
    # IPv6 private in
    ("echo '::1'",                f"--private {lines(1)}", 0),
    ("echo 'fc00::1'",            f"--private {lines(1)}", 0),
    ("echo 'fd00::1'",            f"--private {lines(1)}", 0),
    ("echo 'fe80::1'",            f"--private {lines(1)}", 0),
    # IPv6 public out
    ("echo '2001:db8::1'",        f"--private {lines(0)}", 0),
    # range: all-private in
    ("echo '192.168.1.0/30'",     f"--private {lines(4)}", 0),
    # range: all-public stays empty
    ("echo '8.8.8.0/30'",         f"--private {lines(0)}", 0),
)

# ---------------------------------------------------------------------------
# --public: only output non-private addresses
# ---------------------------------------------------------------------------
public_tests = (
    ("echo '8.8.8.8'",            f"--public {lines(1)}", 0),
    ("echo '1.1.1.1'",            f"--public {lines(1)}", 0),
    ("echo '2001:db8::1'",        f"--public {lines(1)}", 0),
    # private addresses out
    ("echo '10.0.0.1'",           f"--public {lines(0)}", 0),
    ("echo '192.168.1.1'",        f"--public {lines(0)}", 0),
    ("echo '172.16.0.1'",         f"--public {lines(0)}", 0),
    ("echo '127.0.0.1'",          f"--public {lines(0)}", 0),
    ("echo '::1'",                f"--public {lines(0)}", 0),
    ("echo 'fe80::1'",            f"--public {lines(0)}", 0),
    # range: all-public in
    ("echo '8.8.8.0/30'",         f"--public {lines(4)}", 0),
    # range: all-private out
    ("echo '192.168.1.0/30'",     f"--public {lines(0)}", 0),
)

# ---------------------------------------------------------------------------
# --hosts: exclude network (first) and broadcast (last) from ranges of 3+
# ---------------------------------------------------------------------------
hosts_tests = (
    # /32 single host: passes through unchanged
    ("echo '192.168.1.5'",         f"--hosts {lines(1)}", 0),
    ("echo '192.168.1.5/32'",      f"--hosts {lines(1)}", 0),
    # /31 two addresses: both kept (RFC 3021 point-to-point)
    ("echo '192.168.1.0/31'",      f"--hosts {lines(2)}", 0),
    ("echo '192.168.1.0/31'",      "--hosts | grep -Fx '192.168.1.0'", 0),
    ("echo '192.168.1.0/31'",      "--hosts | grep -Fx '192.168.1.1'", 0),
    # /30 four addresses → 2 hosts
    ("echo '192.168.1.0/30'",      f"--hosts {lines(2)}", 0),
    ("echo '192.168.1.0/30'",      "--hosts | grep -Fx '192.168.1.0'", 1),  # network absent
    ("echo '192.168.1.0/30'",      "--hosts | grep -Fx '192.168.1.3'", 1),  # broadcast absent
    ("echo '192.168.1.0/30'",      "--hosts | grep -Fx '192.168.1.1'", 0),  # first host
    ("echo '192.168.1.0/30'",      "--hosts | grep -Fx '192.168.1.2'", 0),  # second host
    # /29 eight addresses → 6 hosts
    ("echo '10.0.0.0/29'",         f"--hosts {lines(6)}", 0),
    ("echo '10.0.0.0/29'",         "--hosts | grep -Fx '10.0.0.0'", 1),  # network absent
    ("echo '10.0.0.0/29'",         "--hosts | grep -Fx '10.0.0.7'", 1),  # broadcast absent
    # /24 → 254 hosts
    ("echo '192.168.1.0/24'",      f"--hosts {lines(254)}", 0),
    # arbitrary range of 4 (skip first and last)
    ("echo '192.168.1.5-8'",       f"--hosts {lines(2)}", 0),
    ("echo '192.168.1.5-8'",       "--hosts | grep -Fx '192.168.1.5'", 1),
    ("echo '192.168.1.5-8'",       "--hosts | grep -Fx '192.168.1.8'", 1),
    ("echo '192.168.1.5-8'",       "--hosts | grep -Fx '192.168.1.6'", 0),
    ("echo '192.168.1.5-8'",       "--hosts | grep -Fx '192.168.1.7'", 0),
    # mask notation
    ("echo '192.168.1.0 255.255.255.252'", f"--hosts {lines(2)}", 0),
    # combined with --sort
    ("echo '192.168.1.0/30'",      f"--hosts --sort {lines(2)}", 0),
    # combined with --count
    ("echo '192.168.1.0/30'",      cnt(4), 0),     # without --hosts: 4
    ("echo '192.168.1.0/30'",
     "--hosts --count | awk '{if ($1 == 2) exit 0; else exit 1}'", 0),  # with --hosts: 2
)

# ---------------------------------------------------------------------------
# --mac-to-ipv6: MAC address → EUI-64 link-local IPv6
# ---------------------------------------------------------------------------
mac_to_ipv6_tests = (
    # colon-separated MAC
    ("echo 'aa:bb:cc:dd:ee:ff'",   "--mac-to-ipv6 | grep -Fx 'fe80::a8bb:ccff:fedd:eeff'", 0),
    # hyphen-separated MAC
    ("echo 'aa-bb-cc-dd-ee-ff'",   "--mac-to-ipv6 | grep -Fx 'fe80::a8bb:ccff:fedd:eeff'", 0),
    # Cisco dot notation
    ("echo 'aabb.ccdd.eeff'",      "--mac-to-ipv6 | grep -Fx 'fe80::a8bb:ccff:fedd:eeff'", 0),
    # bare 12-hex-char MAC
    ("echo 'aabbccddeeff'",        "--mac-to-ipv6 | grep -Fx 'fe80::a8bb:ccff:fedd:eeff'", 0),
    # all four formats produce identical output
    ("printf 'aa:bb:cc:dd:ee:ff\\naa-bb-cc-dd-ee-ff\\naabb.ccdd.eeff\\naabbccddeeff\\n'",
     f"--mac-to-ipv6 {lines(4)}", 0),
    # U/L bit: 0x00 → 0x02
    ("echo '00:50:56:aa:bb:cc'",   "--mac-to-ipv6 | grep -Fx 'fe80::250:56ff:feaa:bbcc'", 0),
    # U/L bit: 0x02 → 0x00
    ("echo '02:00:5e:aa:bb:cc'",   "--mac-to-ipv6 | grep -Fx 'fe80::5eff:feaa:bbcc'", 0),
    # non-MAC input → no output
    ("echo 'notamac'",             f"--mac-to-ipv6 {lines(0)}", 0),
    ("echo '192.168.1.1'",         f"--mac-to-ipv6 {lines(0)}", 0),
    ("echo 'zz:zz:zz:zz:zz:zz'",  f"--mac-to-ipv6 {lines(0)}", 0),
    # too-short or too-long hex → no output
    ("echo 'aa:bb:cc:dd:ee'",      f"--mac-to-ipv6 {lines(0)}", 0),
    ("echo 'aabbccddeeff00'",      f"--mac-to-ipv6 {lines(0)}", 0),
    # multiple MACs → multiple IPv6 addresses
    ("printf 'aa:bb:cc:dd:ee:ff\\n00:50:56:aa:bb:cc\\n'",
     f"--mac-to-ipv6 {lines(2)}", 0),
    # bad line mixed with good → only good output
    ("printf 'aa:bb:cc:dd:ee:ff\\nnotamac\\n'",
     f"--mac-to-ipv6 {lines(1)}", 0),
)

# ---------------------------------------------------------------------------
# --ipv6-to-mac: EUI-64 IPv6 → MAC address
# ---------------------------------------------------------------------------
ipv6_to_mac_tests = (
    # standard EUI-64 link-local → MAC
    ("echo 'fe80::a8bb:ccff:fedd:eeff'",
     "--ipv6-to-mac | grep -Fx 'aa:bb:cc:dd:ee:ff'", 0),
    # U/L bit flip back: 0x02 → 0x00
    ("echo 'fe80::250:56ff:feaa:bbcc'",
     "--ipv6-to-mac | grep -Fx '00:50:56:aa:bb:cc'", 0),
    # non-link-local EUI-64 also works (FF:FE is the only criterion)
    ("echo '2001:db8::a8bb:ccff:fedd:eeff'",
     "--ipv6-to-mac | grep -Fx 'aa:bb:cc:dd:ee:ff'", 0),
    # fe80 without FF:FE → no output
    ("echo 'fe80::1'",               f"--ipv6-to-mac {lines(0)}", 0),
    ("echo 'fe80::dead:beef'",       f"--ipv6-to-mac {lines(0)}", 0),
    # loopback → no output
    ("echo '::1'",                   f"--ipv6-to-mac {lines(0)}", 0),
    # IPv4 → no output
    ("echo '192.168.1.1'",           f"--ipv6-to-mac {lines(0)}", 0),
    # non-IP → no output
    ("echo 'notanip'",               f"--ipv6-to-mac {lines(0)}", 0),
    # round-trip: MAC → IPv6 → MAC
    ("echo 'aa:bb:cc:dd:ee:ff' | ./xips --mac-to-ipv6",
     "--ipv6-to-mac | grep -Fx 'aa:bb:cc:dd:ee:ff'", 0),
    ("echo '00:50:56:aa:bb:cc' | ./xips --mac-to-ipv6",
     "--ipv6-to-mac | grep -Fx '00:50:56:aa:bb:cc'", 0),
    # multiple EUI-64 addresses
    ("printf 'fe80::a8bb:ccff:fedd:eeff\\nfe80::250:56ff:feaa:bbcc\\n'",
     f"--ipv6-to-mac {lines(2)}", 0),
    # mixed EUI-64 and non-EUI-64 → only EUI-64 converted
    ("printf 'fe80::a8bb:ccff:fedd:eeff\\nfe80::1\\n'",
     f"--ipv6-to-mac {lines(1)}", 0),
)

# ---------------------------------------------------------------------------
# -s / --summarize: collapse IPs/ranges into minimal CIDR blocks
# ---------------------------------------------------------------------------
summarize_tests = (
    # single /24 stays as /24
    ("echo '192.168.1.0/24'",      f"-s {lines(1)}", 0),
    ("echo '192.168.1.0/24'",      "-s | grep -Fx '192.168.1.0/24'", 0),
    # two adjacent /25s collapse to one /24
    ("printf '192.168.1.0/25\\n192.168.1.128/25\\n'",
     "-s | grep -Fx '192.168.1.0/24'", 0),
    # four /27s collapse to one /25
    ("printf '192.168.1.0/27\\n192.168.1.32/27\\n192.168.1.64/27\\n192.168.1.96/27\\n'",
     "-s | grep -Fx '192.168.1.0/25'", 0),
    # individual IPs that fill a /30 → summarize to /30
    ("printf '10.0.0.0\\n10.0.0.1\\n10.0.0.2\\n10.0.0.3\\n'",
     "-s | grep -Fx '10.0.0.0/30'", 0),
    # --max-depth caps the prefix length
    ("echo '192.168.1.0/24'",      "-s -m 16 | grep -Fx '192.168.0.0/16'", 0),
    # IPv6 default max-depth is /64; with --max-depth-ipv6 128, collapse to exact prefix
    ("printf 'fe80::0\\nfe80::1\\nfe80::2\\nfe80::3\\n'",
     "-s --max-depth-ipv6 128 | grep -Fx 'fe80::/126'", 0),
    # single address with full depth → /128
    ("echo '::1'",                 "-s --max-depth-ipv6 128 | grep -Fx '::1/128'", 0),
    # default depth (64): single address collapses to its /64
    ("echo '::1'",                 "-s | grep -Fx '::/64'", 0),
)

# ---------------------------------------------------------------------------
# --mask: output subnet mask notation instead of CIDR in summarize mode
# ---------------------------------------------------------------------------
mask_format_tests = (
    ("echo '192.168.1.0/24'",  "-s       | grep -Fx '192.168.1.0/24'",              0),  # default: CIDR
    ("echo '192.168.1.0/24'",  "-s --mask | grep -Fx '192.168.1.0 255.255.255.0'",  0),
    ("echo '192.168.1.0/25'",  "-s --mask | grep -Fx '192.168.1.0 255.255.255.128'", 0),
    ("echo '10.0.0.0/24'",     "-s --mask | grep -Fx '10.0.0.0 255.255.255.0'",     0),
    ("echo '10.0.0.0/30'",     "-s --mask | grep -Fx '10.0.0.0 255.255.255.252'",   0),
)

# ---------------------------------------------------------------------------
# --asa: network-object output for Cisco ASA firewall syntax
# ---------------------------------------------------------------------------
asa_tests = (
    # single IP expansion with --asa
    ("echo '192.168.1.1'",
     "--asa | grep -Fx 'network-object host 192.168.1.1'", 0),
    ("echo '::1'",
     "--asa | grep -Fx 'network-object host ::1'", 0),
    # range expansion with --asa
    ("echo '192.168.1.0/30'",
     "--asa | grep -Fx 'network-object host 192.168.1.1'", 0),
    # summarize + --mask + --asa → network-object <net> <mask>
    ("echo '192.168.1.0/24'",
     "-s --mask --asa | grep -Fx 'network-object 192.168.1.0 255.255.255.0'", 0),
    # summarize + --asa alone → CIDR format (--asa only affects non-summarize output)
    ("echo '192.168.1.0/24'",
     "-s --asa | grep -Fx '192.168.1.0/24'", 0),
)

# ---------------------------------------------------------------------------
# Guard rail: warn and skip ranges > 1000000 addresses; --anyway bypasses
# ---------------------------------------------------------------------------
guardrail_tests = (
    # large IPv4 range produces no stdout (warning goes to stderr)
    ("echo '10.0.0.0/8'",              f"2>/dev/null {lines(0)}", 0),
    # exactly 1000000 addresses: no warning, output flows
    ("echo '10.0.0.0-10.15.66.63'",   lines(1000000), 0),
    # 1000001 addresses: blocked without --anyway
    ("echo '10.0.0.0-10.15.66.64'",   f"2>/dev/null {lines(0)}", 0),
    # --anyway bypasses the guard; verify via --count
    ("echo '10.0.0.0-10.15.66.64'",
     "--anyway --count | awk '{if ($1 == 1000001) exit 0; else exit 1}'", 0),
    # small ranges still work normally
    ("echo '192.168.1.0/30'",          lines(4), 0),
    # large IPv6 range blocked
    ("echo 'fe80::/10'",               f"2>/dev/null {lines(0)}", 0),
    # small IPv6 range unaffected
    ("echo 'fe80::1-fe80::4'",         lines(4), 0),
)

# ---------------------------------------------------------------------------
# CRLF tolerance: strip trailing \r from Windows-style line endings
# ---------------------------------------------------------------------------
crlf_tests = (
    ("printf '192.168.1.1\\r\\n'",          lines(1), 0),
    ("printf '192.168.1.0/30\\r\\n'",       lines(4), 0),
    ("printf '192.168.1.1\\r\\n192.168.1.2\\r\\n'", lines(2), 0),
)

# ---------------------------------------------------------------------------
# --aton: old inet_aton behavior (e.g. "10.5" → 10.0.0.5)
# ---------------------------------------------------------------------------
aton_tests = (
    ("echo '10.5'",       f"--aton {lines(1)}", 0),
    ("echo '10.5'",       f"{lines(0)}", 0),    # not recognised without --aton
)

# ---------------------------------------------------------------------------
# File input: -f / --file
# ---------------------------------------------------------------------------
# These are emitted separately (write content to temp file, invoke with -f)
file_tests = (
    # (content_cmd, xips_suffix, expected)
    ("echo '192.168.1.1'",            "",                        lines(1)),
    ("echo 'rabbit'",                 "",                        lines(0)),
    ("echo '192.168.1.0/30'",         "",                        lines(4)),
    ("echo '192.168.1.0/30'",         "--hosts",                 lines(2)),
    ("printf '192.168.1.1\\n192.168.1.1\\n'", "--sort",          lines(1)),
    ("echo '192.168.1.1'",            "--port 80",               f"| grep -Fx '192.168.1.1:80'"),
)


# ---------------------------------------------------------------------------
# --exclude: subtract ranges from expansion
# ---------------------------------------------------------------------------
exclude_tests = (
    # /24 minus /29 = 256 - 8 = 248
    ("echo '192.168.1.0/24'",
     f"--exclude 192.168.1.0/29 {lines(248)}", 0),
    # excluded address is absent
    ("echo '192.168.1.0/24'",
     "--exclude 192.168.1.0/29 | grep -Fx '192.168.1.0'", 1),  # absent
    # address just past excluded block is present
    ("echo '192.168.1.0/24'",
     "--exclude 192.168.1.0/29 | grep -Fx '192.168.1.8'", 0),
    # two --exclude flags (256 - 8 - 64 = 184)
    ("echo '192.168.1.0/24'",
     f"--exclude 192.168.1.0/29 --exclude 192.168.1.64/26 {lines(184)}", 0),
    # exclude entire range → empty output
    ("echo '192.168.1.0/30'",
     f"--exclude 192.168.1.0/30 {lines(0)}", 0),
    # IPv6 exclude (use /126 = 4 addresses: fe80::4-7)
    ("echo 'fe80::0-fe80::f'",
     "--exclude fe80::4/126 | grep -Fx 'fe80::4'", 1),  # fe80::4 is excluded
    ("echo 'fe80::0-fe80::f'",
     "--exclude fe80::4/126 | grep -Fx 'fe80::8'", 0),  # fe80::8 is present
)

# ---------------------------------------------------------------------------
# --cidr-list: decompose range into exact covering CIDR blocks
# ---------------------------------------------------------------------------
cidr_list_tests = (
    # clean /24 range collapses to one /24 (tree-based: all 256 addresses present)
    ("echo '192.168.1.0/24'",      "--cidr-list | grep -Fx '192.168.1.0/24'", 0),
    ("echo '192.168.1.0/24'",      f"--cidr-list {lines(1)}", 0),
    # individual IPs that fill a /30 collapse to that /30
    ("printf '192.168.1.0\\n192.168.1.1\\n192.168.1.2\\n192.168.1.3\\n'",
     "--cidr-list | grep -Fx '192.168.1.0/30'", 0),
    ("printf '192.168.1.0\\n192.168.1.1\\n192.168.1.2\\n192.168.1.3\\n'",
     f"--cidr-list {lines(1)}", 0),
    # single IP gets /32
    ("echo '1.2.3.4'",             "--cidr-list | grep -Fx '1.2.3.4/32'", 0),
    # partial block: 3 of 4 addresses → 2 blocks (/31 + /32)
    ("printf '192.168.1.0\\n192.168.1.1\\n192.168.1.2\\n'",
     f"--cidr-list {lines(2)}", 0),
    # arbitrary range 192.168.1.5-20 → 5 blocks
    ("echo '192.168.1.5-192.168.1.20'",
     f"--cidr-list {lines(5)}", 0),
    ("echo '192.168.1.5-192.168.1.20'",
     "--cidr-list | grep -Fx '192.168.1.8/29'", 0),
    # --threshold 100 still uses strict > so behaves like threshold 99 at capacity;
    # use --cidr-list for exact cover (tested above)
    ("printf '192.168.1.0\\n192.168.1.1\\n192.168.1.2\\n192.168.1.3\\n'",
     "--cidr-list | grep -Fx '192.168.1.0/30'", 0),
    # IPv6 clean CIDR collapses to itself
    ("echo '2001:db8::/126'",      "--cidr-list | grep -Fx '2001:db8::/126'", 0),
    ("echo '2001:db8::/126'",      f"--cidr-list {lines(1)}", 0),
    # IPv6 single address gets /128
    ("echo '::1'",                 "--cidr-list | grep -Fx '::1/128'", 0),
    # IPv6 arbitrary range: fe80::1-7 → 3 blocks
    ("echo 'fe80::1-fe80::7'",     f"--cidr-list {lines(3)}", 0),
)

# ---------------------------------------------------------------------------
# --hex-be / --hex-le: hex output; hex input parsing
# ---------------------------------------------------------------------------
hex_tests = (
    # big-endian hex output
    ("echo '192.168.1.1'",   "--hex-be | grep -Fx '0xc0a80101'",  0),
    ("echo '192.168.1.1'",   "--hex    | grep -Fx '0xc0a80101'",  0),  # --hex alias
    # little-endian output (bytes reversed)
    ("echo '192.168.1.1'",   "--hex-le | grep -Fx '0x0101a8c0'",  0),
    # 0.0.0.0
    ("echo '0.0.0.0'",       "--hex-be | grep -Fx '0x00000000'",  0),
    # 255.255.255.255
    ("echo '255.255.255.255'", "--hex-be | grep -Fx '0xffffffff'", 0),
    # hex input: 0x prefix
    ("echo '0xc0a80101'",    "| grep -Fx '192.168.1.1'",           0),
    # hex input: bare
    ("echo 'c0a80101'",      "| grep -Fx '192.168.1.1'",           0),
    # hex input: \x escape
    (r"printf '\\xc0\\xa8\\x01\\x01'", "| grep -Fx '192.168.1.1'", 0),
    # hex input: URL encoding
    ("echo '%c0%a8%01%01'",  "| grep -Fx '192.168.1.1'",           0),
    # hex input: binary prefix (32 bits)
    ("echo '0b11000000101010000000000100000001'", "| grep -Fx '192.168.1.1'", 0),
    # hex input: 0x00000000 → 0.0.0.0
    ("echo '0x00000000'",    "| grep -Fx '0.0.0.0'",                0),
    # IPv6 hex-be output
    ("echo '::1'",  "--hex-be | grep -Fx '0x00000000000000000000000000000001'", 0),
    # IPv6 hex-le output
    ("echo '::1'",  "--hex-le | grep -Fx '0x01000000000000000000000000000000'", 0),
)

# ---------------------------------------------------------------------------
# --to-mapped: IPv4 output as ::ffff:x.x.x.x; v4mapped auto-detect on input
# ---------------------------------------------------------------------------
v4mapped_tests = (
    # --to-mapped: singleton IPv4 → mapped form
    ("echo '10.0.0.1'",       "--to-mapped | grep -Fx '::ffff:10.0.0.1'", 0),
    ("echo '192.168.1.1'",    "--to-mapped | grep -Fx '::ffff:192.168.1.1'", 0),
    # --to-mapped: range expansion → each in mapped form
    ("echo '192.168.1.0/30'", "--to-mapped | grep -Fx '::ffff:192.168.1.0'", 0),
    ("echo '192.168.1.0/30'", f"--to-mapped {lines(4)}", 0),
    # auto-detect: ::ffff:x.x.x.x singleton input treated as IPv4
    ("echo '::ffff:10.0.0.1'",      "| grep -Fx '10.0.0.1'", 0),
    ("echo '::ffff:192.168.1.1'",   "| grep -Fx '192.168.1.1'", 0),
    # auto-detect: explicit v4mapped range (both endpoints mapped) treated as IPv4
    ("echo '::ffff:10.0.0.1-::ffff:10.0.0.4'", f"{lines(4)}", 0),
    ("echo '::ffff:10.0.0.1-::ffff:10.0.0.4'", "| grep -Fx '10.0.0.3'", 0),
    # auto-detect + --to-mapped: input in mapped form, output in mapped form
    ("echo '::ffff:10.0.0.1'",      "--to-mapped | grep -Fx '::ffff:10.0.0.1'", 0),
    ("echo '::ffff:10.0.0.1-::ffff:10.0.0.4'", "--to-mapped | grep -Fx '::ffff:10.0.0.3'", 0),
)


# ---------------------------------------------------------------------------
# --arpa: reverse-DNS zone name output
# ---------------------------------------------------------------------------
arpa_tests = (
    # IPv4 singleton
    ("echo '1.2.3.4'",        "--arpa | grep -Fx '4.3.2.1.in-addr.arpa'", 0),
    ("echo '192.168.1.1'",    "--arpa | grep -Fx '1.1.168.192.in-addr.arpa'", 0),
    ("echo '10.0.0.1'",       "--arpa | grep -Fx '1.0.0.10.in-addr.arpa'", 0),
    # IPv4 range expansion — each address gets arpa form
    ("echo '192.168.1.0/30'", "--arpa | grep -Fx '0.1.168.192.in-addr.arpa'", 0),
    ("echo '192.168.1.0/30'", f"--arpa {lines(4)}", 0),
    # IPv6 singleton
    ("echo '::1'",            "--arpa | grep -Fx '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa'", 0),
    ("echo '2001:db8::1'",    "--arpa | grep -Fx '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa'", 0),
    # IPv6 range expansion
    ("echo '::1-::2'",        f"--arpa {lines(2)}", 0),
)


# ---------------------------------------------------------------------------
# --subnet-info: TSV one-row-per-subnet with subnet metadata
# ---------------------------------------------------------------------------
subnet_info_tests = (
    # /24 header row + 1 data row = 2 lines total
    ("echo '192.168.1.0/24'",   f"--subnet-info {lines(2)}", 0),
    # /24 Network field
    ("echo '192.168.1.0/24'",   "--subnet-info | grep -F '192.168.1.0'", 0),
    # /24 Broadcast field
    ("echo '192.168.1.0/24'",   "--subnet-info | grep -F '192.168.1.255'", 0),
    # /24 Mask field
    ("echo '192.168.1.0/24'",   "--subnet-info | grep -F '255.255.255.0'", 0),
    # /24 Wildcard field
    ("echo '192.168.1.0/24'",   "--subnet-info | grep -F '0.0.0.255'", 0),
    # /24 Prefix field
    ("echo '192.168.1.0/24'",   "--subnet-info | grep -F '/24'", 0),
    # /32 single host: Total=1, Usable=1
    ("echo '10.0.0.1'",         "--subnet-info | grep -F '10.0.0.1'", 0),
    ("echo '10.0.0.1'",         "--subnet-info | grep -F '/32'", 0),
    # arbitrary range: Mask = N/A
    ("echo '192.168.1.5-192.168.1.100'", "--subnet-info | grep -F 'N/A'", 0),
    # multiple subnets → header + 2 data rows = 3 lines
    ("printf '10.0.0.0/8\\n192.168.1.0/24\\n'", f"--subnet-info {lines(3)}", 0),
    # IPv6 /128
    ("echo '::1'",              "--subnet-info | grep -F '/128'", 0),
    # IPv6 Broadcast = N/A
    ("echo '::1'",              "--subnet-info | grep -F 'N/A'", 0),
    # IPv6 /64 total = 2^64
    ("echo 'fe80::/64'",        "--subnet-info | grep -F '2^64'", 0),
)

# ---------------------------------------------------------------------------
# --check / --check-file: test if IPs/subnets are within input ranges
# ---------------------------------------------------------------------------
check_tests = (
    # single IP contained in /8
    ("echo '10.0.0.0/8'",
     "--check 10.5.5.5 | grep -F 'Contained in'", 0),
    # single IP not in range
    ("echo '10.0.0.0/8'",
     "--check 192.168.1.1 | grep -F 'No overlap'", 0),
    # subnet fully contained
    ("echo '10.0.0.0/8'",
     "--check 10.0.0.0/24 | grep -F 'Contained in'", 0),
    # partial overlap shows Overlaps
    ("echo '10.0.0.0 - 10.0.0.100'",
     "--check 10.0.0.50/25 | grep -F 'Overlaps'", 0),
    # check range larger than input: overlaps
    ("echo '192.168.1.0/24'",
     "--check 192.168.0.0/16 | grep -F 'Overlaps'", 0),
    # multiple --check flags each produce a result
    ("printf '10.0.0.0/8\\n192.168.0.0/16\\n'",
     f"--check 10.5.5.5 --check 172.16.0.1 {lines(6)}", 0),
    # IPv6 contained
    ("echo 'fe80::/10'",
     "--check fe80::1 | grep -F 'Contained in'", 0),
    # IPv6 no overlap
    ("echo 'fe80::/10'",
     "--check ::1 | grep -F 'No overlap'", 0),
)


diff_tests = (
    # Complete exclusion: same range in both → no output
    ("echo '10.0.0.0/24'",
     "--diff <(echo '10.0.0.0/24') | wc -l | awk '{if ($1 == 0) exit 0; else exit 1}'", 0),
    # No overlap: diff file doesn't touch input → full output
    ("echo '192.168.1.0/24'",
     "--diff <(echo '10.0.0.0/8') | grep -Fx '192.168.1.0/24'", 0),
    # Partial exclusion: /16 minus /24 → pipe to second xips for count
    ("echo '10.0.0.0/16'",
     "--diff <(echo '10.0.5.0/24') | ./xips --count | awk '{if ($1 == 65280) exit 0; else exit 1}'", 0),
    # Two exclusions in diff file
    ("echo '10.0.0.0/16'",
     "--diff <(printf '10.0.5.0/24\\n10.0.6.0/24\\n') | ./xips --count | awk '{if ($1 == 65024) exit 0; else exit 1}'", 0),
    # Range notation in diff file
    ("echo '10.0.5.0/24'",
     "--diff <(echo '10.0.5.1-10.0.5.10') | ./xips --count | awk '{if ($1 == 246) exit 0; else exit 1}'", 0),
    # Single IP excluded
    ("echo '10.0.0.0/24'",
     "--diff <(echo '10.0.0.5') | grep -v '10.0.0.5' | grep -F '10.0.0.'", 0),
    # Single IP not in diff → kept
    ("echo '10.0.0.5'",
     "--diff <(echo '192.168.1.0/24') | grep -Fx '10.0.0.5'", 0),
    # IPv6: complete exclusion
    ("echo '2001:db8::/32'",
     "--diff <(echo '2001:db8::/32') | wc -l | awk '{if ($1 == 0) exit 0; else exit 1}'", 0),
    # IPv6: partial exclusion produces output
    ("echo '2001:db8::/32'",
     "--diff <(echo '2001:db8:1::/48') | grep -F '2001:db8'", 0),
)


def run_file_test(content_cmd, xips_args, suffix):
    """
    Generates:
        content_cmd > /tmp/xips_test.txt && ./xips -f /tmp/xips_test.txt xips_args suffix > /dev/null
    suffix is the post-processing pipeline (lines(...), grep -Fx, etc).
    Expected exit code is always 0 because lines() and has() both normalise to 0-means-pass.
    Special case: grep check expected is 0 (found).
    """
    # For lines()-style checks, expected is always 0.
    # For grep checks (has()), expected is also 0 (found).
    # We detect by whether suffix starts with '|'.
    if suffix.startswith("|"):
        full = f"{content_cmd} > /tmp/xips_test.txt && ./xips -f /tmp/xips_test.txt {xips_args} {suffix} > /dev/null"
        expected = 0
    else:
        # suffix is a lines() result that starts with '| wc...'
        full = f"{content_cmd} > /tmp/xips_test.txt && ./xips -f /tmp/xips_test.txt {xips_args} {suffix} > /dev/null"
        expected = 0
    print(full)
    print("if [ $? -eq", expected, "]; then\ntrue\nelse\n\techo FAIL. File test:",
          content_cmd, "xips_args:", xips_args, "\nfi\n")


def main(argv=None):
    print("#!/bin/bash")
    print()

    print("# --- Basic IPv4 expansion ---")
    for t in expansion_tests:
        run_test(*t)

    print("# --- IPv6 expansion ---")
    for t in ipv6_expansion_tests:
        run_test(*t)

    print("# --- Protocol filter (-4 / -6) ---")
    for t in ipv4_ipv6_filter_tests:
        run_test(*t)

    print("# --- --sort (dedup + numeric order) ---")
    for t in sort_tests:
        run_test(*t)

    print("# --- --count ---")
    for t in count_tests:
        run_test(*t)

    print("# --- --shuffle ---")
    for t in shuffle_tests:
        run_test(*t)

    print("# --- --port N ---")
    for t in port_tests:
        run_test(*t)

    print("# --- --private ---")
    for t in private_tests:
        run_test(*t)

    print("# --- --public ---")
    for t in public_tests:
        run_test(*t)

    print("# --- --hosts (exclude network/broadcast) ---")
    for t in hosts_tests:
        run_test(*t)

    print("# --- --mac-to-ipv6 ---")
    for t in mac_to_ipv6_tests:
        run_test(*t)

    print("# --- --ipv6-to-mac ---")
    for t in ipv6_to_mac_tests:
        run_test(*t)

    print("# --- -s / --summarize ---")
    for t in summarize_tests:
        run_test(*t)

    print("# --- --mask output format ---")
    for t in mask_format_tests:
        run_test(*t)

    print("# --- --asa output format ---")
    for t in asa_tests:
        run_test(*t)

    print("# --- guard rail (>1M address warning) ---")
    for t in guardrail_tests:
        run_test(*t)

    print("# --- CRLF tolerance ---")
    for t in crlf_tests:
        run_test(*t)

    print("# --- --aton (old inet_aton behavior) ---")
    for t in aton_tests:
        run_test(*t)

    print("# --- file input (-f) ---")
    for content_cmd, xips_args, suffix in file_tests:
        run_file_test(content_cmd, xips_args, suffix)

    print("# --- --exclude (subtract ranges) ---")
    for t in exclude_tests:
        run_test(*t)

    print("# --- --cidr-list (exact covering CIDRs) ---")
    for t in cidr_list_tests:
        run_test(*t)

    print("# --- --hex-be / --hex-le output; hex input ---")
    for t in hex_tests:
        run_test(*t)

    print("# --- --to-mapped / v4mapped auto-detect ---")
    for t in v4mapped_tests:
        run_test(*t)

    print("# --- --arpa reverse-DNS zone output ---")
    for t in arpa_tests:
        run_test(*t)

    print("# --- --subnet-info TSV output ---")
    for t in subnet_info_tests:
        run_test(*t)

    print("# --- --check / --check-file range overlap ---")
    for t in check_tests:
        run_test(*t)

    print("# --- --diff set subtraction ---")
    for t in diff_tests:
        run_test(*t)

    print("echo Test Complete")
    return 0


if __name__ == "__main__":
    sys.exit(main())
