package scenarios_test

// Security scenario tests for Route Guards.
//
// These verify that sensitive endpoints require proper authentication
// and authorization, and that security infrastructure tables are
// protected even from admin users.

import (
	"encoding/json"
	"fmt"
	"net/http"
	"testing"
	"time"

	"github.com/nexuscrm/tests/contract/internal/liveapi"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ---------- SEC-005: pprof endpoints require authentication ----------

func TestSEC005_PprofRequiresAuth(t *testing.T) {
	// Scenario: GET /debug/pprof/heap without any auth token.
	// Should return 401/403, not a heap dump.
	//
	// Evidence: main.go:333 registers pprof routes with zero middleware.
	// Note: This test only works if ENABLE_PPROF=true on the server.
	// If pprof is disabled (default), endpoints return 404 — that's also safe.

	cfg := liveapi.LoadConfig(t)
	httpClient := &http.Client{Timeout: 10 * time.Second}

	// Unauthenticated request
	resp, err := httpClient.Get(cfg.RootBaseURL + "/debug/pprof/heap")
	require.NoError(t, err)
	defer resp.Body.Close()

	switch resp.StatusCode {
	case http.StatusNotFound:
		t.Log("pprof disabled (404) — safe, no issue")
	case http.StatusUnauthorized, http.StatusForbidden:
		t.Log("pprof requires auth — correct behavior")
	case http.StatusOK:
		t.Errorf("SECURITY BUG: /debug/pprof/heap returned 200 without authentication. "+
			"This exposes memory dumps containing tokens and credentials.")
	default:
		t.Logf("pprof returned unexpected status %d — investigate", resp.StatusCode)
	}
}

// ---------- SEC-006: File download requires object permission ----------

func TestSEC006_FileDownloadRequiresPermission(t *testing.T) {
	// Scenario: Test two levels of file download protection:
	// (a) Unauthenticated request should be rejected (route-level auth)
	// (b) Authenticated user should still need object-level permission
	//
	// The actual C6 bug: file_upload_handler.go:136-188 has route-level auth
	// (requireAuth middleware) but no object-level permission check. Any
	// authenticated user can download any file by ID.
	//
	// This test verifies both layers.

	cfg := liveapi.LoadConfig(t)
	httpClient := &http.Client{Timeout: 10 * time.Second}

	// Test (a): Unauthenticated request — should be 401
	fakeID := "00000000-0000-0000-0000-000000000001.pdf"
	unauthResp, err := httpClient.Get(cfg.RootBaseURL + "/services/data/v63.0/sobjects/ContentVersion/" + fakeID + "/VersionData")
	require.NoError(t, err)
	defer unauthResp.Body.Close()

	assert.NotEqual(t, http.StatusOK, unauthResp.StatusCode,
		"File download should require authentication (route-level)")
	t.Logf("Unauthenticated download returned %d (expected 401)", unauthResp.StatusCode)

	// Test (b): Authenticated as sales user — should be rejected for files
	// they don't have object permission on. If the server returns 200 with
	// file content for a non-existent file ID, that indicates the handler
	// doesn't check permissions before attempting to serve.
	sales := salesClient(t)
	authResp, err := sales.Do(http.MethodGet,
		"/sobjects/ContentVersion/"+fakeID+"/VersionData", "", nil)
	require.NoError(t, err)
	defer authResp.Body.Close()

	// 404 (file not found) is acceptable — but if a real file existed,
	// we need the handler to check object permissions before serving it.
	// This test documents the expected behavior. A full test would require
	// uploading a file as admin, then downloading as sales user.
	t.Logf("Authenticated (sales) download returned %d for non-existent file", authResp.StatusCode)

	if authResp.StatusCode == http.StatusOK {
		t.Error("SECURITY: Authenticated sales user got 200 for file download. " +
			"Handler may serve files without object-level permission check.")
	}
}

// ---------- SEC-007: BLOCKED tables deny even SuperUser writes ----------

func TestSEC007_BlockedTablesDenySuperUser(t *testing.T) {
	// Scenario: Admin (system_admin profile = SuperUser) attempts to POST
	// to _System_Session. Should be rejected because _System_Session has
	// RequiredWritePermission = "BLOCKED" — not writable via any API.
	//
	// Evidence: system_tables.json marks _System_Session as BLOCKED.
	// Risk: hasAnySystemPermission (service.go:122) grants SuperUser
	// bypass on all permissions including BLOCKED sentinel.

	admin := adminClient(t)

	resp, err := admin.DoJSON(http.MethodPost, "/sobjects/_System_Session", map[string]any{
		"UserId": "test",
		"Token":  "fake-token",
	})
	require.NoError(t, err)
	defer resp.Body.Close()
	body := liveapi.ReadBody(t, resp)

	require.NotEqual(t, http.StatusCreated, resp.StatusCode,
		"SECURITY BUG: Admin was able to create _System_Session record "+
			"(BLOCKED table should deny even SuperUser): %s", body)

	t.Logf("_System_Session POST as admin returned %d — correct denial", resp.StatusCode)
}

// ---------- SEC-008: Auth rate limit stricter than global ----------

func TestSEC008_AuthRateLimitStricterThanGlobal(t *testing.T) {
	// Scenario: Send rapid invalid token requests. With dedicated auth
	// rate limiter (5 req/s), later requests should get 429. With global
	// limiter (100 req/s), all succeed (with 400/401 for invalid creds).
	//
	// Evidence: routes.go:47,50 — auth endpoints share global 100 req/s.
	// Design requires dedicated 5 req/s.
	//
	// Note: This test sends invalid credentials intentionally.

	cfg := liveapi.LoadConfig(t)
	httpClient := &http.Client{Timeout: 5 * time.Second}

	got429 := false
	for i := 0; i < 20; i++ {
		req, _ := http.NewRequest(http.MethodPost, cfg.TokenURL, nil)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		resp, err := httpClient.Do(req)
		if err != nil {
			continue
		}
		resp.Body.Close()
		if resp.StatusCode == http.StatusTooManyRequests {
			got429 = true
			t.Logf("Got 429 on request %d — rate limiter active", i+1)
			break
		}
	}

	if !got429 {
		t.Error("SECURITY: 20 rapid auth requests without 429. Auth endpoint lacks " +
			"dedicated rate limiter (sharing global 100 req/s). Brute-force risk.")
	}
}

// ---------- SEC-010: Role hierarchy cycle rejected ----------

func TestSEC010_RoleHierarchyCycleRejected(t *testing.T) {
	// Scenario: Create two roles A→B (B's parent is A). Then attempt
	// to set A's parent to B, creating a cycle. Should return validation
	// error, not succeed silently.
	//
	// Evidence: service.go:580-605 only checks self-reference, not
	// indirect cycles (A→B→A).

	admin := adminClient(t)

	// Create role A
	roleAName := fmt.Sprintf("SEC010-A-%d", time.Now().UnixNano())
	respA, err := admin.DoJSON(http.MethodPost, "/sobjects/UserRole", map[string]any{
		"Name":          roleAName,
		"DeveloperName": roleAName,
	})
	require.NoError(t, err)
	defer respA.Body.Close()
	bodyA := liveapi.ReadBody(t, respA)
	if respA.StatusCode != http.StatusCreated {
		t.Skipf("Cannot create role A (%d): %s", respA.StatusCode, bodyA)
	}
	var roleA struct{ ID string `json:"id"` }
	require.NoError(t, json.Unmarshal(bodyA, &roleA))

	// Create role B with parent = A
	roleBName := fmt.Sprintf("SEC010-B-%d", time.Now().UnixNano())
	respB, err := admin.DoJSON(http.MethodPost, "/sobjects/UserRole", map[string]any{
		"Name":          roleBName,
		"DeveloperName": roleBName,
		"ParentRoleId":  roleA.ID,
	})
	require.NoError(t, err)
	defer respB.Body.Close()
	bodyB := liveapi.ReadBody(t, respB)
	if respB.StatusCode != http.StatusCreated {
		t.Skipf("Cannot create role B (%d): %s", respB.StatusCode, bodyB)
	}
	var roleB struct{ ID string `json:"id"` }
	require.NoError(t, json.Unmarshal(bodyB, &roleB))

	t.Cleanup(func() {
		admin.Do(http.MethodDelete, "/sobjects/UserRole/"+roleB.ID, "", nil)
		admin.Do(http.MethodDelete, "/sobjects/UserRole/"+roleA.ID, "", nil)
	})

	// Attempt to set A's parent to B (creates cycle A→B→A)
	cycleResp, err := admin.DoJSON(http.MethodPatch,
		"/sobjects/UserRole/"+roleA.ID, map[string]any{
			"ParentRoleId": roleB.ID,
		})
	require.NoError(t, err)
	defer cycleResp.Body.Close()
	cycleBody := liveapi.ReadBody(t, cycleResp)

	// Should be rejected (400 validation error), not accepted (204)
	assert.NotEqual(t, http.StatusNoContent, cycleResp.StatusCode,
		"SECURITY BUG: Role hierarchy cycle was accepted (A→B→A). "+
			"Should return validation error. Got: %s", cycleBody)

	t.Logf("Cycle attempt returned %d: %s", cycleResp.StatusCode, cycleBody)
}
