package httpx

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

	"bountyboard/internal/domain"
)

func userCtx(r *http.Request, u *domain.User) *http.Request {
	return r.WithContext(context.WithValue(r.Context(), ctxKeyUser, u))
}

// TestRBACMatrix exercises requireRole for every role/requirement combination.
func TestRBACMatrix(t *testing.T) {
	s := newTestServer(t)
	roles := func(admin, consultant, developer bool) domain.Roles {
		return domain.Roles{Admin: admin, Consultant: consultant, Developer: developer}
	}
	tests := []struct {
		name     string
		user     *domain.User
		required string
		wantCode int
	}{
		{"admin passes admin gate", &domain.User{Roles: roles(true, false, false)}, "admin", http.StatusOK},
		{"developer blocked from admin gate", &domain.User{Roles: roles(false, false, true)}, "admin", http.StatusForbidden},
		{"consultant blocked from admin gate", &domain.User{Roles: roles(false, true, false)}, "admin", http.StatusForbidden},
		{"consultant passes consultant gate", &domain.User{Roles: roles(false, true, false)}, "consultant", http.StatusOK},
		{"developer blocked from consultant gate", &domain.User{Roles: roles(false, false, true)}, "consultant", http.StatusForbidden},
		{"admin blocked from consultant gate without flag", &domain.User{Roles: roles(true, false, false)}, "consultant", http.StatusForbidden},
		{"developer passes developer gate", &domain.User{Roles: roles(false, false, true)}, "developer", http.StatusOK},
		{"multi-role user passes both gates", &domain.User{Roles: roles(false, true, true)}, "developer", http.StatusOK},
		{"no user in context", nil, "developer", http.StatusForbidden},
		{"unknown role never passes", &domain.User{Roles: roles(true, true, true)}, "superuser", http.StatusForbidden},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := s.requireRole(tt.required, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			}))
			r := httptest.NewRequest(http.MethodGet, "/probe", nil)
			if tt.user != nil {
				r = userCtx(r, tt.user)
			}
			rec := httptest.NewRecorder()
			h.ServeHTTP(rec, r)
			if rec.Code != tt.wantCode {
				t.Errorf("code = %d, want %d", rec.Code, tt.wantCode)
			}
		})
	}
}

func TestCSRFDoubleSubmit(t *testing.T) {
	s := newTestServer(t)
	ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })

	tests := []struct {
		name     string
		method   string
		cookie   string
		header   string
		wantCode int
	}{
		{"GET passes without token", http.MethodGet, "", "", http.StatusOK},
		{"HEAD passes without token", http.MethodHead, "", "", http.StatusOK},
		{"POST without anything blocked", http.MethodPost, "", "", http.StatusForbidden},
		{"POST with matching pair passes", http.MethodPost, "tok123", "tok123", http.StatusOK},
		{"POST with mismatched header blocked", http.MethodPost, "tok123", "other", http.StatusForbidden},
		{"POST with header but no cookie blocked", http.MethodPost, "", "tok123", http.StatusForbidden},
		{"POST with cookie but no header blocked", http.MethodPost, "tok123", "", http.StatusForbidden},
		{"DELETE requires token", http.MethodDelete, "tok123", "tok123", http.StatusOK},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := httptest.NewRequest(tt.method, "/probe", nil)
			if tt.cookie != "" {
				r.AddCookie(&http.Cookie{Name: csrfCookie, Value: tt.cookie})
			}
			if tt.header != "" {
				r.Header.Set(csrfHeader, tt.header)
			}
			rec := httptest.NewRecorder()
			s.requireCSRF(ok).ServeHTTP(rec, r)
			if rec.Code != tt.wantCode {
				t.Errorf("code = %d, want %d", rec.Code, tt.wantCode)
			}
		})
	}
}
