// Package httputil provides shared HTTP test utilities for internal/api domain
// handler tests. It is a sub-package of testkit, kept separate to avoid import
// cycles (testkit imports domain store types that this package's consumers also
// belong to).
package httputil

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"

	"github.com/gin-gonic/gin"
)

// DoJSON sends an authenticated JSON request through a test router and returns
// the response recorder. It sets Authorization and X-Workspace-Id headers
// automatically.
func DoJSON(t testing.TB, router http.Handler, method, path string, body any) *httptest.ResponseRecorder {
	t.Helper()
	var reader io.Reader = http.NoBody
	if body != nil {
		payload, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("marshal JSON request body: %v", err)
		}
		reader = bytes.NewReader(payload)
	}
	request := httptest.NewRequest(method, path, reader)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")
	if body != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, request)
	return recorder
}

// DoRaw sends a raw-body request through a test router using the provided
// headers verbatim (no automatic auth injection).
func DoRaw(t testing.TB, router http.Handler, method, path string, headers http.Header, body []byte) *httptest.ResponseRecorder {
	t.Helper()
	var reader io.Reader = http.NoBody
	if body != nil {
		reader = bytes.NewReader(body)
	}
	request := httptest.NewRequest(method, path, reader)
	for key, values := range headers {
		for _, value := range values {
			request.Header.Add(key, value)
		}
	}
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, request)
	return recorder
}

// RouteCounts returns a method+path registration count map for a Gin router.
func RouteCounts(router *gin.Engine) map[string]int {
	counts := make(map[string]int)
	if router == nil {
		return counts
	}
	for _, route := range router.Routes() {
		counts[route.Method+" "+route.Path]++
	}
	return counts
}

// BlockCall records a single Block/Unblock invocation.
type BlockCall struct {
	WorkspaceID   string
	Provider      string
	ConnectionRef string
}

// FakeBlocker is a test double for the service-event App facade compatibility
// port used by the app and provider API packages.
type FakeBlocker struct {
	mu           sync.Mutex
	BlockCalls   []BlockCall
	UnblockCalls []BlockCall
	BlockErr     error
	UnblockErr   error
}

func (fake *FakeBlocker) BlockServiceEventSubscriptions(_ context.Context, workspaceID, provider, connectionRef string) (int, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.BlockCalls = append(fake.BlockCalls, BlockCall{workspaceID, provider, connectionRef})
	if fake.BlockErr != nil {
		return 0, fake.BlockErr
	}
	return len(fake.BlockCalls), nil
}

func (fake *FakeBlocker) UnblockServiceEventSubscriptions(_ context.Context, workspaceID, provider, connectionRef string) (int, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.UnblockCalls = append(fake.UnblockCalls, BlockCall{workspaceID, provider, connectionRef})
	if fake.UnblockErr != nil {
		return 0, fake.UnblockErr
	}
	return len(fake.UnblockCalls), nil
}

// ErrSentinel is a sentinel error used across API handler tests for default
// error-mapping assertions.
var ErrSentinel = errors.New("boom")
