package shared

import (
	"os"
	"strings"
	"testing"
)

// Closes audit R-001's worst case (accidental non-loopback deploy)
// at the predicate layer. Run integration coverage lives in the
// existing smoke chain — this file proves the rules.

func TestIsLoopbackAddr(t *testing.T) {
	cases := []struct {
		name string
		addr string
		want bool
	}{
		// Pass cases — every shape we accept.
		{"127.0.0.1 standard", "127.0.0.1:3214", true},
		{"127.0.0.0/8 mid-range", "127.5.6.7:3214", true},
		{"127.255.255.254 edge", "127.255.255.254:3214", true},
		{"IPv6 loopback", "[::1]:3214", true},
		{"localhost hostname", "localhost:3214", true},

		// Reject cases — every shape that should fail-loud.
		{"empty addr", "", false},
		{"empty host (all interfaces)", ":3214", false},
		{"explicit any IPv4", "0.0.0.0:3214", false},
		{"explicit any IPv6", "[::]:3214", false},
		{"public IPv4", "8.8.8.8:3214", false},
		{"private LAN IPv4", "192.168.1.176:3214", false},
		{"hostname (not localhost)", "myhost.example.com:3214", false},
		{"missing port", "127.0.0.1", false},
		{"garbage", "not an addr", false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := isLoopbackAddr(tc.addr)
			if got != tc.want {
				t.Errorf("isLoopbackAddr(%q) = %v, want %v", tc.addr, got, tc.want)
			}
		})
	}
}

func TestRequireLoopback_AcceptsLoopback(t *testing.T) {
	if err := requireLoopbackOrOverride("queryd", "127.0.0.1:3214"); err != nil {
		t.Errorf("loopback should pass without env, got %v", err)
	}
	if err := requireLoopbackOrOverride("vectord", "[::1]:3215"); err != nil {
		t.Errorf("IPv6 loopback should pass, got %v", err)
	}
}

func TestRequireLoopback_RejectsNonLoopback(t *testing.T) {
	cases := []string{
		"0.0.0.0:3214",
		":3214",
		"192.168.1.176:3214",
	}
	for _, addr := range cases {
		t.Run(addr, func(t *testing.T) {
			err := requireLoopbackOrOverride("queryd", addr)
			if err == nil {
				t.Fatalf("expected error on %q without override, got nil", addr)
			}
			// Error message should cite the override env so operators
			// can quickly see how to opt in if intentional.
			if !strings.Contains(err.Error(), "LH_QUERYD_ALLOW_NONLOOPBACK") {
				t.Errorf("error should cite override env, got %q", err.Error())
			}
			// And reference R-001 so the audit trail is explicit.
			if !strings.Contains(err.Error(), "R-001") {
				t.Errorf("error should cite R-001, got %q", err.Error())
			}
		})
	}
}

func TestRequireLoopback_OverrideEnvAllowsNonLoopback(t *testing.T) {
	t.Setenv("LH_QUERYD_ALLOW_NONLOOPBACK", "1")
	if err := requireLoopbackOrOverride("queryd", "0.0.0.0:3214"); err != nil {
		t.Errorf("override should permit non-loopback, got %v", err)
	}
}

func TestRequireLoopback_OverrideEnvOnlyApplies_ExactValue1(t *testing.T) {
	// "true", "yes", anything else != "1" should NOT trigger the
	// override. Strict matching prevents silent acceptance of typos.
	cases := []string{"true", "yes", "TRUE", "01", " 1", ""}
	for _, val := range cases {
		t.Run("val="+val, func(t *testing.T) {
			t.Setenv("LH_QUERYD_ALLOW_NONLOOPBACK", val)
			err := requireLoopbackOrOverride("queryd", "0.0.0.0:3214")
			if err == nil {
				t.Fatalf("override value %q should NOT permit non-loopback", val)
			}
		})
	}
}

func TestRequireLoopback_EnvIsPerService(t *testing.T) {
	// Setting queryd's override should NOT affect vectord. Each binary
	// must opt in explicitly so a single-service exposure decision
	// doesn't silently apply to others.
	t.Setenv("LH_QUERYD_ALLOW_NONLOOPBACK", "1")
	// queryd allowed:
	if err := requireLoopbackOrOverride("queryd", "0.0.0.0:3214"); err != nil {
		t.Errorf("queryd should be allowed, got %v", err)
	}
	// vectord still rejected:
	if err := requireLoopbackOrOverride("vectord", "0.0.0.0:3215"); err == nil {
		t.Error("vectord should still be rejected — env is per-service")
	}
}

// Sanity: the env override variable name composition. If anyone ever
// renames the prefix or casing, every cmd/<svc>/main.go behavior breaks.
func TestRequireLoopback_OverrideEnvName(t *testing.T) {
	// Make sure the env we expect users to set actually triggers the
	// path. Helps catch a refactor that changes the prefix without
	// updating docs.
	defer os.Unsetenv("LH_GATEWAY_ALLOW_NONLOOPBACK")
	os.Setenv("LH_GATEWAY_ALLOW_NONLOOPBACK", "1")
	if err := requireLoopbackOrOverride("gateway", "0.0.0.0:3110"); err != nil {
		t.Errorf("gateway override env should work, got %v", err)
	}
}
