#!/bin/bash
# shunit2 tests for pbr_get_gateway4 and pbr_get_gateway6 gateway-discovery.
# Covers: successful discovery via each code path, sentinel "empty" values,
# warningInterfaceRoutingUnknownGateway when every fallback produces no
# gateway, and point-to-point suppression of that warning — for both stacks.

# ---------------------------------------------------------------------------
# Load both functions under test with all external symbols stubbed
# ---------------------------------------------------------------------------

_load_functions_under_test() {
	# Uplink-remap guards – both return 1 so the iface argument passes through.
	is_uplink6() { return 1; }
	is_uplink4() { return 1; }

	# Gateway-lookup stubs – safe no-ops; individual tests override.
	network_get_gateway()  { return 0; }
	network_get_gateway6() { return 0; }

	# json stub – individual tests override; overridden again in setUp.
	json() { :; }

	# --- IPv4 ---
	pbr_get_gateway4() {
		local iface="$2" dev="$3" gw
		is_uplink6 "$iface" && iface="$uplink_interface4"
		network_get_gateway gw "$iface" true
		if [ -z "$gw" ] || [ "$gw" = '0.0.0.0' ]; then
			gw="$(ip -4 route show dev "$dev" table all 2>/dev/null | awk '$1=="default" {for (i=1; i<NF; i++) if ($i=="via") {print $(i+1); exit}}')"
			[ -z "$gw" ] && gw="$(ip -4 route show dev "$dev" table all 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="via") {print $(i+1); exit}}')"
			[ -z "$gw" ] && gw="$(ip -4 route get 1.1.1.1 oif "$dev" 2>/dev/null | awk '/via/ {print $3; exit}')"
			{ [ -z "$gw" ] && ! ip address show dev "$dev" 2>/dev/null | grep -q "POINTOPOINT"; } && json add warning 'warningInterfaceRoutingUnknownGateway' "$dev"
		fi
		eval "$1"='$gw'
	}

	# --- IPv6 ---
	pbr_get_gateway6() {
		local iface="$2" dev="$3" gw
		is_uplink4 "$iface" && iface="$uplink_interface6"
		network_get_gateway6 gw "$iface" true
		if [ -z "$gw" ] || [ "$gw" = '::/0' ] || [ "$gw" = '::0/0' ] || [ "$gw" = '::' ]; then
			gw="$(ip -6 route show dev "$dev" table all 2>/dev/null | awk '$1=="default" {for (i=1; i<NF; i++) if ($i=="via") {print $(i+1); exit}}')"
			[ -z "$gw" ] && gw="$(ip -6 route show dev "$dev" table all 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="via") {print $(i+1); exit}}')"
			[ -z "$gw" ] && gw="$(ip -6 neigh show dev "$dev" 2>/dev/null | awk '/^fe80:.*router/ {print $1; exit}')"
			{ [ -z "$gw" ] && ! ip address show dev "$dev" 2>/dev/null | grep -q "POINTOPOINT"; } && json add warning 'warningInterfaceRoutingUnknownGateway' "$dev"
		fi
		eval "$1"='$gw'
	}
}

# ---------------------------------------------------------------------------
# Per-test setup
# ---------------------------------------------------------------------------

setUp() {
	_JSON_CALLS=""
	_GW_RESULT=""

	network_get_gateway()  { return 0; }
	network_get_gateway6() { return 0; }
	ip()                   { :; }
	json()                 { _JSON_CALLS="${_JSON_CALLS}$*|"; }
}

# ===========================================================================
# IPv4 tests  (prefix: test_4_)
# ===========================================================================

# ---------------------------------------------------------------------------
# Happy path: network_get_gateway returns a valid address
# ---------------------------------------------------------------------------

test_4_successful_gateway_via_network_get_gateway() {
	network_get_gateway() { eval "$1='192.168.1.1'"; }

	pbr_get_gateway4 _GW_RESULT eth0 eth0

	assertEquals "gw should be populated"      "192.168.1.1" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""            "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Sentinel '0.0.0.0' falls through; ip -4 route default awk finds a gateway
# ---------------------------------------------------------------------------

test_4_sentinel_zero_falls_through_to_route_default() {
	network_get_gateway() { eval "$1='0.0.0.0'"; }

	ip() { [ "$1 $2" = "-4 route" ] && echo "default via 10.0.0.1 dev eth0"; }

	pbr_get_gateway4 _GW_RESULT eth0 eth0

	assertEquals "gw should be 10.0.0.1"       "10.0.0.1" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""         "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# First fallback: ip -4 route show default awk finds a gateway
# ---------------------------------------------------------------------------

test_4_successful_gateway_via_ip4_route_default() {
	ip() { [ "$1 $2" = "-4 route" ] && echo "default via 10.0.0.1 dev eth0"; }

	pbr_get_gateway4 _GW_RESULT eth0 eth0

	assertEquals "gw should be 10.0.0.1"       "10.0.0.1" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""         "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Second fallback: no default keyword; any-via awk finds a gateway
# ---------------------------------------------------------------------------

test_4_successful_gateway_via_ip4_route_any_via() {
	ip() { [ "$1 $2" = "-4 route" ] && echo "10.0.0.0/24 via 172.16.0.1 dev eth0"; }

	pbr_get_gateway4 _GW_RESULT eth0 eth0

	assertEquals "gw should be 172.16.0.1"     "172.16.0.1" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""           "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Third fallback: ip -4 route get succeeds
# ---------------------------------------------------------------------------

test_4_successful_gateway_via_ip4_route_get() {
	ip() {
		case "$1 $2 $3" in
			"-4 route show") : ;;
			"-4 route get")  echo "1.1.1.1 via 192.168.50.1 dev eth0 src 192.168.50.10" ;;
		esac
	}

	pbr_get_gateway4 _GW_RESULT eth0 eth0

	assertEquals "gw should be 192.168.50.1"   "192.168.50.1" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""             "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# All fallbacks fail; interface is NOT point-to-point → warning must fire
# ---------------------------------------------------------------------------

test_4_warning_emitted_when_no_gateway_and_not_ptp() {
	ip() {
		case "$1 $2" in
			"-4 route")     : ;;
			"address show") echo "2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
		esac
	}

	pbr_get_gateway4 _GW_RESULT eth0 eth0

	assertEquals "gw result should be empty" "" "$_GW_RESULT"
	echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway"
	assertTrue "warning token should be present in json call" $?
	echo "$_JSON_CALLS" | grep -q "eth0"
	assertTrue "device name should appear in json call" $?
}

# ---------------------------------------------------------------------------
# All fallbacks fail; interface IS point-to-point → no warning
# ---------------------------------------------------------------------------

test_4_no_warning_when_no_gateway_but_ptp_interface() {
	ip() {
		case "$1 $2" in
			"-4 route")     : ;;
			"address show") echo "3: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP>" ;;
		esac
	}

	pbr_get_gateway4 _GW_RESULT ppp0 ppp0

	assertEquals "gw result should be empty"         "" "$_GW_RESULT"
	assertEquals "no warning for point-to-point link" "" "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Sentinel '0.0.0.0' from network_get_gateway, all fallbacks fail → warning
# ---------------------------------------------------------------------------

test_4_warning_emitted_when_gw_is_zero_and_fallbacks_fail() {
	network_get_gateway() { eval "$1='0.0.0.0'"; }

	ip() {
		case "$1 $2" in
			"-4 route")     : ;;
			"address show") echo "2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
		esac
	}

	pbr_get_gateway4 _GW_RESULT eth1 eth1

	assertEquals "gw result should be empty" "" "$_GW_RESULT"
	echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway"
	assertTrue "warning token should be present in json call" $?
	echo "$_JSON_CALLS" | grep -q "eth1"
	assertTrue "device name should appear in json call" $?
}

# ===========================================================================
# IPv6 tests  (prefix: test_6_)
# ===========================================================================

# ---------------------------------------------------------------------------
# Happy path: network_get_gateway6 returns a valid global unicast address
# ---------------------------------------------------------------------------

test_6_successful_gateway_via_network_get_gateway6() {
	network_get_gateway6() { eval "$1='2001:db8::1'"; }

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw should be populated"      "2001:db8::1" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""            "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Each sentinel value is treated as absent; route default fallback succeeds
# ---------------------------------------------------------------------------

test_6_sentinel_colon_slash_zero_falls_through_to_route_default() {
	network_get_gateway6() { eval "$1='::/0'"; }

	ip() { [ "$1 $2" = "-6 route" ] && echo "default via fe80::1 dev eth0"; }

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw should be fe80::1"        "fe80::1" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""        "$_JSON_CALLS"
}

test_6_sentinel_double_colon_zero_slash_zero_falls_through_to_route_default() {
	network_get_gateway6() { eval "$1='::0/0'"; }

	ip() { [ "$1 $2" = "-6 route" ] && echo "default via fe80::2 dev eth0"; }

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw should be fe80::2"        "fe80::2" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""        "$_JSON_CALLS"
}

test_6_sentinel_double_colon_falls_through_to_route_default() {
	network_get_gateway6() { eval "$1='::'"; }

	ip() { [ "$1 $2" = "-6 route" ] && echo "default via fe80::3 dev eth0"; }

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw should be fe80::3"        "fe80::3" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""        "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# First fallback: ip -6 route show default awk finds a gateway
# ---------------------------------------------------------------------------

test_6_successful_gateway_via_ip6_route_default() {
	ip() { [ "$1 $2" = "-6 route" ] && echo "default via 2001:db8::fffe dev eth0 metric 100"; }

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw should be 2001:db8::fffe" "2001:db8::fffe" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""               "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Second fallback: no default keyword; any-via awk finds a gateway
# ---------------------------------------------------------------------------

test_6_successful_gateway_via_ip6_route_any_via() {
	ip() { [ "$1 $2" = "-6 route" ] && echo "2001:db8::/32 via fe80::aabb dev eth0"; }

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw should be fe80::aabb"     "fe80::aabb" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""           "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Third fallback: ip -6 neigh show surfaces a link-local router neighbor
# ---------------------------------------------------------------------------

test_6_successful_gateway_via_ip6_neigh_router() {
	ip() {
		case "$1 $2" in
			"-6 route") : ;;
			"-6 neigh")  echo "fe80::cafe dev eth0 lladdr aa:bb:cc:dd:ee:ff router" ;;
		esac
	}

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw should be fe80::cafe"     "fe80::cafe" "$_GW_RESULT"
	assertEquals "no warning should be emitted" ""           "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# Neighbor present but NOT flagged router → not used; warning fires
# (the awk pattern /^fe80:.*router/ requires the 'router' keyword)
# ---------------------------------------------------------------------------

test_6_neigh_without_router_flag_not_used_as_gateway() {
	ip() {
		case "$1 $2" in
			"-6 route")     : ;;
			"-6 neigh")      echo "fe80::dead dev eth0 lladdr aa:bb:cc:dd:ee:ff STALE" ;;
			"address show") echo "2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
		esac
	}

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw result should be empty" "" "$_GW_RESULT"
	echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway"
	assertTrue "warning should fire when non-router neigh is the only candidate" $?
}

# ---------------------------------------------------------------------------
# All fallbacks fail; interface is NOT point-to-point → warning must fire
# ---------------------------------------------------------------------------

test_6_warning_emitted_when_no_gateway_and_not_ptp() {
	ip() {
		case "$1 $2" in
			"-6 route")     : ;;
			"-6 neigh")      : ;;
			"address show") echo "2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
		esac
	}

	pbr_get_gateway6 _GW_RESULT eth0 eth0

	assertEquals "gw result should be empty" "" "$_GW_RESULT"
	echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway"
	assertTrue "warning token should be present in json call" $?
	echo "$_JSON_CALLS" | grep -q "eth0"
	assertTrue "device name should appear in json call" $?
}

# ---------------------------------------------------------------------------
# All fallbacks fail; interface IS point-to-point → no warning
# ---------------------------------------------------------------------------

test_6_no_warning_when_no_gateway_but_ptp_interface() {
	ip() {
		case "$1 $2" in
			"-6 route")     : ;;
			"-6 neigh")      : ;;
			"address show") echo "3: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP>" ;;
		esac
	}

	pbr_get_gateway6 _GW_RESULT ppp0 ppp0

	assertEquals "gw result should be empty"         "" "$_GW_RESULT"
	assertEquals "no warning for point-to-point link" "" "$_JSON_CALLS"
}

# ---------------------------------------------------------------------------
# All three IPv6 sentinel values → each triggers the fallback chain and, when
# that also fails, the warning.  Parametric helper keeps it DRY.
# ---------------------------------------------------------------------------

_assert_6_sentinel_triggers_warning() {
	local sentinel="$1" dev="eth2"

	network_get_gateway6() { eval "$1='$sentinel'"; }
	ip() {
		case "$1 $2" in
			"-6 route")     : ;;
			"-6 neigh")      : ;;
			"address show") echo "2: $dev: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
		esac
	}
	_JSON_CALLS=""
	_GW_RESULT=""

	pbr_get_gateway6 _GW_RESULT "$dev" "$dev"

	assertEquals "gw empty for sentinel '$sentinel'"  ""  "$_GW_RESULT"
	echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway"
	assertTrue   "warning fires for sentinel '$sentinel'" $?
}

test_6_all_three_sentinels_each_trigger_warning() {
	_assert_6_sentinel_triggers_warning '::/0'
	_assert_6_sentinel_triggers_warning '::0/0'
	_assert_6_sentinel_triggers_warning '::'
}

# ---------------------------------------------------------------------------

_load_functions_under_test

# shellcheck disable=SC1091
. shunit2
