package handler

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/go-chi/chi/v5"
)

// TestRequireHumanActor_AllowsHumanRequest pins the happy path: a
// request that passed Auth as a JWT or mul_ PAT does NOT carry
// X-Actor-Source, so the guard lets it through and the inner handler
// runs.
//
// We construct a bare http.Handler chain (no full router) so the test
// exercises only the middleware logic and is independent of any
// downstream wiring.
func TestRequireHumanActor_AllowsHumanRequest(t *testing.T) {
	called := false
	next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		called = true
		w.WriteHeader(http.StatusOK)
	})

	mw := RequireHumanActor(next)

	req := httptest.NewRequest(http.MethodGet, "/api/cloud-billing/balance", nil)
	// No X-Actor-Source — this is the JWT / mul_ PAT shape.
	w := httptest.NewRecorder()
	mw.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", w.Code)
	}
	if !called {
		t.Fatal("inner handler must run for non-task-token requests")
	}
}

// TestRequireHumanActor_BlocksMachineCredentials walks every machine-
// credential X-Actor-Source value the auth middlewares stamp today
// and confirms each is rejected with 403. The two values must stay
// in lockstep with auth.go and daemon_auth.go: a new machine
// credential added there without a corresponding case here would
// silently grant agents/nodes account-level access.
func TestRequireHumanActor_BlocksMachineCredentials(t *testing.T) {
	cases := []struct {
		name        string
		actorSource string
	}{
		// mat_ task token — set in middleware/auth.go's mat_ branch.
		// An agent process holding its task-scoped token must not be
		// able to read its owner's billing data.
		{name: "task_token", actorSource: "task_token"},
		// mcn_ cloud-node PAT — set in BOTH middleware/auth.go and
		// middleware/daemon_auth.go's mcn_ branches. A cloud-runtime
		// EC2 node operating on the owner's behalf is the same kind
		// of machine credential as mat_ for billing-authorization
		// purposes.
		{name: "cloud_pat", actorSource: "cloud_pat"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
				t.Fatalf("inner handler must NOT run for actor source %q", tc.actorSource)
			})
			mw := RequireHumanActor(next)

			req := httptest.NewRequest(http.MethodGet, "/api/cloud-billing/balance", nil)
			// This is what the Auth (or DaemonAuth) middleware sets
			// for the matching token kind. Setting it directly here
			// proves the gate triggers on the header regardless of
			// upstream context — the auth middlewares strip any
			// client-supplied value before stamping their own, so a
			// non-empty value at this point IS authoritative.
			req.Header.Set("X-Actor-Source", tc.actorSource)
			w := httptest.NewRecorder()
			mw.ServeHTTP(w, req)

			if w.Code != http.StatusForbidden {
				t.Fatalf("status = %d, want 403", w.Code)
			}
		})
	}
}

// TestRequireHumanActor_IgnoresUnknownActorSource pins the gate's
// scope: it is an explicit denylist against the known-bad
// "task_token" value, NOT an allowlist against "human only / empty".
//
// Why the denylist shape:
//
//   - The Auth middleware today sets X-Actor-Source for exactly one
//     case: mat_ task tokens. Every other authenticated path (JWT,
//     mul_ PAT) leaves the header empty. So "non-empty AND not
//     task_token" is unreachable in current production.
//
//   - If a future actor kind is added (say a hypothetical
//     `service_account` token), this gate's silence on the new value
//     is a CONSCIOUS DECISION POINT, not an accident. The added auth
//     branch is the right place to decide whether the new kind should
//     be allowed at billing endpoints — and that decision belongs in
//     a security review at the time, not in a default-deny rule here
//     that pre-emptively shuts out hypothetical use cases we cannot
//     reason about today.
//
// If you are reading this comment because a new actor kind needs to
// reach billing or needs to be blocked from it, update
// RequireHumanActor to handle the new kind explicitly (and update
// this test's expectation accordingly).
func TestRequireHumanActor_IgnoresUnknownActorSource(t *testing.T) {
	called := false
	next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		called = true
		w.WriteHeader(http.StatusOK)
	})

	mw := RequireHumanActor(next)

	// A hypothetical future value the gate doesn't know about.
	req := httptest.NewRequest(http.MethodGet, "/api/cloud-billing/balance", nil)
	req.Header.Set("X-Actor-Source", "future_kind")
	w := httptest.NewRecorder()
	mw.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200 — gate should only block exact 'task_token'", w.Code)
	}
	if !called {
		t.Fatal("inner handler must run for unknown actor sources")
	}
}


// TestRequireHumanActor_AppliedViaChiRouterUse pins the wiring side of
// the contract: when the guard is attached to a chi route group via
// r.Use, every endpoint in that group is protected, and a task-token
// request never reaches the handler — even one we add later. This is
// what router.go's r.Route("/api/cloud-billing", ...) + r.Use(...)
// guarantees in production; the test is small but a developer adding
// a new billing endpoint and forgetting to re-attach the middleware
// would not be caught by the per-handler tests above.
func TestRequireHumanActor_AppliedViaChiRouterUse(t *testing.T) {
	// Use a real chi router so we exercise r.Use(), not just the
	// middleware function in isolation.
	r := chi.NewRouter()
	r.Use(RequireHumanActor)
	r.Get("/billing/probe", func(_ http.ResponseWriter, _ *http.Request) {
		t.Fatal("inner handler must NOT run when guard rejects")
	})

	req := httptest.NewRequest(http.MethodGet, "/billing/probe", nil)
	req.Header.Set("X-Actor-Source", "task_token")
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	if w.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403", w.Code)
	}
}
