package sandbox

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"go.starlark.net/starlark"
)

// ─── isPublicIP ─────────────────────────────

func TestIsPublicIP(t *testing.T) {
	tests := []struct {
		ip     string
		public bool
	}{
		// Public
		{"8.8.8.8", true},
		{"1.1.1.1", true},
		{"93.184.216.34", true},
		{"2607:f8b0:4004:800::200e", true}, // google IPv6

		// Loopback
		{"127.0.0.1", false},
		{"127.0.0.2", false},
		{"::1", false},

		// Private RFC1918
		{"10.0.0.1", false},
		{"10.255.255.255", false},
		{"172.16.0.1", false},
		{"172.31.255.255", false},
		{"192.168.0.1", false},
		{"192.168.1.100", false},

		// Link-local
		{"169.254.0.1", false},
		{"169.254.169.254", false}, // AWS metadata
		{"fe80::1", false},

		// Unspecified
		{"0.0.0.0", false},
		{"::", false},

		// Multicast
		{"224.0.0.1", false},
		{"ff02::1", false},

		// Private IPv6
		{"fc00::1", false},
		{"fd00::1", false},
	}

	for _, tt := range tests {
		ip := net.ParseIP(tt.ip)
		if ip == nil {
			t.Fatalf("failed to parse test IP %q", tt.ip)
		}
		got := isPublicIP(ip)
		if got != tt.public {
			t.Errorf("isPublicIP(%s) = %v, want %v", tt.ip, got, tt.public)
		}
	}
}

// ─── ParseNetworkAccess ─────────────────────

func TestParseNetworkAccess(t *testing.T) {
	t.Run("empty manifest", func(t *testing.T) {
		cfg := ParseNetworkAccess(map[string]any{})
		if len(cfg.AllowDomains) != 0 || len(cfg.BlockDomains) != 0 {
			t.Errorf("expected empty config, got allow=%v block=%v", cfg.AllowDomains, cfg.BlockDomains)
		}
	})

	t.Run("allow list", func(t *testing.T) {
		cfg := ParseNetworkAccess(map[string]any{
			"network_access": map[string]any{
				"allow": []any{"API.Example.COM", "hooks.slack.com"},
			},
		})
		if len(cfg.AllowDomains) != 2 {
			t.Fatalf("expected 2 allow domains, got %d", len(cfg.AllowDomains))
		}
		if cfg.AllowDomains[0] != "api.example.com" {
			t.Errorf("allow[0] = %q, want %q", cfg.AllowDomains[0], "api.example.com")
		}
	})

	t.Run("block list", func(t *testing.T) {
		cfg := ParseNetworkAccess(map[string]any{
			"network_access": map[string]any{
				"block": []any{"evil.com"},
			},
		})
		if len(cfg.BlockDomains) != 1 || cfg.BlockDomains[0] != "evil.com" {
			t.Errorf("expected block=[evil.com], got %v", cfg.BlockDomains)
		}
	})

	t.Run("malformed network_access ignored", func(t *testing.T) {
		cfg := ParseNetworkAccess(map[string]any{
			"network_access": "not a map",
		})
		if len(cfg.AllowDomains) != 0 {
			t.Errorf("expected empty allow, got %v", cfg.AllowDomains)
		}
	})

	t.Run("string slice variant", func(t *testing.T) {
		cfg := ParseNetworkAccess(map[string]any{
			"network_access": map[string]any{
				"allow": []string{"a.com", "b.com"},
			},
		})
		if len(cfg.AllowDomains) != 2 {
			t.Errorf("expected 2 allow domains, got %d", len(cfg.AllowDomains))
		}
	})

	t.Run("empty strings filtered", func(t *testing.T) {
		cfg := ParseNetworkAccess(map[string]any{
			"network_access": map[string]any{
				"allow": []any{"", "ok.com", ""},
			},
		})
		if len(cfg.AllowDomains) != 1 || cfg.AllowDomains[0] != "ok.com" {
			t.Errorf("expected [ok.com], got %v", cfg.AllowDomains)
		}
	})
}

// ─── Access Control ─────────────────────────

func TestAccessControl_Allowlist(t *testing.T) {
	ac := &accessControl{cfg: HTTPModuleConfig{
		AllowDomains: []string{"api.example.com", "hooks.slack.com"},
	}}

	if err := ac.checkDomain("api.example.com"); err != nil {
		t.Errorf("allowed domain rejected: %v", err)
	}
	if err := ac.checkDomain("hooks.slack.com"); err != nil {
		t.Errorf("allowed domain rejected: %v", err)
	}
	if err := ac.checkDomain("evil.com"); err == nil {
		t.Error("unlisted domain should be rejected in allowlist mode")
	}
	if err := ac.checkDomain("API.EXAMPLE.COM"); err != nil {
		t.Errorf("case-insensitive match should pass: %v", err)
	}
}

func TestAccessControl_Blocklist(t *testing.T) {
	ac := &accessControl{cfg: HTTPModuleConfig{
		BlockDomains: []string{"evil.com", "malware.org"},
	}}

	if err := ac.checkDomain("api.example.com"); err != nil {
		t.Errorf("unblocked domain rejected: %v", err)
	}
	if err := ac.checkDomain("evil.com"); err == nil {
		t.Error("blocked domain should be rejected")
	}
	if err := ac.checkDomain("malware.org"); err == nil {
		t.Error("blocked domain should be rejected")
	}
}

func TestAccessControl_NoPolicy(t *testing.T) {
	ac := &accessControl{cfg: HTTPModuleConfig{}}

	if err := ac.checkDomain("anything.com"); err != nil {
		t.Errorf("no-policy mode should allow all public domains: %v", err)
	}
}

// ─── dictToStringMap ────────────────────────

func TestDictToStringMap(t *testing.T) {
	t.Run("nil dict", func(t *testing.T) {
		m := dictToStringMap(nil)
		if m != nil {
			t.Errorf("nil input should return nil, got %v", m)
		}
	})

	t.Run("valid dict", func(t *testing.T) {
		d := starlark.NewDict(2)
		_ = d.SetKey(starlark.String("Content-Type"), starlark.String("application/json"))
		_ = d.SetKey(starlark.String("X-Token"), starlark.String("abc"))
		m := dictToStringMap(d)
		if m["Content-Type"] != "application/json" {
			t.Errorf("Content-Type = %q", m["Content-Type"])
		}
		if m["X-Token"] != "abc" {
			t.Errorf("X-Token = %q", m["X-Token"])
		}
	})

	t.Run("non-string values skipped", func(t *testing.T) {
		d := starlark.NewDict(2)
		_ = d.SetKey(starlark.String("ok"), starlark.String("yes"))
		_ = d.SetKey(starlark.String("bad"), starlark.MakeInt(42))
		m := dictToStringMap(d)
		if len(m) != 1 {
			t.Errorf("expected 1 entry (non-string skipped), got %d", len(m))
		}
	})
}

// ─── buildResponseDict ──────────────────────

func TestBuildResponseDict(t *testing.T) {
	h := http.Header{}
	h.Set("Content-Type", "application/json")
	h.Set("X-Request-Id", "abc-123")

	val, err := buildResponseDict(200, h, `{"ok":true}`)
	if err != nil {
		t.Fatalf("buildResponseDict: %v", err)
	}

	d, ok := val.(*starlark.Dict)
	if !ok {
		t.Fatalf("expected *starlark.Dict, got %T", val)
	}

	// Status
	statusVal, found, _ := d.Get(starlark.String("status"))
	if !found {
		t.Fatal("missing 'status' key")
	}
	statusInt, _ := starlark.AsInt32(statusVal)
	if statusInt != 200 {
		t.Errorf("status = %d, want 200", statusInt)
	}

	// Body
	bodyVal, found, _ := d.Get(starlark.String("body"))
	if !found {
		t.Fatal("missing 'body' key")
	}
	bodyStr, _ := starlark.AsString(bodyVal)
	if bodyStr != `{"ok":true}` {
		t.Errorf("body = %q", bodyStr)
	}

	// Headers
	hdrsVal, found, _ := d.Get(starlark.String("headers"))
	if !found {
		t.Fatal("missing 'headers' key")
	}
	hdrs, ok := hdrsVal.(*starlark.Dict)
	if !ok {
		t.Fatalf("headers is %T, want *Dict", hdrsVal)
	}
	ctVal, found, _ := hdrs.Get(starlark.String("content-type"))
	if !found {
		t.Fatal("missing content-type header")
	}
	ct, _ := starlark.AsString(ctVal)
	if ct != "application/json" {
		t.Errorf("content-type = %q", ct)
	}
}

// ─── Integration: HTTP GET against httptest ──

func TestHTTPModule_GET(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "GET" {
			t.Errorf("expected GET, got %s", r.Method)
		}
		if r.Header.Get("X-Custom") != "hello" {
			t.Errorf("missing custom header")
		}
		w.Header().Set("X-Test", "passed")
		w.WriteHeader(200)
		fmt.Fprint(w, `{"result":"ok"}`)
	}))
	defer srv.Close()

	// Extract host for allowlist
	host := strings.TrimPrefix(srv.URL, "http://")
	hostOnly := strings.Split(host, ":")[0]
	_ = hostOnly // httptest uses 127.0.0.1 which is blocked by SSRF

	// For testing, we need to use the module without SSRF checks
	// since httptest binds to 127.0.0.1. Test the module builder
	// API by calling executeHTTPRequest with a permissive client.
	// SSRF checks are tested separately via isPublicIP tests.
	resp, err := executeHTTPRequest(
		context.Background(),
		&accessControl{cfg: HTTPModuleConfig{}},
		"GET", srv.URL, "",
		map[string]string{"X-Custom": "hello"},
		5*time.Second,
		httpMaxResponseBody,
	)
	// This will fail with SSRF block because httptest is 127.0.0.1.
	// That's the correct behavior — verify we get the SSRF error.
	if err == nil {
		// If it somehow succeeded (e.g., test env allows loopback), check response
		d := resp.(*starlark.Dict)
		statusVal, _, _ := d.Get(starlark.String("status"))
		statusInt, _ := starlark.AsInt32(statusVal)
		if statusInt != 200 {
			t.Errorf("status = %d, want 200", statusInt)
		}
	} else if !strings.Contains(err.Error(), "ssrf") && !strings.Contains(err.Error(), "private") {
		t.Errorf("expected SSRF error for loopback, got: %v", err)
	}
}

// TestHTTPModule_SSRFBlocked verifies that private IPs are blocked
// even when disguised as valid URLs.
func TestHTTPModule_SSRFBlocked(t *testing.T) {
	targets := []string{
		"http://127.0.0.1/",
		"http://10.0.0.1/",
		"http://192.168.1.1/",
		"http://169.254.169.254/latest/meta-data/", // AWS metadata
		"http://[::1]/",
	}

	ac := &accessControl{cfg: HTTPModuleConfig{}}

	for _, target := range targets {
		_, err := executeHTTPRequest(
			context.Background(), ac,
			"GET", target, "", nil,
			2*time.Second, httpMaxResponseBody,
		)
		if err == nil {
			t.Errorf("expected SSRF block for %s", target)
		}
	}
}

// TestHTTPModule_SchemeValidation verifies only http/https are allowed.
func TestHTTPModule_SchemeValidation(t *testing.T) {
	ac := &accessControl{cfg: HTTPModuleConfig{}}

	schemes := []string{"ftp://example.com", "file:///etc/passwd", "gopher://evil.com"}
	for _, u := range schemes {
		_, err := executeHTTPRequest(
			context.Background(), ac,
			"GET", u, "", nil,
			2*time.Second, httpMaxResponseBody,
		)
		if err == nil {
			t.Errorf("expected scheme rejection for %s", u)
		}
		if !strings.Contains(err.Error(), "only http/https") {
			t.Errorf("wrong error for %s: %v", u, err)
		}
	}
}

// TestHTTPModule_DomainBlock verifies blocklist enforcement at request time.
func TestHTTPModule_DomainBlock(t *testing.T) {
	ac := &accessControl{cfg: HTTPModuleConfig{
		BlockDomains: []string{"evil.com"},
	}}

	_, err := executeHTTPRequest(
		context.Background(), ac,
		"GET", "https://evil.com/api", "", nil,
		2*time.Second, httpMaxResponseBody,
	)
	if err == nil {
		t.Error("expected block for evil.com")
	}
	if !strings.Contains(err.Error(), "blocked") {
		t.Errorf("wrong error: %v", err)
	}
}

// TestHTTPModule_AllowlistEnforcement verifies allowlist-only mode.
func TestHTTPModule_AllowlistEnforcement(t *testing.T) {
	ac := &accessControl{cfg: HTTPModuleConfig{
		AllowDomains: []string{"api.good.com"},
	}}

	_, err := executeHTTPRequest(
		context.Background(), ac,
		"GET", "https://api.bad.com/data", "", nil,
		2*time.Second, httpMaxResponseBody,
	)
	if err == nil {
		t.Error("expected rejection for unlisted domain")
	}
	if !strings.Contains(err.Error(), "not in network_access allow list") {
		t.Errorf("wrong error: %v", err)
	}
}

// TestHTTPModule_Timeout verifies context-based timeout.
func TestHTTPModule_Timeout(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(5 * time.Second)
		w.WriteHeader(200)
	}))
	defer srv.Close()

	ac := &accessControl{cfg: HTTPModuleConfig{}}
	_, err := executeHTTPRequest(
		context.Background(), ac,
		"GET", srv.URL, "", nil,
		100*time.Millisecond, httpMaxResponseBody,
	)
	// Will either fail with SSRF (127.0.0.1) or timeout — both are correct.
	if err == nil {
		t.Error("expected error (SSRF or timeout)")
	}
}

// TestHTTPModule_Starlark_Integration runs the http module through the
// Starlark interpreter to verify the builtins are properly wired.
// Starlark has no try/except — errors from builtins propagate to Go.
func TestHTTPModule_Starlark_Integration(t *testing.T) {
	mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{
		AllowDomains: []string{"only-this.example.com"},
	})

	sb := New(DefaultConfig())
	modules := map[string]starlark.Value{"http": mod}

	// Call http.get against a domain NOT in the allowlist.
	// The builtin should return an error that propagates to Go.
	script := `
def test():
    return http.get(url="https://blocked.example.com/api")

result = test()
`
	_, err := sb.Exec(context.Background(), "test.star", script, modules)
	if err == nil {
		t.Fatal("expected error for blocked domain, got nil")
	}
	if !strings.Contains(err.Error(), "not in network_access allow list") {
		t.Errorf("expected allowlist error, got: %v", err)
	}
}

// TestHTTPModule_Starlark_POST verifies post binding accepts body/headers kwargs.
// Starlark has no try/except — we verify binding by checking the error is
// a network/DNS error (correct kwargs accepted) not a Starlark type error.
func TestHTTPModule_Starlark_POST(t *testing.T) {
	mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{
		AllowDomains: []string{"only-this.example.com"},
	})

	sb := New(DefaultConfig())
	modules := map[string]starlark.Value{"http": mod}

	// Call http.post with body and headers kwargs against an allowed domain.
	// Will fail at DNS/connection (domain doesn't exist), but the binding
	// itself must parse kwargs without error.
	script := `
def test():
    return http.post(url="https://only-this.example.com/api", body='{"k":"v"}', headers={"Content-Type": "application/json"})

result = test()
`
	_, err := sb.Exec(context.Background(), "test.star", script, modules)
	if err == nil {
		t.Fatal("expected network error (domain doesn't resolve), got nil")
	}
	// Should be a network error (DNS lookup failure), NOT a Starlark
	// argument-parsing error like "unexpected keyword argument".
	errStr := err.Error()
	if strings.Contains(errStr, "unexpected keyword") || strings.Contains(errStr, "missing argument") {
		t.Errorf("binding signature broken: %v", err)
	}
}

// TestHTTPModule_Starlark_Request verifies the generic request binding.
func TestHTTPModule_Starlark_Request(t *testing.T) {
	mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{
		AllowDomains: []string{"nope.invalid"},
	})

	sb := New(DefaultConfig())
	modules := map[string]starlark.Value{"http": mod}

	// Call http.request with method, url, body kwargs.
	// Will fail at DNS, but binding must accept kwargs correctly.
	script := `
def test():
    return http.request(method="PATCH", url="https://nope.invalid/x", body="data")

result = test()
`
	_, err := sb.Exec(context.Background(), "test.star", script, modules)
	if err == nil {
		t.Fatal("expected network error, got nil")
	}
	errStr := err.Error()
	if strings.Contains(errStr, "unexpected keyword") || strings.Contains(errStr, "missing argument") {
		t.Errorf("binding signature broken: %v", err)
	}
}
